autumnnote 2.0.0 → 2.2.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.
Files changed (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. package/src/styles/autumnnote.scss +0 -2866
@@ -1,512 +0,0 @@
1
- /**
2
- * FindReplace.js - Find & Replace dialog
3
- *
4
- * Opens via Ctrl+F (find only) or Ctrl+H (find + replace).
5
- * Uses TreeWalker to locate text matches and wraps them with <mark> elements
6
- * for highlighting. Supports case-sensitive search, Prev/Next navigation and
7
- * single / replace-all replacement.
8
- */
9
-
10
- import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
11
- import { BaseDialog } from './BaseDialog.js';
12
-
13
- export class FindReplace extends BaseDialog {
14
- /** @param {import('../Context.js').Context} context */
15
- constructor(context) {
16
- super(context);
17
-
18
- /** @type {HTMLInputElement|null} */
19
- this._findInput = null;
20
- /** @type {HTMLInputElement|null} */
21
- this._replaceInput = null;
22
- /** @type {HTMLInputElement|null} */
23
- this._caseCheckbox = null;
24
- /** @type {HTMLElement|null} */
25
- this._counterEl = null;
26
- /** @type {HTMLElement|null} */
27
- this._closeBtn = null;
28
-
29
- /** Live matches — each entry is { mark: HTMLElement } after highlighting */
30
- this._matches = [];
31
- this._currentIndex = -1;
32
- this._caseSensitive = false;
33
- this._useRegex = false;
34
- this._wholeWord = false;
35
- /** @type {'find'|'replace'} */
36
- this._mode = 'find';
37
-
38
- /** Cached compiled regex — reused when query and case-sensitivity are unchanged */
39
- this._queryRegex = null;
40
- this._lastQuery = null;
41
- this._lastCaseSensitive = null;
42
- this._lastUseRegex = null;
43
- this._lastWholeWord = null;
44
-
45
- this._focusTimer = null;
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // Lifecycle
50
- // ---------------------------------------------------------------------------
51
-
52
- destroy() {
53
- clearTimeout(this._focusTimer);
54
- this._focusTimer = null;
55
- this._clearHighlights();
56
- super.destroy();
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Public API
61
- // ---------------------------------------------------------------------------
62
-
63
- /**
64
- * Opens the dialog in 'find' or 'replace' mode.
65
- * @param {'find'|'replace'} [mode='find']
66
- */
67
- show(mode = 'find') {
68
- this._mode = mode;
69
- this._updateMode();
70
- this._open();
71
- // Pre-select whatever was previously typed so the user can retype immediately
72
- clearTimeout(this._focusTimer);
73
- this._focusTimer = setTimeout(() => {
74
- if (this._findInput) {
75
- this._findInput.select();
76
- this._findInput.focus();
77
- }
78
- }, 50);
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Open / close
83
- // ---------------------------------------------------------------------------
84
-
85
- _open() {
86
- if (!this._dialog) return;
87
- // If already visible, just focus the search input — don't re-trap.
88
- if (this._dialog.style.display === 'flex') {
89
- if (this._findInput) this._findInput.focus();
90
- return;
91
- }
92
- this._dialog.style.display = 'flex';
93
- // Release any previous trap before installing a new one.
94
- if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
95
- this._removeTrap = trapFocus(this._dialog, () => this._close());
96
- }
97
-
98
- _close() {
99
- this._clearHighlights();
100
- if (this._dialog) this._dialog.style.display = 'none';
101
- if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
102
- // Return focus to the editor
103
- this.context.invoke('editor.focus');
104
- }
105
-
106
- _updateMode() {
107
- if (!this._dialog) return;
108
- const replaceRow = this._dialog.querySelector('.an-fr-replace-row');
109
- const replaceActions = this._dialog.querySelector('.an-fr-replace-actions');
110
- const title = this._dialog.querySelector('.an-dialog-title');
111
-
112
- const isReplace = this._mode === 'replace';
113
- if (replaceRow) /** @type {HTMLElement} */ (replaceRow).style.display = isReplace ? '' : 'none';
114
- if (replaceActions) /** @type {HTMLElement} */ (replaceActions).style.display = isReplace ? '' : 'none';
115
- if (title) title.textContent = isReplace ? this.context.locale.findReplace.findReplaceTitle : this.context.locale.findReplace.findTitle;
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Build dialog
120
- // ---------------------------------------------------------------------------
121
-
122
- _buildDialog() {
123
- const L = this.context.locale.findReplace;
124
- const overlay = createElement('div', {
125
- class: 'an-dialog-overlay an-fr-dialog',
126
- role: 'dialog',
127
- 'aria-modal': 'true',
128
- 'aria-label': L.findReplaceTitle,
129
- });
130
- const box = createElement('div', { class: 'an-dialog-box an-fr-box' });
131
-
132
- // ---- Header: [icon][title] [×] ----
133
- const header = createElement('div', { class: 'an-fr-header' });
134
- const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
135
- const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
136
- iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
137
- const title = createElement('h3', { class: 'an-dialog-title' });
138
- title.textContent = L.findTitle;
139
- titleGroup.append(iconEl, title);
140
- const closeBtn = createElement('button', {
141
- type: 'button',
142
- class: 'an-icon-close',
143
- title: L.close,
144
- 'aria-label': L.close,
145
- });
146
- closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
147
- this._closeBtn = closeBtn;
148
- header.append(titleGroup, closeBtn);
149
- box.appendChild(header);
150
-
151
- // ---- Search bar: [input] [Aa] [↑] [↓] counter ----
152
- const searchBar = createElement('div', { class: 'an-fr-search-bar' });
153
- const findInput = /** @type {HTMLInputElement} */ (createElement('input', {
154
- type: 'text',
155
- class: 'an-input an-fr-input',
156
- placeholder: L.findPlaceholder,
157
- 'aria-label': L.searchAriaLabel,
158
- }));
159
- this._findInput = findInput;
160
-
161
- // Case-sensitive toggle (visual button; hidden checkbox kept for state)
162
- const caseCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
163
- type: 'checkbox',
164
- style: 'display:none',
165
- 'aria-hidden': 'true',
166
- }));
167
- this._caseCheckbox = caseCheckbox;
168
- const caseBtn = createElement('button', {
169
- type: 'button',
170
- class: 'an-fr-icon-btn',
171
- title: 'Case sensitive',
172
- 'aria-label': 'Case sensitive',
173
- });
174
- caseBtn.textContent = 'Aa';
175
-
176
- const regexBtn = createElement('button', {
177
- type: 'button',
178
- class: 'an-fr-icon-btn',
179
- title: L.useRegex,
180
- 'aria-label': L.useRegex,
181
- });
182
- regexBtn.textContent = '.*';
183
-
184
- const wholeWordBtn = createElement('button', {
185
- type: 'button',
186
- class: 'an-fr-icon-btn',
187
- title: L.wholeWord,
188
- 'aria-label': L.wholeWord,
189
- });
190
- wholeWordBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="10" rx="2"/><line x1="7" y1="21" x2="7" y2="17"/><line x1="17" y1="21" x2="17" y2="17"/></svg>`;
191
-
192
- const prevBtn = createElement('button', {
193
- type: 'button',
194
- class: 'an-fr-icon-btn',
195
- title: 'Previous (Shift+Enter)',
196
- 'aria-label': 'Previous',
197
- });
198
- prevBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`;
199
-
200
- const nextBtn = createElement('button', {
201
- type: 'button',
202
- class: 'an-fr-icon-btn',
203
- title: 'Next (Enter)',
204
- 'aria-label': 'Next',
205
- });
206
- nextBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
207
-
208
- const counter = createElement('span', { class: 'an-fr-counter' });
209
- this._counterEl = counter;
210
-
211
- searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, wholeWordBtn, prevBtn, nextBtn, counter);
212
- box.appendChild(searchBar);
213
-
214
- // ---- Replace row: [input] [Replace] [All] (hidden by default) ----
215
- const replaceRow = createElement('div', { class: 'an-fr-replace-row' });
216
- replaceRow.style.display = 'none';
217
- const replaceInput = /** @type {HTMLInputElement} */ (createElement('input', {
218
- type: 'text',
219
- class: 'an-input an-fr-input',
220
- placeholder: L.replacePlaceholder,
221
- 'aria-label': L.replaceAriaLabel,
222
- }));
223
- this._replaceInput = replaceInput;
224
- const replaceBtn = createElement('button', { type: 'button', class: 'an-btn an-fr-replace-btn' });
225
- replaceBtn.textContent = L.replaceBtn;
226
- const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary an-fr-replace-btn' });
227
- replaceAllBtn.textContent = L.replaceAllBtn;
228
- replaceRow.append(replaceInput, replaceBtn, replaceAllBtn);
229
- box.appendChild(replaceRow);
230
-
231
- // Compat div — _updateMode toggles this; kept empty so existing logic works
232
- const replaceActions = createElement('div', { class: 'an-fr-replace-actions' });
233
- replaceActions.style.display = 'none';
234
- box.appendChild(replaceActions);
235
-
236
- overlay.appendChild(box);
237
- makeDraggable(header, box);
238
-
239
- // ---- Event bindings ----
240
- const d1 = on(closeBtn, 'click', () => this._close());
241
- const d2 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
242
- const d3 = on(findInput, 'input', () => this._onSearch());
243
- const d4 = on(caseBtn, 'click', () => {
244
- this._caseSensitive = !this._caseSensitive;
245
- caseCheckbox.checked = this._caseSensitive;
246
- caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
247
- this._onSearch();
248
- });
249
- // Hidden checkbox change handler keeps backward-compat with programmatic toggles
250
- const d5 = on(caseCheckbox, 'change', () => {
251
- this._caseSensitive = caseCheckbox.checked;
252
- caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
253
- this._onSearch();
254
- });
255
- const d6 = on(nextBtn, 'click', () => this._next());
256
- const d7 = on(prevBtn, 'click', () => this._prev());
257
- const d8 = on(replaceBtn, 'click', () => this._replace());
258
- const d9 = on(replaceAllBtn, 'click', () => this._replaceAll());
259
- const d10 = on(findInput, 'keydown', (e) => {
260
- const ke = /** @type {KeyboardEvent} */ (e);
261
- if (ke.key === 'Enter') {
262
- e.preventDefault();
263
- ke.shiftKey ? this._prev() : this._next();
264
- }
265
- });
266
- const d11 = on(replaceInput, 'keydown', (e) => {
267
- if (/** @type {KeyboardEvent} */ (e).key === 'Enter') {
268
- e.preventDefault();
269
- this._replace();
270
- }
271
- });
272
- const dRegex = on(regexBtn, 'click', () => {
273
- this._useRegex = !this._useRegex;
274
- regexBtn.classList.toggle('an-fr-icon-btn--active', this._useRegex);
275
- this._queryRegex = null; // force recompile
276
- this._lastQuery = null;
277
- this._onSearch();
278
- });
279
- const dWholeWord = on(wholeWordBtn, 'click', () => {
280
- this._wholeWord = !this._wholeWord;
281
- wholeWordBtn.classList.toggle('an-fr-icon-btn--active', this._wholeWord);
282
- this._queryRegex = null;
283
- this._lastQuery = null;
284
- this._onSearch();
285
- });
286
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex, dWholeWord);
287
-
288
- return overlay;
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Search logic
293
- // ---------------------------------------------------------------------------
294
-
295
- _onSearch() {
296
- this._clearHighlights();
297
- const query = this._findInput ? this._findInput.value : '';
298
- if (!query) {
299
- this._updateCounter();
300
- return;
301
- }
302
- this._findAndHighlight(query);
303
- this._updateCounter();
304
- }
305
-
306
- /**
307
- * Finds all occurrences of `query` in the editable area text nodes,
308
- * then wraps each match in a <mark class="an-highlight"> element.
309
- * Iterates text nodes in reverse so earlier offsets remain valid.
310
- * @param {string} query
311
- */
312
- _findAndHighlight(query) {
313
- const editable = this.context.layoutInfo.editable;
314
- if (!editable) return;
315
-
316
- // Collect raw matches: { node, start, end }
317
- const rawMatches = this._findRawMatches(query, editable);
318
- if (rawMatches.length === 0) return;
319
-
320
- // Wrap matches in reverse order so earlier text offsets stay valid when
321
- // later sections of the same text node are split by surroundContents().
322
- // Use push() instead of unshift() to avoid O(n²) shifting on every insert;
323
- // reverse() at the end restores forward document order in O(n).
324
- for (let i = rawMatches.length - 1; i >= 0; i--) {
325
- const { node, start, end } = rawMatches[i];
326
- try {
327
- const range = document.createRange();
328
- range.setStart(node, start);
329
- range.setEnd(node, end);
330
- const mark = document.createElement('mark');
331
- mark.className = 'an-highlight';
332
- range.surroundContents(mark);
333
- this._matches.push({ mark });
334
- } catch (_) {
335
- void _; // surroundContents fails when the range crosses element boundaries
336
- this._matches.push({ mark: null });
337
- }
338
- }
339
- this._matches.reverse(); // O(n) — restore forward document order
340
-
341
- // Drop entries where wrapping failed so the counter and navigation are accurate
342
- this._matches = this._matches.filter((m) => m.mark);
343
- this._currentIndex = 0;
344
- if (this._matches.length === 0) return;
345
-
346
- // Highlight the first (current) match
347
- if (this._matches[0]?.mark) {
348
- this._matches[0].mark.className = 'an-highlight an-highlight-current';
349
- this._matches[0].mark.scrollIntoView({ block: 'center', behavior: 'smooth' });
350
- }
351
- }
352
-
353
- /**
354
- * Walks all text nodes under `root` and returns positional match descriptors.
355
- * @param {string} query
356
- * @param {HTMLElement} root
357
- * @returns {{ node: Text, start: number, end: number }[]}
358
- */
359
- _findRawMatches(query, root) {
360
- const results = [];
361
- // Reuse compiled regex when query, case-sensitivity, regex mode, and whole-word haven't changed
362
- if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex || this._lastWholeWord !== this._wholeWord) {
363
- const flags = this._caseSensitive ? 'g' : 'gi';
364
- try {
365
- let pattern = this._useRegex
366
- ? query
367
- : query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
368
- if (this._wholeWord) pattern = `\\b${pattern}\\b`;
369
- this._queryRegex = new RegExp(pattern, flags);
370
- } catch (_) {
371
- this._queryRegex = null;
372
- }
373
- this._lastQuery = query;
374
- this._lastCaseSensitive = this._caseSensitive;
375
- this._lastUseRegex = this._useRegex;
376
- this._lastWholeWord = this._wholeWord;
377
- }
378
- if (!this._queryRegex) return results;
379
- const re = this._queryRegex;
380
-
381
- // Cap results to prevent blocking the main thread on very large documents
382
- const MAX_RESULTS = 500;
383
- const walker = document.createTreeWalker(root, 0x4 /* NodeFilter.SHOW_TEXT */);
384
- let node = walker.nextNode();
385
- while (node && results.length < MAX_RESULTS) {
386
- re.lastIndex = 0;
387
- let m;
388
- while ((m = re.exec(/** @type {Text} */ (node).textContent)) !== null && results.length < MAX_RESULTS) {
389
- results.push({ node: /** @type {Text} */ (node), start: m.index, end: m.index + m[0].length });
390
- }
391
- node = walker.nextNode();
392
- }
393
- return results;
394
- }
395
-
396
- // ---------------------------------------------------------------------------
397
- // Navigation
398
- // ---------------------------------------------------------------------------
399
-
400
- _next() {
401
- if (this._matches.length === 0) return;
402
- this._currentIndex = (this._currentIndex + 1) % this._matches.length;
403
- this._scrollToMatch(this._currentIndex);
404
- this._updateCounter();
405
- }
406
-
407
- _prev() {
408
- if (this._matches.length === 0) return;
409
- this._currentIndex = (this._currentIndex - 1 + this._matches.length) % this._matches.length;
410
- this._scrollToMatch(this._currentIndex);
411
- this._updateCounter();
412
- }
413
-
414
- _scrollToMatch(index) {
415
- const match = this._matches[index];
416
- if (!match?.mark) return;
417
- // Update CSS classes
418
- this._matches.forEach((m, i) => {
419
- if (m.mark) {
420
- m.mark.className = i === index
421
- ? 'an-highlight an-highlight-current'
422
- : 'an-highlight';
423
- }
424
- });
425
- match.mark.scrollIntoView({ block: 'center', behavior: 'smooth' });
426
- }
427
-
428
- // ---------------------------------------------------------------------------
429
- // Replace
430
- // ---------------------------------------------------------------------------
431
-
432
- _replace() {
433
- if (this._matches.length === 0 || this._currentIndex < 0) return;
434
- const match = this._matches[this._currentIndex];
435
- if (!match?.mark?.parentNode) return;
436
-
437
- const replacement = this._replaceInput ? this._replaceInput.value : '';
438
- const parent = match.mark.parentNode;
439
- const textNode = document.createTextNode(replacement);
440
- parent.insertBefore(textNode, match.mark);
441
- match.mark.remove();
442
- parent.normalize();
443
- this.context.invoke('editor.afterCommand');
444
- const savedIndex = this._currentIndex;
445
- this._onSearch();
446
- if (this._matches.length > 0) {
447
- this._currentIndex = Math.min(savedIndex, this._matches.length - 1);
448
- this._scrollToMatch(this._currentIndex);
449
- this._updateCounter();
450
- }
451
- }
452
-
453
- _replaceAll() {
454
- if (this._matches.length === 0) return;
455
- const replacement = this._replaceInput ? this._replaceInput.value : '';
456
-
457
- this._matches.forEach(({ mark }) => {
458
- if (!mark?.parentNode) return;
459
- const textNode = document.createTextNode(replacement);
460
- mark.parentNode.insertBefore(textNode, mark);
461
- mark.remove();
462
- });
463
-
464
- if (this.context.layoutInfo.editable) {
465
- this.context.layoutInfo.editable.normalize();
466
- }
467
- this._matches = [];
468
- this._currentIndex = -1;
469
- this.context.invoke('editor.afterCommand');
470
- this._onSearch();
471
- }
472
-
473
- // ---------------------------------------------------------------------------
474
- // Highlight management
475
- // ---------------------------------------------------------------------------
476
-
477
- /**
478
- * Removes all <mark class="an-highlight"> elements from the editable area,
479
- * restoring the original text nodes via normalization.
480
- */
481
- _clearHighlights() {
482
- const editable = this.context.layoutInfo.editable;
483
- if (!editable) return;
484
-
485
- editable.querySelectorAll('mark.an-highlight').forEach((mark) => {
486
- const parent = mark.parentNode;
487
- if (!parent) return;
488
- while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
489
- mark.remove();
490
- });
491
- // Merge adjacent text nodes after unwrapping
492
- editable.normalize();
493
-
494
- this._matches = [];
495
- this._currentIndex = -1;
496
- }
497
-
498
- // ---------------------------------------------------------------------------
499
- // Counter display
500
- // ---------------------------------------------------------------------------
501
-
502
- _updateCounter() {
503
- if (!this._counterEl) return;
504
- const total = this._matches.length;
505
- if (total === 0) {
506
- const query = this._findInput ? this._findInput.value : '';
507
- this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : '';
508
- } else {
509
- this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
510
- }
511
- }
512
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Fullscreen.js - Toggles the editor in/out of fullscreen mode
3
- * Inspired by Summernote's Fullscreen module
4
- */
5
-
6
- import { on } from '../core/dom.js';
7
- import { key, isKey } from '../core/key.js';
8
-
9
- export class Fullscreen {
10
- /**
11
- * @param {import('../Context.js').Context} context
12
- */
13
- constructor(context) {
14
- this.context = context;
15
- this._active = false;
16
- this._disposers = [];
17
- /** @type {string} cached editable height before fullscreen */
18
- this._prevHeight = '';
19
- }
20
-
21
- initialize() {
22
- // Press Escape to exit fullscreen
23
- const d = on(document, 'keydown', (event) => {
24
- if (this._active && isKey(/** @type {KeyboardEvent} */ (event), key.ESCAPE)) {
25
- this.deactivate();
26
- }
27
- });
28
- this._disposers.push(d);
29
- return this;
30
- }
31
-
32
- destroy() {
33
- this._disposers.forEach((d) => d());
34
- this._disposers = [];
35
- if (this._active) this.deactivate();
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Public API
40
- // ---------------------------------------------------------------------------
41
-
42
- toggle() {
43
- if (this._active) {
44
- this.deactivate();
45
- } else {
46
- this.activate();
47
- }
48
- }
49
-
50
- isActive() {
51
- return this._active;
52
- }
53
-
54
- activate() {
55
- if (this._active) return;
56
- const container = this.context.layoutInfo.container;
57
- // Save the container's explicit height (set by the resize handle) so it
58
- // can be restored when exiting fullscreen.
59
- this._prevHeight = container.style.height;
60
-
61
- container.classList.add('an-fullscreen');
62
- // Clear any resize-imposed height so the fullscreen CSS (inset:0) takes over.
63
- container.style.height = '';
64
- document.body.style.overflow = 'hidden';
65
- this._active = true;
66
- this.context.invoke('toolbar.refresh');
67
- }
68
-
69
- deactivate() {
70
- if (!this._active) return;
71
- const container = this.context.layoutInfo.container;
72
-
73
- container.classList.remove('an-fullscreen');
74
- // Restore whatever explicit height the user had set via the resize handle.
75
- container.style.height = this._prevHeight;
76
- document.body.style.overflow = '';
77
- this._active = false;
78
- this.context.invoke('toolbar.refresh');
79
- }
80
- }