UNPKG

@atlaskit/editor-plugin-find-replace

Version:

find replace plugin for @atlaskit/editor-core

297 lines (294 loc) 12.3 kB
import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { FindReplaceActionTypes } from './actions'; import { createCommand, getPluginState } from './plugin-factory'; import { createDecoration, findClosestMatch, findDecorationFromMatch, findMatches, findSearchIndex, getSelectedText, getSelectionForMatch, nextIndex, prevIndex, removeDecorationsFromSet, removeMatchesFromSet } from './utils'; import batchDecorations from './utils/batch-decorations'; import { withScrollIntoView } from './utils/commands'; export var activate = function activate() { return createCommand(function (state) { var selection = state.selection; var findText; var matches; var index; // if user has selected text and hit cmd-f, set that as the keyword if (selection instanceof TextSelection && !selection.empty) { findText = getSelectedText(selection); var _getPluginState = getPluginState(state), shouldMatchCase = _getPluginState.shouldMatchCase, getIntl = _getPluginState.getIntl, api = _getPluginState.api; matches = findMatches({ content: state.doc, searchText: findText, shouldMatchCase: shouldMatchCase, getIntl: getIntl, api: api }); index = expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? findClosestMatch(selection.from, matches) : findSearchIndex(selection.from, matches); } return { type: FindReplaceActionTypes.ACTIVATE, findText: findText, matches: matches, index: index }; }); }; export var find = function find(editorView, containerElement, keyword) { return withScrollIntoView(createCommand(function (state) { var selection = state.selection; var _getPluginState2 = getPluginState(state), shouldMatchCase = _getPluginState2.shouldMatchCase, getIntl = _getPluginState2.getIntl, api = _getPluginState2.api; var matches = keyword !== undefined ? findMatches({ content: state.doc, searchText: keyword, shouldMatchCase: shouldMatchCase, getIntl: getIntl, api: api }) : []; var index = expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? findClosestMatch(selection.from, matches) : findSearchIndex(selection.from, matches); // we can't just apply all the decorations to highlight the search results at once // as if there are a lot ProseMirror cries :'( batchDecorations.applyAllSearchDecorations(editorView, containerElement, function (decorations) { return addDecorations(decorations)(editorView.state, editorView.dispatch); }, function (decorations) { return removeDecorations(decorations)(editorView.state, editorView.dispatch); }); return { type: FindReplaceActionTypes.FIND, findText: keyword || '', matches: matches, index: index }; }, function (tr, state) { var selection = state.selection; var _getPluginState3 = getPluginState(state), shouldMatchCase = _getPluginState3.shouldMatchCase, getIntl = _getPluginState3.getIntl, api = _getPluginState3.api; var matches = keyword !== undefined ? findMatches({ content: state.doc, searchText: keyword, shouldMatchCase: shouldMatchCase, getIntl: getIntl, api: api }) : []; if (matches.length > 0) { var _api$expand; var index = expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? findClosestMatch(selection.from, matches) : findSearchIndex(selection.from, matches); var newSelection = getSelectionForMatch(tr.selection, tr.doc, index, matches); api === null || api === void 0 || (_api$expand = api.expand) === null || _api$expand === void 0 || _api$expand.commands.toggleExpandWithMatch(newSelection)({ tr: tr }); return tr.setSelection(newSelection); } return tr; })); }; export var findNext = function findNext(editorView) { return withScrollIntoView(createCommand(function (state) { return findInDirection(state, 'next'); }, function (tr, state) { var _api$expand2; var _getPluginState4 = getPluginState(state), matches = _getPluginState4.matches, index = _getPluginState4.index, api = _getPluginState4.api; // can't use index from plugin state because if the cursor has moved, it will still be the // OLD index (the find next operation should look for the first match forward starting // from the current cursor position) var searchIndex = findSearchIndex(state.selection.from, matches); if (searchIndex === index) { // cursor has not moved, so we just want to find the next in matches array searchIndex = nextIndex(searchIndex, matches.length); } var newSelection = getSelectionForMatch(tr.selection, tr.doc, searchIndex, matches); api === null || api === void 0 || (_api$expand2 = api.expand) === null || _api$expand2 === void 0 || _api$expand2.commands.toggleExpandWithMatch(newSelection)({ tr: tr }); return tr.setSelection(newSelection); })); }; export var findPrevious = function findPrevious(editorView) { return withScrollIntoView(createCommand(function (state) { return findInDirection(state, 'previous'); }, function (tr, state) { var _api$expand3; var _getPluginState5 = getPluginState(state), matches = _getPluginState5.matches, api = _getPluginState5.api; // can't use index from plugin state because if the cursor has moved, it will still be the // OLD index (the find prev operation should look for the first match backward starting // from the current cursor position) var searchIndex = findSearchIndex(state.selection.from, matches, true); var newSelection = getSelectionForMatch(tr.selection, tr.doc, searchIndex, matches); api === null || api === void 0 || (_api$expand3 = api.expand) === null || _api$expand3 === void 0 || _api$expand3.commands.toggleExpandWithMatch(newSelection)({ tr: tr }); return tr.setSelection(newSelection); })); }; var findInDirection = function findInDirection(state, dir) { var pluginState = getPluginState(state); var matches = pluginState.matches, findText = pluginState.findText; var decorationSet = pluginState.decorationSet, index = pluginState.index; if (findText) { var searchIndex = findSearchIndex(state.selection.from, matches, dir === 'previous'); // compare index from plugin state and index of first match forward from cursor position if (index === searchIndex) { // normal case, cycling through matches index = dir === 'next' ? nextIndex(index, matches.length) : prevIndex(index, matches.length); } else { // cursor has moved index = searchIndex; } decorationSet = updateSelectedHighlight(state, index); } return { type: dir === 'next' ? FindReplaceActionTypes.FIND_NEXT : FindReplaceActionTypes.FIND_PREVIOUS, index: index, decorationSet: decorationSet }; }; export var replace = function replace(replaceText) { return withScrollIntoView(createCommand(function (state) { var pluginState = getPluginState(state); var findText = pluginState.findText, matches = pluginState.matches; var decorationSet = pluginState.decorationSet, index = pluginState.index; decorationSet = updateSelectedHighlight(state, nextIndex(index, matches.length)); if (replaceText.toLowerCase().indexOf(findText.toLowerCase()) === -1) { decorationSet = removeMatchesFromSet(decorationSet, [matches[index]], state.doc); matches.splice(index, 1); if (index > matches.length - 1) { index = 0; } } else { index = nextIndex(index, matches.length); } return { type: FindReplaceActionTypes.REPLACE, replaceText: replaceText, decorationSet: decorationSet, matches: matches, index: index }; }, function (tr, state) { var _getPluginState6 = getPluginState(state), matches = _getPluginState6.matches, index = _getPluginState6.index, findText = _getPluginState6.findText; if (matches[index]) { if (!matches[index].canReplace && expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true)) { return tr; } var _matches$index = matches[index], start = _matches$index.start, end = _matches$index.end; var newIndex = nextIndex(index, matches.length); tr.insertText(replaceText, start, end).setSelection(getSelectionForMatch(tr.selection, tr.doc, newIndex, matches, newIndex === 0 ? 0 : replaceText.length - findText.length)); } return tr; })); }; export var replaceAll = function replaceAll(replaceText) { return createCommand({ type: FindReplaceActionTypes.REPLACE_ALL, replaceText: replaceText, decorationSet: DecorationSet.empty, matches: [], index: 0 }, function (tr, state) { var pluginState = getPluginState(state); pluginState.matches.forEach(function (match) { if (!match.canReplace && expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true)) { return tr; } tr.insertText(replaceText, tr.mapping.map(match.start), tr.mapping.map(match.end)); }); tr.setMeta('scrollIntoView', false); return tr; }); }; export var addDecorations = function addDecorations(decorations) { return createCommand(function (state) { var _getPluginState7 = getPluginState(state), decorationSet = _getPluginState7.decorationSet; return { type: FindReplaceActionTypes.UPDATE_DECORATIONS, decorationSet: decorationSet.add(state.doc, decorations) }; }); }; export var removeDecorations = function removeDecorations(decorations) { return createCommand(function (state) { var _getPluginState8 = getPluginState(state), decorationSet = _getPluginState8.decorationSet; return { type: FindReplaceActionTypes.UPDATE_DECORATIONS, decorationSet: removeDecorationsFromSet(decorationSet, decorations, state.doc) }; }); }; export var cancelSearch = function cancelSearch() { return createCommand(function () { batchDecorations.stop(); return { type: FindReplaceActionTypes.CANCEL }; }); }; export var blur = function blur() { return createCommand({ type: FindReplaceActionTypes.BLUR }); }; export var toggleMatchCase = function toggleMatchCase() { return createCommand({ type: FindReplaceActionTypes.TOGGLE_MATCH_CASE }); }; var updateSelectedHighlight = function updateSelectedHighlight(state, nextSelectedIndex) { var _getPluginState9 = getPluginState(state), index = _getPluginState9.index, matches = _getPluginState9.matches; var _getPluginState0 = getPluginState(state), decorationSet = _getPluginState0.decorationSet; var currentSelectedMatch = matches[index]; var nextSelectedMatch = matches[nextSelectedIndex]; if (index === nextSelectedIndex) { return decorationSet; } var currentSelectedDecoration = findDecorationFromMatch(decorationSet, currentSelectedMatch); var nextSelectedDecoration = findDecorationFromMatch(decorationSet, nextSelectedMatch); // Update decorations so the current selected match becomes a normal match // and the next selected gets the selected styling var decorationsToRemove = []; if (currentSelectedDecoration) { decorationsToRemove.push(currentSelectedDecoration); } if (nextSelectedDecoration) { decorationsToRemove.push(nextSelectedDecoration); } if (decorationsToRemove.length > 0) { // removeDecorationsFromSet depends on decorations being pre-sorted decorationsToRemove.sort(function (a, b) { return a.from < b.from ? -1 : 1; }); decorationSet = removeDecorationsFromSet(decorationSet, decorationsToRemove, state.doc); } if (currentSelectedMatch) { decorationSet = decorationSet.add(state.doc, [createDecoration(currentSelectedMatch)]); } if (nextSelectedMatch) { decorationSet = decorationSet.add(state.doc, [createDecoration(nextSelectedMatch, true)]); } return decorationSet; };