@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.
Files changed (29) hide show
  1. package/dist/basic-sdk/assets/css/layout.css +12 -2
  2. package/dist/basic-sdk/constants/index.js +3 -1
  3. package/dist/basic-sdk/editor/sdoc-editor.js +7 -2
  4. package/dist/basic-sdk/extension/constants/menus-config.js +6 -0
  5. package/dist/basic-sdk/extension/plugins/code-block/render-elem.js +4 -0
  6. package/dist/basic-sdk/extension/plugins/image/hover-menu/index.js +6 -0
  7. package/dist/basic-sdk/extension/plugins/image/plugin.js +8 -2
  8. package/dist/basic-sdk/extension/plugins/image/render-elem.js +5 -1
  9. package/dist/basic-sdk/extension/plugins/index.js +3 -2
  10. package/dist/basic-sdk/extension/plugins/search-replace/constant.js +2 -0
  11. package/dist/basic-sdk/extension/plugins/search-replace/helper.js +331 -0
  12. package/dist/basic-sdk/extension/plugins/search-replace/index.js +10 -0
  13. package/dist/basic-sdk/extension/plugins/search-replace/menu/index.css +14 -0
  14. package/dist/basic-sdk/extension/plugins/search-replace/menu/index.js +78 -0
  15. package/dist/basic-sdk/extension/plugins/search-replace/plugin.js +21 -0
  16. package/dist/basic-sdk/extension/plugins/search-replace/popover/index.css +82 -0
  17. package/dist/basic-sdk/extension/plugins/search-replace/popover/index.js +211 -0
  18. package/dist/basic-sdk/extension/plugins/search-replace/popover/replace-all-confirm-modal.js +36 -0
  19. package/dist/basic-sdk/extension/toolbar/header-toolbar/index.js +7 -1
  20. package/dist/basic-sdk/utils/debounce.js +14 -0
  21. package/package.json +1 -1
  22. package/public/locales/cs/sdoc-editor.json +15 -3
  23. package/public/locales/de/sdoc-editor.json +15 -3
  24. package/public/locales/en/sdoc-editor.json +15 -3
  25. package/public/locales/es/sdoc-editor.json +15 -3
  26. package/public/locales/fr/sdoc-editor.json +15 -3
  27. package/public/locales/it/sdoc-editor.json +15 -3
  28. package/public/locales/ru/sdoc-editor.json +15 -3
  29. 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 > div {
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: setSlateValue
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.trim());
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
- const Plugins = [MarkDownPlugin, HtmlPlugin, HeaderPlugin, LinkPlugin, BlockquotePlugin, ListPlugin, CheckListPlugin, CodeBlockPlugin, ImagePlugin, TablePlugin, TextPlugin, TextAlignPlugin, FontPlugin, SdocLinkPlugin, FileLinkPlugin, CalloutPlugin];
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,2 @@
1
+ export const FOCUSSED_SEARCH_HIGHLIGHT_FILL_COLOR = '#f19d38';
2
+ export const DEFAULT_SEARCH_HIGHLIGHT_FILL_COLOR = '#fef500';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seafile/sdoc-editor",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "private": false,
5
5
  "description": "This is a sdoc editor",
6
6
  "main": "dist/index.js",
@@ -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
  }