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