@atlaskit/editor-plugin-paste
Version:
Paste plugin for @atlaskit/editor-core
307 lines (293 loc) • 12.1 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import { isListNode, mapChildren, mapSlice } from '@atlaskit/editor-common/utils';
import { autoJoin } from '@atlaskit/editor-prosemirror/commands';
import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
import { EditorState } from '@atlaskit/editor-prosemirror/state';
import { PastePluginActionTypes as ActionTypes } from '../editor-actions/actions';
import { createCommand } from '../pm-plugins/plugin-factory';
/**
* Use this to register macro link positions during a paste operation, that you
* want to track in a document over time, through any document changes.
*
* @param positions a map of string keys (custom position references) and position values e.g. { ['my-key-1']: 11 }
*
* **Context**: This is neccessary if there is an async process or an unknown period of time
* between obtaining an original position, and wanting to know about what its final eventual
* value. In that scenario, positions will need to be actively tracked and mapped in plugin
* state so that they can be mapped through any other independent document change transactions being
* dispatched to the editor that could affect their value.
*/
export var startTrackingPastedMacroPositions = function startTrackingPastedMacroPositions(pastedMacroPositions) {
return createCommand(function () {
return {
type: ActionTypes.START_TRACKING_PASTED_MACRO_POSITIONS,
pastedMacroPositions: pastedMacroPositions
};
});
};
export var stopTrackingPastedMacroPositions = function stopTrackingPastedMacroPositions(pastedMacroPositionKeys) {
return createCommand(function () {
return {
type: ActionTypes.STOP_TRACKING_PASTED_MACRO_POSITIONS,
pastedMacroPositionKeys: pastedMacroPositionKeys
};
});
};
// matchers for text lists
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var bullets = /^\s*[\*\-\u2022](\s+|\s+$)/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var numbers = /^\s*\d[\.\)](\s+|$)/;
var isListItem = function isListItem(node, schema) {
return Boolean(node && node.type === schema.nodes.listItem);
};
var getListType = function getListType(node, schema) {
if (!node || !node.text) {
return null;
}
var _schema$nodes = schema.nodes,
bulletList = _schema$nodes.bulletList,
orderedList = _schema$nodes.orderedList;
return [{
node: bulletList,
matcher: bullets
}, {
node: orderedList,
matcher: numbers
}].reduce(function (lastMatch, listType) {
if (lastMatch) {
return lastMatch;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var match = node.text.match(listType.matcher);
return match ? [listType.node, match[0].length] : lastMatch;
}, null);
};
// Splits array of nodes by hardBreak. E.g.:
// [text "1. ", em "hello", date, hardbreak, text "2. ",
// subsup "world", hardbreak, text "smile"]
// => [
// [ text "1. ", em "hello", date ],
// [hardbreak, text "2. ", subsup "world"],
// [hardbreak, text "smile"]
// ]
export var _contentSplitByHardBreaks = function _contentSplitByHardBreaks(content, schema) {
var wrapperContent = [];
var nextContent = [];
content.forEach(function (node) {
if (node.type === schema.nodes.hardBreak) {
if (nextContent.length !== 0) {
wrapperContent.push(nextContent);
}
nextContent = [node];
} else {
nextContent.push(node);
}
});
wrapperContent.push(nextContent);
return wrapperContent;
};
export var extractListFromParagraph = function extractListFromParagraph(node, parent, schema) {
var content = mapChildren(node.content, function (node) {
return node;
});
var linesSplitByHardbreaks = _contentSplitByHardBreaks(content, schema);
var _schema$nodes2 = schema.nodes,
paragraph = _schema$nodes2.paragraph,
hardBreak = _schema$nodes2.hardBreak,
listItem = _schema$nodes2.listItem,
orderedList = _schema$nodes2.orderedList;
var splitListsAndParagraphs = [];
var paragraphParts = [];
// Ignored via go/ees005
// eslint-disable-next-line no-var
for (var index = 0; index < linesSplitByHardbreaks.length; index = index + 1) {
var _firstNonHardBreakNod;
var line = linesSplitByHardbreaks[index];
var listMatch = void 0;
if (index === 0) {
var _line$;
if (((_line$ = line[0]) === null || _line$ === void 0 ? void 0 : _line$.type) === hardBreak) {
paragraphParts.push(line);
continue;
} else {
// the first line the potential list item is at postion 0
listMatch = getListType(line[0], schema);
}
} else {
// lines after the first the potential list item is at postion 1
if (line.length === 1) {
// if the line only has a line break return as is
paragraphParts.push(line);
continue;
}
listMatch = getListType(line[1], schema);
}
if (!listMatch ||
// CONFCLOUD-79708: If we are inside a list - let's not try to upgrade list as it resolves
// to invalid content
isListItem(parent, schema)) {
// if there is not list match return as is
paragraphParts.push(line);
continue;
}
var _listMatch = listMatch,
_listMatch2 = _slicedToArray(_listMatch, 2),
nodeType = _listMatch2[0],
length = _listMatch2[1];
var firstNonHardBreakNode = line.find(function (node) {
return node.type !== hardBreak;
});
var chunksWithoutLeadingHardBreaks = line.slice(line.findIndex(function (node) {
return node.type !== hardBreak;
}));
// retain text after bullet or number-dot e.g. 1. Hello
var startingText = firstNonHardBreakNode === null || firstNonHardBreakNode === void 0 || (_firstNonHardBreakNod = firstNonHardBreakNode.text) === null || _firstNonHardBreakNod === void 0 ? void 0 : _firstNonHardBreakNod.substr(length);
var restOfChunk = startingText ? // apply transformation to first entry
[schema.text(startingText, firstNonHardBreakNode === null || firstNonHardBreakNode === void 0 ? void 0 : firstNonHardBreakNode.marks)].concat(_toConsumableArray(chunksWithoutLeadingHardBreaks.slice(1))) : chunksWithoutLeadingHardBreaks.slice(1);
// convert to list
var listItemNode = listItem === null || listItem === void 0 ? void 0 : listItem.createAndFill(undefined, paragraph.createChecked(undefined, restOfChunk));
if (!listItemNode) {
paragraphParts.push(line);
continue;
}
var attrs = nodeType === orderedList ? {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
order: parseInt(firstNonHardBreakNode.text.split('.')[0])
} : undefined;
var newList = nodeType.createChecked(attrs, [listItemNode]);
if (paragraphParts.length !== 0 && paragraph.validContent(Fragment.from(paragraphParts.flat()))) {
splitListsAndParagraphs.push(paragraph.createAndFill(node.attrs, paragraphParts.flat(), node.marks));
paragraphParts = [];
}
splitListsAndParagraphs.push(newList);
}
if (paragraphParts.length !== 0 && paragraph.validContent(Fragment.from(paragraphParts.flat()))) {
splitListsAndParagraphs.push(schema.nodes.paragraph.createAndFill(node.attrs, paragraphParts.flat(), node.marks));
}
var result = splitListsAndParagraphs.flat();
// try to join
var mockState = EditorState.create({
schema: schema
});
var joinedListsTr;
var mockDispatch = function mockDispatch(tr) {
joinedListsTr = tr;
};
// Return false to prevent replaceWith from wrapping the text node in a paragraph
// paragraph since that will be done later. If it's done here, it will fail
// the paragraph.validContent check.
// Dont return false if there are lists, as they arent validContent for paragraphs
// and will result in hanging textNodes
autoJoin(function (state, dispatch) {
if (!dispatch) {
return false;
}
var containsList = result.some(function (node) {
return node.type === schema.nodes.bulletList || node.type === schema.nodes.orderedList;
});
if (result.some(function (node) {
return node.isText;
}) && !containsList) {
return false;
}
dispatch(state.tr.replaceWith(0, 2, result));
return true;
}, function (before, after) {
return isListNode(before) && isListNode(after);
})(mockState, mockDispatch);
var fragment = joinedListsTr ? joinedListsTr.doc.content : Fragment.from(result);
return fragment;
};
// above will wrap everything in paragraphs for us
export var upgradeTextToLists = function upgradeTextToLists(slice, schema) {
return mapSlice(slice, function (node, parent) {
if (node.type === schema.nodes.paragraph) {
return extractListFromParagraph(node, parent, schema);
}
return node;
});
};
export var splitParagraphs = function splitParagraphs(slice, schema) {
// exclude Text nodes with a code mark, since we transform those later
// into a codeblock
var hasCodeMark = false;
slice.content.forEach(function (child) {
hasCodeMark = hasCodeMark || child.marks.some(function (mark) {
return mark.type === schema.marks.code;
});
});
// slice might just be a raw text string
if (schema.nodes.paragraph.validContent(slice.content) && !hasCodeMark) {
var replSlice = splitIntoParagraphs({
fragment: slice.content,
schema: schema
});
return new Slice(replSlice, slice.openStart + 1, slice.openEnd + 1);
}
return mapSlice(slice, function (node) {
if (node.type === schema.nodes.paragraph) {
return splitIntoParagraphs({
fragment: node.content,
blockMarks: node.marks,
schema: schema
});
}
return node;
});
};
/**
* Walks the slice, creating paragraphs that were previously separated by hardbreaks.
* Returns the original paragraph node (as a fragment), or a fragment containing multiple nodes.
*/
export var splitIntoParagraphs = function splitIntoParagraphs(_ref) {
var fragment = _ref.fragment,
_ref$blockMarks = _ref.blockMarks,
blockMarks = _ref$blockMarks === void 0 ? [] : _ref$blockMarks,
schema = _ref.schema;
var paragraphs = [];
var curChildren = [];
var lastNode = null;
var _schema$nodes3 = schema.nodes,
hardBreak = _schema$nodes3.hardBreak,
paragraph = _schema$nodes3.paragraph;
fragment.forEach(function (node, i) {
var isNodeValidContentForParagraph = schema.nodes.paragraph.validContent(Fragment.from(node));
if (!isNodeValidContentForParagraph) {
paragraphs.push(node);
return;
}
// ED-14725 Fixed the issue that it make duplicated line
// when pasting <br /> from google docs.
if (i === 0 && node.type === hardBreak) {
paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
lastNode = node;
return;
} else if (lastNode && lastNode.type === hardBreak && node.type === hardBreak) {
// double hardbreak
// backtrack a little; remove the trailing hardbreak we added last loop
curChildren.pop();
// create a new paragraph
paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
curChildren = [];
return;
}
// add to this paragraph
curChildren.push(node);
lastNode = node;
});
if (curChildren.length) {
paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
}
return Fragment.from(paragraphs.length ? paragraphs :
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
[paragraph.createAndFill(undefined, undefined, _toConsumableArray(blockMarks))]);
};