@mdxeditor/editor 4.0.4 → 4.1.1

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 (40) hide show
  1. package/dist/MDXEditor.js +138 -53
  2. package/dist/RealmWithPlugins.js +27 -9
  3. package/dist/exportMarkdownFromLexical.js +1 -1
  4. package/dist/index.d.ts +12 -10
  5. package/dist/index.js +6 -6
  6. package/dist/mdastUtilHtmlComment.js +2 -1
  7. package/dist/plugins/codeblock/CodeBlockNode.js +4 -5
  8. package/dist/plugins/codeblock/index.js +1 -1
  9. package/dist/plugins/codemirror/CodeMirrorEditor.js +1 -1
  10. package/dist/plugins/core/GenericHTMLNode.js +2 -5
  11. package/dist/plugins/core/NestedLexicalEditor.js +58 -38
  12. package/dist/plugins/core/index.js +26 -30
  13. package/dist/plugins/core/lexicalExtensions.js +143 -0
  14. package/dist/plugins/diff-source/SourceEditor.js +1 -1
  15. package/dist/plugins/directives/DirectiveNode.js +1 -4
  16. package/dist/plugins/frontmatter/FrontmatterNode.js +1 -4
  17. package/dist/plugins/frontmatter/index.js +1 -1
  18. package/dist/plugins/headings/index.js +2 -1
  19. package/dist/plugins/image/ImageDialog.js +1 -2
  20. package/dist/plugins/image/ImageEditor.js +1 -2
  21. package/dist/plugins/image/ImageNode.js +1 -4
  22. package/dist/plugins/image/index.js +4 -6
  23. package/dist/plugins/jsx/LexicalJsxNode.js +1 -4
  24. package/dist/plugins/jsx/LexicalMdxExpressionNode.js +1 -4
  25. package/dist/plugins/link-dialog/LinkDialog.js +3 -3
  26. package/dist/plugins/link-dialog/index.js +16 -4
  27. package/dist/plugins/lists/index.js +3 -3
  28. package/dist/plugins/markdown-shortcut/index.js +1 -1
  29. package/dist/plugins/search/index.js +259 -179
  30. package/dist/plugins/table/TableEditor.js +38 -24
  31. package/dist/plugins/table/TableNode.js +1 -4
  32. package/dist/plugins/thematic-break/LexicalThematicBreakVisitor.js +2 -2
  33. package/dist/plugins/toolbar/components/KitchenSinkToolbar.js +1 -1
  34. package/dist/plugins/toolbar/primitives/toolbar.js +1 -1
  35. package/dist/realmSession.js +38 -0
  36. package/dist/style.css +1511 -1525
  37. package/dist/utils/lexicalHelpers.js +53 -74
  38. package/package.json +23 -12
  39. package/dist/plugins/core/SharedHistoryPlugin.js +0 -10
  40. package/dist/styles/globals.css.js +0 -1
@@ -1,7 +1,9 @@
1
1
  import { Cell, debounceTime, useRealm, useCellValue, useCell } from "@mdxeditor/gurx";
2
- import { getNearestEditorFromDOMNode, $getNearestNodeFromDOMNode, $isTextNode, $createRangeSelection } from "lexical";
2
+ import { createDOMRange } from "@lexical/selection";
3
+ import { $getRoot, $getNodeByKey, $isTextNode, $createRangeSelection, HISTORY_PUSH_TAG } from "lexical";
3
4
  import { realmPlugin } from "../../RealmWithPlugins.js";
