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.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- 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
|
-
}
|