diff --git a/cypress/e2e/rich-text/RichTextEditor.spec.ts b/cypress/e2e/rich-text/RichTextEditor.spec.ts index 66ae1a02c..4b1888b9f 100644 --- a/cypress/e2e/rich-text/RichTextEditor.spec.ts +++ b/cypress/e2e/rich-text/RichTextEditor.spec.ts @@ -57,7 +57,7 @@ describe('Rich Text Editor', { viewportHeight: 2000 }, () => { ]; beforeEach(() => { - cy.viewport(1000, 2000); + cy.viewport(1280, 720); richText = new RichTextPage(); richText.visit(); }); @@ -392,7 +392,7 @@ describe('Rich Text Editor', { viewportHeight: 2000 }, () => { // temporarily skipped. Snapshots don't match. Will be fixed in a follow up PR // eslint-disable-next-line - it.skip('runs initial normalization without triggering a value change', () => { + it('runs initial normalization without triggering a value change', () => { cy.setInitialValue(validDocumentThatRequiresNormalization); cy.reload(); diff --git a/packages/rich-text/src/RichTextEditor.tsx b/packages/rich-text/src/RichTextEditor.tsx index c1f1bf7dd..163dc307b 100644 --- a/packages/rich-text/src/RichTextEditor.tsx +++ b/packages/rich-text/src/RichTextEditor.tsx @@ -4,14 +4,12 @@ import { FieldExtensionSDK } from '@contentful/app-sdk'; import { EntityProvider } from '@contentful/field-editor-reference'; import { FieldConnector } from '@contentful/field-editor-shared'; import * as Contentful from '@contentful/rich-text-types'; -import { Document } from '@contentful/rich-text-types'; import { Plate, PlateProvider } from '@udecode/plate-core'; import { css, cx } from 'emotion'; import deepEquals from 'fast-deep-equal'; import noop from 'lodash/noop'; import { ContentfulEditorIdProvider, getContentfulEditorId } from './ContentfulEditorProvider'; -import { createOnChangeCallback } from './helpers/callbacks'; import { toSlateValue } from './helpers/toSlateValue'; import { normalizeInitialValue } from './internal/misc'; import { getPlugins, disableCorePlugins } from './plugins'; @@ -43,8 +41,6 @@ export const ConnectedRichTextEditor = (props: ConnectedProps) => { [sdk, onAction, restrictedMarks] ); - const handleChange = props.onChange; - const initialValue = React.useMemo(() => { return normalizeInitialValue( { @@ -55,14 +51,6 @@ export const ConnectedRichTextEditor = (props: ConnectedProps) => { ); }, [props.value, plugins]); - const onChange = React.useMemo( - () => - createOnChangeCallback((document: Document) => { - handleChange?.(document); - }), - [handleChange] - ); - const classNames = cx( styles.editor, props.minHeight !== undefined ? css({ minHeight: props.minHeight }) : undefined, @@ -79,13 +67,13 @@ export const ConnectedRichTextEditor = (props: ConnectedProps) => { initialValue={initialValue} plugins={plugins} disableCorePlugins={disableCorePlugins} - onChange={onChange}> + > {!props.isToolbarHidden && ( )} - + { field={sdk.field} isInitiallyDisabled={isInitiallyDisabled} isEmptyValue={isEmptyValue} - isEqualValues={deepEquals}> + isEqualValues={deepEquals} + > {({ lastRemoteValue, disabled, setValue }) => ( { export type SyncEditorStateProps = { incomingValue?: Value; + onChange?: (doc: Contentful.Document) => unknown; }; /** @@ -47,8 +51,29 @@ export type SyncEditorStateProps = { * where we can no longer access the editor instance outside the Plate * provider. */ -export const SyncEditorValue = ({ incomingValue }: SyncEditorStateProps) => { +export const SyncEditorValue = ({ incomingValue, onChange }: SyncEditorStateProps) => { const editor = usePlateSelectors().editor(); + const setEditorOnChange = usePlateActions().onChange(); + + React.useEffect(() => { + const cb = createOnChangeCallback(onChange); + + setEditorOnChange({ + fn: (document) => { + console.log(editor.operations); + // Skip irrelevant events e.g. mouse selection + const operations = editor?.operations.filter((op) => { + return op.type !== 'set_selection'; + }); + + if (operations.length === 0) { + return; + } + + cb(document); + }, + }); + }, [editor, onChange, setEditorOnChange]); // Cache latest editor value to avoid unnecessary updates const lastIncomingValue = React.useRef(incomingValue); diff --git a/packages/rich-text/src/helpers/callbacks.ts b/packages/rich-text/src/helpers/callbacks.ts index 4949b03d9..35c761e9f 100644 --- a/packages/rich-text/src/helpers/callbacks.ts +++ b/packages/rich-text/src/helpers/callbacks.ts @@ -1,35 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { toContentfulDocument } from '@contentful/contentful-slatejs-adapter'; import { Document } from '@contentful/rich-text-types'; -import equal from 'fast-deep-equal'; import debounce from 'lodash/debounce'; import schema from '../constants/Schema'; import { removeInternalMarks } from './removeInternalMarks'; -export const createOnChangeCallback = (handler?: (value: Document) => void) => { - // Cache previous value to avoid firing the handler unnecessarily - // - // Note: We are not using lodash/memoize here to avoid memory leaks - // due to having an infinite cache while we only care about the last - // value. - let cache: unknown = null; - - return debounce((document: unknown) => { - if (equal(document, cache)) { - return; - } - - cache = document; +export const createOnChangeCallback = (handler?: (value: Document) => void) => + debounce((document: unknown) => { const doc = removeInternalMarks( toContentfulDocument({ - // eslint-disable-next-line -- parameter type is not exported @typescript-eslint/no-explicit-any document: document as any, schema: schema, - // eslint-disable-next-line -- parameter type is not exported @typescript-eslint/no-explicit-any }) as any ); - // eslint-disable-next-line -- correct parameter type is not defined @typescript-eslint/no-explicit-any + const cleanedDocument = removeInternalMarks(doc as Record); handler?.(cleanedDocument); }, 500); -};