4
- import { contentEditableRef$, createRootEditorSubscription$ } from "../core/index.js";
5
+ import { contentEditableRef$, createActiveEditorSubscription$, activeEditor$, historyState$ } from "../core/index.js";
6
+ import { editorUsesHistoryState } from "../core/lexicalExtensions.js";
5
7
  const EmptyTextNodeIndex = {
6
8
  allText: "",
7
9
  nodeIndex: [],
@@ -19,7 +21,7 @@ const editorSearchScrollableContent$ = Cell(
19
21
  null,
20
22
  (r) => r.sub(contentEditableRef$, (cref) => {
21
23
  var _a;
22
- r.pub(editorSearchScrollableContent$, ((_a = cref == null ? void 0 : cref.current) == null ? void 0 : _a.parentNode) ?? null);
24
+ r.pub(editorSearchScrollableContent$, ((_a = cref == null ? void 0 : cref.current) == null ? void 0 : _a.parentElement) ?? null);
23
25
  })
24
26
  );
25
27
  const MDX_SEARCH_NAME = "MdxSearch";
@@ -27,6 +29,8 @@ const MDX_FOCUS_SEARCH_NAME = "MdxFocusSearch";
27
29
  const debouncedIndexer$ = Cell(EmptyTextNodeIndex, (realm) => {
28
30
  realm.link(debouncedIndexer$, realm.pipe(editorSearchTextNodeIndex$, realm.transformer(debounceTime(250))));
29
31
  });
32
+ const editorSearchActiveEditor$ = Cell(null);
33
+ const editorSearchStateMatches$ = Cell([]);
30
34
  function* searchText(allText, searchQuery) {
31
35
  if (!searchQuery) {
32
36
  return;
@@ -34,8 +38,7 @@ function* searchText(allText, searchQuery) {
34
38
  let regex;
35
39
  try {
36
40
  regex = new RegExp(searchQuery, "gi");
37
- } catch (e) {
38
- console.error("Invalid search pattern:", e);
41
+ } catch {
39
42
  return;
40
43
  }
41
44
  let match;
@@ -46,50 +49,17 @@ function* searchText(allText, searchQuery) {
46
49
  }
47
50
  continue;
48
51
  }
49
- const start = match.index;
50
- const end = start + match[0].length - 1;
51
- yield [start, end];
52
+ yield [match.index, match.index + match[0].length - 1];
52
53
  }
53
54
  }
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
55
  function* rangeSearchScan(searchQuery, { allText, offsetIndex, nodeIndex }) {
87
56
  for (const [start, end] of searchText(allText, searchQuery)) {
88
57
  const startOffset = offsetIndex[start];
89
58
  const endOffset = offsetIndex[end];
90
59
  const startNode = nodeIndex[start];
91
60
  const endNode = nodeIndex[end];
92
- const range = new Range();
61
+ const ownerDocument = (startNode == null ? void 0 : startNode.ownerDocument) ?? document;
62
+ const range = ownerDocument.createRange();
93
63
  if (startNode === void 0 || endNode === void 0 || startOffset === void 0 || endOffset === void 0) {
94
64
  throw new Error("Invalid range: startNode, endNode, startOffset, or endOffset is undefined.");
95
65
  }
@@ -98,15 +68,96 @@ function* rangeSearchScan(searchQuery, { allText, offsetIndex, nodeIndex }) {
98
68
  yield range;
99
69
  }
100
70
  }
71
+ function createSearchSnapshot(editor) {
72
+ let text = "";
73
+ const units = [];
74
+ for (const node of $getRoot().getAllTextNodes()) {
75
+ const nodeText = node.getTextContent();
76
+ let sourceOffset = 0;
77
+ while (sourceOffset < nodeText.length) {
78
+ const codePoint = nodeText.codePointAt(sourceOffset);
79
+ if (codePoint === void 0) break;
80
+ const sourceUnit = String.fromCodePoint(codePoint);
81
+ const normalizedUnit = sourceUnit.normalize("NFKD");
82
+ const mapsOneToOne = normalizedUnit === sourceUnit;
83
+ for (let normalizedOffset = 0; normalizedOffset < normalizedUnit.length; normalizedOffset++) {
84
+ const startOffset = mapsOneToOne ? sourceOffset + normalizedOffset : sourceOffset;
85
+ const endOffset = mapsOneToOne ? startOffset + 1 : sourceOffset + sourceUnit.length;
86
+ units.push({ key: node.getKey(), startOffset, endOffset });
87
+ text += normalizedUnit[normalizedOffset] ?? "";
88
+ }
89
+ sourceOffset += sourceUnit.length;
90
+ }
91
+ }
92
+ return { editor, text, units };
93
+ }
94
+ function findStateMatches(snapshot, searchQuery) {
95
+ const matches = [];
96
+ for (const [startIndex, endIndex] of searchText(snapshot.text, searchQuery)) {
97
+ const startUnit = snapshot.units[startIndex];
98
+ const endUnit = snapshot.units[endIndex];
99
+ if (!startUnit || !endUnit) continue;
100
+ matches.push({
101
+ editor: snapshot.editor,
102
+ start: { key: startUnit.key, offset: startUnit.startOffset },
103
+ end: { key: endUnit.key, offset: endUnit.endOffset }
104
+ });
105
+ }
106
+ return matches;
107
+ }
108
+ function projectSnapshot(snapshot, matches) {
109
+ const nodeIndex = [];
110
+ const offsetIndex = [];
111
+ const domTextNodes = /* @__PURE__ */ new Map();
112
+ const projectedMatches = [];
113
+ let projectionComplete = true;
114
+ snapshot.editor.getEditorState().read(() => {
115
+ for (const unit of snapshot.units) {
116
+ let domTextNode = domTextNodes.get(unit.key);
117
+ if (!domTextNode) {
118
+ const lexicalNode = $getNodeByKey(unit.key);
119
+ if (!$isTextNode(lexicalNode)) {
120
+ projectionComplete = false;
121
+ break;
122
+ }
123
+ const nodeRange = createDOMRange(snapshot.editor, lexicalNode, 0, lexicalNode, lexicalNode.getTextContentSize());
124
+ if (!nodeRange || nodeRange.startContainer !== nodeRange.endContainer) {
125
+ projectionComplete = false;
126
+ break;
127
+ }
128
+ domTextNode = nodeRange.startContainer;
129
+ domTextNodes.set(unit.key, domTextNode);
130
+ }
131
+ nodeIndex.push(domTextNode);
132
+ offsetIndex.push(unit.startOffset);
133
+ }
134
+ for (const match of matches) {
135
+ const startNode = $getNodeByKey(match.start.key);
136
+ const endNode = $getNodeByKey(match.end.key);
137
+ if (!$isTextNode(startNode) || !$isTextNode(endNode)) continue;
138
+ const range = createDOMRange(snapshot.editor, startNode, match.start.offset, endNode, match.end.offset);
139
+ if (range) projectedMatches.push({ match, range });
140
+ }
141
+ });
142
+ return {
143
+ index: projectionComplete ? { allText: snapshot.text, nodeIndex, offsetIndex } : EmptyTextNodeIndex,
144
+ projectedMatches
145
+ };
146
+ }
147
+ function supportsHighlights() {
148
+ return typeof CSS !== "undefined" && typeof CSS.highlights !== "undefined" && typeof Highlight !== "undefined";
149
+ }
101
150
  const focusHighlightRange = (range) => {
151
+ if (!supportsHighlights()) return;
102
152
  CSS.highlights.delete(MDX_FOCUS_SEARCH_NAME);
103
- if (range)
104
- CSS.highlights.set(MDX_FOCUS_SEARCH_NAME, new Highlight(range));
153
+ if (range) CSS.highlights.set(MDX_FOCUS_SEARCH_NAME, new Highlight(range));
105
154
  };
106
155
  const highlightRanges = (ranges) => {
156
+ if (!supportsHighlights()) return;
107
157
  CSS.highlights.set(MDX_SEARCH_NAME, new Highlight(...ranges));
108
158
  };
109
159
  const resetHighlights = () => {
160
+ if (!supportsHighlights()) return;
110
161
  CSS.highlights.delete(MDX_SEARCH_NAME);
111
162
  CSS.highlights.delete(MDX_FOCUS_SEARCH_NAME);
112
163
  };
@@ -130,56 +181,114 @@ const scrollToRange = (range, contentEditable, options) => {
130
181
  const rangeBottom = bottomRelativeToContainer + contentEditable.scrollTop;
131
182
  const visibleTop = contentEditable.scrollTop;
132
183
  const visibleBottom = visibleTop + contentEditable.clientHeight;
133
- const inView = rangeTop >= visibleTop && rangeBottom <= visibleBottom;
134
- if (inView)
135
- return;
184
+ if (rangeTop >= visibleTop && rangeBottom <= visibleBottom) return;
136
185
  }
137
186
  const top = topRelativeToContainer + contentEditable.scrollTop - first.height;
138
187
  contentEditable.scrollTo({ top, behavior });
139
188
  };
140
- function isSimilarRange(range1, range2) {
141
- return range1.startContainer === range2.startContainer && range1.startOffset === range2.startOffset;
189
+ function isSameStart(first, second) {
190
+ return (first == null ? void 0 : first.editor) === second.editor && first.start.key === second.start.key && first.start.offset === second.start.offset;
191
+ }
192
+ function clearSearchResults(realm) {
193
+ realm.pubIn({
194
+ [editorSearchStateMatches$]: [],
195
+ [editorSearchRanges$]: [],
196
+ [editorSearchCursor$]: 0
197
+ });
198
+ resetHighlights();
142
199
  }
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.");
200
+ function refreshSearch(realm, editor, advanceFrom) {
201
+ if (realm.getValue(editorSearchActiveEditor$) !== editor) return;
202
+ let snapshot;
203
+ editor.getEditorState().read(() => {
204
+ snapshot = createSearchSnapshot(editor);
205
+ });
206
+ const searchOpen = realm.getValue(searchOpen$);
207
+ const searchQuery = realm.getValue(editorSearchTerm$);
208
+ const stateMatches = searchOpen ? findStateMatches(snapshot, searchQuery) : [];
209
+ const { index, projectedMatches } = projectSnapshot(snapshot, stateMatches);
210
+ if (searchOpen) {
211
+ realm.pub(editorSearchTextNodeIndex$, index);
212
+ } else {
213
+ realm.pub(debouncedIndexer$, index);
214
+ }
215
+ if (!searchOpen || !searchQuery || projectedMatches.length === 0) {
216
+ clearSearchResults(realm);
151
217
  return;
152
218
  }
219
+ const matches = projectedMatches.map(({ match }) => match);
220
+ const ranges = projectedMatches.map(({ range }) => range);
221
+ const previousCursor = realm.getValue(editorSearchCursor$);
222
+ let cursor = Math.min(Math.max(previousCursor, 1), ranges.length);
223
+ if (advanceFrom && isSameStart(matches[advanceFrom.cursor - 1], advanceFrom.match)) {
224
+ cursor = advanceFrom.cursor % ranges.length + 1;
225
+ }
226
+ realm.pubIn({
227
+ [editorSearchStateMatches$]: matches,
228
+ [editorSearchRanges$]: ranges,
229
+ [editorSearchCursor$]: cursor
230
+ });
231
+ highlightRanges(ranges);
232
+ focusHighlightRange(ranges[cursor - 1]);
233
+ }
234
+ function resolveMatch(match) {
235
+ const startNode = $getNodeByKey(match.start.key);
236
+ const endNode = $getNodeByKey(match.end.key);
237
+ if (!$isTextNode(startNode) || !$isTextNode(endNode)) return null;
238
+ if (match.start.offset < 0 || match.start.offset > startNode.getTextContentSize()) return null;
239
+ if (match.end.offset < 0 || match.end.offset > endNode.getTextContentSize()) return null;
240
+ return { startNode, endNode };
241
+ }
242
+ function replaceStateMatches(editor, matches, replacement, onUpdate) {
243
+ if (matches.length === 0 || matches.some((match) => match.editor !== editor)) return;
153
244
  editor.update(
154
245
  () => {
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();
246
+ if (matches.some((match) => resolveMatch(match) === null)) return;
247
+ for (let index = matches.length - 1; index >= 0; index--) {
248
+ const match = matches[index];
249
+ if (!match) continue;
250
+ const resolved = resolveMatch(match);
251
+ if (!resolved) {
252
+ throw new Error("Search match became stale during replacement.");
253
+ }
254
+ if (resolved.startNode.is(resolved.endNode)) {
255
+ const replacedNode = resolved.startNode.spliceText(match.start.offset, match.end.offset - match.start.offset, replacement, true);
256
+ if (replacedNode.getTextContentSize() === 0) {
257
+ replacedNode.remove();
258
+ }
259
+ continue;
169
260
  }
261
+ const selection = $createRangeSelection();
262
+ selection.anchor.set(resolved.startNode.getKey(), match.start.offset, "text");
263
+ selection.focus.set(resolved.endNode.getKey(), match.end.offset, "text");
264
+ selection.insertText(replacement);
170
265
  }
171
266
  },
172
267
  {
268
+ tag: HISTORY_PUSH_TAG,
173
269
  onUpdate
174
270
  }
175
271
  );
176
272
  }
273
+ function ensureHistoryBaseline(realm, editor) {
274
+ var _a;
275
+ const historyState = realm.getValue(historyState$);
276
+ if (!editorUsesHistoryState(editor, historyState)) {
277
+ return;
278
+ }
279
+ if (((_a = historyState.current) == null ? void 0 : _a.editor) !== editor) {
280
+ if (historyState.current) {
281
+ historyState.undoStack.push(historyState.current);
282
+ }
283
+ historyState.current = { editor, editorState: editor.getEditorState() };
284
+ }
285
+ }
177
286
  function useEditorSearch() {
178
287
  const realm = useRealm();
179
288
  const ranges = useCellValue(editorSearchRanges$);
180
289
  const cursor = useCellValue(editorSearchCursor$);
181
290
  const search = useCellValue(editorSearchTerm$);
182
- const currentRange = ranges[cursor - 1] ?? null;
291
+ const currentRange = cursor > 0 ? ranges.at(cursor - 1) ?? null : null;
183
292
  const contentEditable = useCellValue(editorSearchScrollableContent$);
184
293
  const [isSearchOpen, setIsSearchOpen] = useCell(searchOpen$);
185
294
  const openSearch = () => {
@@ -191,13 +300,12 @@ function useEditorSearch() {
191
300
  const toggleSearch = () => {
192
301
  setIsSearchOpen(!isSearchOpen);
193
302
  };
194
- const rangeCount = ranges.length;
195
303
  const scrollToRangeOrIndex = (range, options) => {
196
304
  const scrollRange = typeof range === "number" ? ranges[range - 1] : range;
197
305
  if (!scrollRange) {
198
306
  throw new Error("Error scrolling to range, range does not exist");
199
307
  }
200
- scrollToRange(scrollRange, contentEditable, options);
308
+ scrollToRange(scrollRange, contentEditable ?? void 0, options);
201
309
  };
202
310
  const setSearch = (term) => {
203
311
  if ((term ?? "") !== search) {
@@ -206,64 +314,42 @@ function useEditorSearch() {
206
314
  realm.pub(editorSearchTermDebounced$, term ?? "");
207
315
  };
208
316
  const next = () => {
209
- if (!ranges.length)
210
- return;
211
- const newVal = cursor % ranges.length + 1;
212
- scrollToRangeOrIndex(newVal);
213
- realm.pub(editorSearchCursor$, newVal);
317
+ if (!ranges.length) return;
318
+ const newCursor = cursor % ranges.length + 1;
319
+ scrollToRangeOrIndex(newCursor);
320
+ realm.pub(editorSearchCursor$, newCursor);
214
321
  };
215
322
  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);
323
+ if (!ranges.length) return;
324
+ const newCursor = cursor <= 1 ? ranges.length : cursor - 1;
325
+ scrollToRangeOrIndex(newCursor);
326
+ realm.pub(editorSearchCursor$, newCursor);
221
327
  };
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
- });
328
+ const replace = (replacement, onUpdate) => {
329
+ const editor = realm.getValue(editorSearchActiveEditor$);
330
+ const matches = realm.getValue(editorSearchStateMatches$);
331
+ const match = matches[cursor - 1];
332
+ if (!editor || !match) return;
333
+ ensureHistoryBaseline(realm, editor);
334
+ replaceStateMatches(editor, [match], replacement, () => {
335
+ refreshSearch(realm, editor, { cursor, match });
238
336
  onUpdate == null ? void 0 : onUpdate();
239
337
  });
240
338
  };
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
- }
339
+ const replaceAll = (replacement, onUpdate) => {
340
+ const editor = realm.getValue(editorSearchActiveEditor$);
341
+ const matches = realm.getValue(editorSearchStateMatches$);
342
+ if (!editor || matches.length === 0) return;
343
+ ensureHistoryBaseline(realm, editor);
344
+ replaceStateMatches(editor, matches, replacement, () => {
345
+ refreshSearch(realm, editor);
346
+ onUpdate == null ? void 0 : onUpdate();
347
+ });
262
348
  };
263
349
  return {
264
350
  next,
265
351
  prev,
266
- total: rangeCount,
352
+ total: ranges.length,
267
353
  cursor,
268
354
  setSearch,
269
355
  search,
@@ -280,73 +366,67 @@ function useEditorSearch() {
280
366
  };
281
367
  }
282
368
  const searchPlugin = realmPlugin({
283
- //TODO: ensure proper event cleanup
284
369
  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;
370
+ if (!supportsHighlights()) {
371
+ console.warn("CSS.highlights is not supported in this browser. Search highlighting will be unavailable.");
288
372
  }
289
373
  realm.sub(editorSearchCursor$, (cursor) => {
290
- const ranges = realm.getValue(editorSearchRanges$);
291
- focusHighlightRange(ranges[cursor - 1]);
374
+ if (realm.getValue(searchOpen$)) {
375
+ focusHighlightRange(realm.getValue(editorSearchRanges$)[cursor - 1]);
376
+ }
292
377
  });
293
- const updateHighlights = (searchQuery, textNodeIndex) => {
294
- if (!searchQuery) {
295
- realm.pub(editorSearchCursor$, 0);
296
- realm.pub(editorSearchRanges$, []);
297
- resetHighlights();
378
+ realm.sub(editorSearchTerm$, () => {
379
+ const editor = realm.getValue(editorSearchActiveEditor$);
380
+ if (editor) refreshSearch(realm, editor);
381
+ });
382
+ realm.sub(searchOpen$, (searchOpen) => {
383
+ if (!searchOpen) {
384
+ clearSearchResults(realm);
298
385
  return;
299
386
  }
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(editorSearchScrollableContent$);
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);
387
+ const editor = realm.getValue(editorSearchActiveEditor$);
388
+ if (editor) refreshSearch(realm, editor);
320
389
  });
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();
390
+ realm.pub(createActiveEditorSubscription$, (editor) => {
391
+ var _a;
392
+ realm.pubIn({
393
+ [editorSearchActiveEditor$]: editor,
394
+ [editorSearchTextNodeIndex$]: EmptyTextNodeIndex,
395
+ [editorSearchScrollableContent$]: ((_a = editor.getRootElement()) == null ? void 0 : _a.parentElement) ?? null
396
+ });
397
+ clearSearchResults(realm);
398
+ refreshSearch(realm, editor);
399
+ const unregisterUpdate = editor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
400
+ if (dirtyElements.size > 0 || dirtyLeaves.size > 0) refreshSearch(realm, editor);
401
+ });
402
+ const unregisterRoot = editor.registerRootListener((rootElement) => {
403
+ if (!rootElement) {
404
+ if (realm.getValue(editorSearchActiveEditor$) === editor) {
405
+ realm.pubIn({
406
+ [editorSearchTextNodeIndex$]: EmptyTextNodeIndex,
407
+ [editorSearchScrollableContent$]: null
408
+ });
409
+ clearSearchResults(realm);
410
+ }
411
+ return;
348
412
  }
413
+ realm.pub(editorSearchScrollableContent$, rootElement.parentElement);
414
+ refreshSearch(realm, editor);
349
415
  });
416
+ return () => {
417
+ unregisterUpdate();
418
+ unregisterRoot();
419
+ if (realm.getValue(editorSearchActiveEditor$) === editor) {
420
+ realm.pub(editorSearchActiveEditor$, null);
421
+ clearSearchResults(realm);
422
+ }
423
+ };
424
+ });
425
+ realm.sub(activeEditor$, (editor) => {
426
+ if (!editor) {
427
+ realm.pub(editorSearchScrollableContent$, null);
428
+ clearSearchResults(realm);
429
+ }
350
430
  });
351
431
  }
352
432
  });