@atlaskit/editor-plugin-find-replace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +26 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE.md +13 -0
- package/README.md +30 -0
- package/dist/cjs/FindReplaceToolbarButtonWithState.js +166 -0
- package/dist/cjs/actions.js +19 -0
- package/dist/cjs/commands-with-analytics.js +101 -0
- package/dist/cjs/commands.js +255 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/plugin.js +93 -0
- package/dist/cjs/pm-plugins/keymap.js +24 -0
- package/dist/cjs/pm-plugins/main.js +39 -0
- package/dist/cjs/pm-plugins/plugin-factory.js +109 -0
- package/dist/cjs/pm-plugins/plugin-key.js +8 -0
- package/dist/cjs/reducer.js +61 -0
- package/dist/cjs/styles.js +17 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/ui/Find.js +309 -0
- package/dist/cjs/ui/FindReplace.js +104 -0
- package/dist/cjs/ui/FindReplaceToolbarButton.js +133 -0
- package/dist/cjs/ui/FindReplaceTooltipButton.js +77 -0
- package/dist/cjs/ui/Replace.js +176 -0
- package/dist/cjs/ui/styles.js +46 -0
- package/dist/cjs/utils/array.js +13 -0
- package/dist/cjs/utils/batch-decorations.js +310 -0
- package/dist/cjs/utils/commands.js +16 -0
- package/dist/cjs/utils/index.js +290 -0
- package/dist/es2019/FindReplaceToolbarButtonWithState.js +153 -0
- package/dist/es2019/actions.js +13 -0
- package/dist/es2019/commands-with-analytics.js +72 -0
- package/dist/es2019/commands.js +240 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/plugin.js +88 -0
- package/dist/es2019/pm-plugins/keymap.js +16 -0
- package/dist/es2019/pm-plugins/main.js +30 -0
- package/dist/es2019/pm-plugins/plugin-factory.js +91 -0
- package/dist/es2019/pm-plugins/plugin-key.js +2 -0
- package/dist/es2019/reducer.js +56 -0
- package/dist/es2019/styles.js +18 -0
- package/dist/es2019/types.js +1 -0
- package/dist/es2019/ui/Find.js +286 -0
- package/dist/es2019/ui/FindReplace.js +81 -0
- package/dist/es2019/ui/FindReplaceToolbarButton.js +122 -0
- package/dist/es2019/ui/FindReplaceTooltipButton.js +51 -0
- package/dist/es2019/ui/Replace.js +155 -0
- package/dist/es2019/ui/styles.js +50 -0
- package/dist/es2019/utils/array.js +3 -0
- package/dist/es2019/utils/batch-decorations.js +189 -0
- package/dist/es2019/utils/commands.js +6 -0
- package/dist/es2019/utils/index.js +249 -0
- package/dist/esm/FindReplaceToolbarButtonWithState.js +157 -0
- package/dist/esm/actions.js +13 -0
- package/dist/esm/commands-with-analytics.js +95 -0
- package/dist/esm/commands.js +248 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/plugin.js +86 -0
- package/dist/esm/pm-plugins/keymap.js +18 -0
- package/dist/esm/pm-plugins/main.js +33 -0
- package/dist/esm/pm-plugins/plugin-factory.js +104 -0
- package/dist/esm/pm-plugins/plugin-key.js +2 -0
- package/dist/esm/reducer.js +54 -0
- package/dist/esm/styles.js +11 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/ui/Find.js +304 -0
- package/dist/esm/ui/FindReplace.js +100 -0
- package/dist/esm/ui/FindReplaceToolbarButton.js +126 -0
- package/dist/esm/ui/FindReplaceTooltipButton.js +70 -0
- package/dist/esm/ui/Replace.js +171 -0
- package/dist/esm/ui/styles.js +39 -0
- package/dist/esm/utils/array.js +7 -0
- package/dist/esm/utils/batch-decorations.js +304 -0
- package/dist/esm/utils/commands.js +10 -0
- package/dist/esm/utils/index.js +280 -0
- package/dist/types/FindReplaceToolbarButtonWithState.d.ts +4 -0
- package/dist/types/actions.d.ts +64 -0
- package/dist/types/commands-with-analytics.d.ts +27 -0
- package/dist/types/commands.d.ts +12 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/plugin.d.ts +2 -0
- package/dist/types/pm-plugins/keymap.d.ts +4 -0
- package/dist/types/pm-plugins/main.d.ts +5 -0
- package/dist/types/pm-plugins/plugin-factory.d.ts +2 -0
- package/dist/types/pm-plugins/plugin-key.d.ts +3 -0
- package/dist/types/reducer.d.ts +4 -0
- package/dist/types/styles.d.ts +3 -0
- package/dist/types/types.d.ts +76 -0
- package/dist/types/ui/Find.d.ts +71 -0
- package/dist/types/ui/FindReplace.d.ts +43 -0
- package/dist/types/ui/FindReplaceToolbarButton.d.ts +21 -0
- package/dist/types/ui/FindReplaceTooltipButton.d.ts +18 -0
- package/dist/types/ui/Replace.d.ts +27 -0
- package/dist/types/ui/styles.d.ts +6 -0
- package/dist/types/utils/array.d.ts +1 -0
- package/dist/types/utils/batch-decorations.d.ts +36 -0
- package/dist/types/utils/commands.d.ts +2 -0
- package/dist/types/utils/index.d.ts +49 -0
- package/dist/types-ts4.5/FindReplaceToolbarButtonWithState.d.ts +4 -0
- package/dist/types-ts4.5/actions.d.ts +64 -0
- package/dist/types-ts4.5/commands-with-analytics.d.ts +27 -0
- package/dist/types-ts4.5/commands.d.ts +12 -0
- package/dist/types-ts4.5/index.d.ts +2 -0
- package/dist/types-ts4.5/plugin.d.ts +2 -0
- package/dist/types-ts4.5/pm-plugins/keymap.d.ts +4 -0
- package/dist/types-ts4.5/pm-plugins/main.d.ts +5 -0
- package/dist/types-ts4.5/pm-plugins/plugin-factory.d.ts +2 -0
- package/dist/types-ts4.5/pm-plugins/plugin-key.d.ts +3 -0
- package/dist/types-ts4.5/reducer.d.ts +4 -0
- package/dist/types-ts4.5/styles.d.ts +3 -0
- package/dist/types-ts4.5/types.d.ts +76 -0
- package/dist/types-ts4.5/ui/Find.d.ts +71 -0
- package/dist/types-ts4.5/ui/FindReplace.d.ts +43 -0
- package/dist/types-ts4.5/ui/FindReplaceToolbarButton.d.ts +21 -0
- package/dist/types-ts4.5/ui/FindReplaceTooltipButton.d.ts +18 -0
- package/dist/types-ts4.5/ui/Replace.d.ts +27 -0
- package/dist/types-ts4.5/ui/styles.d.ts +6 -0
- package/dist/types-ts4.5/utils/array.d.ts +1 -0
- package/dist/types-ts4.5/utils/batch-decorations.d.ts +36 -0
- package/dist/types-ts4.5/utils/commands.d.ts +2 -0
- package/dist/types-ts4.5/utils/index.d.ts +49 -0
- package/package.json +117 -0
- package/styles/package.json +17 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { pluginFactory, stepHasSlice } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
3
|
+
import reducer from '../reducer';
|
|
4
|
+
import { createDecorations, findDecorationFromMatch, findMatches, findSearchIndex, isMatchAffectedByStep, removeDecorationsFromSet, removeMatchesFromSet } from '../utils';
|
|
5
|
+
import { findUniqueItemsIn } from '../utils/array'; // TODO: move into index export
|
|
6
|
+
|
|
7
|
+
import { initialState } from './main';
|
|
8
|
+
import { findReplacePluginKey } from './plugin-key';
|
|
9
|
+
const handleDocChanged = (tr, pluginState) => {
|
|
10
|
+
const {
|
|
11
|
+
isActive,
|
|
12
|
+
findText
|
|
13
|
+
} = pluginState;
|
|
14
|
+
if (!isActive || !findText) {
|
|
15
|
+
return pluginState;
|
|
16
|
+
}
|
|
17
|
+
if (!tr.steps.find(stepHasSlice)) {
|
|
18
|
+
return pluginState;
|
|
19
|
+
}
|
|
20
|
+
let {
|
|
21
|
+
index,
|
|
22
|
+
decorationSet,
|
|
23
|
+
matches,
|
|
24
|
+
shouldMatchCase
|
|
25
|
+
} = pluginState;
|
|
26
|
+
const newMatches = findMatches(tr.doc, findText, shouldMatchCase);
|
|
27
|
+
decorationSet = decorationSet.map(tr.mapping, tr.doc);
|
|
28
|
+
const numDecorations = decorationSet.find().length;
|
|
29
|
+
const mappedMatches = matches.map(match => ({
|
|
30
|
+
start: tr.mapping.map(match.start),
|
|
31
|
+
end: tr.mapping.map(match.end)
|
|
32
|
+
}));
|
|
33
|
+
let matchesToAdd = [];
|
|
34
|
+
let matchesToDelete = [];
|
|
35
|
+
if (newMatches.length > 0 && numDecorations === 0) {
|
|
36
|
+
matchesToAdd = newMatches;
|
|
37
|
+
} else if (newMatches.length === 0 && numDecorations > 0) {
|
|
38
|
+
decorationSet = DecorationSet.empty;
|
|
39
|
+
} else if (newMatches.length > 0 || numDecorations > 0) {
|
|
40
|
+
// go through tr steps and find any new matches from user adding content or
|
|
41
|
+
// any dead matches from user deleting content
|
|
42
|
+
tr.steps.forEach(step => {
|
|
43
|
+
if (stepHasSlice(step)) {
|
|
44
|
+
// add all matches that are between the affected positions and don't already have
|
|
45
|
+
// corresponding decorations
|
|
46
|
+
matchesToAdd = [...matchesToAdd, ...newMatches.filter(match => isMatchAffectedByStep(match, step, tr) && !findDecorationFromMatch(decorationSet, match))];
|
|
47
|
+
|
|
48
|
+
// delete any matches that are missing from the newMatches array and have a
|
|
49
|
+
// corresponding decoration
|
|
50
|
+
matchesToDelete = [...matchesToDelete, ...findUniqueItemsIn(mappedMatches.filter(match => isMatchAffectedByStep(match, step, tr) && !!findDecorationFromMatch(decorationSet, match)), newMatches, (firstMatch, secondMatch) => firstMatch.start === secondMatch.start && firstMatch.end === secondMatch.end)];
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// update decorations if matches changed following document update
|
|
56
|
+
if (matchesToDelete.length > 0) {
|
|
57
|
+
const decorationsToDelete = matchesToDelete.reduce((decorations, match) => [...decorations, ...decorationSet.find(match.start, match.end)], []);
|
|
58
|
+
decorationSet = removeDecorationsFromSet(decorationSet, decorationsToDelete, tr.doc);
|
|
59
|
+
}
|
|
60
|
+
if (matchesToAdd.length > 0) {
|
|
61
|
+
decorationSet = decorationSet.add(tr.doc, createDecorations(tr.selection.from, matchesToAdd));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// update selected match if it has changed
|
|
65
|
+
let newIndex = index;
|
|
66
|
+
const selectedMatch = mappedMatches[index];
|
|
67
|
+
if (selectedMatch) {
|
|
68
|
+
newIndex = newMatches.findIndex(match => match.start === selectedMatch.start);
|
|
69
|
+
}
|
|
70
|
+
if (newIndex === undefined || newIndex === -1) {
|
|
71
|
+
newIndex = findSearchIndex(tr.selection.from, newMatches);
|
|
72
|
+
}
|
|
73
|
+
const newSelectedMatch = newMatches[newIndex];
|
|
74
|
+
decorationSet = removeMatchesFromSet(decorationSet, [selectedMatch, newSelectedMatch], tr.doc);
|
|
75
|
+
if (newSelectedMatch) {
|
|
76
|
+
decorationSet = decorationSet.add(tr.doc, createDecorations(0, [newSelectedMatch]));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...pluginState,
|
|
80
|
+
matches: newMatches,
|
|
81
|
+
index: newIndex,
|
|
82
|
+
decorationSet
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export const {
|
|
86
|
+
createCommand,
|
|
87
|
+
getPluginState,
|
|
88
|
+
createPluginState
|
|
89
|
+
} = pluginFactory(findReplacePluginKey, reducer(() => initialState), {
|
|
90
|
+
onDocChanged: handleDocChanged
|
|
91
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FindReplaceActionTypes } from './actions';
|
|
2
|
+
const reducer = getInitialState => (state, action) => {
|
|
3
|
+
switch (action.type) {
|
|
4
|
+
case FindReplaceActionTypes.ACTIVATE:
|
|
5
|
+
case FindReplaceActionTypes.FIND:
|
|
6
|
+
return {
|
|
7
|
+
...state,
|
|
8
|
+
isActive: true,
|
|
9
|
+
shouldFocus: action.type === FindReplaceActionTypes.ACTIVATE,
|
|
10
|
+
findText: action.findText !== undefined ? action.findText : state.findText,
|
|
11
|
+
matches: action.matches || state.matches,
|
|
12
|
+
index: action.index !== undefined ? action.index : state.index
|
|
13
|
+
};
|
|
14
|
+
case FindReplaceActionTypes.UPDATE_DECORATIONS:
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
decorationSet: action.decorationSet
|
|
18
|
+
};
|
|
19
|
+
case FindReplaceActionTypes.FIND_NEXT:
|
|
20
|
+
return {
|
|
21
|
+
...state,
|
|
22
|
+
index: action.index,
|
|
23
|
+
decorationSet: action.decorationSet
|
|
24
|
+
};
|
|
25
|
+
case FindReplaceActionTypes.FIND_PREVIOUS:
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
index: action.index,
|
|
29
|
+
decorationSet: action.decorationSet
|
|
30
|
+
};
|
|
31
|
+
case FindReplaceActionTypes.REPLACE:
|
|
32
|
+
case FindReplaceActionTypes.REPLACE_ALL:
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
replaceText: action.replaceText,
|
|
36
|
+
decorationSet: action.decorationSet,
|
|
37
|
+
matches: action.matches,
|
|
38
|
+
index: action.index
|
|
39
|
+
};
|
|
40
|
+
case FindReplaceActionTypes.CANCEL:
|
|
41
|
+
return getInitialState();
|
|
42
|
+
case FindReplaceActionTypes.BLUR:
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
shouldFocus: false
|
|
46
|
+
};
|
|
47
|
+
case FindReplaceActionTypes.TOGGLE_MATCH_CASE:
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
shouldMatchCase: !state.shouldMatchCase
|
|
51
|
+
};
|
|
52
|
+
default:
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export default reducer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable @atlaskit/design-system/ensure-design-token-usage/preview */
|
|
2
|
+
/* eslint-disable @atlaskit/design-system/ensure-design-token-usage */
|
|
3
|
+
|
|
4
|
+
// TODO: https://product-fabric.atlassian.net/browse/DSP-4290
|
|
5
|
+
import { css } from '@emotion/react';
|
|
6
|
+
import { B200, B75 } from '@atlaskit/theme/colors';
|
|
7
|
+
export const searchMatchClass = 'search-match';
|
|
8
|
+
export const selectedSearchMatchClass = 'selected-search-match';
|
|
9
|
+
export const findReplaceStyles = css`
|
|
10
|
+
.${searchMatchClass} {
|
|
11
|
+
background-color: ${B75};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.${selectedSearchMatchClass} {
|
|
15
|
+
background-color: ${B200};
|
|
16
|
+
color: white;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
|
|
3
|
+
/** @jsx jsx */
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { jsx } from '@emotion/react';
|
|
6
|
+
import debounce from 'lodash/debounce';
|
|
7
|
+
import rafSchd from 'raf-schd';
|
|
8
|
+
import { defineMessages, injectIntl } from 'react-intl-next';
|
|
9
|
+
import { TRIGGER_METHOD } from '@atlaskit/editor-common/analytics';
|
|
10
|
+
import EditorCloseIcon from '@atlaskit/icon/glyph/editor/close';
|
|
11
|
+
import MatchCaseIcon from '@atlaskit/icon/glyph/emoji/keyboard';
|
|
12
|
+
import ChevronDownIcon from '@atlaskit/icon/glyph/hipchat/chevron-down';
|
|
13
|
+
import ChevronUpIcon from '@atlaskit/icon/glyph/hipchat/chevron-up';
|
|
14
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
15
|
+
import Textfield from '@atlaskit/textfield';
|
|
16
|
+
import { FindReplaceTooltipButton } from './FindReplaceTooltipButton';
|
|
17
|
+
import { countStyles, countWrapperStyles, sectionWrapperStyles } from './styles';
|
|
18
|
+
export const FIND_DEBOUNCE_MS = 100;
|
|
19
|
+
const messages = defineMessages({
|
|
20
|
+
find: {
|
|
21
|
+
id: 'fabric.editor.find',
|
|
22
|
+
defaultMessage: 'Find',
|
|
23
|
+
description: 'The word or phrase to search for on the document'
|
|
24
|
+
},
|
|
25
|
+
matchCase: {
|
|
26
|
+
id: 'fabric.editor.matchCase',
|
|
27
|
+
defaultMessage: 'Match case',
|
|
28
|
+
description: 'Toggle whether should also match case when searching for text'
|
|
29
|
+
},
|
|
30
|
+
findNext: {
|
|
31
|
+
id: 'fabric.editor.findNext',
|
|
32
|
+
defaultMessage: 'Find next',
|
|
33
|
+
description: 'Locate the next occurrence of the word or phrase that was searched for'
|
|
34
|
+
},
|
|
35
|
+
findPrevious: {
|
|
36
|
+
id: 'fabric.editor.findPrevious',
|
|
37
|
+
defaultMessage: 'Find previous',
|
|
38
|
+
description: 'Locate the previous occurrence of the word or phrase that was searched for'
|
|
39
|
+
},
|
|
40
|
+
closeFindReplaceDialog: {
|
|
41
|
+
id: 'fabric.editor.closeFindReplaceDialog',
|
|
42
|
+
defaultMessage: 'Close',
|
|
43
|
+
description: 'Cancel search and close the "Find and Replace" dialog'
|
|
44
|
+
},
|
|
45
|
+
noResultsFound: {
|
|
46
|
+
id: 'fabric.editor.noResultsFound',
|
|
47
|
+
defaultMessage: 'No results',
|
|
48
|
+
description: 'No matches were found for the word or phrase that was searched for'
|
|
49
|
+
},
|
|
50
|
+
resultsCount: {
|
|
51
|
+
id: 'fabric.editor.resultsCount',
|
|
52
|
+
description: 'Text for selected search match position and total results count',
|
|
53
|
+
defaultMessage: '{selectedMatchPosition} of {totalResultsCount}'
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// eslint-disable-next-line @repo/internal/react/no-class-components
|
|
57
|
+
class Find extends React.Component {
|
|
58
|
+
constructor(props) {
|
|
59
|
+
super(props);
|
|
60
|
+
_defineProperty(this, "findTextfieldRef", /*#__PURE__*/React.createRef());
|
|
61
|
+
_defineProperty(this, "isComposing", false);
|
|
62
|
+
_defineProperty(this, "syncFindText", onSynced => {
|
|
63
|
+
var _this$state;
|
|
64
|
+
// If the external prop findText changes and we aren't in a composition we should update to
|
|
65
|
+
// use the external prop value.
|
|
66
|
+
//
|
|
67
|
+
// An example of where this may happen is when a find occurs through the user selecting some text
|
|
68
|
+
// and pressing Mod-f.
|
|
69
|
+
if (!this.isComposing && this.props.findText !== ((_this$state = this.state) === null || _this$state === void 0 ? void 0 : _this$state.localFindText)) {
|
|
70
|
+
this.updateFindValue(this.props.findText || '', onSynced);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
_defineProperty(this, "focusFindTextfield", () => {
|
|
74
|
+
const input = this.findTextfieldRef.current;
|
|
75
|
+
if (this.props.shouldFocus && input) {
|
|
76
|
+
input.select();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
_defineProperty(this, "handleFindChange", event => {
|
|
80
|
+
this.updateFindValue(event.target.value);
|
|
81
|
+
});
|
|
82
|
+
// debounce (vs throttle) to not block typing inside find input while onFind runs
|
|
83
|
+
_defineProperty(this, "debouncedFind", debounce(value => {
|
|
84
|
+
this.props.onFind(value);
|
|
85
|
+
}, FIND_DEBOUNCE_MS));
|
|
86
|
+
_defineProperty(this, "updateFindValue", (value, onSynced) => {
|
|
87
|
+
this.setState({
|
|
88
|
+
localFindText: value
|
|
89
|
+
}, () => {
|
|
90
|
+
if (this.isComposing) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
onSynced && onSynced();
|
|
94
|
+
this.debouncedFind(value);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
// throtlle between animation frames gives better experience on Enter compared to arbitrary value
|
|
98
|
+
// it adjusts based on performance (and document size)
|
|
99
|
+
_defineProperty(this, "handleFindKeyDownThrottled", rafSchd(event => {
|
|
100
|
+
if (event.key === 'Enter') {
|
|
101
|
+
if (event.shiftKey) {
|
|
102
|
+
this.props.onFindPrev({
|
|
103
|
+
triggerMethod: TRIGGER_METHOD.KEYBOARD
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
this.props.onFindNext({
|
|
107
|
+
triggerMethod: TRIGGER_METHOD.KEYBOARD
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} else if (event.key === 'ArrowDown') {
|
|
111
|
+
// we want to move focus between find & replace texfields when user hits up/down arrows
|
|
112
|
+
this.props.onArrowDown();
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
_defineProperty(this, "handleFindKeyDown", event => {
|
|
116
|
+
if (this.isComposing) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
event.persist();
|
|
120
|
+
this.handleFindKeyDownThrottled(event);
|
|
121
|
+
});
|
|
122
|
+
_defineProperty(this, "handleFindKeyUp", () => {
|
|
123
|
+
this.handleFindKeyDownThrottled.cancel();
|
|
124
|
+
});
|
|
125
|
+
_defineProperty(this, "handleFindNextClick", ref => {
|
|
126
|
+
if (this.isComposing) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.props.onFindNext({
|
|
130
|
+
triggerMethod: TRIGGER_METHOD.BUTTON
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
_defineProperty(this, "handleFindPrevClick", ref => {
|
|
134
|
+
if (this.isComposing) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.props.onFindPrev({
|
|
138
|
+
triggerMethod: TRIGGER_METHOD.BUTTON
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
_defineProperty(this, "handleCompositionStart", () => {
|
|
142
|
+
this.isComposing = true;
|
|
143
|
+
});
|
|
144
|
+
_defineProperty(this, "handleCompositionEnd", event => {
|
|
145
|
+
this.isComposing = false;
|
|
146
|
+
// type for React.CompositionEvent doesn't set type for target correctly
|
|
147
|
+
this.updateFindValue(event.target.value);
|
|
148
|
+
});
|
|
149
|
+
_defineProperty(this, "clearSearch", () => {
|
|
150
|
+
this.props.onCancel({
|
|
151
|
+
triggerMethod: TRIGGER_METHOD.BUTTON
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
_defineProperty(this, "handleMatchCaseClick", buttonRef => {
|
|
155
|
+
if (this.props.allowMatchCase && this.props.onToggleMatchCase) {
|
|
156
|
+
this.props.onToggleMatchCase();
|
|
157
|
+
this.props.onFind(this.props.findText);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const {
|
|
161
|
+
intl: {
|
|
162
|
+
formatMessage
|
|
163
|
+
}
|
|
164
|
+
} = props;
|
|
165
|
+
this.find = formatMessage(messages.find);
|
|
166
|
+
this.closeFindReplaceDialog = formatMessage(messages.closeFindReplaceDialog);
|
|
167
|
+
this.noResultsFound = formatMessage(messages.noResultsFound);
|
|
168
|
+
this.findNext = formatMessage(messages.findNext);
|
|
169
|
+
this.findPrevious = formatMessage(messages.findPrevious);
|
|
170
|
+
this.matchCase = formatMessage(messages.matchCase);
|
|
171
|
+
this.matchCaseIcon = jsx(MatchCaseIcon, {
|
|
172
|
+
label: this.matchCase
|
|
173
|
+
});
|
|
174
|
+
this.findNextIcon = jsx(ChevronDownIcon, {
|
|
175
|
+
label: this.findNext
|
|
176
|
+
});
|
|
177
|
+
this.findPrevIcon = jsx(ChevronUpIcon, {
|
|
178
|
+
label: this.findPrevious
|
|
179
|
+
});
|
|
180
|
+
this.closeIcon = jsx(EditorCloseIcon, {
|
|
181
|
+
label: this.closeFindReplaceDialog
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// We locally manage the value of the input inside this component in order to support compositions.
|
|
185
|
+
// This requires some additional work inside componentDidUpdate to ensure we support changes that
|
|
186
|
+
// occur to this value which do not originate from this component.
|
|
187
|
+
this.state = {
|
|
188
|
+
localFindText: ''
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
componentDidMount() {
|
|
192
|
+
this.props.onFindTextfieldRefSet(this.findTextfieldRef);
|
|
193
|
+
|
|
194
|
+
// focus initially on dialog mount if there is no find text provided
|
|
195
|
+
if (!this.props.findText) {
|
|
196
|
+
this.focusFindTextfield();
|
|
197
|
+
}
|
|
198
|
+
this.syncFindText(() => {
|
|
199
|
+
// focus after input is synced if find text provided
|
|
200
|
+
if (this.props.findText) {
|
|
201
|
+
this.focusFindTextfield();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
componentDidUpdate(prevProps) {
|
|
206
|
+
// focus on update if find text did not change
|
|
207
|
+
if (!getBooleanFF('platform.editor.a11y-find-replace')) {
|
|
208
|
+
var _this$state2;
|
|
209
|
+
if (this.props.findText === ((_this$state2 = this.state) === null || _this$state2 === void 0 ? void 0 : _this$state2.localFindText)) {
|
|
210
|
+
this.focusFindTextfield();
|
|
211
|
+
}
|
|
212
|
+
if (this.props.findText !== prevProps.findText) {
|
|
213
|
+
this.syncFindText(() => {
|
|
214
|
+
// focus after input is synced if find text provided
|
|
215
|
+
if (this.props.findText) {
|
|
216
|
+
this.focusFindTextfield();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
componentWillUnmount() {
|
|
223
|
+
this.debouncedFind.cancel();
|
|
224
|
+
this.handleFindKeyDownThrottled.cancel();
|
|
225
|
+
}
|
|
226
|
+
render() {
|
|
227
|
+
const {
|
|
228
|
+
findText,
|
|
229
|
+
count,
|
|
230
|
+
allowMatchCase,
|
|
231
|
+
shouldMatchCase,
|
|
232
|
+
intl: {
|
|
233
|
+
formatMessage
|
|
234
|
+
}
|
|
235
|
+
} = this.props;
|
|
236
|
+
const resultsCount = formatMessage(messages.resultsCount, {
|
|
237
|
+
selectedMatchPosition: count.index + 1,
|
|
238
|
+
totalResultsCount: count.total
|
|
239
|
+
});
|
|
240
|
+
return jsx("div", {
|
|
241
|
+
css: sectionWrapperStyles
|
|
242
|
+
}, jsx(Textfield, {
|
|
243
|
+
name: "find",
|
|
244
|
+
appearance: "none",
|
|
245
|
+
placeholder: this.find,
|
|
246
|
+
value: this.state.localFindText,
|
|
247
|
+
ref: this.findTextfieldRef,
|
|
248
|
+
autoComplete: "off",
|
|
249
|
+
onChange: this.handleFindChange,
|
|
250
|
+
onKeyDown: this.handleFindKeyDown,
|
|
251
|
+
onKeyUp: this.handleFindKeyUp,
|
|
252
|
+
onBlur: this.props.onFindBlur,
|
|
253
|
+
onCompositionStart: this.handleCompositionStart,
|
|
254
|
+
onCompositionEnd: this.handleCompositionEnd
|
|
255
|
+
}), jsx("div", {
|
|
256
|
+
css: countWrapperStyles,
|
|
257
|
+
"aria-live": "polite"
|
|
258
|
+
}, findText && jsx("span", {
|
|
259
|
+
"data-testid": "textfield-count",
|
|
260
|
+
css: countStyles
|
|
261
|
+
}, count.total === 0 ? this.noResultsFound : resultsCount)), allowMatchCase && jsx(FindReplaceTooltipButton, {
|
|
262
|
+
title: this.matchCase,
|
|
263
|
+
icon: this.matchCaseIcon,
|
|
264
|
+
onClick: this.handleMatchCaseClick,
|
|
265
|
+
isPressed: shouldMatchCase
|
|
266
|
+
}), jsx(FindReplaceTooltipButton, {
|
|
267
|
+
title: this.findNext,
|
|
268
|
+
icon: this.findNextIcon,
|
|
269
|
+
keymapDescription: 'Enter',
|
|
270
|
+
onClick: this.handleFindNextClick,
|
|
271
|
+
disabled: count.total <= 1
|
|
272
|
+
}), jsx(FindReplaceTooltipButton, {
|
|
273
|
+
title: this.findPrevious,
|
|
274
|
+
icon: this.findPrevIcon,
|
|
275
|
+
keymapDescription: 'Shift Enter',
|
|
276
|
+
onClick: this.handleFindPrevClick,
|
|
277
|
+
disabled: count.total <= 1
|
|
278
|
+
}), jsx(FindReplaceTooltipButton, {
|
|
279
|
+
title: this.closeFindReplaceDialog,
|
|
280
|
+
icon: this.closeIcon,
|
|
281
|
+
keymapDescription: 'Escape',
|
|
282
|
+
onClick: this.clearSearch
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export default injectIntl(Find);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
|
|
3
|
+
/* eslint-disable @atlaskit/design-system/prefer-primitives */
|
|
4
|
+
/** @jsx jsx */
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { jsx } from '@emotion/react';
|
|
7
|
+
import Find from './Find';
|
|
8
|
+
import Replace from './Replace';
|
|
9
|
+
import { ruleStyles, wrapperStyles } from './styles';
|
|
10
|
+
// eslint-disable-next-line @repo/internal/react/no-class-components
|
|
11
|
+
class FindReplace extends React.PureComponent {
|
|
12
|
+
constructor(...args) {
|
|
13
|
+
super(...args);
|
|
14
|
+
_defineProperty(this, "findTextfield", null);
|
|
15
|
+
_defineProperty(this, "replaceTextfield", null);
|
|
16
|
+
_defineProperty(this, "setFindTextfieldRef", findTextfieldRef => {
|
|
17
|
+
this.findTextfield = findTextfieldRef.current;
|
|
18
|
+
});
|
|
19
|
+
_defineProperty(this, "setReplaceTextfieldRef", replaceTextfieldRef => {
|
|
20
|
+
this.replaceTextfield = replaceTextfieldRef.current;
|
|
21
|
+
});
|
|
22
|
+
_defineProperty(this, "setFocusToFind", () => {
|
|
23
|
+
if (this.findTextfield) {
|
|
24
|
+
this.findTextfield.focus();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
_defineProperty(this, "setFocusToReplace", () => {
|
|
28
|
+
if (this.replaceTextfield) {
|
|
29
|
+
this.replaceTextfield.focus();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
const {
|
|
35
|
+
findText,
|
|
36
|
+
count,
|
|
37
|
+
shouldFocus,
|
|
38
|
+
onFind,
|
|
39
|
+
onFindBlur,
|
|
40
|
+
onFindNext,
|
|
41
|
+
onFindPrev,
|
|
42
|
+
onCancel,
|
|
43
|
+
replaceText,
|
|
44
|
+
onReplace,
|
|
45
|
+
onReplaceAll,
|
|
46
|
+
dispatchAnalyticsEvent,
|
|
47
|
+
allowMatchCase,
|
|
48
|
+
shouldMatchCase,
|
|
49
|
+
onToggleMatchCase
|
|
50
|
+
} = this.props;
|
|
51
|
+
return jsx("div", {
|
|
52
|
+
css: wrapperStyles
|
|
53
|
+
}, jsx(Find, {
|
|
54
|
+
allowMatchCase: allowMatchCase,
|
|
55
|
+
shouldMatchCase: shouldMatchCase,
|
|
56
|
+
onToggleMatchCase: onToggleMatchCase,
|
|
57
|
+
findText: findText,
|
|
58
|
+
count: count,
|
|
59
|
+
shouldFocus: shouldFocus,
|
|
60
|
+
onFind: onFind,
|
|
61
|
+
onFindBlur: onFindBlur,
|
|
62
|
+
onFindPrev: onFindPrev,
|
|
63
|
+
onFindNext: onFindNext,
|
|
64
|
+
onFindTextfieldRefSet: this.setFindTextfieldRef,
|
|
65
|
+
onCancel: onCancel,
|
|
66
|
+
onArrowDown: this.setFocusToReplace
|
|
67
|
+
}), jsx("hr", {
|
|
68
|
+
css: ruleStyles,
|
|
69
|
+
id: "replace-hr-element"
|
|
70
|
+
}), jsx(Replace, {
|
|
71
|
+
canReplace: count.total > 0,
|
|
72
|
+
replaceText: replaceText,
|
|
73
|
+
onReplace: onReplace,
|
|
74
|
+
onReplaceAll: onReplaceAll,
|
|
75
|
+
onReplaceTextfieldRefSet: this.setReplaceTextfieldRef,
|
|
76
|
+
onArrowUp: this.setFocusToFind,
|
|
77
|
+
dispatchAnalyticsEvent: dispatchAnalyticsEvent
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export default FindReplace;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
2
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
3
|
+
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
|
|
4
|
+
/** @jsx jsx */
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { css, jsx } from '@emotion/react';
|
|
7
|
+
import { defineMessages, injectIntl } from 'react-intl-next';
|
|
8
|
+
import { TRIGGER_METHOD } from '@atlaskit/editor-common/analytics';
|
|
9
|
+
import { findKeymapByDescription, getAriaKeyshortcuts, tooltip, ToolTipContent } from '@atlaskit/editor-common/keymaps';
|
|
10
|
+
import { ArrowKeyNavigationType, Dropdown, TOOLBAR_BUTTON, ToolbarButton } from '@atlaskit/editor-common/ui-menu';
|
|
11
|
+
import { akEditorFloatingPanelZIndex, akEditorMobileMaxWidth } from '@atlaskit/editor-shared-styles';
|
|
12
|
+
import EditorSearchIcon from '@atlaskit/icon/glyph/editor/search';
|
|
13
|
+
import FindReplace from './FindReplace';
|
|
14
|
+
const toolbarButtonWrapper = css`
|
|
15
|
+
display: flex;
|
|
16
|
+
flex: 1 1 auto;
|
|
17
|
+
flex-grow: 0;
|
|
18
|
+
justify-content: flex-end;
|
|
19
|
+
align-items: center;
|
|
20
|
+
padding: 0 ${"var(--ds-space-100, 8px)"};
|
|
21
|
+
@media (max-width: ${akEditorMobileMaxWidth}px) {
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
const toolbarButtonWrapperFullWith = css`
|
|
27
|
+
flex-grow: 1;
|
|
28
|
+
`;
|
|
29
|
+
const wrapper = css`
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
`;
|
|
33
|
+
const messages = defineMessages({
|
|
34
|
+
findReplaceToolbarButton: {
|
|
35
|
+
id: 'fabric.editor.findReplaceToolbarButton',
|
|
36
|
+
defaultMessage: 'Find and replace',
|
|
37
|
+
description: '"Find" highlights all instances of a word or phrase on the document, and "Replace" changes one or all of those instances to something else'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// eslint-disable-next-line @repo/internal/react/no-class-components
|
|
41
|
+
class FindReplaceToolbarButton extends React.PureComponent {
|
|
42
|
+
constructor(...args) {
|
|
43
|
+
super(...args);
|
|
44
|
+
_defineProperty(this, "toggleOpen", () => {
|
|
45
|
+
if (this.props.isActive) {
|
|
46
|
+
this.props.onCancel({
|
|
47
|
+
triggerMethod: TRIGGER_METHOD.TOOLBAR
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
this.props.onActivate();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
render() {
|
|
55
|
+
const {
|
|
56
|
+
popupsMountPoint,
|
|
57
|
+
popupsBoundariesElement,
|
|
58
|
+
popupsScrollableElement,
|
|
59
|
+
isReducedSpacing,
|
|
60
|
+
findText,
|
|
61
|
+
replaceText,
|
|
62
|
+
isActive,
|
|
63
|
+
index,
|
|
64
|
+
numMatches,
|
|
65
|
+
intl: {
|
|
66
|
+
formatMessage
|
|
67
|
+
},
|
|
68
|
+
takeFullWidth
|
|
69
|
+
} = this.props;
|
|
70
|
+
const title = formatMessage(messages.findReplaceToolbarButton);
|
|
71
|
+
const stackBelowOtherEditorFloatingPanels = akEditorFloatingPanelZIndex - 1;
|
|
72
|
+
const keymap = findKeymapByDescription('Find');
|
|
73
|
+
return jsx("div", {
|
|
74
|
+
css: [toolbarButtonWrapper, takeFullWidth && toolbarButtonWrapperFullWith]
|
|
75
|
+
}, jsx(Dropdown, {
|
|
76
|
+
mountTo: popupsMountPoint,
|
|
77
|
+
boundariesElement: popupsBoundariesElement,
|
|
78
|
+
scrollableElement: popupsScrollableElement,
|
|
79
|
+
isOpen: isActive,
|
|
80
|
+
handleEscapeKeydown: () => {
|
|
81
|
+
if (isActive) {
|
|
82
|
+
this.props.onCancel({
|
|
83
|
+
triggerMethod: TRIGGER_METHOD.KEYBOARD
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
fitWidth: 352,
|
|
88
|
+
zIndex: stackBelowOtherEditorFloatingPanels,
|
|
89
|
+
arrowKeyNavigationProviderOptions: {
|
|
90
|
+
type: ArrowKeyNavigationType.MENU,
|
|
91
|
+
disableArrowKeyNavigation: true
|
|
92
|
+
},
|
|
93
|
+
trigger: jsx(ToolbarButton, {
|
|
94
|
+
buttonId: TOOLBAR_BUTTON.FIND_REPLACE,
|
|
95
|
+
spacing: isReducedSpacing ? 'none' : 'default',
|
|
96
|
+
selected: isActive,
|
|
97
|
+
title: jsx(ToolTipContent, {
|
|
98
|
+
description: title,
|
|
99
|
+
keymap: keymap
|
|
100
|
+
}),
|
|
101
|
+
iconBefore: jsx(EditorSearchIcon, {
|
|
102
|
+
label: title
|
|
103
|
+
}),
|
|
104
|
+
onClick: this.toggleOpen,
|
|
105
|
+
"aria-expanded": isActive,
|
|
106
|
+
"aria-haspopup": true,
|
|
107
|
+
"aria-label": keymap ? tooltip(keymap, title) : title,
|
|
108
|
+
"aria-keyshortcuts": getAriaKeyshortcuts(keymap)
|
|
109
|
+
})
|
|
110
|
+
}, jsx("div", {
|
|
111
|
+
css: wrapper
|
|
112
|
+
}, jsx(FindReplace, _extends({
|
|
113
|
+
findText: findText,
|
|
114
|
+
replaceText: replaceText,
|
|
115
|
+
count: {
|
|
116
|
+
index,
|
|
117
|
+
total: numMatches
|
|
118
|
+
}
|
|
119
|
+
}, this.props)))));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export default injectIntl(FindReplaceToolbarButton);
|