UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

1,005 lines (960 loc) 71.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.checkIfSelectionInNestedList = checkIfSelectionInNestedList; exports.checkTaskListInList = checkTaskListInList; exports.doesSelectionWhichStartsOrEndsInListContainEntireList = void 0; exports.flattenNestedListInSlice = flattenNestedListInSlice; exports.handleCodeBlock = handleCodeBlock; exports.handleExpandPaste = handleExpandPaste; exports.handleMacroAutoConvert = handleMacroAutoConvert; exports.handleMarkdown = handleMarkdown; exports.handleMediaSingle = handleMediaSingle; exports.handleMention = handleMention; exports.handleNestedTablePaste = handleNestedTablePaste; exports.handleParagraphBlockMarks = handleParagraphBlockMarks; exports.handlePasteAsPlainText = handlePasteAsPlainText; exports.handlePasteExpand = handlePasteExpand; exports.handlePasteIntoCaption = handlePasteIntoCaption; exports.handlePasteIntoTaskOrDecisionOrPanel = handlePasteIntoTaskOrDecisionOrPanel; exports.handlePasteLinkOnSelectedText = handlePasteLinkOnSelectedText; exports.handlePasteNonNestableBlockNodesIntoList = handlePasteNonNestableBlockNodesIntoList; exports.handlePastePanelOrDecisionContentIntoList = handlePastePanelOrDecisionContentIntoList; exports.handlePastePreservingMarks = handlePastePreservingMarks; exports.handleRichText = handleRichText; exports.handleSelectedTable = void 0; exports.handleTableContentPasteInBodiedExtension = handleTableContentPasteInBodiedExtension; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _v = _interopRequireDefault(require("uuid/v4")); var _analytics = require("@atlaskit/editor-common/analytics"); var _card = require("@atlaskit/editor-common/card"); var _coreUtils = require("@atlaskit/editor-common/core-utils"); var _lists = require("@atlaskit/editor-common/lists"); var _mark = require("@atlaskit/editor-common/mark"); var _nesting = require("@atlaskit/editor-common/nesting"); var _selection = require("@atlaskit/editor-common/selection"); var _utils = require("@atlaskit/editor-common/utils"); var _model = require("@atlaskit/editor-prosemirror/model"); var _state = require("@atlaskit/editor-prosemirror/state"); var _utils2 = require("@atlaskit/editor-prosemirror/utils"); var _utils3 = require("@atlaskit/editor-tables/utils"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _prosemirrorHistory = require("@atlaskit/prosemirror-history"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _commands = require("../../editor-commands/commands"); var _pluginFactory = require("../plugin-factory"); var _edgeCases = require("./edge-cases"); var _lists2 = require("./edge-cases/lists"); var _index = require("./index"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } 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) { (0, _defineProperty2.default)(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; } // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead // TODO: ED-20519 - Needs Macro extraction var insideExpand = function insideExpand(state) { var _state$schema$nodes = state.schema.nodes, expand = _state$schema$nodes.expand, nestedExpand = _state$schema$nodes.nestedExpand; return (0, _utils2.hasParentNodeOfType)([expand, nestedExpand])(state.selection); }; /** Helper type for single arg function */ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Compose 1 to n functions. * @param func first function * @param funcs additional functions */ function compose(func) { for (var _len = arguments.length, funcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { funcs[_key - 1] = arguments[_key]; } var allFuncs = [func].concat(funcs); return function composed(raw) { return allFuncs.reduceRight(function (memo, func) { return func(memo); }, raw); }; } /* eslint-enable @typescript-eslint/no-explicit-any */ // remove text attribute from mention for copy/paste (GDPR) function handleMention(slice, schema) { return (0, _utils.mapSlice)(slice, function (node) { var _schema$nodes$mention; // We should move this to the mention plugin when we refactor how paste works in the future // For now we can just null check mention exists in the schema to ensure we don't crash if it doesn't // exist. if (node.type.name === ((_schema$nodes$mention = schema.nodes.mention) === null || _schema$nodes$mention === void 0 ? void 0 : _schema$nodes$mention.name)) { var mention = node.attrs; var newMention = _objectSpread(_objectSpread({}, mention), {}, { text: '' }); return schema.nodes.mention.create(newMention, node.content, node.marks); } return node; }); } function handlePasteIntoTaskOrDecisionOrPanel(slice, queueCardsFromChangedTr) { return function (state, dispatch) { var _slice$content$firstC, _slice$content$firstC2, _slice$content$firstC3, _transformedSlice$con; var schema = state.schema, selection = state.tr.selection; var codeMark = schema.marks.code, _schema$nodes = schema.nodes, decisionItem = _schema$nodes.decisionItem, emoji = _schema$nodes.emoji, hardBreak = _schema$nodes.hardBreak, mention = _schema$nodes.mention, paragraph = _schema$nodes.paragraph, taskItem = _schema$nodes.taskItem, text = _schema$nodes.text, panel = _schema$nodes.panel, bulletList = _schema$nodes.bulletList, orderedList = _schema$nodes.orderedList, taskList = _schema$nodes.taskList, listItem = _schema$nodes.listItem, expand = _schema$nodes.expand, heading = _schema$nodes.heading, codeBlock = _schema$nodes.codeBlock; var selectionIsValidNode = state.selection instanceof _state.NodeSelection && ['decisionList', 'decisionItem', 'taskList', 'taskItem'].includes(state.selection.node.type.name); var selectionHasValidParentNode = (0, _utils2.hasParentNodeOfType)([decisionItem, taskItem, panel])(state.selection); var selectionIsCodeBlock = (0, _utils2.hasParentNodeOfType)([codeBlock])(state.selection); var selectionIsListItem = (0, _utils2.hasParentNodeOfType)([listItem])(state.selection); var panelNode = (0, _index.isSelectionInsidePanel)(selection); var selectionIsPanel = Boolean(panelNode); var isSliceWholePanel = ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type) === panel && slice.openStart === 0 && slice.openEnd === 0; // we avoid handling codeBlock-in-panel use case in this function // returning false will allow code to flow into `handleCodeBlock` function // Partial content copied from panels will have panel in the slice // Return false to avoid handling this situation when pasted into list in panel and let `handlePastePanelOrDecisionContentIntoList` handle it if (selectionIsPanel && (selectionIsCodeBlock || selectionIsListItem && !isSliceWholePanel && (0, _expValEquals.expValEquals)('platform_editor_pasting_text_in_panel', 'isEnabled', true))) { return false; } // Some types of content should be handled by the default handler, not this function. // Check through slice content to see if it contains an invalid node. var sliceIsInvalid = false; var sliceHasTask = false; slice.content.nodesBetween(0, slice.content.size, function (node) { if (node.type === bulletList || node.type === orderedList || node.type === expand || node.type === heading || node.type === listItem) { sliceIsInvalid = true; } if (selectionIsPanel && node.type === taskList) { sliceHasTask = true; } }); // If the selection is a panel, // and the slice's first node is a paragraph // and it is not from a depth that would indicate it being from inside from another node (e.g. text from a decision) // then we can rely on the default behaviour. var selectionIsTaskOrDecision = (0, _utils2.hasParentNode)(function (node) { return node.type === taskItem || node.type === decisionItem; })(selection); var sliceIsAPanelReceivingLowDepthText = selectionIsPanel && !selectionIsTaskOrDecision && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.type) === paragraph && slice.openEnd < 2; if (sliceIsInvalid || sliceIsAPanelReceivingLowDepthText || !selectionIsValidNode && !selectionHasValidParentNode) { return false; } var filters = [(0, _utils.linkifyContent)(schema)]; var selectionMarks = selection.$head.marks(); if (selection instanceof _state.TextSelection && Array.isArray(selectionMarks) && selectionMarks.length > 0 && (0, _index.hasOnlyNodesOfType)(paragraph, text, emoji, mention, hardBreak)(slice) && (!codeMark.isInSet(selectionMarks) || (0, _mark.anyMarkActive)(state, codeMark)) // check if there is a code mark anywhere in the selection ) { filters.push((0, _index.applyTextMarksToSlice)(schema, selection.$head.marks())); } var transformedSlice = compose.apply(null, filters)(slice); var isFirstChildTaskNode = transformedSlice.content.firstChild.type === taskList || transformedSlice.content.firstChild.type === taskItem; var tr = (0, _prosemirrorHistory.closeHistory)(state.tr); if (panelNode && sliceHasTask && ((_slice$content$firstC3 = slice.content.firstChild) === null || _slice$content$firstC3 === void 0 ? void 0 : _slice$content$firstC3.type) === panel && (0, _index.isEmptyNode)(panelNode) && selection.$from.node() === selection.$to.node()) { return Boolean((0, _lists2.insertSliceInsideOfPanelNodeSelected)(panelNode)({ tr: tr, slice: slice })); } var transformedSliceIsValidNode = (transformedSlice.content.firstChild.type.inlineContent || ['decisionList', 'decisionItem', 'taskItem', 'taskList', 'panel'].includes(transformedSlice.content.firstChild.type.name)) && (!(0, _utils.isInListItem)(state) || (0, _utils.isInListItem)(state) && isFirstChildTaskNode); // If the slice or the selection are valid nodes to handle, // and the slice is not a whole node (i.e. openStart is 1 and openEnd is 0) // or the slice's first node is a paragraph, // then we can replace the selection with our slice. var pastingIntoExtendedPanel = selectionIsPanel && panel.validContent(transformedSlice.content); if ((transformedSliceIsValidNode || selectionIsValidNode) && !pastingIntoExtendedPanel && !(transformedSlice.openStart === 1 && transformedSlice.openEnd === 0 || // Whole codeblock node has reverse slice depths. transformedSlice.openStart === 0 && transformedSlice.openEnd === 1) || ((_transformedSlice$con = transformedSlice.content.firstChild) === null || _transformedSlice$con === void 0 ? void 0 : _transformedSlice$con.type) === paragraph) { tr.replaceSelection(transformedSlice).scrollIntoView(); } else { var isWholeContentSelected = selection.$from.pos === selection.$from.start() && selection.$to.end() === selection.$to.pos; if (pastingIntoExtendedPanel && selection.$from.pos !== selection.$to.pos && !isWholeContentSelected) { // Do a replaceSelection if the entire panel content isn't selected //tr.replaceSelection(transformedSlice).scrollIntoView(); tr.replaceSelection(new _model.Slice(transformedSlice.content, 0, transformedSlice.openEnd)).scrollIntoView(); } else if (['mediaSingle'].includes(transformedSlice.content.firstChild.type.name) && selectionIsPanel) { var parentNode = (0, _utils2.findParentNodeOfType)(panel)(selection); if (selectionIsPanel && parentNode && (0, _utils.isNodeEmpty)(parentNode.node)) { tr.insert(selection.$from.pos, transformedSlice.content).scrollIntoView(); // Place the cursor at the the end of the insersertion var endPos = tr.selection.from + transformedSlice.size; tr.setSelection(new _state.TextSelection(tr.doc.resolve(endPos))); } else { tr.replaceSelection(transformedSlice).scrollIntoView(); } } else { var _transformedSlice$con2; if (pastingIntoExtendedPanel && isWholeContentSelected) { // if the entire panel content is selected, doing a replaceSelection removes the panel as well. Hence we do delete followed by safeInsert tr.delete(selection.$from.pos, selection.$to.pos); } // This maintains both the selection (destination) and the slice (paste content). (0, _utils2.safeInsert)(transformedSlice.content)(tr).scrollIntoView(); if (((_transformedSlice$con2 = transformedSlice.content.lastChild) === null || _transformedSlice$con2 === void 0 || (_transformedSlice$con2 = _transformedSlice$con2.type) === null || _transformedSlice$con2 === void 0 ? void 0 : _transformedSlice$con2.name) === 'rule') { tr.setSelection(_state.TextSelection.near(tr.doc.resolve(tr.selection.$from.pos + transformedSlice.content.size))); } else { // safeInsert doesn't set correct cursor position // it moves the cursor to beginning of the node // we manually shift the cursor to end of the node var nextPos = tr.doc.resolve(tr.selection.$from.end()); tr.setSelection(new _state.TextSelection(nextPos)); } } } queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 || queueCardsFromChangedTr(state, tr, _analytics.INPUT_METHOD.CLIPBOARD); if (dispatch) { dispatch(tr); } return true; }; } function handlePasteNonNestableBlockNodesIntoList(slice) { return function (state, dispatch) { var _tr$doc$nodeAt, _slice$content$firstC4, _sliceContent$firstCh, _findParentNodeOfType; var tr = state.tr; var selection = tr.selection; var $from = selection.$from, $to = selection.$to, from = selection.from, to = selection.to; var _state$schema$nodes2 = state.schema.nodes, orderedList = _state$schema$nodes2.orderedList, bulletList = _state$schema$nodes2.bulletList, listItem = _state$schema$nodes2.listItem; // Selected nodes var selectionParentListItemNode = (0, _utils2.findParentNodeOfType)(listItem)(selection); var selectionParentListNodeWithPos = (0, _utils2.findParentNodeOfType)([bulletList, orderedList])(selection); var selectionParentListNode = selectionParentListNodeWithPos === null || selectionParentListNodeWithPos === void 0 ? void 0 : selectionParentListNodeWithPos.node; // Slice info var sliceContent = slice.content; var sliceIsListItems = (0, _utils.isListNode)(sliceContent.firstChild) && (0, _utils.isListNode)(sliceContent.lastChild); // Find case of slices that can be inserted into a list item // (eg. paragraphs, list items, code blocks, media single) // These scenarios already get handled elsewhere and don't need to split the list var sliceContainsBlockNodesOtherThanThoseAllowedInListItem = false; slice.content.forEach(function (child) { var _listItem$spec$conten; if (!listItem || child.isBlock && !((_listItem$spec$conten = listItem.spec.content) !== null && _listItem$spec$conten !== void 0 && _listItem$spec$conten.includes(child.type.name))) { sliceContainsBlockNodesOtherThanThoseAllowedInListItem = true; } }); if (!selectionParentListItemNode || !sliceContent || (0, _utils2.canInsert)($from, sliceContent) || // eg. inline nodes that can be inserted in a list item !sliceContainsBlockNodesOtherThanThoseAllowedInListItem || sliceIsListItems || !selectionParentListNodeWithPos) { return false; } // Offsets var listWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth + 1; // difference in depth between to position and list node var listItemWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth; // difference in depth between to position and list item node // Anything to do with nested lists should safeInsert and not be handled here if (checkIfSelectionInNestedList(state)) { return false; } // Node after the insert position var nodeAfterInsertPositionIsListItem = ((_tr$doc$nodeAt = tr.doc.nodeAt(to + listItemWrappingOffset)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type.name) === 'listItem'; // Get the next list items position (used later to find the split out ordered list) var indexOfNextListItem = $to.indexAfter($to.depth - listItemWrappingOffset); var positionOfNextListItem = tr.doc.resolve(selectionParentListNodeWithPos.pos + 1).posAtIndex(indexOfNextListItem); // These nodes paste as plain text by default so need to be handled differently var sliceContainsNodeThatPastesAsPlainText = sliceContent.firstChild && ['taskItem', 'taskList', 'heading', 'blockquote'].includes(sliceContent.firstChild.type.name); // Work out position to replace up to var replaceTo; if (sliceContainsNodeThatPastesAsPlainText && nodeAfterInsertPositionIsListItem) { replaceTo = to + listItemWrappingOffset; } else if (sliceContainsNodeThatPastesAsPlainText || !nodeAfterInsertPositionIsListItem) { replaceTo = to; } else { replaceTo = to + listWrappingOffset; } // handle the insertion of the slice if (((_slice$content$firstC4 = slice.content.firstChild) === null || _slice$content$firstC4 === void 0 ? void 0 : _slice$content$firstC4.type.name) === 'blockquote' && (0, _utils2.contains)(slice.content.firstChild, state.schema.nodes.listItem)) { (0, _edgeCases.insertSliceInsideBlockquote)({ tr: tr, slice: slice }); } else if (sliceContainsNodeThatPastesAsPlainText || nodeAfterInsertPositionIsListItem || sliceContent.childCount > 1 && ((_sliceContent$firstCh = sliceContent.firstChild) === null || _sliceContent$firstCh === void 0 ? void 0 : _sliceContent$firstCh.type.name) !== 'paragraph') { tr.replaceWith(from, replaceTo, sliceContent).scrollIntoView(); } else { // When the selection is not at the end of a list item // eg. middle of list item, start of list item tr.replaceSelection(slice).scrollIntoView(); } // Find the ordered list node after the pasted content so we can set it's order var mappedPositionOfNextListItem = tr.mapping.map(positionOfNextListItem); if (mappedPositionOfNextListItem > tr.doc.nodeSize) { return false; } var nodeAfterPastedContentResolvedPos = (0, _utils2.findParentNodeOfTypeClosestToPos)(tr.doc.resolve(mappedPositionOfNextListItem), [orderedList]); // Work out the new split out lists 'order' (the number it starts from) var originalParentOrderedListNodeOrder = selectionParentListNode === null || selectionParentListNode === void 0 ? void 0 : selectionParentListNode.attrs.order; var numOfListItemsInOriginalList = (_findParentNodeOfType = (0, _utils2.findParentNodeOfTypeClosestToPos)(tr.doc.resolve(from - 1), [orderedList])) === null || _findParentNodeOfType === void 0 ? void 0 : _findParentNodeOfType.node.childCount; // Set the new split out lists order attribute if (typeof originalParentOrderedListNodeOrder === 'number' && numOfListItemsInOriginalList && nodeAfterPastedContentResolvedPos) { tr.setNodeMarkup(nodeAfterPastedContentResolvedPos.pos, orderedList, _objectSpread(_objectSpread({}, nodeAfterPastedContentResolvedPos.node.attrs), {}, { order: originalParentOrderedListNodeOrder + numOfListItemsInOriginalList })); } // dispatch transaction if (tr.docChanged) { if (dispatch) { dispatch(tr); } return true; } return false; }; } var doesSelectionWhichStartsOrEndsInListContainEntireList = exports.doesSelectionWhichStartsOrEndsInListContainEntireList = function doesSelectionWhichStartsOrEndsInListContainEntireList(selection, findRootParentListNode) { var $from = selection.$from, $to = selection.$to, from = selection.from, to = selection.to; var selectionParentListItemNodeResolvedPos = findRootParentListNode ? findRootParentListNode($from) || findRootParentListNode($to) : null; var selectionParentListNode = selectionParentListItemNodeResolvedPos === null || selectionParentListItemNodeResolvedPos === void 0 ? void 0 : selectionParentListItemNodeResolvedPos.parent; if (!selectionParentListItemNodeResolvedPos || !selectionParentListNode) { return false; } var startOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + $from.depth - 1 : selectionParentListItemNodeResolvedPos.pos + $to.depth - 1; var endOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $to.depth - 1 : selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $from.depth - 1; if (!startOfEntireList || !endOfEntireList) { return false; } if (from < to) { return startOfEntireList >= $from.pos && endOfEntireList <= $to.pos; } else if (from > to) { return startOfEntireList >= $to.pos && endOfEntireList <= $from.pos; } else { return false; } }; function handlePastePanelOrDecisionContentIntoList(slice, findRootParentListNode) { return function (state, dispatch) { var schema = state.schema, tr = state.tr; var selection = tr.selection; // Check this pasting action is related to copy content from panel node into a selected the list node var blockNode = slice.content.firstChild; var isSliceWholeNode = slice.openStart === 0 && slice.openEnd === 0; var selectionParentListItemNode = selection.$to.node(selection.$to.depth - 1); var sliceIsWholeNodeButShouldNotReplaceSelection = isSliceWholeNode && !doesSelectionWhichStartsOrEndsInListContainEntireList(selection, findRootParentListNode); if (!selectionParentListItemNode || (selectionParentListItemNode === null || selectionParentListItemNode === void 0 ? void 0 : selectionParentListItemNode.type) !== schema.nodes.listItem || !blockNode || !['panel', 'decisionList'].includes(blockNode === null || blockNode === void 0 ? void 0 : blockNode.type.name) || slice.content.childCount > 1 || (blockNode === null || blockNode === void 0 ? void 0 : blockNode.content.firstChild) === undefined || sliceIsWholeNodeButShouldNotReplaceSelection) { return false; } // Paste the panel node contents extracted instead of pasting the entire panel node tr.replaceSelection(slice).scrollIntoView(); if (dispatch) { dispatch(tr); } return true; }; } var innerTextRangeOfTextblock = function innerTextRangeOfTextblock(doc, posOfBlock) { var block = doc.nodeAt(posOfBlock); if (!block || !block.isTextblock) { return null; } // raw content bounds var contentStart = posOfBlock + 1; // +1 to move from node's start token to content start var contentEnd = contentStart + block.content.size; // clamp to doc coord space var start = Math.max(0, Math.min(contentStart, doc.content.size)); var end = Math.max(0, Math.min(contentEnd, doc.content.size)); if (end <= start) { return null; } // snap to nearest valid text positions var startSel = _state.TextSelection.findFrom(doc.resolve(start), 1, true); var endSel = _state.TextSelection.findFrom(doc.resolve(end), -1, true); if (!startSel || !endSel) { return null; } var from = startSel.$from.pos; var to = endSel.$to.pos; return to > from ? { from: from, to: to } : null; }; function resolveSingleTextblockRangeIfAllSelected(state) { var sel = state.selection; if (!(sel instanceof _state.AllSelection)) { return null; } var count = 0; var posOfBlock = -1; state.doc.nodesBetween(sel.from, sel.to, function (node, pos) { if (!node.isTextblock) { return true; } count++; if (count > 1) { return false; } posOfBlock = pos; return true; }); if (count !== 1) { return null; } return innerTextRangeOfTextblock(state.doc, posOfBlock); } // If we paste a link onto some selected text, apply the link as a mark function handlePasteLinkOnSelectedText(slice) { return function (state, dispatch) { var _selectAllRange$from, _selectAllRange$to; var schema = state.schema, selection = state.selection, _state$selection = state.selection, from = _state$selection.from, to = _state$selection.to, tr = state.tr; var linkMark; // check if we have a link on the clipboard if (slice.content.childCount === 1 && (0, _utils.isParagraph)(slice.content.child(0), schema)) { var paragraph = slice.content.child(0); if (paragraph.content.childCount === 1 && (0, _utils.isText)(paragraph.content.child(0), schema)) { var text = paragraph.content.child(0); // If pasteType is plain text, then // @atlaskit/editor-markdown-transformer in getMarkdownSlice decode // url before setting text property of text node. // However href of marks will be without decoding. // So, if there is character (e.g space) in url eligible escaping then // mark.attrs.href will not be equal to text.text. // That's why decoding mark.attrs.href before comparing. // However, if pasteType is richText, that means url in text.text // and href in marks, both won't be decoded. linkMark = text.marks.find(function (mark) { return (0, _utils.isLinkMark)(mark, schema) && (mark.attrs.href === text.text || decodeURI(mark.attrs.href) === text.text); }); } } // derive a linkable range if possible for Select‑All over a single textblock var selectAllRange = (0, _platformFeatureFlags.fg)('platform_editor_link_paste_select_all') ? resolveSingleTextblockRangeIfAllSelected(state) : null; var rangeFrom = (_selectAllRange$from = selectAllRange === null || selectAllRange === void 0 ? void 0 : selectAllRange.from) !== null && _selectAllRange$from !== void 0 ? _selectAllRange$from : from; var rangeTo = (_selectAllRange$to = selectAllRange === null || selectAllRange === void 0 ? void 0 : selectAllRange.to) !== null && _selectAllRange$to !== void 0 ? _selectAllRange$to : to; // if we have a link, apply it to the selected text if we have any and it's allowed if (linkMark && (selection instanceof _state.TextSelection || Boolean(selectAllRange)) && !selection.empty && (0, _utils.canLinkBeCreatedInRange)(rangeFrom, rangeTo)(state)) { tr.addMark(rangeFrom, rangeTo, linkMark); if (dispatch) { dispatch(tr); } return true; } return false; }; } function handlePasteAsPlainText(slice, _event, editorAnalyticsAPI) { return function (state, dispatch, view) { var _input; if (!view) { return false; } // prosemirror-bump-fix // Yes, this is wrong by default. But, we need to keep the private PAI usage to unblock the prosemirror bump // So, this code will make sure we are checking for both version (current and the newest prosemirror-view version var isShiftKeyPressed = // eslint-disable-next-line @typescript-eslint/no-explicit-any view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey); // In case of SHIFT+CMD+V ("Paste and Match Style") we don't want to run the usual // fuzzy matching of content. ProseMirror already handles this scenario and will // provide us with slice containing paragraphs with plain text, which we decorate // with "stored marks". // @see prosemirror-view/src/clipboard.js:parseFromClipboard()). // @see prosemirror-view/src/input.js:doPaste(). if (isShiftKeyPressed) { var tr = (0, _prosemirrorHistory.closeHistory)(state.tr); var _tr = tr, selection = _tr.selection; // <- using the same internal flag that prosemirror-view is using // if user has selected table we need custom logic to replace the table tr = (0, _utils3.replaceSelectedTable)(state, slice); // add analytics after replacing selected table tr = (0, _index.addReplaceSelectedTableAnalytics)(state, tr, editorAnalyticsAPI); // otherwise just replace the selection if (!tr.docChanged) { tr.replaceSelection(slice); } (state.storedMarks || []).forEach(function (mark) { tr.addMark(selection.from, selection.from + slice.size, mark); }); tr.scrollIntoView(); if (dispatch) { dispatch(tr); } return true; } return false; }; } function handlePastePreservingMarks(slice, queueCardsFromChangedTr) { return function (state, dispatch) { var schema = state.schema, selection = state.tr.selection; var _schema$marks = schema.marks, codeMark = _schema$marks.code, annotationMark = _schema$marks.annotation, _schema$nodes2 = schema.nodes, bulletList = _schema$nodes2.bulletList, emoji = _schema$nodes2.emoji, hardBreak = _schema$nodes2.hardBreak, heading = _schema$nodes2.heading, listItem = _schema$nodes2.listItem, mention = _schema$nodes2.mention, orderedList = _schema$nodes2.orderedList, text = _schema$nodes2.text; if (!(selection instanceof _state.TextSelection)) { return false; } var selectionMarks = selection.$head.marks(); if (selectionMarks.length === 0) { return false; } // special case for codeMark: will preserve mark only if codeMark is currently active // won't preserve mark if cursor is on the edge on the mark (namely inactive) var hasActiveCodeMark = codeMark && codeMark.isInSet(selectionMarks) && (0, _mark.anyMarkActive)(state, codeMark); var hasAnnotationMark = annotationMark && annotationMark.isInSet(selectionMarks); var selectionIsHeading = (0, _utils2.hasParentNodeOfType)([heading])(state.selection); // if the pasted data is one of the node types below // we apply current selection marks to the pasted slice if ((0, _index.hasOnlyNodesOfType)(bulletList, hardBreak, heading, listItem, text, emoji, mention, orderedList)(slice) || selectionIsHeading || hasActiveCodeMark || hasAnnotationMark) { var transformedSlice = (0, _index.applyTextMarksToSlice)(schema, selectionMarks)(slice); var tr = (0, _prosemirrorHistory.closeHistory)(state.tr).replaceSelection(transformedSlice).setStoredMarks(selectionMarks).scrollIntoView(); queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 || queueCardsFromChangedTr(state, tr, _analytics.INPUT_METHOD.CLIPBOARD); if (dispatch) { dispatch(tr); } return true; } return false; }; } function getSmartLinkAdf(_x, _x2, _x3) { return _getSmartLinkAdf.apply(this, arguments); } function _getSmartLinkAdf() { _getSmartLinkAdf = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(text, type, cardOptions) { var provider; return _regenerator.default.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: if (cardOptions.provider) { _context.next = 2; break; } throw Error('No card provider found'); case 2: _context.next = 4; return cardOptions.provider; case 4: provider = _context.sent; _context.next = 7; return provider.resolve(text, type); case 7: return _context.abrupt("return", _context.sent); case 8: case "end": return _context.stop(); } }, _callee); })); return _getSmartLinkAdf.apply(this, arguments); } function insertAutoMacro(slice, macro, view, from, to) { if (view) { // insert the text or linkified/md-converted clipboard data var selection = view.state.tr.selection; var tr; var before; if (typeof from === 'number' && typeof to === 'number') { tr = view.state.tr.replaceRange(from, to, slice); before = tr.mapping.map(from, -1); } else { tr = view.state.tr.replaceSelection(slice); before = tr.mapping.map(selection.from, -1); } view.dispatch(tr); // replace the text with the macro as a separate transaction // so the autoconversion generates 2 undo steps var macroTr = (0, _prosemirrorHistory.closeHistory)(view.state.tr).replaceRangeWith(before, before + slice.size, macro).scrollIntoView(); (0, _card.addLinkMetadata)(view.state.selection, macroTr, { inputMethod: _analytics.INPUT_METHOD.CLIPBOARD, cardAction: 'AUTO_CONVERT' }); view.dispatch(macroTr); return true; } return false; } function handleMacroAutoConvert(text, slice, queueCardsFromChangedTr, runMacroAutoConvert, cardsOptions, extensionAutoConverter) { return function (state, dispatch, view) { var macro = null; // try to use auto convert from extension provider first if (extensionAutoConverter) { var extension = extensionAutoConverter(text); if (extension) { macro = _model.Node.fromJSON(state.schema, extension); } } // then try from macro provider (which will be removed some time in the future) if (!macro) { var _runMacroAutoConvert; macro = (_runMacroAutoConvert = runMacroAutoConvert === null || runMacroAutoConvert === void 0 ? void 0 : runMacroAutoConvert(state, text)) !== null && _runMacroAutoConvert !== void 0 ? _runMacroAutoConvert : null; } if (macro) { /** * if FF enabled, run through smart links and check for result */ if (cardsOptions && cardsOptions.resolveBeforeMacros && cardsOptions.resolveBeforeMacros.length) { if (cardsOptions.resolveBeforeMacros.indexOf(macro.attrs.extensionKey) < 0) { return insertAutoMacro(slice, macro, view); } if (!view) { throw new Error('View is missing'); } // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead var trackingId = (0, _v.default)(); var trackingFrom = "handleMacroAutoConvert-from-".concat(trackingId); var trackingTo = "handleMacroAutoConvert-to-".concat(trackingId); (0, _commands.startTrackingPastedMacroPositions)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, trackingFrom, state.selection.from), trackingTo, state.selection.to))(state, dispatch); getSmartLinkAdf(text, 'inline', cardsOptions).then(function () { // we use view.state rather than state because state becomes a stale // state reference after getSmartLinkAdf's async work var _getPastePluginState = (0, _pluginFactory.getPluginState)(view.state), pastedMacroPositions = _getPastePluginState.pastedMacroPositions; if (dispatch) { handleMarkdown(slice, queueCardsFromChangedTr, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo])(view.state, dispatch); } }).catch(function () { var _getPastePluginState2 = (0, _pluginFactory.getPluginState)(view.state), pastedMacroPositions = _getPastePluginState2.pastedMacroPositions; insertAutoMacro(slice, macro, view, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo]); }).finally(function () { (0, _commands.stopTrackingPastedMacroPositions)([trackingFrom, trackingTo])(view.state, dispatch); }); return true; } return insertAutoMacro(slice, macro, view); } return !!macro; }; } function handleCodeBlock(text) { return function (state, dispatch) { var codeBlock = state.schema.nodes.codeBlock; if (text && (0, _utils2.hasParentNodeOfType)(codeBlock)(state.selection)) { var tr = (0, _prosemirrorHistory.closeHistory)(state.tr); tr.scrollIntoView(); if (dispatch) { dispatch(tr.insertText(text)); } return true; } return false; }; } function isOnlyMedia(state, slice) { var media = state.schema.nodes.media; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return slice.content.childCount === 1 && slice.content.firstChild.type === media; } function isOnlyMediaSingle(state, slice) { var mediaSingle = state.schema.nodes.mediaSingle; return ( // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion mediaSingle && slice.content.childCount === 1 && slice.content.firstChild.type === mediaSingle ); } function handleMediaSingle(inputMethod, insertMediaAsMediaSingle) { return function (slice) { return function (state, dispatch, view) { if (view) { if (isOnlyMedia(state, slice)) { var _insertMediaAsMediaSi; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return (_insertMediaAsMediaSi = insertMediaAsMediaSingle === null || insertMediaAsMediaSingle === void 0 ? void 0 : insertMediaAsMediaSingle(view, slice.content.firstChild, inputMethod)) !== null && _insertMediaAsMediaSi !== void 0 ? _insertMediaAsMediaSi : false; } if ((0, _coreUtils.insideTable)(state) && isOnlyMediaSingle(state, slice)) { var tr = state.tr.replaceSelection(slice); var nextPos = tr.doc.resolve(tr.mapping.map(state.selection.$from.pos)); if (dispatch) { dispatch(tr.setSelection(new _selection.GapCursorSelection(nextPos, _selection.Side.RIGHT))); } return true; } } return false; }; }; } var hasTopLevelExpand = function hasTopLevelExpand(slice) { var hasExpand = false; slice.content.forEach(function (node) { if (node.type.name === 'expand' || node.type.name === 'nestedExpand') { hasExpand = true; } }); return hasExpand; }; function handleTableContentPasteInBodiedExtension(slice) { return function (state, dispatch) { var isInsideBodyExtension = (0, _utils2.hasParentNodeOfType)(state.schema.nodes.bodiedExtension)(state.selection); if (!(0, _coreUtils.insideTable)(state) || !isInsideBodyExtension) { return false; } var bodiedExtension = state.schema.nodes.bodiedExtension; var newSlice = (0, _utils.mapSlice)(slice, function (maybeNode) { if (maybeNode.type === bodiedExtension) { return bodiedExtension.createChecked(maybeNode.attrs, maybeNode.content, maybeNode.marks); } return maybeNode; }); if (dispatch) { dispatch(state.tr.replaceSelection(newSlice)); return true; } return false; }; } function handleNestedTablePaste(slice, isNestingTablesSupported) { return function (state, dispatch) { if (!isNestingTablesSupported || !(0, _coreUtils.insideTable)(state)) { return false; } var schema = state.schema, selection = state.selection; var sliceHasTable = false; slice.content.forEach(function (node) { if (node.type === state.schema.nodes.table) { sliceHasTable = true; } }); if (sliceHasTable) { // if slice has table - if pasting to deeply nested location place paste after top table if ((0, _nesting.getParentOfTypeCount)(schema.nodes.table)(selection.$from) > 1) { var positionAfterTopTable = (0, _nesting.getPositionAfterTopParentNodeOfType)(schema.nodes.table)(selection.$from); var tr = state.tr; tr = (0, _utils2.safeInsert)(slice.content, positionAfterTopTable)(tr); tr.scrollIntoView(); if (dispatch) { dispatch(tr); return true; } } } return false; }; } function handleExpandPaste(slice) { return function (state, dispatch) { var isInsideNestableExpand = !!insideExpand(state); // Do not handle expand if it's not being pasted into a table or expand // OR if it's nested within another node when being pasted into a table/expand if (!(0, _coreUtils.insideTable)(state) && !isInsideNestableExpand || !hasTopLevelExpand(slice)) { return false; } var _state$schema$nodes3 = state.schema.nodes, expand = _state$schema$nodes3.expand, nestedExpand = _state$schema$nodes3.nestedExpand; var tr = state.tr; var hasExpand = false; var newSlice = (0, _utils.mapSlice)(slice, function (maybeNode) { if (maybeNode.type === expand || maybeNode.type === nestedExpand) { hasExpand = true; try { return nestedExpand.createChecked(maybeNode.attrs, maybeNode.content, maybeNode.marks); } catch (_unused) { tr = (0, _utils2.safeInsert)(maybeNode, tr.selection.$to.pos)(tr); return _model.Fragment.empty; } } return maybeNode; }); if (hasExpand && dispatch) { // If the slice is a subset, we can let PM replace the selection // it will insert as text where it can't place the node. // Otherwise we use safeInsert to insert below instead of // replacing/splitting the current node. if (slice.openStart > 1 && slice.openEnd > 1) { dispatch(tr.replaceSelection(newSlice)); } else { dispatch((0, _utils2.safeInsert)(newSlice.content)(tr)); } return true; } return false; }; } function handleMarkdown(markdownSlice, queueCardsFromChangedTr, from, to) { return function (state, dispatch) { var tr = (0, _prosemirrorHistory.closeHistory)(state.tr); var pastesFrom = typeof from === 'number' ? from : tr.selection.from; if (typeof from === 'number' && typeof to === 'number') { tr.replaceRange(from, to, markdownSlice); } else { tr.replaceSelection(markdownSlice); } var textPosition = tr.doc.resolve(Math.min(pastesFrom + markdownSlice.size, tr.doc.content.size)); tr.setSelection(_state.TextSelection.near(textPosition, -1)); queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 || queueCardsFromChangedTr(state, tr, _analytics.INPUT_METHOD.CLIPBOARD); if (dispatch) { dispatch(tr.scrollIntoView()); } return true; }; } function removePrecedingBackTick(tr) { var _tr$selection = tr.selection, nodeBefore = _tr$selection.$from.nodeBefore, from = _tr$selection.from; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (nodeBefore && nodeBefore.isText && nodeBefore.text.endsWith('`')) { tr.delete(from - 1, from); } } function hasInlineCode(state, slice) { return slice.content.firstChild && slice.content.firstChild.marks.some(function (m) { return m.type === state.schema.marks.code; }); } function rollupLeafListItems(list, leafListItems) { list.content.forEach(function (child) { if ((0, _utils.isListNode)(child) || (0, _utils.isListItemNode)(child) && (0, _utils.isListNode)(child.firstChild)) { rollupLeafListItems(child, leafListItems); } else { leafListItems.push(child); } }); } function shouldFlattenList(state, slice) { var node = slice.content.firstChild; return node && (0, _coreUtils.insideTable)(state) && (0, _utils.isListNode)(node) && slice.openStart > slice.openEnd; } function sliceHasTopLevelMarks(slice) { var hasTopLevelMarks = false; slice.content.descendants(function (node) { if (node.marks.length > 0) { hasTopLevelMarks = true; } return false; }); return hasTopLevelMarks; } function getTopLevelMarkTypesInSlice(slice) { var markTypes = new Set(); slice.content.descendants(function (node) { node.marks.map(function (mark) { return mark.type; }).forEach(function (markType) { return markTypes.add(markType); }); return false; }); return markTypes; } /** * Peels container wrapper nodes (e.g. panel, expand) added by ProseMirror's addContext() * so that fontSize-marked paragraphs become top-level, preserving the mark on paste. */ function unwrapContainerNodesWithBlockMarks(slice, schema, fontSize) { var content = slice.content; var levelsUnwrapped = 0; while (content.childCount === 1 && content.firstChild && !content.firstChild.isTextblock && slice.openStart - levelsUnwrapped > 1) { var hasBlockMarkedParagraph = false; for (var i = 0; i < content.firstChild.childCount; i++) { var child = content.firstChild.child(i); if (child.type === schema.nodes.paragraph && child.marks.some(function (m) { return m.type === fontSize; })) { hasBlockMarkedParagraph = true; break; } } if (!hasBlockMarkedParagraph) { break; } content = content.firstChild.content; levelsUnwrapped++; } if (levelsUnwrapped === 0) { return slice; } return new _model.Slice(content, slice.openStart - levelsUnwrapped, Math.max(0, slice.openEnd - levelsUnwrapped)); } /** * Returns the fontSize attrs to apply at the paste destination, or false if none. * Checks list and task destinations in priority order. */ function getDestinationFontSizeAttrs(destinationListNode, isInSmallTaskContext, $from, currentNode, fontSize) { if (destinationListNode) { return (0, _lists.getFirstParagraphBlockMarkAttrs)(destinationListNode, fontSize); } if (isInSmallTaskContext) { return (0, _lists.getBlockMarkAttrs)($from.parent, fontSize) || (0, _lists.getFirstParagraphBlockMarkAttrs)(currentNode, fontSize); } return false; } /** * Resolves which marks to apply to a paragraph node after filtering forbidden marks. * When a destination block mark is provided, replaces any existing fontSize mark with it. * When normalizing for the target context, removes the fontSize mark entirely. * Otherwise returns the filtered marks unchanged. */ function resolveParagraphMarks(marks, destinationBlockMarkAttrs, shouldNormalize, fontSize) { if (destinationBlockMarkAttrs) { return marks.filter(function (m) { return m.type !== fontSize; }).concat(fontSize.create(destinationBlockMarkAttrs)); } if (shouldNormalize) { return marks.filter(function (m) { return m.type !== fontSize; }); } return marks; } function handleParagraphBlockMarks(state, slice) { var _findParentNodeOfType2; if (slice.content.size === 0) { return slice; } var schema = state.schema, selection = state.selection, $from = state.selection.$from; var _schema$nodes3 = schema.nodes, bulletList = _schema$nodes3.bulletList, orderedList = _schema$nodes3.orderedList, blockTaskItem = _schema$nodes3.blockTaskItem, taskItem = _schema$nodes3.taskItem, paragraph = _schema$nodes3.paragraph, heading = _schema$nodes3.heading; var fontSize = schema.marks.fontSize; var isSmallFontSizeEnabled = !!fontSize && (0, _expValEquals.expValEquals)('platform_editor_small_font_size', 'isEnabled', true); // When copying from inside a container (e.g. panel, expand), ProseMirror wraps the // content back in the container via addContext(), increasing openStart/openEnd. Unwrap // so the paragraph (with its fontSize mark) becomes top-level. if (isSmallFontSizeEnabled) { slice = unwrapContainerNodesWithBlockMarks(slice, schema, fontSize); } var destinationListNode = (_findParentNodeOfType2 = (0, _utils2.findParentNodeOfType)([bulletList, orderedList])(selection)) === null || _findParentNodeOfType2 === void 0 ? void 0 : _findParentNodeOfType2.node; var currentNode = typeof $from.node === 'function' ? $from.node() : undefined; var isInNormalTaskContext = (currentNode === null || currentNode === void 0 ? void 0 : currentNode.type) === taskItem || $from.parent.type === task