@seafile/sdoc-editor 0.4.7 → 0.4.8
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/dist/basic-sdk/assets/css/layout.css +12 -2
- package/dist/basic-sdk/constants/index.js +3 -1
- package/dist/basic-sdk/editor/sdoc-editor.js +7 -2
- package/dist/basic-sdk/extension/constants/menus-config.js +6 -0
- package/dist/basic-sdk/extension/plugins/code-block/render-elem.js +4 -0
- package/dist/basic-sdk/extension/plugins/image/hover-menu/index.js +6 -0
- package/dist/basic-sdk/extension/plugins/image/plugin.js +8 -2
- package/dist/basic-sdk/extension/plugins/image/render-elem.js +5 -1
- package/dist/basic-sdk/extension/plugins/index.js +3 -2
- package/dist/basic-sdk/extension/plugins/search-replace/constant.js +2 -0
- package/dist/basic-sdk/extension/plugins/search-replace/helper.js +331 -0
- package/dist/basic-sdk/extension/plugins/search-replace/index.js +10 -0
- package/dist/basic-sdk/extension/plugins/search-replace/menu/index.css +14 -0
- package/dist/basic-sdk/extension/plugins/search-replace/menu/index.js +78 -0
- package/dist/basic-sdk/extension/plugins/search-replace/plugin.js +21 -0
- package/dist/basic-sdk/extension/plugins/search-replace/popover/index.css +82 -0
- package/dist/basic-sdk/extension/plugins/search-replace/popover/index.js +211 -0
- package/dist/basic-sdk/extension/plugins/search-replace/popover/replace-all-confirm-modal.js +36 -0
- package/dist/basic-sdk/extension/toolbar/header-toolbar/index.js +7 -1
- package/dist/basic-sdk/utils/debounce.js +14 -0
- package/package.json +1 -1
- package/public/locales/cs/sdoc-editor.json +15 -3
- package/public/locales/de/sdoc-editor.json +15 -3
- package/public/locales/en/sdoc-editor.json +15 -3
- package/public/locales/es/sdoc-editor.json +15 -3
- package/public/locales/fr/sdoc-editor.json +15 -3
- package/public/locales/it/sdoc-editor.json +15 -3
- package/public/locales/ru/sdoc-editor.json +15 -3
- package/public/locales/zh_CN/sdoc-editor.json +15 -3
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
.sdoc-editor-container .sdoc-editor-toolbar {
|
|
9
9
|
display: flex;
|
|
10
|
+
flex: 1;
|
|
10
11
|
justify-content: center;
|
|
12
|
+
position: relative;
|
|
11
13
|
height: 44px;
|
|
12
14
|
align-items: center;
|
|
13
15
|
padding: 0 10px;
|
|
@@ -18,6 +20,14 @@
|
|
|
18
20
|
z-index: 102;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
.sdoc-editor-container .sdoc-editor-toolbar .sdoc-editor-toolbar-right-menu {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row-reverse;
|
|
26
|
+
position: absolute;
|
|
27
|
+
right: 20px;
|
|
28
|
+
border-right: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
.sdoc-editor-container .sdoc-editor-content {
|
|
22
32
|
width: 100%;
|
|
23
33
|
height: calc(100% - 44px);
|
|
@@ -64,7 +74,7 @@
|
|
|
64
74
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.06);
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
.sdoc-editor-container .sdoc-editor-content .article
|
|
77
|
+
.sdoc-editor-container .sdoc-editor-content .article>div {
|
|
68
78
|
caret-color: blue;
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -77,7 +87,7 @@
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
.sdoc-editor-container .sdoc-editor-content .article .sdoc-draging {
|
|
80
|
-
border-bottom: 2px solid rgba(35,131,226);
|
|
90
|
+
border-bottom: 2px solid rgba(35, 131, 226);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
.sdoc-editor-container .seafile-block-container {
|
|
@@ -14,7 +14,9 @@ export const INTERNAL_EVENT = {
|
|
|
14
14
|
UPDATE_TAG_VIEW: 'update_tag_view',
|
|
15
15
|
COMMENT_LIST_CLICK: 'comment_list_click',
|
|
16
16
|
UNSEEN_NOTIFICATIONS_COUNT: 'unseen_notifications_count',
|
|
17
|
-
CLOSE_CALLOUT_COLOR_PICKER: 'close_callout_color_picker'
|
|
17
|
+
CLOSE_CALLOUT_COLOR_PICKER: 'close_callout_color_picker',
|
|
18
|
+
OPEN_SEARCH_REPLACE_MODAL: 'open_search_replace_modal',
|
|
19
|
+
UPDATE_SEARCH_REPLACE_HIGHLIGHT: 'update_search_replace_highlight'
|
|
18
20
|
};
|
|
19
21
|
export const REVISION_DIFF_KEY = 'diff';
|
|
20
22
|
export const REVISION_DIFF_VALUE = '1';
|
|
@@ -4,7 +4,7 @@ import { Editor } from '@seafile/slate';
|
|
|
4
4
|
import deepCopy from 'deep-copy';
|
|
5
5
|
import context from '../../context';
|
|
6
6
|
import CommonLoading from '../../components/common-loading';
|
|
7
|
-
import { PAGE_EDIT_AREA_WIDTH } from '../constants';
|
|
7
|
+
import { INTERNAL_EVENT, PAGE_EDIT_AREA_WIDTH } from '../constants';
|
|
8
8
|
import { createDefaultEditor } from '../extension';
|
|
9
9
|
import withNodeId from '../node-id';
|
|
10
10
|
import { withSocketIO } from '../socket';
|
|
@@ -143,6 +143,11 @@ const SdocEditor = forwardRef((_ref, ref) => {
|
|
|
143
143
|
isShowComment: true
|
|
144
144
|
})))));
|
|
145
145
|
}
|
|
146
|
+
const onValueChange = value => {
|
|
147
|
+
const eventBus = EventBus.getInstance();
|
|
148
|
+
eventBus.dispatch(INTERNAL_EVENT.UPDATE_SEARCH_REPLACE_HIGHLIGHT, value);
|
|
149
|
+
setSlateValue(value);
|
|
150
|
+
};
|
|
146
151
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(EditorContainer, {
|
|
147
152
|
editor: validEditor
|
|
148
153
|
}, /*#__PURE__*/React.createElement(CollaboratorsProvider, null, /*#__PURE__*/React.createElement(ColorProvider, null, /*#__PURE__*/React.createElement(HeaderToolbar, {
|
|
@@ -153,7 +158,7 @@ const SdocEditor = forwardRef((_ref, ref) => {
|
|
|
153
158
|
}, /*#__PURE__*/React.createElement(EditableArticle, {
|
|
154
159
|
editor: validEditor,
|
|
155
160
|
slateValue: slateValue,
|
|
156
|
-
updateSlateValue:
|
|
161
|
+
updateSlateValue: onValueChange
|
|
157
162
|
}))))), /*#__PURE__*/React.createElement(InsertElementDialog, {
|
|
158
163
|
editor: validEditor
|
|
159
164
|
}));
|
|
@@ -5,6 +5,7 @@ export const REDO = 'redo';
|
|
|
5
5
|
export const CLEAR_FORMAT = 'clear_format';
|
|
6
6
|
export const REMOVE_TABLE = 'remove_table';
|
|
7
7
|
export const COMBINE_CELL = 'combine_cell';
|
|
8
|
+
export const SEARCH_REPLACE = 'search_replace';
|
|
8
9
|
|
|
9
10
|
// text style
|
|
10
11
|
export const TEXT_STYLE = 'text_style';
|
|
@@ -195,6 +196,11 @@ export const MENUS_CONFIG_MAP = {
|
|
|
195
196
|
id: "sdoc_".concat(CALL_OUT),
|
|
196
197
|
iconClass: 'sdocfont sdoc-callout',
|
|
197
198
|
text: 'Callout'
|
|
199
|
+
},
|
|
200
|
+
[SEARCH_REPLACE]: {
|
|
201
|
+
id: "sdoc_".concat(SEARCH_REPLACE),
|
|
202
|
+
iconClass: 'sdocfont sdoc-find-replace',
|
|
203
|
+
text: 'Search_and_replace'
|
|
198
204
|
}
|
|
199
205
|
};
|
|
200
206
|
|
|
@@ -130,6 +130,9 @@ const CodeBlock = _ref => {
|
|
|
130
130
|
eventBus.subscribe(INTERNAL_EVENT.HIDDEN_CODE_BLOCK_HOVER_MENU, onHiddenHoverMenu);
|
|
131
131
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
132
|
}, []);
|
|
133
|
+
const handleScroll = () => {
|
|
134
|
+
EventBus.getInstance().dispatch(INTERNAL_EVENT.UPDATE_SEARCH_REPLACE_HIGHLIGHT);
|
|
135
|
+
};
|
|
133
136
|
return /*#__PURE__*/React.createElement("div", Object.assign({
|
|
134
137
|
"data-id": element.id,
|
|
135
138
|
"data-root": "true"
|
|
@@ -138,6 +141,7 @@ const CodeBlock = _ref => {
|
|
|
138
141
|
onClick: onFocusCodeBlock,
|
|
139
142
|
onMouseLeave: onMouseLeave
|
|
140
143
|
}), /*#__PURE__*/React.createElement("pre", {
|
|
144
|
+
onScroll: handleScroll,
|
|
141
145
|
className: 'sdoc-code-block-pre',
|
|
142
146
|
ref: codeBlockRef
|
|
143
147
|
}, /*#__PURE__*/React.createElement("code", {
|
|
@@ -18,6 +18,7 @@ const ImageHoverMenu = _ref => {
|
|
|
18
18
|
menuPosition,
|
|
19
19
|
element,
|
|
20
20
|
parentNodeEntry,
|
|
21
|
+
imageCaptionInputRef,
|
|
21
22
|
onHideImageHoverMenu,
|
|
22
23
|
t
|
|
23
24
|
} = _ref;
|
|
@@ -130,6 +131,11 @@ const ImageHoverMenu = _ref => {
|
|
|
130
131
|
}, {
|
|
131
132
|
at: path
|
|
132
133
|
});
|
|
134
|
+
queueMicrotask(() => {
|
|
135
|
+
if (imageCaptionInputRef.current) {
|
|
136
|
+
imageCaptionInputRef.current.focus();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
133
139
|
return;
|
|
134
140
|
}
|
|
135
141
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Transforms, Path, Editor, Element } from '@seafile/slate';
|
|
1
|
+
import { Transforms, Path, Editor, Element, Range } from '@seafile/slate';
|
|
2
2
|
import toaster from '../../../../components/toast';
|
|
3
3
|
import context from '../../../../context';
|
|
4
4
|
import EventBus from '../../../utils/event-bus';
|
|
5
5
|
import { insertImage, hasSdocImages, getImageData, queryCopyMoveProgressView, resetCursor, isSingleImage } from './helpers';
|
|
6
|
-
import { focusEditor, generateEmptyElement } from '../../core';
|
|
6
|
+
import { focusEditor, generateEmptyElement, isBlockAboveEmpty } from '../../core';
|
|
7
7
|
import { getErrorMsg } from '../../../../utils';
|
|
8
8
|
import { getSlateFragmentAttribute } from '../../../utils/document-utils';
|
|
9
9
|
import { INSERT_POSITION, CLIPBOARD_FORMAT_KEY, CLIPBOARD_ORIGIN_SDOC_KEY, IMAGE, IMAGE_BLOCK, PARAGRAPH } from '../../constants';
|
|
@@ -110,10 +110,16 @@ const withImage = editor => {
|
|
|
110
110
|
const {
|
|
111
111
|
selection
|
|
112
112
|
} = editor;
|
|
113
|
+
const focusPoint = Editor.before(editor, selection);
|
|
113
114
|
const point = Editor.before(editor, selection, {
|
|
114
115
|
distance: 2
|
|
115
116
|
});
|
|
116
117
|
const [node, path] = Editor.node(editor, [point.path[0], point.path[1]]);
|
|
118
|
+
if (Range.isCollapsed(selection) && isBlockAboveEmpty(editor) && !Path.isCommon(path, selection.anchor.path)) {
|
|
119
|
+
deleteBackward(unit);
|
|
120
|
+
focusEditor(newEditor, Editor.end(newEditor, focusPoint));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
117
123
|
if (Element.isElement(node) && node.type === IMAGE) {
|
|
118
124
|
// If the wrapping element is image_block, delete the wrapping element
|
|
119
125
|
const [parentNode, p] = Editor.node(editor, [path[0]]);
|
|
@@ -39,6 +39,7 @@ const Image = _ref => {
|
|
|
39
39
|
const readOnly = useReadOnly();
|
|
40
40
|
const imageRef = useRef(null);
|
|
41
41
|
const resizerRef = useRef(null);
|
|
42
|
+
const imageCaptionInputRef = useRef(null);
|
|
42
43
|
const scrollRef = useScrollContext();
|
|
43
44
|
const [movingWidth, setMovingWidth] = useState(null);
|
|
44
45
|
const [isResizing, setIsResizing] = useState(false);
|
|
@@ -231,15 +232,17 @@ const Image = _ref => {
|
|
|
231
232
|
contentEditable: false
|
|
232
233
|
}, /*#__PURE__*/React.createElement("span", null, t('Width'), ':', parseInt(movingWidth || ((_imageRef$current = imageRef.current) === null || _imageRef$current === void 0 ? void 0 : _imageRef$current.clientWidth))), /*#__PURE__*/React.createElement("span", null, "\xA0\xA0"), /*#__PURE__*/React.createElement("span", null, t('Height'), ':', imageRef.current.clientHeight))), nodeEntry[0].type === IMAGE_BLOCK && show_caption && /*#__PURE__*/React.createElement("input", {
|
|
233
234
|
id: "sdoc-image-caption-input",
|
|
235
|
+
ref: imageCaptionInputRef,
|
|
234
236
|
className: "sdoc-image-caption-input-wrapper",
|
|
235
237
|
style: {
|
|
236
238
|
width: (data === null || data === void 0 ? void 0 : data.width) || ((_imageRef$current2 = imageRef.current) === null || _imageRef$current2 === void 0 ? void 0 : _imageRef$current2.clientWidth)
|
|
237
239
|
},
|
|
238
240
|
placeholder: t('Insert_caption'),
|
|
241
|
+
autoComplete: "off",
|
|
239
242
|
value: caption,
|
|
240
243
|
onBlur: onSetCaption,
|
|
241
244
|
onChange: e => {
|
|
242
|
-
setCaption(e.target.value
|
|
245
|
+
setCaption(e.target.value);
|
|
243
246
|
},
|
|
244
247
|
onCompositionStart: e => {
|
|
245
248
|
e.stopPropagation();
|
|
@@ -249,6 +252,7 @@ const Image = _ref => {
|
|
|
249
252
|
menuPosition: menuPosition,
|
|
250
253
|
element: element,
|
|
251
254
|
parentNodeEntry: nodeEntry,
|
|
255
|
+
imageCaptionInputRef: imageCaptionInputRef,
|
|
252
256
|
onHideImageHoverMenu: () => {
|
|
253
257
|
setIsShowImageHoverMenu(false);
|
|
254
258
|
}
|
|
@@ -15,6 +15,7 @@ import SdocLinkPlugin from './sdoc-link';
|
|
|
15
15
|
import FileLinkPlugin from './file-link';
|
|
16
16
|
import ParagraphPlugin from './paragraph';
|
|
17
17
|
import CalloutPlugin from './callout';
|
|
18
|
-
|
|
18
|
+
import SearchReplacePlugin from './search-replace';
|
|
19
|
+
const Plugins = [MarkDownPlugin, HtmlPlugin, HeaderPlugin, LinkPlugin, BlockquotePlugin, ListPlugin, CheckListPlugin, CodeBlockPlugin, ImagePlugin, TablePlugin, TextPlugin, TextAlignPlugin, FontPlugin, SdocLinkPlugin, FileLinkPlugin, CalloutPlugin, SearchReplacePlugin];
|
|
19
20
|
export default Plugins;
|
|
20
|
-
export { MarkDownPlugin, HeaderPlugin, LinkPlugin, BlockquotePlugin, ListPlugin, CheckListPlugin, CodeBlockPlugin, ImagePlugin, TablePlugin, TextPlugin, HtmlPlugin, TextAlignPlugin, FontPlugin, SdocLinkPlugin, ParagraphPlugin, FileLinkPlugin, CalloutPlugin };
|
|
21
|
+
export { MarkDownPlugin, HeaderPlugin, LinkPlugin, BlockquotePlugin, ListPlugin, CheckListPlugin, CodeBlockPlugin, ImagePlugin, TablePlugin, TextPlugin, HtmlPlugin, TextAlignPlugin, FontPlugin, SdocLinkPlugin, ParagraphPlugin, FileLinkPlugin, CalloutPlugin, SearchReplacePlugin };
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
import { Editor, Element, Node, Text, Transforms } from '@seafile/slate';
|
|
3
|
+
import { ReactEditor } from '@seafile/slate-react';
|
|
4
|
+
import { CODE_BLOCK, IMAGE } from '../../constants';
|
|
5
|
+
import { DEFAULT_SEARCH_HIGHLIGHT_FILL_COLOR, FOCUSSED_SEARCH_HIGHLIGHT_FILL_COLOR } from './constant';
|
|
6
|
+
|
|
7
|
+
// Check the node iff contains text or inline node
|
|
8
|
+
const isInlineContainer = (editor, node) => {
|
|
9
|
+
if (Text.isText(node)) return false;
|
|
10
|
+
if (node.children) {
|
|
11
|
+
return node.children.every(child => Text.isText(child) || Editor.isInline(editor, child));
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
const formatTextEntries = textEntries => {
|
|
16
|
+
return textEntries.reduce((pre, cur) => {
|
|
17
|
+
var _pre$passedLength, _pre;
|
|
18
|
+
const currentLength = cur[0].text.length;
|
|
19
|
+
const previousLength = (_pre$passedLength = (_pre = pre[pre.length - 1]) === null || _pre === void 0 ? void 0 : _pre.passedLength) !== null && _pre$passedLength !== void 0 ? _pre$passedLength : 0;
|
|
20
|
+
const currentItem = {
|
|
21
|
+
passedLength: previousLength + currentLength,
|
|
22
|
+
textEntry: [...cur]
|
|
23
|
+
};
|
|
24
|
+
return pre.concat(currentItem);
|
|
25
|
+
}, []);
|
|
26
|
+
};
|
|
27
|
+
const splitTextNode = node => {
|
|
28
|
+
return node.children.reduce((pre, cur) => {
|
|
29
|
+
if (cur.type === IMAGE) {
|
|
30
|
+
pre.push(_objectSpread(_objectSpread({}, node), {}, {
|
|
31
|
+
children: []
|
|
32
|
+
}));
|
|
33
|
+
} else {
|
|
34
|
+
pre[pre.length - 1].children.push(cur);
|
|
35
|
+
}
|
|
36
|
+
return pre;
|
|
37
|
+
}, [_objectSpread(_objectSpread({}, node), {}, {
|
|
38
|
+
children: []
|
|
39
|
+
})]);
|
|
40
|
+
};
|
|
41
|
+
const matchSearchWordPosition = (node, searchWord) => {
|
|
42
|
+
const content = Node.string(node);
|
|
43
|
+
const regex = new RegExp(searchWord, 'gi');
|
|
44
|
+
const matches = [...content.matchAll(regex)];
|
|
45
|
+
return matches.map(match => match.index) || [];
|
|
46
|
+
};
|
|
47
|
+
const getMatchedTextInfos = (editor, node, keyword) => {
|
|
48
|
+
const matchedTextNodeEntires = [];
|
|
49
|
+
if (node.children) {
|
|
50
|
+
// If node is text container, match keyword in text
|
|
51
|
+
if (isInlineContainer(editor, node)) {
|
|
52
|
+
const splitNodes = splitTextNode(node);
|
|
53
|
+
splitNodes.forEach(node => {
|
|
54
|
+
const textEntries = Array.from(Node.texts(node));
|
|
55
|
+
if (!textEntries) return;
|
|
56
|
+
const newTextEntries = formatTextEntries(textEntries);
|
|
57
|
+
const positions = matchSearchWordPosition(node, keyword);
|
|
58
|
+
const res = positions.reduce((pre, position) => {
|
|
59
|
+
const {
|
|
60
|
+
ranges,
|
|
61
|
+
startMatchIndex
|
|
62
|
+
} = pre;
|
|
63
|
+
let anchor;
|
|
64
|
+
for (let index = startMatchIndex; index < newTextEntries.length; index++) {
|
|
65
|
+
const {
|
|
66
|
+
passedLength,
|
|
67
|
+
textEntry
|
|
68
|
+
} = newTextEntries[index];
|
|
69
|
+
const passedNodeLength = passedLength - textEntry[0].text.length;
|
|
70
|
+
if (!anchor && passedLength > position) {
|
|
71
|
+
anchor = {
|
|
72
|
+
path: ReactEditor.findPath(editor, textEntry[0]),
|
|
73
|
+
offset: position - passedNodeLength
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (passedLength >= position + keyword.length) {
|
|
77
|
+
const range = {
|
|
78
|
+
anchor,
|
|
79
|
+
focus: {
|
|
80
|
+
path: ReactEditor.findPath(editor, textEntry[0]),
|
|
81
|
+
offset: position + keyword.length - passedNodeLength
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
ranges: [...ranges, range],
|
|
86
|
+
startMatchIndex: index
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return pre;
|
|
91
|
+
}, {
|
|
92
|
+
ranges: [],
|
|
93
|
+
startMatchIndex: 0
|
|
94
|
+
});
|
|
95
|
+
matchedTextNodeEntires.push(res.ranges);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return matchedTextNodeEntires;
|
|
100
|
+
};
|
|
101
|
+
const generateRangeWhenWrapLine = (editor, path, index, count, domRange, baseHeight) => {
|
|
102
|
+
let i = 0;
|
|
103
|
+
let j = 1;
|
|
104
|
+
let isOverrideForwardRange = true;
|
|
105
|
+
const subHighlightInfos = [];
|
|
106
|
+
while (j <= count) {
|
|
107
|
+
const subSplitRange = {
|
|
108
|
+
anchor: {
|
|
109
|
+
path,
|
|
110
|
+
offset: index + i
|
|
111
|
+
},
|
|
112
|
+
focus: {
|
|
113
|
+
path,
|
|
114
|
+
offset: index + j
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const subRange = ReactEditor.toDOMRange(editor, subSplitRange);
|
|
118
|
+
if (subRange.getBoundingClientRect().height === baseHeight) {
|
|
119
|
+
isOverrideForwardRange && subHighlightInfos.pop();
|
|
120
|
+
if (!isOverrideForwardRange) isOverrideForwardRange = true;
|
|
121
|
+
subHighlightInfos.push({
|
|
122
|
+
rangeInfo: subRange.getBoundingClientRect(),
|
|
123
|
+
domRange
|
|
124
|
+
});
|
|
125
|
+
j++;
|
|
126
|
+
} else {
|
|
127
|
+
i = j - 1;
|
|
128
|
+
isOverrideForwardRange = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return subHighlightInfos;
|
|
132
|
+
};
|
|
133
|
+
const findHighlightTextInfos = (editor, keyword) => {
|
|
134
|
+
const matchedBlockEntries = [...Editor.nodes(editor, {
|
|
135
|
+
match: n => {
|
|
136
|
+
if (Element.isElement(n) && Editor.isBlock(editor, n)) {
|
|
137
|
+
try {
|
|
138
|
+
const blockString = Node.string(n);
|
|
139
|
+
return blockString.toLowerCase().includes(keyword.toLowerCase());
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
mode: 'lowest',
|
|
146
|
+
at: []
|
|
147
|
+
})];
|
|
148
|
+
const matchedTextEntriesList = Array.from(matchedBlockEntries).reduce((pre, _ref) => {
|
|
149
|
+
let [node] = _ref;
|
|
150
|
+
return [...pre, ...getMatchedTextInfos(editor, node, keyword.toLowerCase())];
|
|
151
|
+
}, []).flat();
|
|
152
|
+
return matchedTextEntriesList;
|
|
153
|
+
};
|
|
154
|
+
const getBaseHeight = (editor, range) => {
|
|
155
|
+
const {
|
|
156
|
+
anchor: {
|
|
157
|
+
path
|
|
158
|
+
}
|
|
159
|
+
} = range;
|
|
160
|
+
const subRange = {
|
|
161
|
+
anchor: {
|
|
162
|
+
path,
|
|
163
|
+
offset: 0
|
|
164
|
+
},
|
|
165
|
+
focus: {
|
|
166
|
+
path,
|
|
167
|
+
offset: 1
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
return ReactEditor.toDOMRange(editor, subRange).getBoundingClientRect().height;
|
|
171
|
+
};
|
|
172
|
+
export const getHighlightInfos = (editor, keyword) => {
|
|
173
|
+
if (keyword === '') return [];
|
|
174
|
+
const highlightTextInfos = findHighlightTextInfos(editor, keyword);
|
|
175
|
+
const rangeList = highlightTextInfos === null || highlightTextInfos === void 0 ? void 0 : highlightTextInfos.map(range => {
|
|
176
|
+
const domRange = ReactEditor.toDOMRange(editor, range);
|
|
177
|
+
const rangeInfo = domRange.getBoundingClientRect();
|
|
178
|
+
const baseHeight = getBaseHeight(editor, range);
|
|
179
|
+
// highlight word wrap line, assume line height is more then letter height
|
|
180
|
+
if (rangeInfo.height > baseHeight) return generateRangeWhenWrapLine(editor, range.anchor.path, range.anchor.offset, keyword.length, domRange, baseHeight);
|
|
181
|
+
return [{
|
|
182
|
+
rangeInfo,
|
|
183
|
+
domRange
|
|
184
|
+
}];
|
|
185
|
+
});
|
|
186
|
+
return rangeList;
|
|
187
|
+
};
|
|
188
|
+
export const handleReplaceKeyword = (editor, highlightInfos, replacedContent) => {
|
|
189
|
+
if (!highlightInfos || !highlightInfos.length) return;
|
|
190
|
+
// Delete from backward avoiding the range changed
|
|
191
|
+
highlightInfos.reverse().forEach(highlightInfo => {
|
|
192
|
+
const {
|
|
193
|
+
domRange
|
|
194
|
+
} = highlightInfo[highlightInfo.length - 1];
|
|
195
|
+
const slateRange = ReactEditor.toSlateRange(editor, domRange, {
|
|
196
|
+
exactMatch: true
|
|
197
|
+
});
|
|
198
|
+
Transforms.insertText(editor, replacedContent, {
|
|
199
|
+
at: Editor.end(editor, slateRange)
|
|
200
|
+
});
|
|
201
|
+
Transforms.delete(editor, {
|
|
202
|
+
at: slateRange
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
export const clearCanvas = canvases => {
|
|
207
|
+
canvases.forEach(canvas => canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height));
|
|
208
|
+
};
|
|
209
|
+
export const scrollIntoView = (articleContainerTop, highlightX, highlightY, codeBlockDom, width) => {
|
|
210
|
+
if (!articleContainerTop) return;
|
|
211
|
+
const scrollContainer = document.getElementById('sdoc-scroll-container');
|
|
212
|
+
// Scroll into view when highlight block overflow y
|
|
213
|
+
const scrollTop = highlightY - articleContainerTop - 20;
|
|
214
|
+
const isOverflowY = scrollContainer.scrollTop > scrollTop || scrollContainer.scrollTop + scrollContainer.clientHeight < scrollTop;
|
|
215
|
+
isOverflowY && scrollContainer.scrollTo({
|
|
216
|
+
top: scrollTop
|
|
217
|
+
});
|
|
218
|
+
// Scroll into view when code block overflow x
|
|
219
|
+
if (codeBlockDom) {
|
|
220
|
+
let isOverflowX = false;
|
|
221
|
+
const codeBlockDomLeft = codeBlockDom.getBoundingClientRect().left;
|
|
222
|
+
const leftDistance = codeBlockDomLeft + 50;
|
|
223
|
+
const rightDistance = leftDistance + codeBlockDom.clientWidth - 50;
|
|
224
|
+
isOverflowX = leftDistance > highlightX + width || rightDistance < highlightX + width;
|
|
225
|
+
isOverflowX && codeBlockDom.scrollTo({
|
|
226
|
+
left: highlightX - leftDistance + width
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const getNowrapCodeBlockInfos = editor => {
|
|
231
|
+
const codeBlockEntries = Editor.nodes(editor, {
|
|
232
|
+
match: n => n.type === CODE_BLOCK && n.style['white_space'] === 'nowrap',
|
|
233
|
+
at: []
|
|
234
|
+
}) || [];
|
|
235
|
+
|
|
236
|
+
// Get code block dom and range info
|
|
237
|
+
const codeBlockInfos = Array.from(codeBlockEntries).map(_ref2 => {
|
|
238
|
+
let [codeBlockNode] = _ref2;
|
|
239
|
+
const codeBlockRange = ReactEditor.toDOMNode(editor, codeBlockNode).getBoundingClientRect();
|
|
240
|
+
return {
|
|
241
|
+
codeBlockRange,
|
|
242
|
+
codeBlockNode
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return codeBlockInfos;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Hide highlight block when overflow article container
|
|
249
|
+
const updateInfoAsMatchedInCodeBlock = (editor, codeBlockInfos, highlightX, highlightY, highlightHeight, highlightWidth) => {
|
|
250
|
+
if (!codeBlockInfos.length) return;
|
|
251
|
+
let codeBlockDom = null;
|
|
252
|
+
codeBlockInfos.some(_ref3 => {
|
|
253
|
+
let {
|
|
254
|
+
codeBlockRange,
|
|
255
|
+
codeBlockNode
|
|
256
|
+
} = _ref3;
|
|
257
|
+
const isInCodeBlockArea = codeBlockRange.y <= highlightY && codeBlockRange.y + codeBlockRange.height > highlightY + highlightHeight;
|
|
258
|
+
if (isInCodeBlockArea) {
|
|
259
|
+
codeBlockDom = ReactEditor.toDOMNode(editor, codeBlockNode).querySelector('.sdoc-code-block-pre');
|
|
260
|
+
const codeBlockRightSidePosition = codeBlockRange.x + codeBlockRange.width;
|
|
261
|
+
const isOverflowX = codeBlockRange.x > highlightX || codeBlockRightSidePosition < highlightX + highlightWidth;
|
|
262
|
+
if (isOverflowX) {
|
|
263
|
+
// Calculate forward and backward hidden width
|
|
264
|
+
const overflowForward = codeBlockRange.x - highlightX > 0 ? codeBlockRange.x - highlightX : 0;
|
|
265
|
+
const overflowBackward = highlightX + highlightWidth - codeBlockRightSidePosition > 0 ? highlightX + highlightWidth - codeBlockRightSidePosition : 0;
|
|
266
|
+
highlightWidth = highlightWidth - overflowForward - overflowBackward;
|
|
267
|
+
}
|
|
268
|
+
if (highlightWidth < 0) highlightWidth = 0;
|
|
269
|
+
if (highlightX < codeBlockRange.x) highlightX = codeBlockRange.x;
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
codeBlockDom,
|
|
276
|
+
highlightX,
|
|
277
|
+
highlightWidth
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
export const drawHighlights = function (editor, ranges, selectIndex) {
|
|
281
|
+
let isMoveIntoView = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
|
|
282
|
+
const canvases = document.querySelectorAll('.sdoc-find-search-highlight-canvas');
|
|
283
|
+
clearCanvas(canvases);
|
|
284
|
+
if (ranges.length === 0) return;
|
|
285
|
+
const articleContainer = document.querySelector('.sdoc-article-container');
|
|
286
|
+
const {
|
|
287
|
+
top,
|
|
288
|
+
left
|
|
289
|
+
} = articleContainer.getBoundingClientRect();
|
|
290
|
+
let rangeIndex = 0;
|
|
291
|
+
let splitRangeIndex = 0;
|
|
292
|
+
let canvasIndex = 0;
|
|
293
|
+
const codeBlockInfos = getNowrapCodeBlockInfos(editor);
|
|
294
|
+
do {
|
|
295
|
+
let canvas = canvases[canvasIndex];
|
|
296
|
+
if (!canvas) return;
|
|
297
|
+
const ctx = canvas.getContext('2d');
|
|
298
|
+
const splitRanges = ranges[rangeIndex];
|
|
299
|
+
for (let j = splitRangeIndex; j < splitRanges.length; j++) {
|
|
300
|
+
const isFocussedHighlight = rangeIndex === selectIndex;
|
|
301
|
+
let {
|
|
302
|
+
x,
|
|
303
|
+
y,
|
|
304
|
+
width,
|
|
305
|
+
height
|
|
306
|
+
} = splitRanges[j].rangeInfo;
|
|
307
|
+
let codeBlockDom = null;
|
|
308
|
+
if (y - top < (canvasIndex + 1) * 5000) {
|
|
309
|
+
// Hide highlight block when overflow article container
|
|
310
|
+
const updateInfo = updateInfoAsMatchedInCodeBlock(editor, codeBlockInfos, x, y, height, width);
|
|
311
|
+
if (updateInfo) {
|
|
312
|
+
x = updateInfo.highlightX;
|
|
313
|
+
width = updateInfo.highlightWidth;
|
|
314
|
+
if (isFocussedHighlight) codeBlockDom = updateInfo.codeBlockDom;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Draw highlight block
|
|
318
|
+
ctx.fillStyle = isFocussedHighlight ? FOCUSSED_SEARCH_HIGHLIGHT_FILL_COLOR : DEFAULT_SEARCH_HIGHLIGHT_FILL_COLOR;
|
|
319
|
+
ctx.fillRect(x - left, y - top - canvasIndex * 5000, width, height);
|
|
320
|
+
|
|
321
|
+
// Scroll into view
|
|
322
|
+
isMoveIntoView && isFocussedHighlight && scrollIntoView(top, x, y, codeBlockDom, width);
|
|
323
|
+
if (j === splitRanges.length - 1) rangeIndex++;
|
|
324
|
+
splitRangeIndex = 0;
|
|
325
|
+
} else {
|
|
326
|
+
splitRangeIndex = j;
|
|
327
|
+
canvasIndex = Math.ceil((y - top) / 5000 - 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} while (rangeIndex < ranges.length);
|
|
331
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SEARCH_REPLACE } from '../../constants/menus-config';
|
|
2
|
+
import SearchReplaceMenu from './menu';
|
|
3
|
+
import withSearchReplace from './plugin';
|
|
4
|
+
const SearchReplacePlugin = {
|
|
5
|
+
type: SEARCH_REPLACE,
|
|
6
|
+
editorMenus: [SearchReplaceMenu],
|
|
7
|
+
editorPlugin: withSearchReplace,
|
|
8
|
+
renderElements: []
|
|
9
|
+
};
|
|
10
|
+
export default SearchReplacePlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.sdoc-search-highlight-container {
|
|
2
|
+
position: absolute;
|
|
3
|
+
left: 0;
|
|
4
|
+
top: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
z-index: 1000;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
mix-blend-mode: multiply;
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.sdoc-find-search-highlight-canvas {
|
|
13
|
+
position: absolute;
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { MenuItem } from '../../../commons';
|
|
4
|
+
import { MENUS_CONFIG_MAP } from '../../../constants';
|
|
5
|
+
import { SEARCH_REPLACE } from '../../../constants/menus-config';
|
|
6
|
+
import SearchReplacePopover from '../popover';
|
|
7
|
+
import EventBus from '../../../../utils/event-bus';
|
|
8
|
+
import { INTERNAL_EVENT } from '../../../../constants';
|
|
9
|
+
import './index.css';
|
|
10
|
+
const menuConfig = MENUS_CONFIG_MAP[SEARCH_REPLACE];
|
|
11
|
+
const SearchReplaceMenu = _ref => {
|
|
12
|
+
let {
|
|
13
|
+
isRichEditor,
|
|
14
|
+
className,
|
|
15
|
+
editor
|
|
16
|
+
} = _ref;
|
|
17
|
+
const [isOpenPopover, setIsOpenPopover] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const eventBus = EventBus.getInstance();
|
|
20
|
+
const unsubscribe = eventBus.subscribe(INTERNAL_EVENT.OPEN_SEARCH_REPLACE_MODAL, () => setIsOpenPopover(true));
|
|
21
|
+
return () => unsubscribe();
|
|
22
|
+
}, [isOpenPopover]);
|
|
23
|
+
const onMouseDown = useCallback(() => {
|
|
24
|
+
setIsOpenPopover(!isOpenPopover);
|
|
25
|
+
}, [isOpenPopover]);
|
|
26
|
+
const articleContainer = document.querySelector('.sdoc-article-container');
|
|
27
|
+
const articleContainerSize = useMemo(() => {
|
|
28
|
+
const articleContainer = document.querySelector('.sdoc-article-container');
|
|
29
|
+
if (!articleContainer) return null;
|
|
30
|
+
const {
|
|
31
|
+
offsetHeight,
|
|
32
|
+
offsetWidth,
|
|
33
|
+
clientHeight
|
|
34
|
+
} = articleContainer;
|
|
35
|
+
return {
|
|
36
|
+
offsetHeight,
|
|
37
|
+
offsetWidth,
|
|
38
|
+
clientHeight
|
|
39
|
+
};
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [isOpenPopover]);
|
|
42
|
+
const renderCanvasses = useMemo(() => {
|
|
43
|
+
if (!isOpenPopover) return false;
|
|
44
|
+
const generateCount = Math.ceil(articleContainerSize.offsetHeight / 5000);
|
|
45
|
+
const canvasList = [];
|
|
46
|
+
for (let index = 0; index < generateCount; index++) {
|
|
47
|
+
const top = index * 5000;
|
|
48
|
+
canvasList.push( /*#__PURE__*/React.createElement("canvas", {
|
|
49
|
+
key: 'sdoc-find-search-' + index,
|
|
50
|
+
id: "sdoc-find-search-".concat(index),
|
|
51
|
+
className: "sdoc-find-search-highlight-canvas",
|
|
52
|
+
width: articleContainerSize.offsetWidth,
|
|
53
|
+
height: 5000,
|
|
54
|
+
style: {
|
|
55
|
+
top
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
return canvasList;
|
|
60
|
+
}, [articleContainerSize, isOpenPopover]);
|
|
61
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MenuItem, Object.assign({
|
|
62
|
+
isRichEditor: isRichEditor,
|
|
63
|
+
className: className,
|
|
64
|
+
disabled: false,
|
|
65
|
+
isActive: isOpenPopover,
|
|
66
|
+
onMouseDown: onMouseDown
|
|
67
|
+
}, menuConfig)), isOpenPopover && /*#__PURE__*/React.createElement(SearchReplacePopover, {
|
|
68
|
+
editor: editor,
|
|
69
|
+
isOpen: isOpenPopover,
|
|
70
|
+
closePopover: onMouseDown
|
|
71
|
+
}), isOpenPopover && createPortal( /*#__PURE__*/React.createElement("div", {
|
|
72
|
+
style: {
|
|
73
|
+
height: articleContainerSize.clientHeight
|
|
74
|
+
},
|
|
75
|
+
className: "sdoc-search-highlight-container"
|
|
76
|
+
}, renderCanvasses), articleContainer));
|
|
77
|
+
};
|
|
78
|
+
export default SearchReplaceMenu;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import isHotkey from 'is-hotkey';
|
|
2
|
+
import EventBus from '../../../utils/event-bus';
|
|
3
|
+
import { INTERNAL_EVENT } from '../../../constants';
|
|
4
|
+
const withSearchReplace = editor => {
|
|
5
|
+
const {
|
|
6
|
+
onHotKeyDown
|
|
7
|
+
} = editor;
|
|
8
|
+
const newEditor = editor;
|
|
9
|
+
newEditor.onHotKeyDown = event => {
|
|
10
|
+
if (isHotkey('mod+f', event)) {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
event.stopPropagation();
|
|
13
|
+
const eventBus = EventBus.getInstance();
|
|
14
|
+
eventBus.dispatch(INTERNAL_EVENT.OPEN_SEARCH_REPLACE_MODAL);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return onHotKeyDown && onHotKeyDown(event);
|
|
18
|
+
};
|
|
19
|
+
return newEditor;
|
|
20
|
+
};
|
|
21
|
+
export default withSearchReplace;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
.sdoc-search-replace-popover-container {
|
|
2
|
+
position: absolute;
|
|
3
|
+
width: 400px;
|
|
4
|
+
height: 280px;
|
|
5
|
+
z-index: 103;
|
|
6
|
+
background: #fff;
|
|
7
|
+
border: 1px solid #ebebeb;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sdoc-search-replace-popover-container .sdoc-search-replace-popover-title {
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
padding: 10px 15px;
|
|
14
|
+
border-bottom: 1px solid #ebebeb;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
user-select: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.sdoc-search-replace-popover-container .sdoc-search-replace-popover-title .sdoc-search-replace-title-text {
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
color: #333;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sdoc-search-replace-popover-container .sdoc-search-replace-popover-title .sdoc-search-replace-title-close {
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
pointer-events: all;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sdoc-search-replace-popover-container .sdoc-search-replace-popover-body {
|
|
30
|
+
padding: 10px 15px;
|
|
31
|
+
pointer-events: visibleFill;
|
|
32
|
+
pointer-events: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sdoc-search-replace-popover-body label,
|
|
36
|
+
.sdoc-search-replace-popover-body input,
|
|
37
|
+
.sdoc-search-replace-popover-body button {
|
|
38
|
+
pointer-events: auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sdoc-search-replace-popover-body label {
|
|
42
|
+
user-select: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sdoc-search-replace-popover-body .sdoc-replace-ipt-label {
|
|
46
|
+
margin-top: 10px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sdoc-search-replace-popover-body .sdoc-search-replace-popover-btn-group {
|
|
50
|
+
margin-top: 20px;
|
|
51
|
+
display: flex;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.sdoc-replace-all-confirm-modal {
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 0;
|
|
59
|
+
z-index: 104;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
background: rgba(0, 0, 0, 0.5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.sdoc-replace-ipt-container {
|
|
66
|
+
display: flex;
|
|
67
|
+
position: relative;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sdoc-replace-ipt-container input {
|
|
71
|
+
padding-right: 85px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sdoc-replace-ipt-container .sdoc-replace-ipt-tip {
|
|
75
|
+
position: absolute;
|
|
76
|
+
right: 15px;
|
|
77
|
+
top: 0;
|
|
78
|
+
height: 100%;
|
|
79
|
+
display: flex;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
align-items: center;
|
|
82
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import isHotkey from 'is-hotkey';
|
|
5
|
+
import { Input, Label } from 'reactstrap';
|
|
6
|
+
import debounce from '../../../../utils/debounce';
|
|
7
|
+
import { handleReplaceKeyword, getHighlightInfos, drawHighlights } from '../helper';
|
|
8
|
+
import ReplaceAllConfirmModal from './replace-all-confirm-modal';
|
|
9
|
+
import EventBus from '../../../../utils/event-bus';
|
|
10
|
+
import { INTERNAL_EVENT } from '../../../../constants';
|
|
11
|
+
import './index.css';
|
|
12
|
+
const SearchReplacePopover = _ref => {
|
|
13
|
+
let {
|
|
14
|
+
editor,
|
|
15
|
+
closePopover
|
|
16
|
+
} = _ref;
|
|
17
|
+
const [searchContent, setSearchContent] = useState('');
|
|
18
|
+
const [replacementContent, setReplacementContent] = useState('');
|
|
19
|
+
const [highlightInfos, setHighlightInfos] = useState([]);
|
|
20
|
+
const [isMoving, setIsMoving] = useState(false);
|
|
21
|
+
const [popoverPosition, setPopoverPosition] = useState({
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 100
|
|
24
|
+
});
|
|
25
|
+
const [currentSelectIndex, setCurrentSelectIndex] = useState(0);
|
|
26
|
+
const [isOpenReplaceAllModal, setIsOpenReplaceAllModal] = useState(false);
|
|
27
|
+
const pageInnerSizeRef = useRef({
|
|
28
|
+
x: window.innerWidth,
|
|
29
|
+
y: window.innerHeight
|
|
30
|
+
});
|
|
31
|
+
const shouldScrollIntoView = useRef(false);
|
|
32
|
+
const popoverContainerRef = useRef(null);
|
|
33
|
+
const searchInputRef = useRef(null);
|
|
34
|
+
const {
|
|
35
|
+
t
|
|
36
|
+
} = useTranslation();
|
|
37
|
+
const searchInputSuffixContent = useMemo(() => {
|
|
38
|
+
if (!!searchContent.length && !highlightInfos.length) return t('Search_not_found');
|
|
39
|
+
if (highlightInfos.length) return "".concat(currentSelectIndex + 1, " / ").concat(highlightInfos.length);
|
|
40
|
+
}, [currentSelectIndex, highlightInfos.length, searchContent.length, t]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setPopoverPosition({
|
|
43
|
+
x: pageInnerSizeRef.current.x - 420,
|
|
44
|
+
y: 95
|
|
45
|
+
});
|
|
46
|
+
}, []);
|
|
47
|
+
const handleDrawHighlight = useCallback((editor, keyword) => {
|
|
48
|
+
const newHighlightInfos = getHighlightInfos(editor, keyword);
|
|
49
|
+
setHighlightInfos(newHighlightInfos);
|
|
50
|
+
let newSelectIndex = currentSelectIndex;
|
|
51
|
+
if (!shouldScrollIntoView.current && newHighlightInfos.length !== highlightInfos.length) {
|
|
52
|
+
newSelectIndex = 0;
|
|
53
|
+
}
|
|
54
|
+
if (newSelectIndex >= newHighlightInfos.length) {
|
|
55
|
+
newSelectIndex = newHighlightInfos.length - 1;
|
|
56
|
+
}
|
|
57
|
+
if (newSelectIndex < 0 && newHighlightInfos.length) {
|
|
58
|
+
newSelectIndex = 0;
|
|
59
|
+
}
|
|
60
|
+
setCurrentSelectIndex(newSelectIndex);
|
|
61
|
+
}, [currentSelectIndex, highlightInfos.length, shouldScrollIntoView]);
|
|
62
|
+
const handleDrawHighlightLister = useCallback(() => {
|
|
63
|
+
handleDrawHighlight(editor, searchContent);
|
|
64
|
+
pageInnerSizeRef.current = {
|
|
65
|
+
x: window.innerWidth,
|
|
66
|
+
y: window.innerHeight
|
|
67
|
+
};
|
|
68
|
+
}, [editor, handleDrawHighlight, searchContent]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const highlightInfos = getHighlightInfos(editor, searchContent);
|
|
71
|
+
drawHighlights(editor, highlightInfos, currentSelectIndex, shouldScrollIntoView.current);
|
|
72
|
+
shouldScrollIntoView.current = false;
|
|
73
|
+
}, [currentSelectIndex, editor, searchContent, highlightInfos, shouldScrollIntoView]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const eventBus = EventBus.getInstance();
|
|
76
|
+
const unSubscribe = eventBus.subscribe(INTERNAL_EVENT.UPDATE_SEARCH_REPLACE_HIGHLIGHT, handleDrawHighlightLister);
|
|
77
|
+
return () => {
|
|
78
|
+
unSubscribe();
|
|
79
|
+
};
|
|
80
|
+
}, [editor, handleDrawHighlight, handleDrawHighlightLister, highlightInfos.length, searchContent]);
|
|
81
|
+
const handleSearchInputChange = useCallback(e => {
|
|
82
|
+
const keyword = e.target.value;
|
|
83
|
+
setSearchContent(keyword);
|
|
84
|
+
handleDrawHighlight(editor, keyword);
|
|
85
|
+
setCurrentSelectIndex(0);
|
|
86
|
+
}, [editor, handleDrawHighlight]);
|
|
87
|
+
const handleLast = useCallback(() => {
|
|
88
|
+
const currentIndex = currentSelectIndex === 0 ? highlightInfos.length - 1 : currentSelectIndex - 1;
|
|
89
|
+
setCurrentSelectIndex(currentIndex);
|
|
90
|
+
shouldScrollIntoView.current = true;
|
|
91
|
+
}, [currentSelectIndex, highlightInfos.length]);
|
|
92
|
+
const handleNext = useCallback(() => {
|
|
93
|
+
const currentIndex = currentSelectIndex === highlightInfos.length - 1 ? 0 : currentSelectIndex + 1;
|
|
94
|
+
setCurrentSelectIndex(currentIndex);
|
|
95
|
+
shouldScrollIntoView.current = true;
|
|
96
|
+
}, [currentSelectIndex, highlightInfos.length]);
|
|
97
|
+
const handleOpenReplaceAllModal = useCallback(() => {
|
|
98
|
+
setIsOpenReplaceAllModal(true);
|
|
99
|
+
}, []);
|
|
100
|
+
const handleCloseReplaceAllModal = useCallback(() => {
|
|
101
|
+
setIsOpenReplaceAllModal(false);
|
|
102
|
+
}, []);
|
|
103
|
+
const handleReplace = useCallback(() => {
|
|
104
|
+
handleReplaceKeyword(editor, [highlightInfos[currentSelectIndex]], replacementContent);
|
|
105
|
+
shouldScrollIntoView.current = true;
|
|
106
|
+
}, [currentSelectIndex, editor, highlightInfos, replacementContent]);
|
|
107
|
+
const handleReplaceAll = useCallback(() => {
|
|
108
|
+
handleReplaceKeyword(editor, highlightInfos, replacementContent);
|
|
109
|
+
handleCloseReplaceAllModal();
|
|
110
|
+
}, [editor, handleCloseReplaceAllModal, highlightInfos, replacementContent]);
|
|
111
|
+
const handleStartMove = useCallback(e => {
|
|
112
|
+
if (!e.target.className.includes('sdoc-search-replace-popover-container')) return;
|
|
113
|
+
setIsMoving(true);
|
|
114
|
+
}, []);
|
|
115
|
+
const handleMouseMove = useCallback(e => {
|
|
116
|
+
if (!isMoving) return;
|
|
117
|
+
const {
|
|
118
|
+
width,
|
|
119
|
+
height
|
|
120
|
+
} = popoverContainerRef.current.getBoundingClientRect();
|
|
121
|
+
const {
|
|
122
|
+
movementX,
|
|
123
|
+
movementY
|
|
124
|
+
} = e;
|
|
125
|
+
let x = popoverPosition.x + movementX;
|
|
126
|
+
let y = popoverPosition.y + movementY;
|
|
127
|
+
if (x <= 0) x = 0;
|
|
128
|
+
if (y < 0) y = 0;
|
|
129
|
+
if (x + width >= pageInnerSizeRef.current.x) x = pageInnerSizeRef.current.x - width;
|
|
130
|
+
if (y + height >= pageInnerSizeRef.current.y) y = pageInnerSizeRef.current.y - height;
|
|
131
|
+
setPopoverPosition({
|
|
132
|
+
x,
|
|
133
|
+
y
|
|
134
|
+
});
|
|
135
|
+
}, [isMoving, popoverPosition.x, popoverPosition.y]);
|
|
136
|
+
const handleFinishMove = useCallback(() => {
|
|
137
|
+
setIsMoving(false);
|
|
138
|
+
}, []);
|
|
139
|
+
const handleInputKeyDown = e => {
|
|
140
|
+
if (!highlightInfos.length) return;
|
|
141
|
+
if (isHotkey('enter', e)) handleNext();
|
|
142
|
+
if (isHotkey('enter+shift', e)) handleLast();
|
|
143
|
+
};
|
|
144
|
+
return createPortal( /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
145
|
+
className: "sdoc-search-replace-popover-container",
|
|
146
|
+
onMouseDown: handleStartMove,
|
|
147
|
+
onMouseMove: handleMouseMove,
|
|
148
|
+
onMouseUp: handleFinishMove,
|
|
149
|
+
onMouseLeave: handleFinishMove,
|
|
150
|
+
ref: popoverContainerRef,
|
|
151
|
+
style: {
|
|
152
|
+
left: popoverPosition.x,
|
|
153
|
+
top: popoverPosition.y
|
|
154
|
+
}
|
|
155
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
156
|
+
className: "sdoc-search-replace-popover-title"
|
|
157
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
158
|
+
className: "sdoc-search-replace-title-text"
|
|
159
|
+
}, t('Search_and_replace')), /*#__PURE__*/React.createElement("i", {
|
|
160
|
+
onClick: closePopover,
|
|
161
|
+
className: "sdocfont sdoc-sm-close sdoc-search-replace-title-close"
|
|
162
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
163
|
+
className: "sdoc-search-replace-popover-body"
|
|
164
|
+
}, /*#__PURE__*/React.createElement(Label, {
|
|
165
|
+
for: "sdoc-search-replace-search-ipt"
|
|
166
|
+
}, t('Search')), /*#__PURE__*/React.createElement("div", {
|
|
167
|
+
className: "sdoc-replace-ipt-container"
|
|
168
|
+
}, /*#__PURE__*/React.createElement(Input, {
|
|
169
|
+
ref: searchInputRef,
|
|
170
|
+
autoFocus: true,
|
|
171
|
+
onKeyUp: handleInputKeyDown,
|
|
172
|
+
onChange: debounce(handleSearchInputChange, 300),
|
|
173
|
+
id: "sdoc-search-replace-search-ipt",
|
|
174
|
+
placeholder: t('Type_search_content')
|
|
175
|
+
}), searchInputSuffixContent && /*#__PURE__*/React.createElement("div", {
|
|
176
|
+
className: "sdoc-replace-ipt-tip"
|
|
177
|
+
}, searchInputSuffixContent)), /*#__PURE__*/React.createElement(Label, {
|
|
178
|
+
className: "sdoc-replace-ipt-label",
|
|
179
|
+
for: "sdoc-search-replace-replace-ipt"
|
|
180
|
+
}, t('Replace_as')), /*#__PURE__*/React.createElement(Input, {
|
|
181
|
+
onChange: e => setReplacementContent(e.target.value),
|
|
182
|
+
id: "sdoc-search-replace-replace-ipt",
|
|
183
|
+
placeholder: t('Type_replace_content')
|
|
184
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
185
|
+
className: "sdoc-search-replace-popover-btn-group"
|
|
186
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
187
|
+
disabled: !highlightInfos.length,
|
|
188
|
+
onClick: handleLast,
|
|
189
|
+
className: "btn btn-secondary"
|
|
190
|
+
}, t('Previous')), /*#__PURE__*/React.createElement("button", {
|
|
191
|
+
disabled: !highlightInfos.length,
|
|
192
|
+
onClick: handleNext,
|
|
193
|
+
className: "btn btn-secondary"
|
|
194
|
+
}, t('Next')), /*#__PURE__*/React.createElement("button", {
|
|
195
|
+
disabled: !highlightInfos.length,
|
|
196
|
+
onClick: handleReplace,
|
|
197
|
+
className: "btn btn-primary"
|
|
198
|
+
}, t('Replace')), /*#__PURE__*/React.createElement("button", {
|
|
199
|
+
disabled: !highlightInfos.length,
|
|
200
|
+
onClick: handleOpenReplaceAllModal,
|
|
201
|
+
className: "btn btn-primary"
|
|
202
|
+
}, t('Replace_all'))))), /*#__PURE__*/React.createElement(ReplaceAllConfirmModal, {
|
|
203
|
+
isOpen: isOpenReplaceAllModal,
|
|
204
|
+
handleConfirm: handleReplaceAll,
|
|
205
|
+
handleCancel: handleCloseReplaceAllModal,
|
|
206
|
+
number: highlightInfos.length,
|
|
207
|
+
originalWord: searchContent,
|
|
208
|
+
replacedWord: replacementContent
|
|
209
|
+
})), document.body);
|
|
210
|
+
};
|
|
211
|
+
export default SearchReplacePopover;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
|
4
|
+
const ReplaceAllConfirmModal = _ref => {
|
|
5
|
+
let {
|
|
6
|
+
isOpen,
|
|
7
|
+
handleConfirm,
|
|
8
|
+
handleCancel,
|
|
9
|
+
number,
|
|
10
|
+
originalWord,
|
|
11
|
+
replacedWord
|
|
12
|
+
} = _ref;
|
|
13
|
+
const {
|
|
14
|
+
t
|
|
15
|
+
} = useTranslation();
|
|
16
|
+
const modalContent = replacedWord === '' ? t('Are_you_sure_to_clear_all_number_xxx_in_this_document', {
|
|
17
|
+
number,
|
|
18
|
+
originalWord
|
|
19
|
+
}) : t('Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy', {
|
|
20
|
+
number,
|
|
21
|
+
originalWord,
|
|
22
|
+
replacedWord
|
|
23
|
+
});
|
|
24
|
+
return /*#__PURE__*/React.createElement(Modal, {
|
|
25
|
+
isOpen: isOpen
|
|
26
|
+
}, /*#__PURE__*/React.createElement(ModalHeader, {
|
|
27
|
+
toggle: handleCancel
|
|
28
|
+
}, t('Tip')), /*#__PURE__*/React.createElement(ModalBody, null, "".concat(modalContent)), /*#__PURE__*/React.createElement(ModalFooter, null, /*#__PURE__*/React.createElement("button", {
|
|
29
|
+
onClick: handleCancel,
|
|
30
|
+
className: "btn btn-secondary"
|
|
31
|
+
}, t('Cancel')), /*#__PURE__*/React.createElement("button", {
|
|
32
|
+
onClick: handleConfirm,
|
|
33
|
+
className: "btn btn-primary"
|
|
34
|
+
}, t('Confirm'))));
|
|
35
|
+
};
|
|
36
|
+
export default ReplaceAllConfirmModal;
|
|
@@ -14,6 +14,7 @@ import Font from '../../plugins/font/menu';
|
|
|
14
14
|
import InsertToolbar from './insert-toolbar';
|
|
15
15
|
import ActiveTableMenu from '../../plugins/table/menu/active-table-menu';
|
|
16
16
|
import CalloutMenu from '../../plugins/callout/menu';
|
|
17
|
+
import SearchReplaceMenu from '../../plugins/search-replace/menu';
|
|
17
18
|
const HeaderToolbar = _ref => {
|
|
18
19
|
let {
|
|
19
20
|
editor,
|
|
@@ -63,7 +64,12 @@ const HeaderToolbar = _ref => {
|
|
|
63
64
|
})), /*#__PURE__*/React.createElement(ActiveTableMenu, {
|
|
64
65
|
editor: editor,
|
|
65
66
|
readonly: readonly
|
|
66
|
-
})
|
|
67
|
+
}), /*#__PURE__*/React.createElement(MenuGroup, {
|
|
68
|
+
className: "menu-group sdoc-editor-toolbar-right-menu"
|
|
69
|
+
}, /*#__PURE__*/React.createElement(SearchReplaceMenu, {
|
|
70
|
+
editor: editor,
|
|
71
|
+
readonly: readonly
|
|
72
|
+
})));
|
|
67
73
|
};
|
|
68
74
|
HeaderToolbar.defaultProps = {
|
|
69
75
|
readonly: false
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Function} fn
|
|
3
|
+
* @param {Number} delay
|
|
4
|
+
*/
|
|
5
|
+
const debounce = (fn, delay) => {
|
|
6
|
+
let timeout = null;
|
|
7
|
+
return function () {
|
|
8
|
+
clearTimeout(timeout);
|
|
9
|
+
timeout = setTimeout(() => {
|
|
10
|
+
fn.apply(this, arguments);
|
|
11
|
+
}, delay);
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export default debounce;
|
package/package.json
CHANGED
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Hledat",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Předchozí",
|
|
438
|
+
"Next": "Další",
|
|
439
|
+
"Replace": "Nahradit",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Suchen",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Vorherige",
|
|
438
|
+
"Next": "Nächste Seite",
|
|
439
|
+
"Replace": "Ersetzen",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Search",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Previous",
|
|
438
|
+
"Next": "Next",
|
|
439
|
+
"Replace": "Replace",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Buscar",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Anterior",
|
|
438
|
+
"Next": "Siguiente",
|
|
439
|
+
"Replace": "Reemplazar",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Chercher",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Précédent",
|
|
438
|
+
"Next": "Suivant",
|
|
439
|
+
"Replace": "Remplacer",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Row number",
|
|
425
425
|
"Column_number": "Column number",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "The maximum row number is {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Unfreeze",
|
|
429
427
|
"Other_modification": "Other's modification",
|
|
430
428
|
"My_modification": "My modification",
|
|
431
|
-
"Document_history": "Document history"
|
|
429
|
+
"Document_history": "Document history",
|
|
430
|
+
"Freeze_Document": "Freeze Document",
|
|
431
|
+
"Unfreeze": "Unfreeze",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Search",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Precedente",
|
|
438
|
+
"Next": "Successivo",
|
|
439
|
+
"Replace": "Sostituire",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "Номер строки",
|
|
425
425
|
"Column_number": "Номер столбца",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "Максимальное количество строк - {number}",
|
|
427
|
-
"Freeze_Document": "Freeze document",
|
|
428
|
-
"Unfreeze": "Разморозить",
|
|
429
427
|
"Other_modification": "Другая модификация",
|
|
430
428
|
"My_modification": "Моя модификация",
|
|
431
|
-
"Document_history": "История документа"
|
|
429
|
+
"Document_history": "История документа",
|
|
430
|
+
"Freeze_Document": "Заморозить документ",
|
|
431
|
+
"Unfreeze": "Разморозить",
|
|
432
|
+
"Search_and_replace": "Search and replace",
|
|
433
|
+
"Search": "Поиск",
|
|
434
|
+
"Type_search_content": "Type search content",
|
|
435
|
+
"Replace_as": "Replace as",
|
|
436
|
+
"Type_replace_content": "Type replace content",
|
|
437
|
+
"Previous": "Предыдущий",
|
|
438
|
+
"Next": "Следующий",
|
|
439
|
+
"Replace": "Заменить",
|
|
440
|
+
"Replace_all": "Replace all",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "Are you sure to replace all {{number}} '{{originalWord}}' in this document with '{{replacedWord}}'?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "Are you sure to clear all {{number}} '{{originalWord}}' in this document?",
|
|
443
|
+
"Search_not_found": "Not found"
|
|
432
444
|
}
|
|
@@ -424,9 +424,21 @@
|
|
|
424
424
|
"Row_number": "行数",
|
|
425
425
|
"Column_number": "列数",
|
|
426
426
|
"The_maximum_row_number_is_{number}": "最大行数为 {number}",
|
|
427
|
-
"Freeze_Document": "冻结文档",
|
|
428
|
-
"Unfreeze": "取消冻结",
|
|
429
427
|
"Other_modification": "他人更改",
|
|
430
428
|
"My_modification": "我的更改",
|
|
431
|
-
"Document_history": "文档历史"
|
|
429
|
+
"Document_history": "文档历史",
|
|
430
|
+
"Freeze_Document": "冻结文档",
|
|
431
|
+
"Unfreeze": "取消冻结",
|
|
432
|
+
"Search_and_replace": "查找和替换",
|
|
433
|
+
"Search": "搜索",
|
|
434
|
+
"Type_search_content": "输入查找内容",
|
|
435
|
+
"Replace_as": "替换为",
|
|
436
|
+
"Type_replace_content": "输入替换内容",
|
|
437
|
+
"Previous": "上一个",
|
|
438
|
+
"Next": "下一页",
|
|
439
|
+
"Replace": "替换",
|
|
440
|
+
"Replace_all": "下一个",
|
|
441
|
+
"Are_you_sure_to_replace_all_number_xxx_in_this_document_with_yyy": "确定要将此文档内的 {{number}} 处 \"{{originalWord}} \" 替换为 \"{{replacedWord}} \" 吗?",
|
|
442
|
+
"Are_you_sure_to_clear_all_number_xxx_in_this_document": "确定将此文档内的 {{number}} 处 \"{{originalWord}}\" 全部清除吗?",
|
|
443
|
+
"Search_not_found": "未找到"
|
|
432
444
|
}
|