UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

338 lines (316 loc) • 14.5 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import { Fragment, Node, Slice } from '@atlaskit/editor-prosemirror/model'; import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform'; import { canInsert, findParentNodeOfType, hasParentNodeOfType, isNodeSelection, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils'; import { GapCursorSelection, Side } from '../selection'; import { isEmptyParagraph, isListItemNode } from '../utils'; export var LookDirection = /*#__PURE__*/function (LookDirection) { LookDirection["Before"] = "before"; LookDirection["After"] = "after"; return LookDirection; }({}); export var normaliseNestedLayout = function normaliseNestedLayout(_ref, node) { var selection = _ref.selection, doc = _ref.doc; if (selection.$from.depth > 1) { if (node.attrs.layout && node.attrs.layout !== 'default') { return node.type.createChecked(_objectSpread(_objectSpread({}, node.attrs), {}, { layout: 'default' }), node.content, node.marks); } // If its a breakout layout, we can remove the mark // Since default isn't a valid breakout mode. var breakoutMark = doc.type.schema.marks.breakout; if (breakoutMark && breakoutMark.isInSet(node.marks)) { var newMarks = breakoutMark.removeFromSet(node.marks); return node.type.createChecked(node.attrs, node.content, newMarks); } } return node; }; var isLastChild = function isLastChild($pos, doc) { return doc.resolve($pos.after()).node().lastChild === $pos.node(); }; var isFirstChild = function isFirstChild($pos, doc) { return doc.resolve($pos.before()).node().firstChild === $pos.node(); }; var nodeIsInsideAList = function nodeIsInsideAList(tr) { var nodes = tr.doc.type.schema.nodes; return hasParentNodeOfType([nodes.orderedList, nodes.bulletList])(tr.selection); }; var selectionIsInsideAPanel = function selectionIsInsideAPanel(tr) { var nodes = tr.doc.type.schema.nodes; return hasParentNodeOfType(nodes.panel)(tr.selection); }; var selectionIsInNestedList = function selectionIsInNestedList(tr) { var nodes = tr.doc.type.schema.nodes; var parentListNode = findParentNodeOfType([nodes.orderedList, nodes.bulletList])(tr.selection); if (!parentListNode) { return false; } return isListItemNode(tr.doc.resolve(parentListNode.pos).parent); }; var insertBeforeOrAfter = function insertBeforeOrAfter(tr, lookDirection, $parentPos, $proposedPosition, content // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/max-params ) { /** * This block caters for the first item in a parent with the cursor being at the very start * or the last item with the cursor being at the very end * * e.g. * ul * li {<>}Scenario one * li * li Scenario two{<>} */ if (isFirstChild($proposedPosition, tr.doc) && lookDirection === LookDirection.Before || isLastChild($proposedPosition, tr.doc) && lookDirection === LookDirection.After) { return tr.insert($parentPos[lookDirection](), content); } return tr.insert($proposedPosition[lookDirection](), content); }; // FIXME: A more sustainable and configurable way to choose when to split // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any var shouldSplit = function shouldSplit(nodeType, schemaNodes) { return [schemaNodes.bulletList, schemaNodes.orderedList, schemaNodes.panel].includes(nodeType); }; export var safeInsert = function safeInsert(content, position) { return function (tr) { var _tr$selection$$from$n; var nodes = tr.doc.type.schema.nodes; var whitelist = [nodes.rule, nodes.mediaSingle]; if (content instanceof Fragment || !whitelist.includes(content.type)) { return null; } // Check for selection if (!tr.selection.empty || isNodeSelection(tr.selection)) { // NOT IMPLEMENTED return null; } var $from = tr.selection.$from; var $insertPos = position ? tr.doc.resolve(position) : isNodeSelection(tr.selection) ? tr.doc.resolve($from.pos + 1) : $from; var lookDirection; var insertPosEnd = $insertPos.end(); var insertPosStart = $insertPos.start(); // When parent node is an empty paragraph, // check the empty paragraph is the first or last node of its parent. if (isEmptyParagraph($insertPos.parent)) { if (isLastChild($insertPos, tr.doc)) { lookDirection = LookDirection.After; } else if (isFirstChild($insertPos, tr.doc)) { lookDirection = LookDirection.Before; } } else { if ($insertPos.pos === insertPosEnd) { lookDirection = LookDirection.After; } else if ($insertPos.pos === insertPosStart) { lookDirection = LookDirection.Before; } } var grandParentNodeType = (_tr$selection$$from$n = tr.selection.$from.node(-1)) === null || _tr$selection$$from$n === void 0 ? void 0 : _tr$selection$$from$n.type; var parentNodeType = tr.selection.$from.parent.type; // if there is no direction, and cannot split for this particular node var noDirectionAndShouldNotSplit = !lookDirection && !shouldSplitSelectedNodeOnNodeInsertion({ parentNodeType: parentNodeType, grandParentNodeType: grandParentNodeType, content: content }); var ruleNodeInANestedListNode = content.type === nodes.rule && selectionIsInNestedList(tr); var nonRuleNodeInListNode = !(content.type === nodes.rule) && nodeIsInsideAList(tr); if (ruleNodeInANestedListNode || noDirectionAndShouldNotSplit && nonRuleNodeInListNode || noDirectionAndShouldNotSplit && !nodeIsInsideAList(tr)) { // node to be inserted is an invalid child of selection so insert below selected node return pmSafeInsert(content, tr.selection.from)(tr); } // if node is a rule and that is a flat list splitting and not at the end of a list var _tr$selection = tr.selection, from = _tr$selection.from, to = _tr$selection.to; var ruleTypeInAList = content.type === nodes.rule && nodeIsInsideAList(tr); if (ruleTypeInAList && !($insertPos.pos === insertPosEnd)) { return tr.replaceRange(from, to, new Slice(Fragment.from(nodes.rule.createChecked()), 0, 0)); } if (!lookDirection) { // fallback to consumer for now return null; } // Replace empty paragraph if (isEmptyParagraph($insertPos.parent) && canInsert(tr.doc.resolve($insertPos[lookDirection]()), content)) { return finaliseInsert(tr.replaceWith($insertPos.before(), $insertPos.after(), content), -1); } var $proposedPosition = $insertPos; while ($proposedPosition.depth > 0) { var $parentPos = tr.doc.resolve($proposedPosition[lookDirection]()); var parentNode = $parentPos.node(); // Insert at position (before or after target pos) if (canInsert($proposedPosition, content)) { return finaliseInsert(tr.insert($proposedPosition.pos, content), content.nodeSize); } // If we can't insert, and we think we should split, we fallback to consumer for now if (shouldSplit(parentNode.type, tr.doc.type.schema.nodes)) { var nextTr = finaliseInsert(insertBeforeOrAfter(tr, lookDirection, $parentPos, $proposedPosition, content), content.nodeSize); // Move selection to the closest text node, otherwise it defaults to the whatever the lookDirection is set to above if ([nodes.orderedList, nodes.bulletList].includes(parentNode.type) && nextTr) { return nextTr.setSelection(TextSelection.between(nextTr.selection.$from, nextTr.selection.$from)); } else { return nextTr; } } // Can not insert into current parent, step up one parent $proposedPosition = $parentPos; } return finaliseInsert(tr.insert($proposedPosition.pos, content), content.nodeSize); }; }; var finaliseInsert = function finaliseInsert(tr, nodeLength) { var lastStep = tr.steps[tr.steps.length - 1]; if (!(lastStep instanceof ReplaceStep || lastStep instanceof ReplaceAroundStep)) { return null; } // Place gap cursor after the newly inserted node var gapCursorPos = lastStep.to + lastStep.slice.openStart + nodeLength; return tr.setSelection(new GapCursorSelection(tr.doc.resolve(gapCursorPos), Side.RIGHT)).scrollIntoView(); }; /** * Method extracted from typeahead plugin to be shared with the element browser on handling element insertion. */ export var insertSelectedItem = function insertSelectedItem(maybeNode) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return function (state, tr, start) { if (!maybeNode) { return tr; } var isInputFragment = maybeNode instanceof Fragment; var node; try { node = maybeNode instanceof Node || isInputFragment ? maybeNode : typeof maybeNode === 'string' ? state.schema.text(maybeNode) : Node.fromJSON(state.schema, maybeNode); } catch (e) { // eslint-disable-next-line no-console console.error(e); return tr; } if (node instanceof Node && node.isText) { tr = tr.replaceWith(start, start, node); /** * * Replacing a type ahead query mark with a block node. * */ } else if (node instanceof Node && node.isBlock) { /** * * Rule has unique insertion behaviour * so using this safeInsert function in order to handle specific cases in flat list vs nested list * instead of a generic pmSafeInsert (i.e appending at the end) * */ var selectionInsideAPanel = selectionIsInsideAPanel(tr); if (node.type.name === 'rule' && !selectionInsideAPanel && // ED-17438 If the selection is not an empty paragraph we want to use pmSafeInsert // This fixes a bug where if a rule was inserted using safeInsert and the selection // was an empty paragraph it would not be inserted !isEmptyParagraph(tr.selection.$from.parent)) { var _safeInsert; tr = (_safeInsert = safeInsert(node, tr.selection.from)(tr)) !== null && _safeInsert !== void 0 ? _safeInsert : tr; } else { tr = pmSafeInsert(normaliseNestedLayout(state, node), undefined, true)(tr); } /** * * Replacing a type ahead query mark with an inline node. * */ } else if (node instanceof Node && node.isInline || isInputFragment) { var fragment = isInputFragment ? node : Fragment.fromArray([node, state.schema.text(' ')]); // For platform_editor_element_level_templates experiment only // clean up ticket ED-24873 // @ts-ignore if (opts.isTemplate) { return insertTemplateFragment({ fragment: fragment, tr: tr, position: { start: start, end: start } }); } tr = tr.replaceWith(start, start, fragment); if (opts.selectInlineNode) { // Select inserted node tr = tr.setSelection(NodeSelection.create(tr.doc, start)); } else { // Placing cursor after node + space. tr = tr.setSelection(Selection.near(tr.doc.resolve(start + fragment.size))); } } return tr; }; }; /** * ED-14584: Util to check if the destination node is a paragraph & the * content being inserted is a valid child of the grandparent node. * In this case, the destination node should split */ export var shouldSplitSelectedNodeOnNodeInsertion = function shouldSplitSelectedNodeOnNodeInsertion(_ref2) { var parentNodeType = _ref2.parentNodeType, grandParentNodeType = _ref2.grandParentNodeType, content = _ref2.content; if (parentNodeType.name === 'doc' || parentNodeType.name === 'paragraph' && grandParentNodeType.validContent(Fragment.from(content))) { return true; } return false; }; /** * Check if the current selection contains any nodes that are not permitted * as codeBlock child nodes. Note that this allows paragraphs and inline nodes * as we extract their text content. */ export function contentAllowedInCodeBlock(state) { var _state$selection = state.selection, $from = _state$selection.$from, $to = _state$selection.$to; var isAllowedChild = true; state.doc.nodesBetween($from.pos, $to.pos, function (node, pos) { var withinSelection = $from.pos <= pos && pos + node.nodeSize <= $to.pos; if (!withinSelection) { return; } if (!isAllowedChild) { return false; } return isAllowedChild = node.type === state.schema.nodes.listItem || node.type === state.schema.nodes.bulletList || node.type === state.schema.nodes.orderedList || node.type === state.schema.nodes.paragraph || node.isInline || node.type === state.schema.nodes.panel || node.isText; }); return isAllowedChild; } /** * Check if a fragment contains a particular node by iterating through all the nodes in the fragment. * If the node type is found will stop looking and return true. * If the node type is not found, it will return false. */ export function fragmentContainsNodeType(fragment, nodeType) { var doesContainNodeType = false; fragment.descendants(function (node) { if (node.type === nodeType) { doesContainNodeType = true; // Stop looking return false; } return true; }); return doesContainNodeType; } // For platform_editor_element_level_templates experiment only // clean up ticket ED-24873 var insertTemplateFragment = function insertTemplateFragment(_ref3) { var fragment = _ref3.fragment, tr = _ref3.tr, position = _ref3.position; var start = position.start; var trWithInsert = pmSafeInsert(fragment, start)(tr); tr.setSelection(trWithInsert.selection); return tr; };