UNPKG

@atlaskit/editor-plugin-find-replace

Version:

find replace plugin for @atlaskit/editor-core

481 lines (470 loc) 19.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createDecorations = exports.createDecoration = void 0; exports.findClosestMatch = findClosestMatch; exports.findLostAdjacentDecorations = exports.findIndexBeforePosition = exports.findDecorationFromMatch = void 0; exports.findMatches = findMatches; exports.findSearchIndex = findSearchIndex; exports.findUniqueItemsIn = findUniqueItemsIn; exports.getSelectedText = getSelectedText; exports.removeMatchesFromSet = exports.removeDecorationsFromSet = exports.prevIndex = exports.nextIndex = exports.isMatchAffectedByStep = exports.getSelectionForMatch = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _classnames4 = _interopRequireDefault(require("classnames")); var _utils = require("@atlaskit/editor-common/utils"); var _state = require("@atlaskit/editor-prosemirror/state"); var _view = require("@atlaskit/editor-prosemirror/view"); var _resource = require("@atlaskit/mention/resource"); var _types = require("@atlaskit/mention/types"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _tokens = require("@atlaskit/tokens"); var _styles = require("../../ui/styles"); function getSelectedText(selection) { var text = ''; var selectedContent = selection.content().content; for (var i = 0; i < selectedContent.childCount; i++) { text += selectedContent.child(i).textContent; } return text; } var createDecorations = exports.createDecorations = function createDecorations(selectedIndex, matches) { return matches.map(function (_ref, i) { var start = _ref.start, end = _ref.end, canReplace = _ref.canReplace, nodeType = _ref.nodeType; return createDecoration({ start: start, end: end, canReplace: canReplace, nodeType: nodeType }, i === selectedIndex); }); }; var isElement = function isElement(nodeType) { return ['blockCard', 'embedCard', 'inlineCard', 'status', 'mention', 'date'].includes(nodeType || ''); }; var isExpandTitle = function isExpandTitle(match) { return ['expand', 'nestedExpand'].includes(match.nodeType || '') && !match.canReplace; }; var createDecoration = exports.createDecoration = function createDecoration(match, isSelected) { var start = match.start, end = match.end, nodeType = match.nodeType; if ((0, _expValEquals.expValEquals)('platform_editor_find_and_replace_improvements', 'isEnabled', true)) { var _getGlobalTheme = (0, _tokens.getGlobalTheme)(), colorMode = _getGlobalTheme.colorMode; if (isElement(nodeType)) { var className = (0, _classnames4.default)(_styles.blockSearchMatchClass, (0, _defineProperty2.default)((0, _defineProperty2.default)({}, _styles.selectedBlockSearchMatchClass, isSelected), _styles.darkModeSearchMatchClass, colorMode === 'dark')); return _view.Decoration.node(start, end, { class: className }); } else if (isExpandTitle(match)) { var _className = (0, _classnames4.default)(_styles.searchMatchExpandTitleClass, (0, _defineProperty2.default)((0, _defineProperty2.default)({}, _styles.selectedSearchMatchClass, isSelected), _styles.darkModeSearchMatchClass, colorMode === 'dark')); return _view.Decoration.node(start, end, { class: _className }); } else { var _className2 = (0, _classnames4.default)(_styles.searchMatchTextClass, (0, _defineProperty2.default)((0, _defineProperty2.default)({}, _styles.selectedSearchMatchClass, isSelected), _styles.darkModeSearchMatchClass, colorMode === 'dark')); return _view.Decoration.inline(start, end, { class: _className2 }); } } else { var _className3 = _styles.searchMatchClass; if (isSelected) { _className3 += " ".concat(_styles.selectedSearchMatchClass); } return _view.Decoration.inline(start, end, { class: _className3 }); } }; function findMatches(_ref2) { var content = _ref2.content, searchText = _ref2.searchText, shouldMatchCase = _ref2.shouldMatchCase, _ref2$contentIndex = _ref2.contentIndex, contentIndex = _ref2$contentIndex === void 0 ? 0 : _ref2$contentIndex, getIntl = _ref2.getIntl, api = _ref2.api; var matches = []; var searchTextLength = searchText.length; var textGrouping = null; var collectTextMatch = function collectTextMatch(textGrouping) { if (!textGrouping) { return; } var text = textGrouping.text; var relativePos = textGrouping.pos; var pos = contentIndex + relativePos; if (!shouldMatchCase) { searchText = searchText.toLowerCase(); text = text.toLowerCase(); } var index = text.indexOf(searchText); while (index !== -1) { // Find the next substring from the end of the first, so that they don't overlap var end = index + searchTextLength; // Add the substring index to the position of the node matches.push({ start: pos + index, end: pos + end, canReplace: (0, _expValEquals.expValEquals)('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? true : undefined, nodeType: (0, _expValEquals.expValEquals)('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? 'text' : undefined }); index = text.indexOf(searchText, end); } }; var collectNodeMatch = function collectNodeMatch(textGrouping, node) { if (!textGrouping) { return; } var pos = textGrouping.pos; var text = textGrouping.text; if (node.type.name === 'status' && shouldMatchCase) { text = text.toUpperCase(); } else if (!shouldMatchCase) { text = text.toLowerCase(); searchText = searchText.toLowerCase(); } var index = text.indexOf(searchText); if (index !== -1) { matches.push({ start: pos, end: pos + node.nodeSize, canReplace: false, nodeType: node.type.name }); } }; var collectCardTitleMatch = function collectCardTitleMatch(node, pos) { var _api$card; var cards = api === null || api === void 0 || (_api$card = api.card) === null || _api$card === void 0 || (_api$card = _api$card.sharedState.currentState()) === null || _api$card === void 0 ? void 0 : _api$card.cards; if (cards) { var relevantCard = cards.find(function (card) { return card.url === node.attrs.url; }); var title = relevantCard === null || relevantCard === void 0 ? void 0 : relevantCard.title; if (relevantCard) { if (title) { collectNodeMatch({ text: title, pos: pos }, node); } else { // when there is no title, e.g. in an error case like unauthorized // the link card just shows the entire url as the title in inline card if (node.type.name === 'inlineCard') { collectNodeMatch({ text: node.attrs.url, pos: pos }, node); } } } } }; if (searchTextLength > 0) { content.descendants(function (node, pos) { if (node.isText) { if (textGrouping === null) { textGrouping = { text: node.text, pos: pos }; } else { textGrouping.text = textGrouping.text + node.text; } } else { collectTextMatch(textGrouping); textGrouping = null; if ((0, _expValEquals.expValEquals)('platform_editor_find_and_replace_improvements', 'isEnabled', true)) { switch (node.type.name) { case 'status': collectNodeMatch({ text: node.attrs.text, pos: pos }, node); break; case 'date': collectNodeMatch({ text: (0, _utils.timestampToString)(node.attrs.timestamp, getIntl ? getIntl() : null), pos: pos }, node); break; case 'expand': case 'nestedExpand': collectNodeMatch({ text: node.attrs.title, pos: pos }, node); break; case 'mention': var text; if (node.attrs.text) { text = node.attrs.text; } else { var _api$mention; // the text may be sanitised from the node for privacy reasons // so we need to use the mentionProvider to resolve it var mentionProvider = api === null || api === void 0 || (_api$mention = api.mention) === null || _api$mention === void 0 || (_api$mention = _api$mention.sharedState.currentState()) === null || _api$mention === void 0 ? void 0 : _api$mention.mentionProvider; if ((0, _resource.isResolvingMentionProvider)(mentionProvider)) { var nameDetail = mentionProvider.resolveMentionName(node.attrs.id); if ((0, _types.isPromise)(nameDetail)) { text = '@...'; } else { if (nameDetail.status === _types.MentionNameStatus.OK) { text = "@".concat(nameDetail.name || ''); } else { text = '@_|unknown|_'; } } } } if (text) { collectNodeMatch({ text: text, pos: pos }, node); } break; case 'inlineCard': case 'blockCard': case 'embedCard': collectCardTitleMatch(node, pos); break; default: break; } } } }); // if there's a dangling text grouping and no non-text node to trigger collectTextMatch, manually collectTextMatch if (textGrouping) { collectTextMatch(textGrouping); } } return matches; } function findClosestMatch(selectionPos, matches) { var forwardMatchIndex = matches.findIndex(function (match) { return match.start >= selectionPos; }); if (forwardMatchIndex === -1) { // if there are no forward matches, it must be the last match return matches.length > 0 ? matches.length - 1 : 0; } else if (forwardMatchIndex === 0) { return forwardMatchIndex; } var backwardMatchIndex = forwardMatchIndex - 1; var forwardMatchPos = matches[forwardMatchIndex].start; var backwardMatchPos = matches[backwardMatchIndex].start; if (forwardMatchPos - selectionPos < selectionPos - backwardMatchPos) { return forwardMatchIndex; } else { return backwardMatchIndex; } } /** * Finds index of first item in matches array that comes after user's cursor pos. * If `backward` is `true`, finds index of first item that comes before instead. */ function findSearchIndex(selectionPos, matches) { var backward = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; if (backward) { var matchIndex = matches.findIndex(function (match) { return match.start >= selectionPos; }) - 1; if (matchIndex < 0) { matchIndex = matches.length - 1; // wrap around from the end } return matchIndex; } return Math.max(matches.findIndex(function (match) { return match.start >= selectionPos; }), 0); } var nextIndex = exports.nextIndex = function nextIndex(currentIndex, total) { return (currentIndex + 1) % total; }; var prevIndex = exports.prevIndex = function prevIndex(currentIndex, total) { return (currentIndex - 1 + total) % total; }; var getSelectionForMatch = exports.getSelectionForMatch = function getSelectionForMatch(selection, doc, index, matches) { var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; if (matches[index]) { if (isExpandTitle(matches[index])) { return _state.NodeSelection.create(doc, matches[index].start); } return _state.TextSelection.create(doc, matches[index].start + offset); } return selection; }; var findDecorationFromMatch = exports.findDecorationFromMatch = function findDecorationFromMatch(decorationSet, match) { if (!match) { return; } var decorations = decorationSet.find(match.start, match.end); return decorations.length ? decorations.find( // decorationSet.find() returns any decorations that touch the specifided // positions, but we want to be stricter function (decoration) { return decoration.from === match.start && decoration.to === match.end; }) : undefined; }; var removeDecorationsFromSet = exports.removeDecorationsFromSet = function removeDecorationsFromSet(decorationSet, decorationsToRemove, doc) { var prevDecorations = decorationSet.find(); // it is essential that we copy the decorations otherwise in some rare cases // prosemirror-view will update our decorationsToRemove array to contain nulls // instead of Decorations which ruins our check for lost decorations below decorationSet = decorationSet.remove(decorationsToRemove.map(function (decoration) { return ( // copy exists but isn't on the type definition decoration.copy(decoration.from, decoration.to) ); })); var newDecorations = decorationSet.find(); // there is a bug in prosemirror-view where it can't cope with deleting inline // decorations from a set in some cases (where there are multiple levels of nested // children arrays), and it deletes more decorations than it should var lostDecorations = findLostAdjacentDecorations(decorationsToRemove, prevDecorations, newDecorations); if (lostDecorations.length > 0) { decorationSet = decorationSet.add(doc, lostDecorations); } return decorationSet; }; var removeMatchesFromSet = exports.removeMatchesFromSet = function removeMatchesFromSet(decorationSet, matches, doc) { var decorationsToRemove = matches.filter(function (match) { return !!match; }).map(function (match) { return findDecorationFromMatch(decorationSet, match); }); decorationsToRemove.forEach(function (decoration) { if (decoration) { decorationSet = removeDecorationsFromSet(decorationSet, [decoration], doc); } }); return decorationSet; }; /** * Finds decorations in prevDecorations that are not in newDecorations or decorationsToRemove * These decorations have been lost by Prosemirror during an over eager decoration removal * We need to be smart to cope with thousands of decorations without crashing everything */ var findLostAdjacentDecorations = exports.findLostAdjacentDecorations = function findLostAdjacentDecorations(decorationsToRemove, prevDecorations, newDecorations) { var lostDecorations = []; if (prevDecorations.length - decorationsToRemove.length > newDecorations.length) { var position = decorationsToRemove.length > 0 ? decorationsToRemove[0].from : 0; var prevDecorationsStartIdx = findIndexBeforePosition(prevDecorations, position); var newDecorationsStartIdx = findIndexBeforePosition(newDecorations, position); var startIdx = Math.min(prevDecorationsStartIdx, newDecorationsStartIdx); var prevDecorationsToCheck = prevDecorations.slice(startIdx); var newDecorationsToCheck = newDecorations.slice(startIdx); var uniqueInPrev = []; var numToFind = prevDecorationsToCheck.length - newDecorationsToCheck.length; var foundAll = false; var newDecorationsIdxOffset = 0; var _loop = function _loop() { var prevDecoration = prevDecorationsToCheck[i]; // this was a legit removal, skip and continue if (decorationsToRemove.find(function (decoration) { return decoration.from === prevDecoration.from; })) { newDecorationsIdxOffset -= 1; return 0; // continue } var j = i + newDecorationsIdxOffset; // this is a lost decoration if (j >= newDecorationsToCheck.length) { uniqueInPrev.push(prevDecoration); if (uniqueInPrev.length === numToFind) { foundAll = true; } } for (; j < newDecorationsToCheck.length; j++) { var newDecoration = newDecorationsToCheck[j]; // decoration found in both arrays, skip and continue if (prevDecoration.from === newDecoration.from) { break; } // this is a lost decoration if (newDecoration.from > prevDecoration.from || j === newDecorationsToCheck.length - 1) { uniqueInPrev.push(prevDecoration); newDecorationsIdxOffset -= 1; if (uniqueInPrev.length === numToFind) { foundAll = true; } break; } } if (foundAll) { return 1; // break } }, _ret; for (var i = 0; i < prevDecorationsToCheck.length; i++) { _ret = _loop(); if (_ret === 0) continue; if (_ret === 1) break; } // make sure we ignore any that we wanted to delete lostDecorations = uniqueInPrev.filter(function (decoration) { return !decorationsToRemove.find(function (decorationToRemove) { return decoration.from === decorationToRemove.from; }); }); } return lostDecorations; }; /** * Searches through array in bumps of 100 to return the index of the first * decoration whose 'from' value is before or equal to the position */ var findIndexBeforePosition = exports.findIndexBeforePosition = function findIndexBeforePosition(items, position) { // jump in batches to cope with arrays with thousands of decorations var increment = 100; var index = 0; for (var i = items.length - 1; i >= 0; i -= increment) { if (items[i].from < position) { // now we have found the 100 range, we can narrow it down to exact index index = i; for (var j = i; j <= items.length - 1; j++) { if (items[j].from <= position) { index = j; } else { break; } } break; } if (i < 100 && i > 0) { i = 100; } } return index; }; /** * Determines whether a find/replace text Match will be changed as a result * of a Step modification to the document. This is evaluated by checking * both mapped and unmapped versions of the Step as in different cases the * matches will match. * * **Note:** Match state received here is after step has been applied. */ var isMatchAffectedByStep = exports.isMatchAffectedByStep = function isMatchAffectedByStep(match, step, tr) { var from = step.from, to = step.to, slice = step.slice; var sliceSize = slice.content.size; return from + sliceSize >= match.start && to - sliceSize <= match.end || tr.mapping.map(from) + sliceSize >= match.start && tr.mapping.map(to) - sliceSize <= match.end; }; function findUniqueItemsIn(findIn, checkWith, comparator) { return findIn.filter(function (firstItem) { return checkWith.findIndex(function (secondItem) { return comparator ? comparator(firstItem, secondItem) : firstItem === secondItem; }) === -1; }); }