UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

228 lines (223 loc) 10.3 kB
import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection'; import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check'; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { selectTableClosestToPos } from '@atlaskit/editor-tables/utils'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; export const getInlineNodePos = (doc, start, nodeSize) => { const $startPos = doc.resolve(start); // To trigger the annotation floating toolbar for non-selectable node, we need to select inline nodes // Find the first inline node in the node let inlineNodePos = start; let foundInlineNode = false; let inlineNodeEndPos = 0; doc.nodesBetween($startPos.pos, $startPos.pos + nodeSize, (n, pos) => { if (n.isInline) { inlineNodeEndPos = pos + n.nodeSize; } if (n.isInline && !foundInlineNode) { inlineNodePos = pos; foundInlineNode = true; } return true; }); return { inlineNodePos, inlineNodeEndPos }; }; export const isNodeWithCodeBlock = (tr, start, nodeSize) => { const $startPos = tr.doc.resolve(start); let hasCodeBlock = false; tr.doc.nodesBetween($startPos.pos, $startPos.pos + nodeSize, n => { if (['codeBlock'].includes(n.type.name)) { hasCodeBlock = true; } }); return hasCodeBlock; }; const isNodeWithMediaOrExtension = (doc, start, nodeSize) => { const $startPos = doc.resolve(start); let hasMediaOrExtension = false; doc.nodesBetween($startPos.pos, $startPos.pos + nodeSize, n => { if (['media', 'extension'].includes(n.type.name)) { hasMediaOrExtension = true; } }); return hasMediaOrExtension; }; const oldGetSelection = (tr, start) => { const node = tr.doc.nodeAt(start); const isNodeSelection = node && NodeSelection.isSelectable(node); const nodeSize = node ? node.nodeSize : 1; const $startPos = tr.doc.resolve(start); const nodeName = node === null || node === void 0 ? void 0 : node.type.name; const isBlockQuoteWithMediaOrExtension = nodeName === 'blockquote' && isNodeWithMediaOrExtension(tr.doc, start, nodeSize); const isListWithMediaOrExtension = nodeName === 'bulletList' && isNodeWithMediaOrExtension(tr.doc, start, nodeSize) || nodeName === 'orderedList' && isNodeWithMediaOrExtension(tr.doc, start, nodeSize); if (isNodeSelection && nodeName !== 'blockquote' || isListWithMediaOrExtension || isBlockQuoteWithMediaOrExtension || // decisionList/layoutColumn node is not selectable, but we want to select the whole node not just text ['decisionList', 'layoutColumn'].includes(nodeName || '')) { return new NodeSelection($startPos); } else if (nodeName === 'mediaGroup' && (node === null || node === void 0 ? void 0 : node.childCount) === 1) { const $mediaStartPos = tr.doc.resolve(start + 1); return new NodeSelection($mediaStartPos); } else if ( // Even though mediaGroup is not selectable, // we need a quick way to make all child media nodes appear as selected without the need for a custom selection nodeName === 'mediaGroup') { return new NodeSelection($startPos); } else if (nodeName === 'taskList') { return TextSelection.create(tr.doc, start, start + nodeSize); } else { const { inlineNodePos, inlineNodeEndPos } = getInlineNodePos(tr.doc, start, nodeSize); return new TextSelection(tr.doc.resolve(inlineNodePos), tr.doc.resolve(inlineNodeEndPos)); } }; /** * Gets the appropriate selection for the node at the given start position. * * @param doc The ProseMirror document. * @param selectionEmpty Indicates if the current selection is empty. * @param start The start position of the node. * @returns The appropriate selection for the node. */ export const newGetSelection = (doc, selectionEmpty, start) => { const node = doc.nodeAt(start); const isNodeSelection = node && NodeSelection.isSelectable(node); const nodeSize = node ? node.nodeSize : 1; const nodeName = node === null || node === void 0 ? void 0 : node.type.name; if (editorExperiment('platform_editor_block_menu', true)) { // if mediaGroup only has a single child, we want to select the child if (nodeName === 'mediaGroup' && (node === null || node === void 0 ? void 0 : node.childCount) === 1) { const $mediaStartPos = doc.resolve(start + 1); return new NodeSelection($mediaStartPos); } return new NodeSelection(doc.resolve(start)); } // this is a fix for empty paragraph selection - put first to avoid any extra work if (nodeName === 'paragraph' && selectionEmpty && (node === null || node === void 0 ? void 0 : node.childCount) === 0) { return false; } const isBlockQuoteWithMediaOrExtension = nodeName === 'blockquote' && isNodeWithMediaOrExtension(doc, start, nodeSize); const isListWithMediaOrExtension = nodeName === 'bulletList' && isNodeWithMediaOrExtension(doc, start, nodeSize) || nodeName === 'orderedList' && isNodeWithMediaOrExtension(doc, start, nodeSize); if (isNodeSelection && nodeName !== 'blockquote' || isListWithMediaOrExtension || isBlockQuoteWithMediaOrExtension || // decisionList/layoutColumn node is not selectable, but we want to select the whole node not just text ['decisionList', 'layoutColumn'].includes(nodeName || '') || nodeName === 'mediaGroup' && typeof (node === null || node === void 0 ? void 0 : node.childCount) === 'number' && (node === null || node === void 0 ? void 0 : node.childCount) > 1) { return new NodeSelection(doc.resolve(start)); } // if mediaGroup only has a single child, we want to select the child if (nodeName === 'mediaGroup') { const $mediaStartPos = doc.resolve(start + 1); return new NodeSelection($mediaStartPos); } if (nodeName === 'taskList') { return TextSelection.create(doc, start, start + nodeSize); } const { inlineNodePos, inlineNodeEndPos } = getInlineNodePos(doc, start, nodeSize); return new TextSelection(doc.resolve(inlineNodePos), doc.resolve(inlineNodeEndPos)); }; export const getSelection = (tr, start, api) => { if (areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar)) || editorExperiment('platform_editor_block_menu', true)) { return newGetSelection(tr.doc, tr.selection.empty, start); } return oldGetSelection(tr, start); }; export const selectNode = (tr, start, nodeType, api) => { // For table, we need to do cell selection instead of node selection if (nodeType === 'table') { tr = selectTableClosestToPos(tr, tr.doc.resolve(start + 1)); return tr; } const selection = getSelection(tr, start, api); if (selection) { tr.setSelection(selection); } return tr; }; export const setCursorPositionAtMovedNode = (tr, start, api) => { const node = tr.doc.nodeAt(start); const isNodeSelection = node && NodeSelection.isSelectable(node); const nodeSize = node ? node.nodeSize : 1; let selection; // decisionList node is not selectable, but we want to select the whole node not just text // blockQuote is selectable, but we want to set cursor at the inline end Pos instead of the gap cursor as this causes jittering post drop if (isNodeSelection && node.type.name !== 'blockquote' || (node === null || node === void 0 ? void 0 : node.type.name) === 'decisionList') { selection = new GapCursorSelection(tr.doc.resolve(start + node.nodeSize), Side.RIGHT); tr.setSelection(selection); return tr; } // this is a fix for empty paragraph selection - can safely use start position as the paragraph is empty if ((node === null || node === void 0 ? void 0 : node.type.name) === 'paragraph' && (node === null || node === void 0 ? void 0 : node.childCount) === 0 && areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar))) { const selection = new TextSelection(tr.doc.resolve(start)); tr.setSelection(selection); return tr; } const { inlineNodeEndPos } = getInlineNodePos(tr.doc, start, nodeSize); selection = new TextSelection(tr.doc.resolve(inlineNodeEndPos)); tr.setSelection(selection); return tr; }; /** * Checks if handle position is with the selection or corresponds to a (partially) selected node * @param state * @param selection * @param handlePos * @returns */ export const isHandleCorrelatedToSelection = (state, selection, handlePos) => { if (selection.empty) { return false; } let nodeStart; const $selectionFrom = selection.$from; nodeStart = $selectionFrom.before($selectionFrom.sharedDepth(selection.to) + 1); if (nodeStart === $selectionFrom.pos) { nodeStart = $selectionFrom.depth ? $selectionFrom.before() : $selectionFrom.pos; } const $resolvedNodePos = state.doc.resolve(nodeStart); if (['tableRow', 'tableCell', 'tableHeader'].includes($resolvedNodePos.node().type.name)) { const parentNodeFindRes = findParentNodeOfType(state.schema.nodes['table'])(selection); const tablePos = parentNodeFindRes === null || parentNodeFindRes === void 0 ? void 0 : parentNodeFindRes.pos; nodeStart = typeof tablePos === 'undefined' ? nodeStart : tablePos; } else if (['listItem'].includes($resolvedNodePos.node().type.name)) { nodeStart = $resolvedNodePos.before(rootListDepth($resolvedNodePos)); } else if (['taskList'].includes($resolvedNodePos.node().type.name)) { const listdepth = rootTaskListDepth($resolvedNodePos); nodeStart = $resolvedNodePos.before(listdepth); } else if (['blockquote'].includes($resolvedNodePos.node().type.name)) { nodeStart = $resolvedNodePos.before(); } return Boolean(handlePos < selection.$to.pos && handlePos >= nodeStart); }; export const rootListDepth = itemPos => { let depth; for (let i = itemPos.depth; i > 1; i -= 2) { const node = itemPos.node(i); if (node.type.name === 'listItem') { depth = i - 1; } else { break; } } return depth; }; export const rootTaskListDepth = taskListPos => { let depth; for (let i = taskListPos.depth; i > 0; i--) { const node = taskListPos.node(i); if (node.type.name === 'taskList' || node.type.name === 'taskItem') { depth = i; } else { break; } } return depth; };