UNPKG

@wordpress/core-data

Version:
8 lines (7 loc) 11 kB
{ "version": 3, "sources": ["../../src/utils/crdt-selection.ts"], "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { dispatch, select } from '@wordpress/data';\n// @ts-expect-error No exported types.\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n// @ts-expect-error No exported types.\nimport { isUnmodifiedBlock } from '@wordpress/blocks';\nimport { type CRDTDoc, Y } from '@wordpress/sync';\n\n/**\n * Internal dependencies\n */\nimport {\n\tcreateBlockSelectionHistory,\n\tYSelectionType,\n\ttype BlockSelectionHistory,\n\ttype YFullSelection,\n\ttype YSelection,\n} from './block-selection-history';\nimport { findBlockByClientIdInDoc } from './crdt-utils';\nimport type { WPBlockSelection, WPSelection } from '../types';\n\n// WeakMap to store BlockSelectionHistory instances per Y.Doc\nconst selectionHistoryMap = new WeakMap< CRDTDoc, BlockSelectionHistory >();\n\n/**\n * Get or create a BlockSelectionHistory instance for a given Y.Doc.\n *\n * @param ydoc The Y.Doc to get the selection history for\n * @return The BlockSelectionHistory instance\n */\nfunction getBlockSelectionHistory( ydoc: CRDTDoc ): BlockSelectionHistory {\n\tlet history = selectionHistoryMap.get( ydoc );\n\n\tif ( ! history ) {\n\t\thistory = createBlockSelectionHistory( ydoc );\n\t\tselectionHistoryMap.set( ydoc, history );\n\t}\n\n\treturn history;\n}\n\nexport function getSelectionHistory( ydoc: CRDTDoc ): YFullSelection[] {\n\treturn getBlockSelectionHistory( ydoc ).getSelectionHistory();\n}\n\nexport function updateSelectionHistory(\n\tydoc: CRDTDoc,\n\twpSelection: WPSelection\n): void {\n\treturn getBlockSelectionHistory( ydoc ).updateSelection( wpSelection );\n}\n\n/**\n * Convert a YSelection to a WPBlockSelection.\n * @param ySelection The YSelection (relative) to convert\n * @param ydoc The Y.Doc to convert the selection to a block selection for\n * @return The converted WPBlockSelection, or null if the conversion fails\n */\nfunction convertYSelectionToBlockSelection(\n\tySelection: YSelection,\n\tydoc: Y.Doc\n): WPBlockSelection | null {\n\tif ( ySelection.type === YSelectionType.RelativeSelection ) {\n\t\tconst { relativePosition, attributeKey, clientId } = ySelection;\n\n\t\tconst absolutePosition = Y.createAbsolutePositionFromRelativePosition(\n\t\t\trelativePosition,\n\t\t\tydoc\n\t\t);\n\n\t\tif ( absolutePosition ) {\n\t\t\treturn {\n\t\t\t\tclientId,\n\t\t\t\tattributeKey,\n\t\t\t\toffset: absolutePosition.index,\n\t\t\t};\n\t\t}\n\t} else if ( ySelection.type === YSelectionType.BlockSelection ) {\n\t\treturn {\n\t\t\tclientId: ySelection.clientId,\n\t\t\tattributeKey: undefined,\n\t\t\toffset: undefined,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Convert a YFullSelection to a WPSelection by resolving relative positions\n * and verifying the blocks exist in the document.\n * @param yFullSelection The YFullSelection to convert\n * @param ydoc The Y.Doc to resolve positions against\n * @return The converted WPSelection, or null if the conversion fails\n */\nfunction convertYFullSelectionToWPSelection(\n\tyFullSelection: YFullSelection,\n\tydoc: Y.Doc\n): WPSelection | null {\n\tconst { start, end } = yFullSelection;\n\tconst startBlock = findBlockByClientIdInDoc( start.clientId, ydoc );\n\tconst endBlock = findBlockByClientIdInDoc( end.clientId, ydoc );\n\n\tif ( ! startBlock || ! endBlock ) {\n\t\treturn null;\n\t}\n\n\tconst startBlockSelection = convertYSelectionToBlockSelection(\n\t\tstart,\n\t\tydoc\n\t);\n\tconst endBlockSelection = convertYSelectionToBlockSelection( end, ydoc );\n\n\tif ( startBlockSelection === null || endBlockSelection === null ) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tselectionStart: startBlockSelection,\n\t\tselectionEnd: endBlockSelection,\n\t};\n}\n\n/**\n * Given a Y.Doc and a selection history, find the most recent selection\n * that exists in the document. Skip any selections that are not in the document.\n * @param ydoc The Y.Doc to find the selection in\n * @param selectionHistory The selection history to check\n * @return The most recent selection that exists in the document, or null if no selection exists.\n */\nfunction findSelectionFromHistory(\n\tydoc: Y.Doc,\n\tselectionHistory: YFullSelection[]\n): WPSelection | null {\n\tfor ( const positionToTry of selectionHistory ) {\n\t\tconst result = convertYFullSelectionToWPSelection(\n\t\t\tpositionToTry,\n\t\t\tydoc\n\t\t);\n\t\tif ( result !== null ) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Restore the selection to the most recent selection in history that is\n * available in the document.\n * @param selectionHistory The selection history to restore\n * @param ydoc The Y.Doc where blocks are stored\n */\nexport function restoreSelection(\n\tselectionHistory: YFullSelection[],\n\tydoc: Y.Doc\n): void {\n\t// Find the most recent selection in history that is available in\n\t// the document.\n\tconst selectionToRestore = findSelectionFromHistory(\n\t\tydoc,\n\t\tselectionHistory\n\t);\n\n\tif ( selectionToRestore === null ) {\n\t\t// Case 1: No blocks in history are available for restoration.\n\t\t// Do nothing.\n\t\treturn;\n\t}\n\n\tconst { getBlock } = select( blockEditorStore );\n\tconst { resetSelection } = dispatch( blockEditorStore );\n\tconst { selectionStart, selectionEnd } = selectionToRestore;\n\tconst isSelectionInSameBlock =\n\t\tselectionStart.clientId === selectionEnd.clientId;\n\n\tif ( isSelectionInSameBlock ) {\n\t\t// Case 2: After content is restored, the selection is available\n\t\t// within the same block\n\n\t\tconst block = getBlock( selectionStart.clientId );\n\t\tconst isBlockEmpty = block && isUnmodifiedBlock( block );\n\t\tconst isBeginningOfEmptyBlock =\n\t\t\t0 === selectionStart.offset &&\n\t\t\t0 === selectionEnd.offset &&\n\t\t\tisBlockEmpty &&\n\t\t\t! selectionStart.attributeKey &&\n\t\t\t! selectionEnd.attributeKey;\n\n\t\tif ( isBeginningOfEmptyBlock ) {\n\t\t\t// Case 2a: When the content in a block has been removed after an\n\t\t\t// undo, WordPress will set the selection to the block's client ID\n\t\t\t// with an undefined startOffset and endOffset.\n\t\t\t//\n\t\t\t// To match the default behavior and tests, exclude the selection\n\t\t\t// offset when resetting to position 0.\n\t\t\tconst selectionStartWithoutOffset = {\n\t\t\t\tclientId: selectionStart.clientId,\n\t\t\t};\n\t\t\tconst selectionEndWithoutOffset = {\n\t\t\t\tclientId: selectionEnd.clientId,\n\t\t\t};\n\n\t\t\tresetSelection(\n\t\t\t\tselectionStartWithoutOffset,\n\t\t\t\tselectionEndWithoutOffset,\n\t\t\t\t0\n\t\t\t);\n\t\t} else {\n\t\t\t// Case 2b: Otherwise, reset including the saved selection offset.\n\t\t\tresetSelection( selectionStart, selectionEnd, 0 );\n\t\t}\n\t} else {\n\t\t// Case 3: A multi-block selection was made. resetSelection() can only\n\t\t// restore selections within the same block.\n\t\t// When a multi-block selection is made, selectionEnd represents\n\t\t// where the user's cursor ended.\n\t\tresetSelection( selectionEnd, selectionEnd, 0 );\n\t}\n}\n\n/**\n * If the latest selection has been shifted by remote edits, resolve and return\n * it as a WPSelection. Returns null when the history is empty or neither\n * endpoint has moved.\n *\n * @param ydoc The Y.Doc to resolve positions against\n * @param selectionHistory The selection history to check\n * @return The shifted WPSelection, or null if nothing moved.\n */\nexport function getShiftedSelection(\n\tydoc: Y.Doc,\n\tselectionHistory: YFullSelection[]\n): WPSelection | null {\n\tif ( selectionHistory.length === 0 ) {\n\t\treturn null;\n\t}\n\n\tconst { start, end } = selectionHistory[ 0 ];\n\n\t// Block-level selections have no offset that can shift.\n\tif (\n\t\tstart.type === YSelectionType.BlockSelection ||\n\t\tend.type === YSelectionType.BlockSelection\n\t) {\n\t\treturn null;\n\t}\n\n\tconst selectionStart = convertYSelectionToBlockSelection( start, ydoc );\n\tconst selectionEnd = convertYSelectionToBlockSelection( end, ydoc );\n\n\tif ( ! selectionStart || ! selectionEnd ) {\n\t\treturn null;\n\t}\n\n\t// Only dispatch if at least one endpoint actually moved.\n\tconst startShifted = selectionStart.offset !== start.offset;\n\tconst endShifted = selectionEnd.offset !== end.offset;\n\n\tif ( ! startShifted && ! endShifted ) {\n\t\treturn null;\n\t}\n\n\treturn { selectionStart, selectionEnd };\n}\n"], "mappings": "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", "names": ["blockEditorStore"] }