@seafile/sdoc-editor 0.4.7 → 0.4.9
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/list/transforms/move-list-items.js +1 -1
- package/dist/basic-sdk/extension/plugins/paragraph/index.js +2 -0
- package/dist/basic-sdk/extension/plugins/paragraph/plugin/index.js +75 -0
- package/dist/basic-sdk/extension/plugins/paragraph/render-elem.js +5 -1
- 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, ParagraphPlugin, 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 };
|
|
@@ -39,7 +39,7 @@ export const moveListItems = function (editor) {
|
|
|
39
39
|
const licPath = licPathRef.unref();
|
|
40
40
|
if (!licPath) return;
|
|
41
41
|
const listItem = Editor.parent(editor, licPath);
|
|
42
|
-
if (!listItem) return;
|
|
42
|
+
if (!listItem || listItem[1].length === 0) return;
|
|
43
43
|
const parentList = Editor.parent(editor, listItem[1]);
|
|
44
44
|
if (!parentList) return;
|
|
45
45
|
let _moved = false;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import isHotkey from 'is-hotkey';
|
|
2
|
+
import { Editor, Range, Transforms } from '@seafile/slate';
|
|
3
|
+
import { PARAGRAPH } from '../../../constants';
|
|
4
|
+
const withParagraph = editor => {
|
|
5
|
+
const {
|
|
6
|
+
handleTab,
|
|
7
|
+
insertText,
|
|
8
|
+
deleteBackward
|
|
9
|
+
} = editor;
|
|
10
|
+
const newEditor = editor;
|
|
11
|
+
newEditor.handleTab = event => {
|
|
12
|
+
var _selectedNode$;
|
|
13
|
+
const {
|
|
14
|
+
selection
|
|
15
|
+
} = newEditor;
|
|
16
|
+
if (!selection) return;
|
|
17
|
+
if (!Range.isCollapsed(selection)) return;
|
|
18
|
+
const selectedNode = Editor.node(newEditor, selection, {
|
|
19
|
+
depth: 1
|
|
20
|
+
});
|
|
21
|
+
if ((selectedNode === null || selectedNode === void 0 ? void 0 : (_selectedNode$ = selectedNode[0]) === null || _selectedNode$ === void 0 ? void 0 : _selectedNode$.type) === PARAGRAPH) {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
const path = Editor.path(newEditor, selection);
|
|
24
|
+
const point = Editor.point(newEditor, selection);
|
|
25
|
+
const isStart = Editor.isStart(newEditor, point, [path[0]]);
|
|
26
|
+
if (isStart) {
|
|
27
|
+
let indent;
|
|
28
|
+
if (isHotkey('shift+tab', event)) {
|
|
29
|
+
indent = false;
|
|
30
|
+
}
|
|
31
|
+
if (isHotkey('tab', event)) {
|
|
32
|
+
indent = true;
|
|
33
|
+
}
|
|
34
|
+
Transforms.setNodes(newEditor, {
|
|
35
|
+
indent: indent
|
|
36
|
+
}, {
|
|
37
|
+
at: [path[0]]
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
if (isHotkey('tab', event)) insertText(' ');
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
return handleTab(event);
|
|
45
|
+
};
|
|
46
|
+
newEditor.deleteBackward = unit => {
|
|
47
|
+
const {
|
|
48
|
+
selection
|
|
49
|
+
} = newEditor;
|
|
50
|
+
if (!selection) return;
|
|
51
|
+
const [selectedNode = {}] = Editor.node(newEditor, selection, {
|
|
52
|
+
depth: 1
|
|
53
|
+
});
|
|
54
|
+
const {
|
|
55
|
+
type,
|
|
56
|
+
indent
|
|
57
|
+
} = selectedNode;
|
|
58
|
+
if (Range.isCollapsed(selection) && type === PARAGRAPH && indent) {
|
|
59
|
+
const path = Editor.path(newEditor, selection);
|
|
60
|
+
const point = Editor.point(newEditor, selection);
|
|
61
|
+
const isStart = Editor.isStart(newEditor, point, [path[0]]);
|
|
62
|
+
if (isStart) {
|
|
63
|
+
Transforms.setNodes(newEditor, {
|
|
64
|
+
indent: false
|
|
65
|
+
}, {
|
|
66
|
+
at: [path[0]]
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return deleteBackward(unit);
|
|
72
|
+
};
|
|
73
|
+
return newEditor;
|
|
74
|
+
};
|
|
75
|
+
export default withParagraph;
|
|
@@ -10,6 +10,9 @@ const Paragraph = _ref => {
|
|
|
10
10
|
attributes,
|
|
11
11
|
children
|
|
12
12
|
} = _ref;
|
|
13
|
+
const {
|
|
14
|
+
indent
|
|
15
|
+
} = element;
|
|
13
16
|
const editor = useSlateStatic();
|
|
14
17
|
let isShowPlaceHolder = false;
|
|
15
18
|
if (editor.children.length === 1) {
|
|
@@ -20,7 +23,8 @@ const Paragraph = _ref => {
|
|
|
20
23
|
isShowPlaceHolder = Node.string(element) === '' && (node === null || node === void 0 ? void 0 : node.id) === (element === null || element === void 0 ? void 0 : element.id) && !isComposing;
|
|
21
24
|
}
|
|
22
25
|
const style = {
|
|
23
|
-
textAlign: element.align
|
|
26
|
+
textAlign: element.align,
|
|
27
|
+
paddingLeft: indent ? '28px' : ''
|
|
24
28
|
};
|
|
25
29
|
const newAttributes = _objectSpread(_objectSpread({}, attributes), typeof attributes.onMouseEnter === 'function' && {
|
|
26
30
|
'data-root': 'true'
|
|
@@ -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;
|