UNPKG

@wordpress/block-editor

Version:
238 lines (227 loc) 8.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useSelectionObserver; var _data = require("@wordpress/data"); var _compose = require("@wordpress/compose"); var _richText = require("@wordpress/rich-text"); var _dom = require("@wordpress/dom"); var _store = require("../../store"); var _dom2 = require("../../utils/dom"); /** * WordPress dependencies */ /** * Internal dependencies */ /** * Extract the selection start node from the selection. When the anchor node is * not a text node, the selection offset is the index of a child node. * * @param {Selection} selection The selection. * * @return {Element} The selection start node. */ function extractSelectionStartNode(selection) { const { anchorNode, anchorOffset } = selection; if (anchorNode.nodeType === anchorNode.TEXT_NODE) { return anchorNode; } if (anchorOffset === 0) { return anchorNode; } return anchorNode.childNodes[anchorOffset - 1]; } /** * Extract the selection end node from the selection. When the focus node is not * a text node, the selection offset is the index of a child node. The selection * reaches up to but excluding that child node. * * @param {Selection} selection The selection. * * @return {Element} The selection start node. */ function extractSelectionEndNode(selection) { const { focusNode, focusOffset } = selection; if (focusNode.nodeType === focusNode.TEXT_NODE) { return focusNode; } if (focusOffset === focusNode.childNodes.length) { return focusNode; } // When the selection is forward (the selection ends with the focus node), // the selection may extend into the next element with an offset of 0. This // may trigger multi selection even though the selection does not visually // end in the next block. if (focusOffset === 0 && (0, _dom.isSelectionForward)(selection)) { var _focusNode$previousSi; return (_focusNode$previousSi = focusNode.previousSibling) !== null && _focusNode$previousSi !== void 0 ? _focusNode$previousSi : focusNode.parentElement; } return focusNode.childNodes[focusOffset]; } function findDepth(a, b) { let depth = 0; while (a[depth] === b[depth]) { depth++; } return depth; } /** * Sets the `contenteditable` wrapper element to `value`. * * @param {HTMLElement} node Block element. * @param {boolean} value `contentEditable` value (true or false) */ function setContentEditableWrapper(node, value) { // Since we are calling this on every selection change, check if the value // needs to be updated first because it trigger the browser to recalculate // style. if (node.contentEditable !== String(value)) { node.contentEditable = value; // Firefox doesn't automatically move focus. if (value) { node.focus(); } } } function getRichTextElement(node) { const element = node.nodeType === node.ELEMENT_NODE ? node : node.parentElement; return element?.closest('[data-wp-block-attribute-key]'); } /** * Sets a multi-selection based on the native selection across blocks. */ function useSelectionObserver() { const { multiSelect, selectBlock, selectionChange } = (0, _data.useDispatch)(_store.store); const { getBlockParents, getBlockSelectionStart, isMultiSelecting } = (0, _data.useSelect)(_store.store); return (0, _compose.useRefEffect)(node => { const { ownerDocument } = node; const { defaultView } = ownerDocument; function onSelectionChange(event) { const selection = defaultView.getSelection(); if (!selection.rangeCount) { return; } const startNode = extractSelectionStartNode(selection); const endNode = extractSelectionEndNode(selection); if (!node.contains(startNode) || !node.contains(endNode)) { return; } // If selection is collapsed and we haven't used `shift+click`, // end multi selection and disable the contentEditable wrapper. // We have to check about `shift+click` case because elements // that don't support text selection might be involved, and we might // update the clientIds to multi-select blocks. // For now we check if the event is a `mouse` event. const isClickShift = event.shiftKey && event.type === 'mouseup'; if (selection.isCollapsed && !isClickShift) { if (node.contentEditable === 'true' && !isMultiSelecting()) { setContentEditableWrapper(node, false); let element = startNode.nodeType === startNode.ELEMENT_NODE ? startNode : startNode.parentElement; element = element?.closest('[contenteditable]'); element?.focus(); } return; } let startClientId = (0, _dom2.getBlockClientId)(startNode); let endClientId = (0, _dom2.getBlockClientId)(endNode); // If the selection has changed and we had pressed `shift+click`, // we need to check if in an element that doesn't support // text selection has been clicked. if (isClickShift) { const selectedClientId = getBlockSelectionStart(); const clickedClientId = (0, _dom2.getBlockClientId)(event.target); // `endClientId` is not defined if we end the selection by clicking a non-selectable block. // We need to check if there was already a selection with a non-selectable focusNode. const focusNodeIsNonSelectable = clickedClientId !== endClientId; if (startClientId === endClientId && selection.isCollapsed || !endClientId || focusNodeIsNonSelectable) { endClientId = clickedClientId; } // Handle the case when we have a non-selectable block // selected and click another one. if (startClientId !== selectedClientId) { startClientId = selectedClientId; } } // If the selection did not involve a block, return. if (startClientId === undefined && endClientId === undefined) { setContentEditableWrapper(node, false); return; } const isSingularSelection = startClientId === endClientId; if (isSingularSelection) { if (!isMultiSelecting()) { selectBlock(startClientId); } else { multiSelect(startClientId, startClientId); } } else { const startPath = [...getBlockParents(startClientId), startClientId]; const endPath = [...getBlockParents(endClientId), endClientId]; const depth = findDepth(startPath, endPath); if (startPath[depth] !== startClientId || endPath[depth] !== endClientId) { multiSelect(startPath[depth], endPath[depth]); return; } const richTextElementStart = getRichTextElement(startNode); const richTextElementEnd = getRichTextElement(endNode); if (richTextElementStart && richTextElementEnd) { var _richTextDataStart$st, _richTextDataEnd$star; const range = selection.getRangeAt(0); const richTextDataStart = (0, _richText.create)({ element: richTextElementStart, range, __unstableIsEditableTree: true }); const richTextDataEnd = (0, _richText.create)({ element: richTextElementEnd, range, __unstableIsEditableTree: true }); const startOffset = (_richTextDataStart$st = richTextDataStart.start) !== null && _richTextDataStart$st !== void 0 ? _richTextDataStart$st : richTextDataStart.end; const endOffset = (_richTextDataEnd$star = richTextDataEnd.start) !== null && _richTextDataEnd$star !== void 0 ? _richTextDataEnd$star : richTextDataEnd.end; selectionChange({ start: { clientId: startClientId, attributeKey: richTextElementStart.dataset.wpBlockAttributeKey, offset: startOffset }, end: { clientId: endClientId, attributeKey: richTextElementEnd.dataset.wpBlockAttributeKey, offset: endOffset } }); } else { multiSelect(startClientId, endClientId); } } } ownerDocument.addEventListener('selectionchange', onSelectionChange); defaultView.addEventListener('mouseup', onSelectionChange); return () => { ownerDocument.removeEventListener('selectionchange', onSelectionChange); defaultView.removeEventListener('mouseup', onSelectionChange); }; }, [multiSelect, selectBlock, selectionChange, getBlockParents]); } //# sourceMappingURL=use-selection-observer.js.map