@atlaskit/editor-plugin-find-replace
Version:
find replace plugin for @atlaskit/editor-core
481 lines (470 loc) • 19.2 kB
JavaScript
"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;
});
}