UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

307 lines (293 loc) 12.1 kB
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))]); };