@mdxeditor/editor 3.39.0 → 3.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +54 -0
- package/dist/index.js +15 -0
- package/dist/plugins/core/index.js +10 -5
- package/dist/plugins/search/index.js +368 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -798,6 +798,8 @@ export declare function curry1to0<T, R>(proc: (arg: T) => R, arg: T): () => R;
|
|
|
798
798
|
*/
|
|
799
799
|
export declare function curry2to1<T, K, R>(proc: (arg1: T, arg2: K) => R, arg1: T): (arg2: K) => R;
|
|
800
800
|
|
|
801
|
+
export declare const debouncedIndexer$: NodeRef<TextNodeIndex>;
|
|
802
|
+
|
|
801
803
|
export declare const DEFAULT_FORMAT: 0;
|
|
802
804
|
|
|
803
805
|
/**
|
|
@@ -1131,6 +1133,18 @@ export declare interface EditorInFocus {
|
|
|
1131
1133
|
*/
|
|
1132
1134
|
export declare const editorRootElementRef$: NodeRef<default_2.RefObject<HTMLDivElement> | null>;
|
|
1133
1135
|
|
|
1136
|
+
export declare const editorSearchCursor$: NodeRef<number>;
|
|
1137
|
+
|
|
1138
|
+
export declare const editorSearchRanges$: NodeRef<Range[]>;
|
|
1139
|
+
|
|
1140
|
+
export declare const editorSearchScollableContent$: NodeRef<HTMLElement | null>;
|
|
1141
|
+
|
|
1142
|
+
export declare const editorSearchTerm$: NodeRef<string>;
|
|
1143
|
+
|
|
1144
|
+
export declare const editorSearchTermDebounced$: NodeRef<string>;
|
|
1145
|
+
|
|
1146
|
+
export declare const editorSearchTextNodeIndex$: NodeRef<TextNodeIndex>;
|
|
1147
|
+
|
|
1134
1148
|
/**
|
|
1135
1149
|
* A function that subscribes to Lexical editor updates or events, and retursns an unsubscribe function.
|
|
1136
1150
|
* @group Core
|
|
@@ -1139,6 +1153,8 @@ export declare type EditorSubscription = (activeEditor: LexicalEditor) => () =>
|
|
|
1139
1153
|
|
|
1140
1154
|
/* Excluded from this release type: editorWrappers$ */
|
|
1141
1155
|
|
|
1156
|
+
export declare const EmptyTextNodeIndex: TextNodeIndex;
|
|
1157
|
+
|
|
1142
1158
|
/* Excluded from this release type: ExportLexicalTreeOptions */
|
|
1143
1159
|
|
|
1144
1160
|
/* Excluded from this release type: exportLexicalTreeToMdast */
|
|
@@ -2134,6 +2150,10 @@ export declare type MdastJsx = MdxJsxTextElement | MdxJsxFlowElement;
|
|
|
2134
2150
|
|
|
2135
2151
|
/* Excluded from this release type: MdastTreeImportOptions */
|
|
2136
2152
|
|
|
2153
|
+
export declare const MDX_FOCUS_SEARCH_NAME = "MdxFocusSearch";
|
|
2154
|
+
|
|
2155
|
+
export declare const MDX_SEARCH_NAME = "MdxSearch";
|
|
2156
|
+
|
|
2137
2157
|
/**
|
|
2138
2158
|
* The MDXEditor React component.
|
|
2139
2159
|
* @group MDXEditor
|
|
@@ -2472,6 +2492,8 @@ export declare const PropertyPopover: default_2.FC<{
|
|
|
2472
2492
|
*/
|
|
2473
2493
|
export declare const quotePlugin: (params?: unknown) => RealmPlugin;
|
|
2474
2494
|
|
|
2495
|
+
export declare function rangeSearchScan(searchQuery: string, { allText, offsetIndex, nodeIndex }: TextNodeIndex): Generator<Range, void, unknown>;
|
|
2496
|
+
|
|
2475
2497
|
/**
|
|
2476
2498
|
* Holds the readOnly state of the editor.
|
|
2477
2499
|
* @group Core
|
|
@@ -2660,6 +2682,10 @@ export declare interface SaveImageParameters extends BaseImageParameters {
|
|
|
2660
2682
|
file?: FileList;
|
|
2661
2683
|
}
|
|
2662
2684
|
|
|
2685
|
+
export declare const searchOpen$: NodeRef<boolean>;
|
|
2686
|
+
|
|
2687
|
+
export declare const searchPlugin: (params?: unknown) => RealmPlugin;
|
|
2688
|
+
|
|
2663
2689
|
/**
|
|
2664
2690
|
* A toolbar primitive you can use to build dropdowns, such as the block type select.
|
|
2665
2691
|
* @group Toolbar Primitives
|
|
@@ -2879,6 +2905,12 @@ export declare const tablePlugin: (params?: Options_3 | undefined) => RealmPlugi
|
|
|
2879
2905
|
*/
|
|
2880
2906
|
export declare function tap<T>(arg: T, proc: (arg: T) => unknown): T;
|
|
2881
2907
|
|
|
2908
|
+
export declare interface TextNodeIndex {
|
|
2909
|
+
allText: string;
|
|
2910
|
+
nodeIndex: Node[];
|
|
2911
|
+
offsetIndex: number[];
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2882
2914
|
/**
|
|
2883
2915
|
* A plugin that adds support for thematic breaks.
|
|
2884
2916
|
* @group Thematic Break
|
|
@@ -2981,6 +3013,28 @@ export declare function useCodeBlockEditorContext(): CodeBlockEditorContextValue
|
|
|
2981
3013
|
*/
|
|
2982
3014
|
export declare const usedLexicalNodes$: NodeRef< KlassConstructor<typeof LexicalNode>[]>;
|
|
2983
3015
|
|
|
3016
|
+
export declare function useEditorSearch(): {
|
|
3017
|
+
next: () => void;
|
|
3018
|
+
prev: () => void;
|
|
3019
|
+
total: number;
|
|
3020
|
+
cursor: number;
|
|
3021
|
+
setSearch: (term: string | null) => void;
|
|
3022
|
+
search: string;
|
|
3023
|
+
currentRange: Range;
|
|
3024
|
+
isSearchOpen: boolean;
|
|
3025
|
+
setIsSearchOpen: (value: boolean) => void;
|
|
3026
|
+
openSearch: () => void;
|
|
3027
|
+
closeSearch: () => void;
|
|
3028
|
+
toggleSearch: () => void;
|
|
3029
|
+
ranges: Range[];
|
|
3030
|
+
scrollToRangeOrIndex: (range: Range | number, options?: {
|
|
3031
|
+
ignoreIfInView?: boolean;
|
|
3032
|
+
behavior?: ScrollBehavior;
|
|
3033
|
+
}) => void;
|
|
3034
|
+
replace: (str: string, onUpdate?: () => void) => void;
|
|
3035
|
+
replaceAll: (str: string, onUpdate?: () => void) => void;
|
|
3036
|
+
};
|
|
3037
|
+
|
|
2984
3038
|
/**
|
|
2985
3039
|
* A hook that returns a function that removes the lexical node from the editor.
|
|
2986
3040
|
* @group Custom Editor Primitives
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { applyLinkChanges$, cancelLinkEdit$, linkAutocompleteSuggestions$, linkD
|
|
|
28
28
|
import { toolbarClassName$, toolbarContents$, toolbarPlugin } from "./plugins/toolbar/index.js";
|
|
29
29
|
import { cmExtensions$, diffMarkdown$, diffSourcePlugin, readOnlyDiff$ } from "./plugins/diff-source/index.js";
|
|
30
30
|
import { markdownShortcutPlugin } from "./plugins/markdown-shortcut/index.js";
|
|
31
|
+
import { EmptyTextNodeIndex, MDX_FOCUS_SEARCH_NAME, MDX_SEARCH_NAME, debouncedIndexer$, editorSearchCursor$, editorSearchRanges$, editorSearchScollableContent$, editorSearchTerm$, editorSearchTermDebounced$, editorSearchTextNodeIndex$, rangeSearchScan, searchOpen$, searchPlugin, useEditorSearch } from "./plugins/search/index.js";
|
|
31
32
|
import { BlockTypeSelect } from "./plugins/toolbar/components/BlockTypeSelect.js";
|
|
32
33
|
import { BoldItalicUnderlineToggles, StrikeThroughSupSubToggles } from "./plugins/toolbar/components/BoldItalicUnderlineToggles.js";
|
|
33
34
|
import { ChangeAdmonitionType, admonitionLabelsMap } from "./plugins/toolbar/components/ChangeAdmonitionType.js";
|
|
@@ -108,6 +109,7 @@ export {
|
|
|
108
109
|
DialogButton,
|
|
109
110
|
DiffSourceToggleWrapper,
|
|
110
111
|
DirectiveNode,
|
|
112
|
+
EmptyTextNodeIndex,
|
|
111
113
|
FrontmatterNode,
|
|
112
114
|
GenericDirectiveEditor,
|
|
113
115
|
GenericHTMLNode,
|
|
@@ -133,6 +135,8 @@ export {
|
|
|
133
135
|
KitchenSinkToolbar,
|
|
134
136
|
ListsToggle,
|
|
135
137
|
MDXEditor,
|
|
138
|
+
MDX_FOCUS_SEARCH_NAME,
|
|
139
|
+
MDX_SEARCH_NAME,
|
|
136
140
|
MarkdownParseError,
|
|
137
141
|
MultipleChoiceToggleGroup,
|
|
138
142
|
NESTED_EDITOR_UPDATED_COMMAND,
|
|
@@ -210,6 +214,7 @@ export {
|
|
|
210
214
|
currentSelection$,
|
|
211
215
|
curry1to0,
|
|
212
216
|
curry2to1,
|
|
217
|
+
debouncedIndexer$,
|
|
213
218
|
defaultCodeBlockLanguage$,
|
|
214
219
|
defaultSvgIcons,
|
|
215
220
|
diffMarkdown$,
|
|
@@ -222,6 +227,12 @@ export {
|
|
|
222
227
|
editImageToolbarComponent$,
|
|
223
228
|
editorInFocus$,
|
|
224
229
|
editorRootElementRef$,
|
|
230
|
+
editorSearchCursor$,
|
|
231
|
+
editorSearchRanges$,
|
|
232
|
+
editorSearchScollableContent$,
|
|
233
|
+
editorSearchTerm$,
|
|
234
|
+
editorSearchTermDebounced$,
|
|
235
|
+
editorSearchTextNodeIndex$,
|
|
225
236
|
editorWrappers$,
|
|
226
237
|
exportLexicalTreeToMdast,
|
|
227
238
|
exportMarkdownFromLexical,
|
|
@@ -296,6 +307,7 @@ export {
|
|
|
296
307
|
placeholder$,
|
|
297
308
|
prop,
|
|
298
309
|
quotePlugin,
|
|
310
|
+
rangeSearchScan,
|
|
299
311
|
readOnly$,
|
|
300
312
|
readOnlyDiff$,
|
|
301
313
|
realmPlugin,
|
|
@@ -307,6 +319,8 @@ export {
|
|
|
307
319
|
sandpackConfig$,
|
|
308
320
|
sandpackPlugin,
|
|
309
321
|
saveImage$,
|
|
322
|
+
searchOpen$,
|
|
323
|
+
searchPlugin,
|
|
310
324
|
setMarkdown$,
|
|
311
325
|
spellCheck$,
|
|
312
326
|
switchFromPreviewToLinkEdit$,
|
|
@@ -324,6 +338,7 @@ export {
|
|
|
324
338
|
translation$,
|
|
325
339
|
updateLink$,
|
|
326
340
|
useCodeBlockEditorContext,
|
|
341
|
+
useEditorSearch,
|
|
327
342
|
useLexicalNodeRemove,
|
|
328
343
|
useMdastNodeUpdater,
|
|
329
344
|
useNestedEditorContext,
|
|
@@ -101,7 +101,7 @@ const mutableMarkdownSignal$ = Signal((r) => {
|
|
|
101
101
|
),
|
|
102
102
|
mutableMarkdownSignal$
|
|
103
103
|
);
|
|
104
|
-
});
|
|
104
|
+
}, true);
|
|
105
105
|
const importVisitors$ = Cell([]);
|
|
106
106
|
const usedLexicalNodes$ = Cell([]);
|
|
107
107
|
const syntaxExtensions$ = Cell([]);
|
|
@@ -475,10 +475,14 @@ const viewMode$ = Cell("rich-text", (r) => {
|
|
|
475
475
|
}
|
|
476
476
|
);
|
|
477
477
|
});
|
|
478
|
-
const markdownSourceEditorValue$ = Cell(
|
|
479
|
-
|
|
480
|
-
r
|
|
481
|
-
|
|
478
|
+
const markdownSourceEditorValue$ = Cell(
|
|
479
|
+
"",
|
|
480
|
+
(r) => {
|
|
481
|
+
r.link(markdown$, markdownSourceEditorValue$);
|
|
482
|
+
r.link(markdownSourceEditorValue$, markdownSignal$);
|
|
483
|
+
},
|
|
484
|
+
true
|
|
485
|
+
);
|
|
482
486
|
const activePlugins$ = Cell([]);
|
|
483
487
|
const addActivePlugin$ = Appender(activePlugins$);
|
|
484
488
|
const translation$ = Cell(() => {
|
|
@@ -491,6 +495,7 @@ const corePlugin = realmPlugin({
|
|
|
491
495
|
r.register(createRootEditorSubscription$);
|
|
492
496
|
r.register(createActiveEditorSubscription$);
|
|
493
497
|
r.register(markdownSignal$);
|
|
498
|
+
r.register(markdownSourceEditorValue$);
|
|
494
499
|
r.pubIn({
|
|
495
500
|
[initialMarkdown$]: (params == null ? void 0 : params.trim) ? initialMarkdown.trim() : initialMarkdown,
|
|
496
501
|
[iconComponentFor$]: params == null ? void 0 : params.iconComponentFor,
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { Cell, debounceTime, useRealm, useCellValue, useCell } from "@mdxeditor/gurx";
|
|
2
|
+
import { getNearestEditorFromDOMNode, $getNearestNodeFromDOMNode, $isTextNode, $createRangeSelection } from "lexical";
|
|
3
|
+
import { realmPlugin } from "../../RealmWithPlugins.js";
|
|
4
|
+
import { contentEditableRef$, createRootEditorSubscription$ } from "../core/index.js";
|
|
5
|
+
const EmptyTextNodeIndex = {
|
|
6
|
+
allText: "",
|
|
7
|
+
nodeIndex: [],
|
|
8
|
+
offsetIndex: []
|
|
9
|
+
};
|
|
10
|
+
const editorSearchTerm$ = Cell("");
|
|
11
|
+
const editorSearchRanges$ = Cell([]);
|
|
12
|
+
const editorSearchCursor$ = Cell(0);
|
|
13
|
+
const editorSearchTextNodeIndex$ = Cell(EmptyTextNodeIndex);
|
|
14
|
+
const searchOpen$ = Cell(false);
|
|
15
|
+
const editorSearchTermDebounced$ = Cell("", (realm) => {
|
|
16
|
+
realm.link(editorSearchTermDebounced$, realm.pipe(editorSearchTerm$, realm.transformer(debounceTime(250))));
|
|
17
|
+
});
|
|
18
|
+
const editorSearchScollableContent$ = Cell(
|
|
19
|
+
null,
|
|
20
|
+
(r) => r.sub(contentEditableRef$, (cref) => {
|
|
21
|
+
var _a;
|
|
22
|
+
r.pub(editorSearchScollableContent$, ((_a = cref == null ? void 0 : cref.current) == null ? void 0 : _a.parentNode) ?? null);
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
const MDX_SEARCH_NAME = "MdxSearch";
|
|
26
|
+
const MDX_FOCUS_SEARCH_NAME = "MdxFocusSearch";
|
|
27
|
+
const debouncedIndexer$ = Cell(EmptyTextNodeIndex, (realm) => {
|
|
28
|
+
realm.link(debouncedIndexer$, realm.pipe(editorSearchTextNodeIndex$, realm.transformer(debounceTime(250))));
|
|
29
|
+
});
|
|
30
|
+
function* searchText(allText, searchQuery) {
|
|
31
|
+
if (!searchQuery) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let regex;
|
|
35
|
+
try {
|
|
36
|
+
regex = new RegExp(searchQuery, "gi");
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("Invalid search pattern:", e);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = regex.exec(allText)) !== null) {
|
|
43
|
+
if (match[0].length === 0) {
|
|
44
|
+
if (regex.lastIndex === match.index) {
|
|
45
|
+
regex.lastIndex++;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const start = match.index;
|
|
50
|
+
const end = start + match[0].length - 1;
|
|
51
|
+
yield [start, end];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function indexAllTextNodes(root) {
|
|
55
|
+
var _a;
|
|
56
|
+
let allText = "";
|
|
57
|
+
const nodeIndex = [];
|
|
58
|
+
const offsetIndex = [];
|
|
59
|
+
if (!root) {
|
|
60
|
+
return { allText: "", nodeIndex, offsetIndex };
|
|
61
|
+
}
|
|
62
|
+
const contentSelector = "p, h1, h2, h3, h4, h5, h6, li, code, pre";
|
|
63
|
+
const treeWalker = document.createTreeWalker(
|
|
64
|
+
root,
|
|
65
|
+
NodeFilter.SHOW_TEXT,
|
|
66
|
+
// The corrected heuristic: accept any text node that is a descendant of a valid content container.
|
|
67
|
+
(node) => {
|
|
68
|
+
var _a2;
|
|
69
|
+
if ((_a2 = node.parentElement) == null ? void 0 : _a2.closest(contentSelector)) {
|
|
70
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
71
|
+
}
|
|
72
|
+
return NodeFilter.FILTER_REJECT;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
let currentNode;
|
|
76
|
+
while (currentNode = treeWalker.nextNode()) {
|
|
77
|
+
const nodeContent = ((_a = currentNode.textContent) == null ? void 0 : _a.normalize("NFKD")) ?? currentNode.textContent ?? "";
|
|
78
|
+
for (let i = 0; i < nodeContent.length; i++) {
|
|
79
|
+
nodeIndex.push(currentNode);
|
|
80
|
+
offsetIndex.push(i);
|
|
81
|
+
allText += nodeContent[i] ?? "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { allText, nodeIndex, offsetIndex };
|
|
85
|
+
}
|
|
86
|
+
function* rangeSearchScan(searchQuery, { allText, offsetIndex, nodeIndex }) {
|
|
87
|
+
for (const [start, end] of searchText(allText, searchQuery)) {
|
|
88
|
+
const startOffset = offsetIndex[start];
|
|
89
|
+
const endOffset = offsetIndex[end];
|
|
90
|
+
const startNode = nodeIndex[start];
|
|
91
|
+
const endNode = nodeIndex[end];
|
|
92
|
+
const range = new Range();
|
|
93
|
+
if (startNode === void 0 || endNode === void 0 || startOffset === void 0 || endOffset === void 0) {
|
|
94
|
+
throw new Error("Invalid range: startNode, endNode, startOffset, or endOffset is undefined.");
|
|
95
|
+
}
|
|
96
|
+
range.setStart(startNode, startOffset);
|
|
97
|
+
range.setEnd(endNode, endOffset + 1);
|
|
98
|
+
yield range;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const focusHighlightRange = (range) => {
|
|
102
|
+
CSS.highlights.delete(MDX_FOCUS_SEARCH_NAME);
|
|
103
|
+
if (range)
|
|
104
|
+
CSS.highlights.set(MDX_FOCUS_SEARCH_NAME, new Highlight(range));
|
|
105
|
+
};
|
|
106
|
+
const highlightRanges = (ranges) => {
|
|
107
|
+
CSS.highlights.set(MDX_SEARCH_NAME, new Highlight(...ranges));
|
|
108
|
+
};
|
|
109
|
+
const resetHighlights = () => {
|
|
110
|
+
CSS.highlights.delete(MDX_SEARCH_NAME);
|
|
111
|
+
CSS.highlights.delete(MDX_FOCUS_SEARCH_NAME);
|
|
112
|
+
};
|
|
113
|
+
const scrollToRange = (range, contentEditable, options) => {
|
|
114
|
+
const ignoreIfInView = (options == null ? void 0 : options.ignoreIfInView) ?? true;
|
|
115
|
+
const behavior = (options == null ? void 0 : options.behavior) ?? "smooth";
|
|
116
|
+
const [first] = range.getClientRects();
|
|
117
|
+
if (!contentEditable) {
|
|
118
|
+
console.warn("No content-editable element found for scrolling.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!first) {
|
|
122
|
+
console.warn("No client rect found for the range, cannot scroll.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const containerRect = contentEditable.getBoundingClientRect();
|
|
126
|
+
const topRelativeToContainer = first.top - containerRect.top;
|
|
127
|
+
const bottomRelativeToContainer = first.bottom - containerRect.top;
|
|
128
|
+
if (ignoreIfInView) {
|
|
129
|
+
const rangeTop = topRelativeToContainer + contentEditable.scrollTop;
|
|
130
|
+
const rangeBottom = bottomRelativeToContainer + contentEditable.scrollTop;
|
|
131
|
+
const visibleTop = contentEditable.scrollTop;
|
|
132
|
+
const visibleBottom = visibleTop + contentEditable.clientHeight;
|
|
133
|
+
const inView = rangeTop >= visibleTop && rangeBottom <= visibleBottom;
|
|
134
|
+
if (inView)
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const top = topRelativeToContainer + contentEditable.scrollTop - first.height;
|
|
138
|
+
contentEditable.scrollTo({ top, behavior });
|
|
139
|
+
};
|
|
140
|
+
function isSimilarRange(range1, range2) {
|
|
141
|
+
return range1.startContainer === range2.startContainer && range1.startOffset === range2.startOffset;
|
|
142
|
+
}
|
|
143
|
+
function replaceTextInRange(range, str, onUpdate) {
|
|
144
|
+
const startDomNode = range.startContainer;
|
|
145
|
+
const endDomNode = range.endContainer;
|
|
146
|
+
const startOffset = range.startOffset;
|
|
147
|
+
const endOffset = range.endOffset;
|
|
148
|
+
const editor = getNearestEditorFromDOMNode(startDomNode);
|
|
149
|
+
if (!editor) {
|
|
150
|
+
console.warn("No editor found for the provided DOM node.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
editor.update(
|
|
154
|
+
() => {
|
|
155
|
+
const startLexicalNode = $getNearestNodeFromDOMNode(startDomNode);
|
|
156
|
+
const endLexicalNode = $getNearestNodeFromDOMNode(endDomNode);
|
|
157
|
+
if (!$isTextNode(startLexicalNode) || !$isTextNode(endLexicalNode)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const selection = $createRangeSelection();
|
|
162
|
+
selection.anchor.set(startLexicalNode.getKey(), startOffset, "text");
|
|
163
|
+
selection.focus.set(endLexicalNode.getKey(), endOffset, "text");
|
|
164
|
+
selection.insertText(str);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.warn("Error replacing text in the editor:", e);
|
|
167
|
+
if (onUpdate) {
|
|
168
|
+
onUpdate();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
onUpdate
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
function useEditorSearch() {
|
|
178
|
+
const realm = useRealm();
|
|
179
|
+
const ranges = useCellValue(editorSearchRanges$);
|
|
180
|
+
const cursor = useCellValue(editorSearchCursor$);
|
|
181
|
+
const search = useCellValue(editorSearchTerm$);
|
|
182
|
+
const currentRange = ranges[cursor - 1] ?? null;
|
|
183
|
+
const contentEditable = useCellValue(editorSearchScollableContent$);
|
|
184
|
+
const [isSearchOpen, setIsSearchOpen] = useCell(searchOpen$);
|
|
185
|
+
const openSearch = () => {
|
|
186
|
+
setIsSearchOpen(true);
|
|
187
|
+
};
|
|
188
|
+
const closeSearch = () => {
|
|
189
|
+
setIsSearchOpen(false);
|
|
190
|
+
};
|
|
191
|
+
const toggleSearch = () => {
|
|
192
|
+
setIsSearchOpen(!isSearchOpen);
|
|
193
|
+
};
|
|
194
|
+
const rangeCount = ranges.length;
|
|
195
|
+
const scrollToRangeOrIndex = (range, options) => {
|
|
196
|
+
const scrollRange = typeof range === "number" ? ranges[range - 1] : range;
|
|
197
|
+
if (!scrollRange) {
|
|
198
|
+
throw new Error("Error scrolling to range, range does not exist");
|
|
199
|
+
}
|
|
200
|
+
scrollToRange(scrollRange, contentEditable, options);
|
|
201
|
+
};
|
|
202
|
+
const setSearch = (term) => {
|
|
203
|
+
if ((term ?? "") !== search) {
|
|
204
|
+
realm.pub(editorSearchCursor$, 0);
|
|
205
|
+
}
|
|
206
|
+
realm.pub(editorSearchTermDebounced$, term ?? "");
|
|
207
|
+
};
|
|
208
|
+
const next = () => {
|
|
209
|
+
if (!ranges.length)
|
|
210
|
+
return;
|
|
211
|
+
const newVal = cursor % ranges.length + 1;
|
|
212
|
+
scrollToRangeOrIndex(newVal);
|
|
213
|
+
realm.pub(editorSearchCursor$, newVal);
|
|
214
|
+
};
|
|
215
|
+
const prev = () => {
|
|
216
|
+
if (!ranges.length)
|
|
217
|
+
return;
|
|
218
|
+
const newVal = cursor <= 1 ? ranges.length : cursor - 1;
|
|
219
|
+
scrollToRangeOrIndex(newVal);
|
|
220
|
+
realm.pub(editorSearchCursor$, newVal);
|
|
221
|
+
};
|
|
222
|
+
const replace = (str, onUpdate) => {
|
|
223
|
+
const currentRange2 = ranges[cursor - 1];
|
|
224
|
+
if (!currentRange2) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const { startContainer, startOffset } = currentRange2 ?? {};
|
|
228
|
+
replaceTextInRange(currentRange2, str, () => {
|
|
229
|
+
const unsub = realm.sub(editorSearchRanges$, (newRanges) => {
|
|
230
|
+
unsub();
|
|
231
|
+
if (isSimilarRange(newRanges[cursor - 1] ?? {}, {
|
|
232
|
+
startOffset,
|
|
233
|
+
startContainer
|
|
234
|
+
})) {
|
|
235
|
+
realm.pub(editorSearchCursor$, (cursor + 1) % (newRanges.length + 1) || 1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
onUpdate == null ? void 0 : onUpdate();
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const replaceAll = (str, onUpdate) => {
|
|
242
|
+
const runReplaceAll = () => {
|
|
243
|
+
let ticks = 0;
|
|
244
|
+
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
245
|
+
const textReplaceRange = ranges[i];
|
|
246
|
+
if (!textReplaceRange) {
|
|
247
|
+
throw new Error("error replacing all text range does not exist");
|
|
248
|
+
}
|
|
249
|
+
replaceTextInRange(textReplaceRange, str, () => {
|
|
250
|
+
ticks++;
|
|
251
|
+
if (ticks >= ranges.length) {
|
|
252
|
+
onUpdate == null ? void 0 : onUpdate();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
if (typeof requestIdleCallback === "function") {
|
|
258
|
+
requestIdleCallback(runReplaceAll);
|
|
259
|
+
} else {
|
|
260
|
+
setTimeout(runReplaceAll, 0);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
next,
|
|
265
|
+
prev,
|
|
266
|
+
total: rangeCount,
|
|
267
|
+
cursor,
|
|
268
|
+
setSearch,
|
|
269
|
+
search,
|
|
270
|
+
currentRange,
|
|
271
|
+
isSearchOpen,
|
|
272
|
+
setIsSearchOpen,
|
|
273
|
+
openSearch,
|
|
274
|
+
closeSearch,
|
|
275
|
+
toggleSearch,
|
|
276
|
+
ranges,
|
|
277
|
+
scrollToRangeOrIndex,
|
|
278
|
+
replace,
|
|
279
|
+
replaceAll
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const searchPlugin = realmPlugin({
|
|
283
|
+
//TODO: ensure proper event cleanup
|
|
284
|
+
init(realm) {
|
|
285
|
+
if (typeof CSS.highlights === "undefined") {
|
|
286
|
+
console.warn("CSS.highlights is not supported in this browser. Search functionality will be limited.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
realm.sub(editorSearchCursor$, (cursor) => {
|
|
290
|
+
const ranges = realm.getValue(editorSearchRanges$);
|
|
291
|
+
focusHighlightRange(ranges[cursor - 1]);
|
|
292
|
+
});
|
|
293
|
+
const updateHighlights = (searchQuery, textNodeIndex) => {
|
|
294
|
+
if (!searchQuery) {
|
|
295
|
+
realm.pub(editorSearchCursor$, 0);
|
|
296
|
+
realm.pub(editorSearchRanges$, []);
|
|
297
|
+
resetHighlights();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const ranges = Array.from(rangeSearchScan(searchQuery, textNodeIndex));
|
|
301
|
+
realm.pub(editorSearchRanges$, ranges);
|
|
302
|
+
highlightRanges(ranges);
|
|
303
|
+
if (ranges.length) {
|
|
304
|
+
const currentCursor = realm.getValue(editorSearchCursor$) || 1;
|
|
305
|
+
focusHighlightRange(ranges[currentCursor - 1]);
|
|
306
|
+
realm.pub(editorSearchCursor$, currentCursor);
|
|
307
|
+
const scrollRange = ranges[currentCursor - 1];
|
|
308
|
+
if (!scrollRange)
|
|
309
|
+
throw new Error("error updating highlights, scroll range does not exist");
|
|
310
|
+
const contentEditable = realm.getValue(editorSearchScollableContent$);
|
|
311
|
+
scrollToRange(scrollRange, contentEditable, {
|
|
312
|
+
ignoreIfInView: true
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
resetHighlights();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
realm.sub(editorSearchTextNodeIndex$, (textNodeIndex) => {
|
|
319
|
+
updateHighlights(realm.getValue(editorSearchTerm$), textNodeIndex);
|
|
320
|
+
});
|
|
321
|
+
realm.sub(editorSearchTerm$, (searchQuery) => {
|
|
322
|
+
updateHighlights(searchQuery, realm.getValue(editorSearchTextNodeIndex$));
|
|
323
|
+
});
|
|
324
|
+
realm.pub(createRootEditorSubscription$, (editor) => {
|
|
325
|
+
let observer = null;
|
|
326
|
+
return editor.registerRootListener((rootElement) => {
|
|
327
|
+
if (observer) {
|
|
328
|
+
observer.disconnect();
|
|
329
|
+
observer = null;
|
|
330
|
+
}
|
|
331
|
+
if (rootElement) {
|
|
332
|
+
const initialIndex = indexAllTextNodes(rootElement);
|
|
333
|
+
realm.pub(editorSearchTextNodeIndex$, initialIndex);
|
|
334
|
+
observer = new MutationObserver(() => {
|
|
335
|
+
const newIndex = indexAllTextNodes(rootElement);
|
|
336
|
+
if (realm.getValue(searchOpen$)) {
|
|
337
|
+
realm.pub(editorSearchTextNodeIndex$, newIndex);
|
|
338
|
+
} else {
|
|
339
|
+
realm.pub(debouncedIndexer$, newIndex);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
observer.observe(rootElement, {
|
|
343
|
+
childList: true,
|
|
344
|
+
subtree: true,
|
|
345
|
+
characterData: true
|
|
346
|
+
});
|
|
347
|
+
return () => observer == null ? void 0 : observer.disconnect();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
export {
|
|
354
|
+
EmptyTextNodeIndex,
|
|
355
|
+
MDX_FOCUS_SEARCH_NAME,
|
|
356
|
+
MDX_SEARCH_NAME,
|
|
357
|
+
debouncedIndexer$,
|
|
358
|
+
editorSearchCursor$,
|
|
359
|
+
editorSearchRanges$,
|
|
360
|
+
editorSearchScollableContent$,
|
|
361
|
+
editorSearchTerm$,
|
|
362
|
+
editorSearchTermDebounced$,
|
|
363
|
+
editorSearchTextNodeIndex$,
|
|
364
|
+
rangeSearchScan,
|
|
365
|
+
searchOpen$,
|
|
366
|
+
searchPlugin,
|
|
367
|
+
useEditorSearch
|
|
368
|
+
};
|