autumnnote 1.0.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/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,440 @@
|
|
|
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 } from '../core/dom.js';
|
|
11
|
+
|
|
12
|
+
export class FindReplace {
|
|
13
|
+
/** @param {import('../Context.js').Context} context */
|
|
14
|
+
constructor(context) {
|
|
15
|
+
this.context = context;
|
|
16
|
+
|
|
17
|
+
/** @type {HTMLElement|null} */
|
|
18
|
+
this._dialog = null;
|
|
19
|
+
/** @type {HTMLInputElement|null} */
|
|
20
|
+
this._findInput = null;
|
|
21
|
+
/** @type {HTMLInputElement|null} */
|
|
22
|
+
this._replaceInput = null;
|
|
23
|
+
/** @type {HTMLInputElement|null} */
|
|
24
|
+
this._caseCheckbox = null;
|
|
25
|
+
/** @type {HTMLElement|null} */
|
|
26
|
+
this._counterEl = null;
|
|
27
|
+
/** @type {HTMLElement|null} */
|
|
28
|
+
this._closeBtn = null;
|
|
29
|
+
|
|
30
|
+
/** Live matches — each entry is { mark: HTMLElement } after highlighting */
|
|
31
|
+
this._matches = [];
|
|
32
|
+
this._currentIndex = -1;
|
|
33
|
+
this._caseSensitive = false;
|
|
34
|
+
/** @type {'find'|'replace'} */
|
|
35
|
+
this._mode = 'find';
|
|
36
|
+
|
|
37
|
+
this._disposers = [];
|
|
38
|
+
this._removeTrap = null;
|
|
39
|
+
this._focusTimer = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Lifecycle
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
initialize() {
|
|
47
|
+
this._dialog = this._buildDialog();
|
|
48
|
+
document.body.appendChild(this._dialog);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
destroy() {
|
|
53
|
+
clearTimeout(this._focusTimer);
|
|
54
|
+
this._focusTimer = null;
|
|
55
|
+
this._clearHighlights();
|
|
56
|
+
this._disposers.forEach((d) => d());
|
|
57
|
+
this._disposers = [];
|
|
58
|
+
if (this._dialog && this._dialog.parentNode) {
|
|
59
|
+
this._dialog.parentNode.removeChild(this._dialog);
|
|
60
|
+
}
|
|
61
|
+
this._dialog = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Public API
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Opens the dialog in 'find' or 'replace' mode.
|
|
70
|
+
* @param {'find'|'replace'} [mode='find']
|
|
71
|
+
*/
|
|
72
|
+
show(mode = 'find') {
|
|
73
|
+
this._mode = mode;
|
|
74
|
+
this._updateMode();
|
|
75
|
+
this._open();
|
|
76
|
+
// Pre-select whatever was previously typed so the user can retype immediately
|
|
77
|
+
clearTimeout(this._focusTimer);
|
|
78
|
+
this._focusTimer = setTimeout(() => {
|
|
79
|
+
if (this._findInput) {
|
|
80
|
+
this._findInput.select();
|
|
81
|
+
this._findInput.focus();
|
|
82
|
+
}
|
|
83
|
+
}, 50);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Open / close
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
_open() {
|
|
91
|
+
if (!this._dialog) return;
|
|
92
|
+
// If already visible, just focus the search input — don't re-trap.
|
|
93
|
+
if (this._dialog.style.display === 'flex') {
|
|
94
|
+
if (this._findInput) this._findInput.focus();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this._dialog.style.display = 'flex';
|
|
98
|
+
// Release any previous trap before installing a new one.
|
|
99
|
+
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
100
|
+
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_close() {
|
|
104
|
+
this._clearHighlights();
|
|
105
|
+
if (this._dialog) this._dialog.style.display = 'none';
|
|
106
|
+
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
107
|
+
// Return focus to the editor
|
|
108
|
+
this.context.invoke('editor.focus');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_updateMode() {
|
|
112
|
+
if (!this._dialog) return;
|
|
113
|
+
const replaceRow = this._dialog.querySelector('.an-fr-replace-row');
|
|
114
|
+
const replaceActions = this._dialog.querySelector('.an-fr-replace-actions');
|
|
115
|
+
const title = this._dialog.querySelector('.an-dialog-title');
|
|
116
|
+
|
|
117
|
+
const isReplace = this._mode === 'replace';
|
|
118
|
+
if (replaceRow) replaceRow.style.display = isReplace ? '' : 'none';
|
|
119
|
+
if (replaceActions) replaceActions.style.display = isReplace ? '' : 'none';
|
|
120
|
+
if (title) title.textContent = isReplace ? 'Find & Replace' : 'Find';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Build dialog
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
_buildDialog() {
|
|
128
|
+
const overlay = createElement('div', {
|
|
129
|
+
class: 'an-dialog-overlay an-fr-dialog',
|
|
130
|
+
role: 'dialog',
|
|
131
|
+
'aria-modal': 'true',
|
|
132
|
+
'aria-label': 'Find and Replace',
|
|
133
|
+
});
|
|
134
|
+
const box = createElement('div', { class: 'an-dialog-box' });
|
|
135
|
+
|
|
136
|
+
// ---- Title row ----
|
|
137
|
+
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
138
|
+
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
139
|
+
title.textContent = 'Find';
|
|
140
|
+
const closeBtn = createElement('button', {
|
|
141
|
+
type: 'button',
|
|
142
|
+
class: 'an-icon-close',
|
|
143
|
+
'aria-label': 'Close',
|
|
144
|
+
});
|
|
145
|
+
closeBtn.textContent = '×';
|
|
146
|
+
this._closeBtn = closeBtn;
|
|
147
|
+
titleRow.append(title, closeBtn);
|
|
148
|
+
box.appendChild(titleRow);
|
|
149
|
+
|
|
150
|
+
// ---- Find row ----
|
|
151
|
+
const findRow = createElement('div', { class: 'an-fr-find-row' });
|
|
152
|
+
const findInput = createElement('input', {
|
|
153
|
+
type: 'text',
|
|
154
|
+
class: 'an-input',
|
|
155
|
+
placeholder: 'Find…',
|
|
156
|
+
'aria-label': 'Search text',
|
|
157
|
+
});
|
|
158
|
+
this._findInput = findInput;
|
|
159
|
+
findRow.appendChild(findInput);
|
|
160
|
+
box.appendChild(findRow);
|
|
161
|
+
|
|
162
|
+
// ---- Options row (case-sensitive toggle + match counter) ----
|
|
163
|
+
const optRow = createElement('div', { class: 'an-fr-options-row' });
|
|
164
|
+
const caseLabel = createElement('label', { class: 'an-label an-label-inline' });
|
|
165
|
+
const caseCheckbox = createElement('input', {
|
|
166
|
+
type: 'checkbox',
|
|
167
|
+
'aria-label': 'Case sensitive',
|
|
168
|
+
});
|
|
169
|
+
this._caseCheckbox = caseCheckbox;
|
|
170
|
+
caseLabel.append(caseCheckbox, document.createTextNode('\u00a0Case sensitive'));
|
|
171
|
+
const counter = createElement('span', { class: 'an-fr-counter' });
|
|
172
|
+
this._counterEl = counter;
|
|
173
|
+
optRow.append(caseLabel, counter);
|
|
174
|
+
box.appendChild(optRow);
|
|
175
|
+
|
|
176
|
+
// ---- Find actions ----
|
|
177
|
+
const findActions = createElement('div', { class: 'an-dialog-actions an-fr-find-actions' });
|
|
178
|
+
const prevBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
179
|
+
prevBtn.textContent = '\u2190 Prev';
|
|
180
|
+
const nextBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
181
|
+
nextBtn.textContent = 'Next \u2192';
|
|
182
|
+
findActions.append(prevBtn, nextBtn);
|
|
183
|
+
box.appendChild(findActions);
|
|
184
|
+
|
|
185
|
+
// ---- Replace row (hidden by default) ----
|
|
186
|
+
const replaceRow = createElement('div', { class: 'an-fr-replace-row' });
|
|
187
|
+
replaceRow.style.display = 'none';
|
|
188
|
+
const replaceInput = createElement('input', {
|
|
189
|
+
type: 'text',
|
|
190
|
+
class: 'an-input',
|
|
191
|
+
placeholder: 'Replace with\u2026',
|
|
192
|
+
'aria-label': 'Replace with',
|
|
193
|
+
});
|
|
194
|
+
this._replaceInput = replaceInput;
|
|
195
|
+
replaceRow.appendChild(replaceInput);
|
|
196
|
+
box.appendChild(replaceRow);
|
|
197
|
+
|
|
198
|
+
// ---- Replace actions (hidden by default) ----
|
|
199
|
+
const replaceActions = createElement('div', { class: 'an-dialog-actions an-fr-replace-actions' });
|
|
200
|
+
replaceActions.style.display = 'none';
|
|
201
|
+
const replaceBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
202
|
+
replaceBtn.textContent = 'Replace';
|
|
203
|
+
const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
204
|
+
replaceAllBtn.textContent = 'Replace All';
|
|
205
|
+
replaceActions.append(replaceBtn, replaceAllBtn);
|
|
206
|
+
box.appendChild(replaceActions);
|
|
207
|
+
|
|
208
|
+
overlay.appendChild(box);
|
|
209
|
+
|
|
210
|
+
// ---- Event bindings ----
|
|
211
|
+
const d1 = on(closeBtn, 'click', () => this._close());
|
|
212
|
+
const d2 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
213
|
+
const d3 = on(findInput, 'input', () => this._onSearch());
|
|
214
|
+
const d4 = on(caseCheckbox, 'change', () => {
|
|
215
|
+
this._caseSensitive = caseCheckbox.checked;
|
|
216
|
+
this._onSearch();
|
|
217
|
+
});
|
|
218
|
+
const d5 = on(nextBtn, 'click', () => this._next());
|
|
219
|
+
const d6 = on(prevBtn, 'click', () => this._prev());
|
|
220
|
+
const d7 = on(replaceBtn, 'click', () => this._replace());
|
|
221
|
+
const d8 = on(replaceAllBtn, 'click', () => this._replaceAll());
|
|
222
|
+
const d9 = on(findInput, 'keydown', (e) => {
|
|
223
|
+
if (e.key === 'Enter') {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
e.shiftKey ? this._prev() : this._next();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const d10 = on(replaceInput, 'keydown', (e) => {
|
|
229
|
+
if (e.key === 'Enter') {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
this._replace();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10);
|
|
235
|
+
|
|
236
|
+
return overlay;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Search logic
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
_onSearch() {
|
|
244
|
+
this._clearHighlights();
|
|
245
|
+
const query = this._findInput ? this._findInput.value : '';
|
|
246
|
+
if (!query) {
|
|
247
|
+
this._updateCounter();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this._findAndHighlight(query);
|
|
251
|
+
this._updateCounter();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Finds all occurrences of `query` in the editable area text nodes,
|
|
256
|
+
* then wraps each match in a <mark class="an-highlight"> element.
|
|
257
|
+
* Iterates text nodes in reverse so earlier offsets remain valid.
|
|
258
|
+
* @param {string} query
|
|
259
|
+
*/
|
|
260
|
+
_findAndHighlight(query) {
|
|
261
|
+
const editable = this.context.layoutInfo.editable;
|
|
262
|
+
if (!editable) return;
|
|
263
|
+
|
|
264
|
+
// Collect raw matches: { node, start, end }
|
|
265
|
+
const rawMatches = this._findRawMatches(query, editable);
|
|
266
|
+
if (rawMatches.length === 0) return;
|
|
267
|
+
|
|
268
|
+
this._currentIndex = 0;
|
|
269
|
+
|
|
270
|
+
// Wrap in reverse order so earlier offsets in the same text node stay valid
|
|
271
|
+
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
272
|
+
const { node, start, end } = rawMatches[i];
|
|
273
|
+
try {
|
|
274
|
+
const range = document.createRange();
|
|
275
|
+
range.setStart(node, start);
|
|
276
|
+
range.setEnd(node, end);
|
|
277
|
+
const mark = document.createElement('mark');
|
|
278
|
+
mark.className = 'an-highlight';
|
|
279
|
+
range.surroundContents(mark);
|
|
280
|
+
this._matches.unshift({ mark });
|
|
281
|
+
} catch (_) {
|
|
282
|
+
// surroundContents fails when the range crosses element boundaries.
|
|
283
|
+
// This can happen with <br> inside matched text — skip safely.
|
|
284
|
+
this._matches.unshift({ mark: null });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Drop entries where wrapping failed so the counter and navigation are accurate
|
|
289
|
+
this._matches = this._matches.filter((m) => m.mark);
|
|
290
|
+
if (this._matches.length === 0) return;
|
|
291
|
+
|
|
292
|
+
// Highlight the first (current) match
|
|
293
|
+
if (this._matches[0] && this._matches[0].mark) {
|
|
294
|
+
this._matches[0].mark.className = 'an-highlight an-highlight-current';
|
|
295
|
+
this._matches[0].mark.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Walks all text nodes under `root` and returns positional match descriptors.
|
|
301
|
+
* @param {string} query
|
|
302
|
+
* @param {HTMLElement} root
|
|
303
|
+
* @returns {{ node: Text, start: number, end: number }[]}
|
|
304
|
+
*/
|
|
305
|
+
_findRawMatches(query, root) {
|
|
306
|
+
const results = [];
|
|
307
|
+
const flags = this._caseSensitive ? 'g' : 'gi';
|
|
308
|
+
// Escape regex special characters in the literal query string
|
|
309
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
310
|
+
const re = new RegExp(escaped, flags);
|
|
311
|
+
|
|
312
|
+
const walker = document.createTreeWalker(root, 0x4 /* NodeFilter.SHOW_TEXT */);
|
|
313
|
+
let node;
|
|
314
|
+
while ((node = walker.nextNode())) {
|
|
315
|
+
re.lastIndex = 0;
|
|
316
|
+
let m;
|
|
317
|
+
while ((m = re.exec(node.textContent)) !== null) {
|
|
318
|
+
results.push({ node, start: m.index, end: m.index + m[0].length });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Navigation
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
_next() {
|
|
329
|
+
if (this._matches.length === 0) return;
|
|
330
|
+
this._currentIndex = (this._currentIndex + 1) % this._matches.length;
|
|
331
|
+
this._scrollToMatch(this._currentIndex);
|
|
332
|
+
this._updateCounter();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_prev() {
|
|
336
|
+
if (this._matches.length === 0) return;
|
|
337
|
+
this._currentIndex = (this._currentIndex - 1 + this._matches.length) % this._matches.length;
|
|
338
|
+
this._scrollToMatch(this._currentIndex);
|
|
339
|
+
this._updateCounter();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_scrollToMatch(index) {
|
|
343
|
+
const match = this._matches[index];
|
|
344
|
+
if (!match || !match.mark) return;
|
|
345
|
+
// Update CSS classes
|
|
346
|
+
this._matches.forEach((m, i) => {
|
|
347
|
+
if (m.mark) {
|
|
348
|
+
m.mark.className = i === index
|
|
349
|
+
? 'an-highlight an-highlight-current'
|
|
350
|
+
: 'an-highlight';
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
match.mark.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Replace
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
_replace() {
|
|
361
|
+
if (this._matches.length === 0 || this._currentIndex < 0) return;
|
|
362
|
+
const match = this._matches[this._currentIndex];
|
|
363
|
+
if (!match || !match.mark || !match.mark.parentNode) return;
|
|
364
|
+
|
|
365
|
+
const replacement = this._replaceInput ? this._replaceInput.value : '';
|
|
366
|
+
const parent = match.mark.parentNode;
|
|
367
|
+
const textNode = document.createTextNode(replacement);
|
|
368
|
+
parent.insertBefore(textNode, match.mark);
|
|
369
|
+
parent.removeChild(match.mark);
|
|
370
|
+
parent.normalize();
|
|
371
|
+
this.context.invoke('editor.afterCommand');
|
|
372
|
+
const savedIndex = this._currentIndex;
|
|
373
|
+
this._onSearch();
|
|
374
|
+
if (this._matches.length > 0) {
|
|
375
|
+
this._currentIndex = Math.min(savedIndex, this._matches.length - 1);
|
|
376
|
+
this._scrollToMatch(this._currentIndex);
|
|
377
|
+
this._updateCounter();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_replaceAll() {
|
|
382
|
+
if (this._matches.length === 0) return;
|
|
383
|
+
const replacement = this._replaceInput ? this._replaceInput.value : '';
|
|
384
|
+
|
|
385
|
+
this._matches.forEach(({ mark }) => {
|
|
386
|
+
if (!mark || !mark.parentNode) return;
|
|
387
|
+
const textNode = document.createTextNode(replacement);
|
|
388
|
+
mark.parentNode.insertBefore(textNode, mark);
|
|
389
|
+
mark.parentNode.removeChild(mark);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (this.context.layoutInfo.editable) {
|
|
393
|
+
this.context.layoutInfo.editable.normalize();
|
|
394
|
+
}
|
|
395
|
+
this._matches = [];
|
|
396
|
+
this._currentIndex = -1;
|
|
397
|
+
this.context.invoke('editor.afterCommand');
|
|
398
|
+
this._onSearch();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Highlight management
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Removes all <mark class="an-highlight"> elements from the editable area,
|
|
407
|
+
* restoring the original text nodes via normalization.
|
|
408
|
+
*/
|
|
409
|
+
_clearHighlights() {
|
|
410
|
+
const editable = this.context.layoutInfo.editable;
|
|
411
|
+
if (!editable) return;
|
|
412
|
+
|
|
413
|
+
editable.querySelectorAll('mark.an-highlight').forEach((mark) => {
|
|
414
|
+
const parent = mark.parentNode;
|
|
415
|
+
if (!parent) return;
|
|
416
|
+
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
|
417
|
+
parent.removeChild(mark);
|
|
418
|
+
});
|
|
419
|
+
// Merge adjacent text nodes after unwrapping
|
|
420
|
+
editable.normalize();
|
|
421
|
+
|
|
422
|
+
this._matches = [];
|
|
423
|
+
this._currentIndex = -1;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Counter display
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
_updateCounter() {
|
|
431
|
+
if (!this._counterEl) return;
|
|
432
|
+
const total = this._matches.length;
|
|
433
|
+
if (total === 0) {
|
|
434
|
+
const query = this._findInput ? this._findInput.value : '';
|
|
435
|
+
this._counterEl.textContent = query ? 'No results' : '';
|
|
436
|
+
} else {
|
|
437
|
+
this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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(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
|
+
}
|