UNPKG

@wordpress/core-data

Version:
8 lines (7 loc) 11.3 kB
{ "version": 3, "sources": ["../../src/awareness/post-editor-awareness.ts"], "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { dispatch, select, subscribe } from '@wordpress/data';\nimport { Y } from '@wordpress/sync';\n// @ts-ignore No exported types for block editor store selectors.\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\n/**\n * Internal dependencies\n */\nimport { BaseAwarenessState, baseEqualityFieldChecks } from './base-awareness';\nimport {\n\tAWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,\n\tLOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,\n} from './config';\nimport { STORE_NAME as coreStore } from '../name';\nimport {\n\tareSelectionsStatesEqual,\n\tgetSelectionState,\n} from '../utils/crdt-user-selections';\n\nimport type { SelectionCursor, WPBlockSelection } from '../types';\nimport type {\n\tDebugCollaboratorData,\n\tEditorState,\n\tPostEditorState,\n\tSerializableYItem,\n\tYDocDebugData,\n} from './types';\n\nexport class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {\n\tprotected equalityFieldChecks = {\n\t\t...baseEqualityFieldChecks,\n\t\teditorState: this.areEditorStatesEqual,\n\t};\n\n\tpublic constructor(\n\t\tdoc: Y.Doc,\n\t\tprivate kind: string,\n\t\tprivate name: string,\n\t\tprivate postId: number\n\t) {\n\t\tsuper( doc );\n\t}\n\n\tprotected onSetUp(): void {\n\t\tsuper.onSetUp();\n\n\t\tthis.subscribeToCollaboratorSelectionChanges();\n\t}\n\n\t/**\n\t * Subscribe to collaborator selection changes and update the selection state.\n\t */\n\tprivate subscribeToCollaboratorSelectionChanges(): void {\n\t\tconst {\n\t\t\tgetSelectionStart,\n\t\t\tgetSelectionEnd,\n\t\t\tgetSelectedBlocksInitialCaretPosition,\n\t\t} = select( blockEditorStore );\n\n\t\t// Keep track of the current selection in the outer scope so we can compare\n\t\t// in the subscription.\n\t\tlet selectionStart = getSelectionStart();\n\t\tlet selectionEnd = getSelectionEnd();\n\t\tlet localCursorTimeout: NodeJS.Timeout | null = null;\n\n\t\tsubscribe( () => {\n\t\t\tconst newSelectionStart = getSelectionStart();\n\t\t\tconst newSelectionEnd = getSelectionEnd();\n\n\t\t\tif (\n\t\t\t\tnewSelectionStart === selectionStart &&\n\t\t\t\tnewSelectionEnd === selectionEnd\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tselectionStart = newSelectionStart;\n\t\t\tselectionEnd = newSelectionEnd;\n\n\t\t\t// Typically selection position is only persisted after typing in a block, which\n\t\t\t// can cause selection position to be reset by other users making block updates.\n\t\t\t// Ensure we update the controlled selection right away, persisting our cursor position locally.\n\t\t\tconst initialPosition = getSelectedBlocksInitialCaretPosition();\n\t\t\tvoid this.updateSelectionInEntityRecord(\n\t\t\t\tselectionStart,\n\t\t\t\tselectionEnd,\n\t\t\t\tinitialPosition\n\t\t\t);\n\n\t\t\t// We receive two selection changes in quick succession\n\t\t\t// from local selection events:\n\t\t\t// { clientId: \"123...\", attributeKey: \"content\", offset: undefined }\n\t\t\t// { clientId: \"123...\", attributeKey: \"content\", offset: 554 }\n\t\t\t// Add a short debounce to avoid sending the first selection change.\n\t\t\tif ( localCursorTimeout ) {\n\t\t\t\tclearTimeout( localCursorTimeout );\n\t\t\t}\n\n\t\t\tlocalCursorTimeout = setTimeout( () => {\n\t\t\t\tconst selectionState = getSelectionState(\n\t\t\t\t\tselectionStart,\n\t\t\t\t\tselectionEnd,\n\t\t\t\t\tthis.doc\n\t\t\t\t);\n\n\t\t\t\tthis.setThrottledLocalStateField(\n\t\t\t\t\t'editorState',\n\t\t\t\t\t{ selection: selectionState },\n\t\t\t\t\tAWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS\n\t\t\t\t);\n\t\t\t}, LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS );\n\t\t} );\n\t}\n\n\t/**\n\t * Update the entity record with the current collaborator's selection.\n\t *\n\t * @param selectionStart - The start position of the selection.\n\t * @param selectionEnd - The end position of the selection.\n\t * @param initialPosition - The initial position of the selection.\n\t */\n\tprivate async updateSelectionInEntityRecord(\n\t\tselectionStart: WPBlockSelection,\n\t\tselectionEnd: WPBlockSelection,\n\t\tinitialPosition: number | null\n\t): Promise< void > {\n\t\t// Send an entityRecord `selection` update if we have a selection.\n\t\t//\n\t\t// Normally WordPress updates the `selection` property of the post when changes are made to blocks.\n\t\t// In a multi-user setup, block changes can occur from other users. When an entity is updated from another\n\t\t// user's changes, useBlockSync() in Gutenberg will reset the user's selection to the last saved selection.\n\t\t//\n\t\t// Manually adding an edit for each movement ensures that other user's changes to the document will\n\t\t// not cause the local user's selection to reset to the last local change location.\n\t\tconst edits = {\n\t\t\tselection: { selectionStart, selectionEnd, initialPosition },\n\t\t};\n\n\t\tconst options = {\n\t\t\tundoIgnore: true,\n\t\t};\n\n\t\t// @ts-ignore Types are not provided when using store name instead of store instance.\n\t\tdispatch( coreStore ).editEntityRecord(\n\t\t\tthis.kind,\n\t\t\tthis.name,\n\t\t\tthis.postId,\n\t\t\tedits,\n\t\t\toptions\n\t\t);\n\t}\n\n\t/**\n\t * Check if two editor states are equal.\n\t *\n\t * @param state1 - The first editor state.\n\t * @param state2 - The second editor state.\n\t * @return True if the editor states are equal, false otherwise.\n\t */\n\tprivate areEditorStatesEqual(\n\t\tstate1?: EditorState,\n\t\tstate2?: EditorState\n\t): boolean {\n\t\tif ( ! state1 || ! state2 ) {\n\t\t\treturn state1 === state2;\n\t\t}\n\n\t\treturn areSelectionsStatesEqual( state1.selection, state2.selection );\n\t}\n\n\t/**\n\t * Get the absolute position index from a selection cursor.\n\t *\n\t * @param selection - The selection cursor.\n\t * @return The absolute position index, or null if not found.\n\t */\n\tpublic getAbsolutePositionIndex(\n\t\tselection: SelectionCursor\n\t): number | null {\n\t\treturn (\n\t\t\tY.createAbsolutePositionFromRelativePosition(\n\t\t\t\tselection.cursorPosition.relativePosition,\n\t\t\t\tthis.doc\n\t\t\t)?.index ?? null\n\t\t);\n\t}\n\n\t/**\n\t * Type guard to check if a struct is a Y.Item (not Y.GC)\n\t * @param struct - The struct to check.\n\t * @return True if the struct is a Y.Item, false otherwise.\n\t */\n\tprivate isYItem( struct: Y.Item | Y.GC ): struct is Y.Item {\n\t\treturn 'content' in struct;\n\t}\n\n\t/**\n\t * Get data for debugging, using the awareness state.\n\t *\n\t * @return {YDocDebugData} The debug data.\n\t */\n\tpublic getDebugData(): YDocDebugData {\n\t\tconst ydoc = this.doc;\n\n\t\t// Manually extract doc data to avoid deprecated toJSON method\n\t\tconst docData: Record< string, unknown > = Object.fromEntries(\n\t\t\tArray.from( ydoc.share, ( [ key, value ] ) => [\n\t\t\t\tkey,\n\t\t\t\tvalue.toJSON(),\n\t\t\t] )\n\t\t);\n\n\t\t// Build collaboratorMap from awareness store (all collaborators seen this session)\n\t\tconst collaboratorMapData = new Map< string, DebugCollaboratorData >(\n\t\t\tArray.from( this.getSeenStates().entries() ).map(\n\t\t\t\t( [ clientId, collaboratorState ] ) => [\n\t\t\t\t\tString( clientId ),\n\t\t\t\t\t{\n\t\t\t\t\t\tname: collaboratorState.collaboratorInfo.name,\n\t\t\t\t\t\twpUserId: collaboratorState.collaboratorInfo.id,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t)\n\t\t);\n\n\t\t// Serialize Yjs client items to avoid deep nesting\n\t\tconst serializableClientItems: Record<\n\t\t\tnumber,\n\t\t\tArray< SerializableYItem >\n\t\t> = {};\n\n\t\tydoc.store.clients.forEach( ( structs, clientId ) => {\n\t\t\t// Filter for Y.Item only (skip Y.GC garbage collection structs)\n\t\t\tconst items = structs.filter( this.isYItem );\n\n\t\t\tserializableClientItems[ clientId ] = items.map( ( item ) => {\n\t\t\t\tconst { left, right, ...rest } = item;\n\n\t\t\t\treturn {\n\t\t\t\t\t...rest,\n\t\t\t\t\tleft: left\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tid: left.id,\n\t\t\t\t\t\t\t\tlength: left.length,\n\t\t\t\t\t\t\t\torigin: left.origin,\n\t\t\t\t\t\t\t\tcontent: left.content,\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: null,\n\t\t\t\t\tright: right\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tid: right.id,\n\t\t\t\t\t\t\t\tlength: right.length,\n\t\t\t\t\t\t\t\torigin: right.origin,\n\t\t\t\t\t\t\t\tcontent: right.content,\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: null,\n\t\t\t\t};\n\t\t\t} );\n\t\t} );\n\n\t\treturn {\n\t\t\tdoc: docData,\n\t\t\tclients: serializableClientItems,\n\t\t\tcollaboratorMap: Object.fromEntries( collaboratorMapData ),\n\t\t};\n\t}\n}\n"], "mappings": "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", "names": ["blockEditorStore", "coreStore"] }