@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
338 lines (316 loc) • 14.5 kB
JavaScript
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;
};