UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

149 lines (146 loc) 5.71 kB
import { createToggleBlockMarkOnRangeNext } from '@atlaskit/editor-common/commands'; import { isListNode } from '@atlaskit/editor-common/utils'; import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model'; import { Selection } from '@atlaskit/editor-prosemirror/state'; import { ReplaceStep } from '@atlaskit/editor-prosemirror/transform'; import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isCursorSelectionAtTextStartOrEnd, isEmptyNode, isSelectionInsidePanel } from '../index'; import { insertSliceAtNodeEdge, insertSliceInsideOfPanelNodeSelected, insertSliceIntoEmptyNode, insertSliceIntoRangeSelectionInsideList } from './lists'; export function insertSliceForLists({ tr, slice, schema }) { var _slice$content$firstC; const { selection, selection: { $to, $from } } = tr; const { $cursor } = selection; const panelNode = isSelectionInsidePanel(selection); const selectionIsInsideList = $from.blockRange($to, isListNode); if (!$cursor && selectionIsInsideList) { return insertSliceIntoRangeSelectionInsideList({ tr, slice }); } // if inside an empty panel, try and insert content inside it rather than replace it if (panelNode && isEmptyNode(panelNode) && $from.node() === $to.node()) { return insertSliceInsideOfPanelNodeSelected(panelNode)({ tr, slice, schema }); } if (!$cursor || selectionIsInsideList) { return tr.replaceSelection(slice); } if (isEmptyNode(tr.doc.resolve($cursor.pos).node())) { return insertSliceIntoEmptyNode({ tr, slice }); } // When pasting a single list item into an action or decision, we skip the special "insert at node edge" // logic so that prosemirror pastes the list's content into the action/decision, rather than // pasting a whole list node directly after the action/decision item. (But we still preserve the // existing "insert at" node edge" behaviour if dealing with a list with more than one item, so that // it still inserts whole list node after the action/decision item). const pastingIntoActionOrDecision = Boolean(findParentNodeOfType([schema.nodes.taskList, schema.nodes.decisionList])(selection)); const oneListItem = slice.content.childCount === 1 && isListNode(slice.content.firstChild) && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.childCount) === 1; if (!(pastingIntoActionOrDecision && oneListItem) && isCursorSelectionAtTextStartOrEnd(selection)) { return insertSliceAtNodeEdge({ tr, slice }); } tr.replaceSelection(slice); } const stripFontSizeInsideBlockquoteLists = (tr, blockquotePos) => { const { marks: { fontSize }, nodes: { paragraph, listItem, taskItem, blockTaskItem } } = tr.doc.type.schema; const listItemParentNodeTypes = [listItem, taskItem, blockTaskItem]; if (!fontSize || !expValEquals('platform_editor_small_font_size', 'isEnabled', true)) { return; } const containingBlockquote = tr.doc.nodeAt(blockquotePos); if (!containingBlockquote) { return; } const from = blockquotePos + 1; const to = blockquotePos + containingBlockquote.nodeSize - 1; createToggleBlockMarkOnRangeNext(fontSize, () => false, (_schema, node, parent) => node.type === paragraph && !!parent && listItemParentNodeTypes.some(nodeType => nodeType === parent.type))(from, to, tr); }; export function insertSliceInsideBlockquote({ tr, slice }) { //insert blockquote explicitly and set the selection in blockquote since replaceSelection will only insert the list const { schema } = tr.doc.type; tr.replaceSelection(new Slice(Fragment.from(schema.nodes.blockquote.createAndFill()), 0, 0)); updateSelectionAfterReplace({ tr }); tr.replaceSelection(slice); if (expValEquals('platform_editor_small_font_size', 'isEnabled', true)) { const insertedBlockquotePos = findParentNodeOfType(schema.nodes.blockquote)(tr.selection); if (!insertedBlockquotePos) { return; } stripFontSizeInsideBlockquoteLists(tr, insertedBlockquotePos.pos); } } export function updateSelectionAfterReplace({ tr }) { // ProseMirror doesn't give a proper way to tell us where something was inserted. // However, we can know "how" it inserted something. // // So, instead of weird depth calculations, we can use the step produced by the transform. // For instance: // The `replaceStep.to and replaceStep.from`, tell us the real position // where the content will be insert. // Then, we can use the `tr.mapping.map` to the updated position after the replace operation const replaceStep = tr.steps[0]; if (!(replaceStep instanceof ReplaceStep)) { return tr; } const nextPosition = tr.mapping.map(replaceStep.to); // The findFrom will make search for both: TextSelection and NodeSelections. const nextSelection = Selection.findFrom(tr.doc.resolve(Math.min(nextPosition, tr.doc.content.size)), -1); if (nextSelection) { tr.setSelection(nextSelection); } } export function insertSliceForTaskInsideList({ tr, slice }) { const { schema } = tr.doc.type; //To avoid the list being replaced with the tasklist, enclose the slice within a taskItem. const selectionBeforeReplace = tr.selection.from; tr.replaceSelection(new Slice(Fragment.from(schema.nodes.taskItem.createAndFill()), 0, 0)); const nextSelection = Selection.near(tr.doc.resolve(selectionBeforeReplace + 1)); tr.setSelection(nextSelection); tr.replaceSelection(slice); }