autumnnote 2.1.0 → 2.3.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 +74 -6
- package/dist/autumnnote.cjs +27 -22
- package/dist/autumnnote.es.js +474 -591
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +27 -22
- package/dist/autumnnote.umd.js +27 -22
- 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 +12 -3
- 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 -38
- 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 -304
- 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 -322
- 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 -1392
- package/src/js/module/Toolbar.js +0 -855
- 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/module/table-grid.js +0 -102
- package/src/js/module/table-icons.js +0 -33
- 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 -2877
package/src/js/module/Mention.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mention.js — @mention autocomplete support.
|
|
3
|
-
*
|
|
4
|
-
* Activated when `mention.onSearch` option is provided.
|
|
5
|
-
*
|
|
6
|
-
* When the user types the trigger character (default `@`) followed by at least
|
|
7
|
-
* `mention.minChars` characters, `onSearch(query, callback)` is called.
|
|
8
|
-
* The callback receives an array of `{ id, label, avatar? }` items which are
|
|
9
|
-
* rendered in a floating dropdown. Selecting an item inserts a non-editable
|
|
10
|
-
* mention chip and fires `onInsert` (if provided) to override the default HTML.
|
|
11
|
-
*
|
|
12
|
-
* Options shape (passed as `mention` option object):
|
|
13
|
-
* {
|
|
14
|
-
* trigger: '@',
|
|
15
|
-
* minChars: 1,
|
|
16
|
-
* maxResults: 8,
|
|
17
|
-
* debounce: 200,
|
|
18
|
-
* onSearch: (query, callback) => void,
|
|
19
|
-
* onInsert: (item) => string | null,
|
|
20
|
-
* onError: (err: Error) => void,
|
|
21
|
-
* mentionClass: 'an-mention',
|
|
22
|
-
* allowSpaces: false,
|
|
23
|
-
* }
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { on } from '../core/dom.js';
|
|
27
|
-
|
|
28
|
-
export class Mention {
|
|
29
|
-
/** @param {import('../Context.js').Context} context */
|
|
30
|
-
constructor(context) {
|
|
31
|
-
this.context = context;
|
|
32
|
-
|
|
33
|
-
/** @type {HTMLElement|null} */
|
|
34
|
-
this._dropdown = null;
|
|
35
|
-
/** @type {string} */
|
|
36
|
-
this._query = '';
|
|
37
|
-
/** @type {number|null} */
|
|
38
|
-
this._debounceTimer = null;
|
|
39
|
-
/** @type {number} current highlighted index */
|
|
40
|
-
this._activeIndex = -1;
|
|
41
|
-
/** @type {Array<{id, label, avatar?}>} */
|
|
42
|
-
this._items = [];
|
|
43
|
-
/** @type {boolean} */
|
|
44
|
-
this._open = false;
|
|
45
|
-
/** Position of trigger character in the text node */
|
|
46
|
-
this._triggerNode = null;
|
|
47
|
-
this._triggerOffset = 0;
|
|
48
|
-
/** @type {DOMRect|null} Caret rect captured synchronously during input event */
|
|
49
|
-
this._caretRect = null;
|
|
50
|
-
|
|
51
|
-
this._disposers = [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
initialize() {
|
|
55
|
-
const cfg = this.context.options.mention;
|
|
56
|
-
if (!cfg || typeof cfg.onSearch !== 'function') return this;
|
|
57
|
-
this._cfg = {
|
|
58
|
-
trigger: cfg.trigger || '@',
|
|
59
|
-
minChars: cfg.minChars ?? 0,
|
|
60
|
-
maxResults: cfg.maxResults ?? 8,
|
|
61
|
-
debounce: cfg.debounce ?? 200,
|
|
62
|
-
onSearch: cfg.onSearch,
|
|
63
|
-
onInsert: cfg.onInsert || null,
|
|
64
|
-
onError: cfg.onError || null,
|
|
65
|
-
mentionClass: cfg.mentionClass || 'an-mention',
|
|
66
|
-
allowSpaces: cfg.allowSpaces || false,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
this._buildDropdown();
|
|
70
|
-
|
|
71
|
-
const editable = this.context.layoutInfo.editable;
|
|
72
|
-
const d1 = on(editable, 'keydown', (e) => this._onKeydown(e));
|
|
73
|
-
const d2 = on(editable, 'input', () => this._onInput());
|
|
74
|
-
const d3 = on(document, 'click', (e) => this._onDocClick(e));
|
|
75
|
-
this._disposers.push(d1, d2, d3);
|
|
76
|
-
return this;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
destroy() {
|
|
80
|
-
clearTimeout(this._debounceTimer);
|
|
81
|
-
this._dropdown?.remove();
|
|
82
|
-
this._dropdown = null;
|
|
83
|
-
this._disposers.forEach((d) => d());
|
|
84
|
-
this._disposers = [];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Dropdown DOM
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
_buildDropdown() {
|
|
92
|
-
const el = document.createElement('div');
|
|
93
|
-
el.className = 'an-mention-dropdown';
|
|
94
|
-
el.setAttribute('role', 'listbox');
|
|
95
|
-
|
|
96
|
-
// Event delegation: single listeners on the container instead of per-item
|
|
97
|
-
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
98
|
-
el.addEventListener('click', (e) => {
|
|
99
|
-
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
|
|
100
|
-
if (item) this._select(+item.dataset.index);
|
|
101
|
-
});
|
|
102
|
-
el.addEventListener('mousemove', (e) => {
|
|
103
|
-
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
|
|
104
|
-
if (item) this._highlightItem(+item.dataset.index);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
document.body.appendChild(el);
|
|
108
|
-
this._dropdown = el;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
_renderItems(items) {
|
|
112
|
-
const dd = this._dropdown;
|
|
113
|
-
this._items = items.slice(0, this._cfg.maxResults);
|
|
114
|
-
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
115
|
-
|
|
116
|
-
// Build all items in a DocumentFragment — one batch DOM insertion
|
|
117
|
-
const frag = document.createDocumentFragment();
|
|
118
|
-
this._items.forEach((item, i) => {
|
|
119
|
-
const li = document.createElement('div');
|
|
120
|
-
li.className = 'an-mention-item';
|
|
121
|
-
li.setAttribute('role', 'option');
|
|
122
|
-
li.dataset.index = String(i);
|
|
123
|
-
if (item.avatar) {
|
|
124
|
-
const img = document.createElement('img');
|
|
125
|
-
img.src = item.avatar;
|
|
126
|
-
img.className = 'an-mention-avatar';
|
|
127
|
-
img.alt = '';
|
|
128
|
-
li.appendChild(img);
|
|
129
|
-
}
|
|
130
|
-
const label = document.createElement('span');
|
|
131
|
-
label.textContent = item.label;
|
|
132
|
-
li.appendChild(label);
|
|
133
|
-
frag.appendChild(li);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
dd.innerHTML = ''; // one clear
|
|
137
|
-
dd.appendChild(frag); // one batch insert
|
|
138
|
-
|
|
139
|
-
this._highlightItem(this._activeIndex);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
_highlightItem(index) {
|
|
143
|
-
if (!this._dropdown) return;
|
|
144
|
-
this._dropdown.querySelectorAll('.an-mention-item').forEach((el, i) => {
|
|
145
|
-
el.classList.toggle('an-mention-active', i === index);
|
|
146
|
-
});
|
|
147
|
-
this._activeIndex = index;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
_positionDropdown() {
|
|
151
|
-
const dd = this._dropdown;
|
|
152
|
-
const rect = this._caretRect;
|
|
153
|
-
if (!rect || rect.height === 0) return;
|
|
154
|
-
|
|
155
|
-
// Measure while invisible to avoid layout flash
|
|
156
|
-
dd.style.visibility = 'hidden';
|
|
157
|
-
dd.style.display = 'block';
|
|
158
|
-
|
|
159
|
-
const ddh = dd.offsetHeight;
|
|
160
|
-
const ddw = dd.offsetWidth;
|
|
161
|
-
|
|
162
|
-
// position:fixed — coords are already viewport-relative, no scroll offset needed
|
|
163
|
-
let top = rect.bottom + 4;
|
|
164
|
-
let left = rect.left;
|
|
165
|
-
|
|
166
|
-
if (rect.bottom + ddh + 8 > globalThis.innerHeight) {
|
|
167
|
-
top = rect.top - ddh - 4;
|
|
168
|
-
}
|
|
169
|
-
left = Math.max(8, Math.min(left, globalThis.innerWidth - ddw - 8));
|
|
170
|
-
|
|
171
|
-
dd.style.top = `${top}px`;
|
|
172
|
-
dd.style.left = `${left}px`;
|
|
173
|
-
dd.style.visibility = '';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
_showDropdown() {
|
|
177
|
-
this._open = true;
|
|
178
|
-
this._positionDropdown();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
_hideDropdown() {
|
|
182
|
-
if (this._dropdown) this._dropdown.style.display = 'none';
|
|
183
|
-
this._open = false;
|
|
184
|
-
this._items = [];
|
|
185
|
-
this._activeIndex = -1;
|
|
186
|
-
this._triggerNode = null;
|
|
187
|
-
this._caretRect = null;
|
|
188
|
-
this._query = '';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Captures the caret rect synchronously during the input event.
|
|
193
|
-
* Must be called while the DOM event is still live — getClientRects() on a
|
|
194
|
-
* collapsed range is reliable at this point but often empty inside async callbacks.
|
|
195
|
-
*/
|
|
196
|
-
_captureCaretRect() {
|
|
197
|
-
// Prefer a range over the trigger character — it has non-zero width and
|
|
198
|
-
// getBoundingClientRect() reliably returns a valid rect.
|
|
199
|
-
if (this._triggerNode?.isConnected) {
|
|
200
|
-
try {
|
|
201
|
-
const r = document.createRange();
|
|
202
|
-
const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
|
|
203
|
-
r.setStart(this._triggerNode, this._triggerOffset);
|
|
204
|
-
r.setEnd(this._triggerNode, end);
|
|
205
|
-
const candidate = r.getBoundingClientRect();
|
|
206
|
-
if (candidate.height > 0) {
|
|
207
|
-
this._caretRect = candidate;
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) { void _; }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Fallback: use getClientRects() on the current collapsed selection
|
|
214
|
-
const sel = globalThis.getSelection();
|
|
215
|
-
if (!sel?.rangeCount) return;
|
|
216
|
-
const rects = sel.getRangeAt(0).getClientRects();
|
|
217
|
-
if (rects.length > 0) {
|
|
218
|
-
this._caretRect = rects[rects.length - 1];
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// Query detection
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
_getQueryAtCursor() {
|
|
227
|
-
const sel = globalThis.getSelection();
|
|
228
|
-
if (!sel?.rangeCount) return null;
|
|
229
|
-
const range = sel.getRangeAt(0);
|
|
230
|
-
if (!range.collapsed) return null;
|
|
231
|
-
|
|
232
|
-
const node = range.startContainer;
|
|
233
|
-
if (node.nodeType !== Node.TEXT_NODE) return null;
|
|
234
|
-
|
|
235
|
-
const text = node.textContent.slice(0, range.startOffset);
|
|
236
|
-
const trigger = this._cfg.trigger;
|
|
237
|
-
|
|
238
|
-
// Find the last occurrence of trigger in the text before cursor
|
|
239
|
-
const triggerIdx = text.lastIndexOf(trigger);
|
|
240
|
-
if (triggerIdx === -1) return null;
|
|
241
|
-
|
|
242
|
-
const afterTrigger = text.slice(triggerIdx + trigger.length);
|
|
243
|
-
|
|
244
|
-
// No spaces allowed unless configured
|
|
245
|
-
if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
|
|
246
|
-
|
|
247
|
-
if (afterTrigger.length < this._cfg.minChars) return null;
|
|
248
|
-
|
|
249
|
-
this._triggerNode = node;
|
|
250
|
-
this._triggerOffset = triggerIdx;
|
|
251
|
-
return afterTrigger;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// Events
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
|
|
258
|
-
_onInput() {
|
|
259
|
-
if (!this._cfg) return;
|
|
260
|
-
const query = this._getQueryAtCursor();
|
|
261
|
-
if (query === null) {
|
|
262
|
-
this._hideDropdown();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Capture caret rect NOW, synchronously while the input event is live.
|
|
267
|
-
// Inside a setTimeout/debounce callback the selection may still be valid
|
|
268
|
-
// but getClientRects() on a collapsed range often returns empty in that context.
|
|
269
|
-
this._captureCaretRect();
|
|
270
|
-
|
|
271
|
-
this._query = query;
|
|
272
|
-
clearTimeout(this._debounceTimer);
|
|
273
|
-
this._debounceTimer = setTimeout(() => {
|
|
274
|
-
const cb = (items) => {
|
|
275
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
276
|
-
this._hideDropdown();
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
this._renderItems(items);
|
|
280
|
-
this._showDropdown();
|
|
281
|
-
};
|
|
282
|
-
let result;
|
|
283
|
-
try {
|
|
284
|
-
result = this._cfg.onSearch(this._query, cb);
|
|
285
|
-
} catch (err) {
|
|
286
|
-
this._hideDropdown();
|
|
287
|
-
if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (result && typeof result.then === 'function') {
|
|
291
|
-
result.then(cb).catch((err) => {
|
|
292
|
-
this._hideDropdown();
|
|
293
|
-
if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}, this._cfg.debounce);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
_onKeydown(e) {
|
|
300
|
-
if (!this._cfg || !this._open) return;
|
|
301
|
-
|
|
302
|
-
if (e.key === 'ArrowDown') {
|
|
303
|
-
e.preventDefault();
|
|
304
|
-
const next = (this._activeIndex + 1) % this._items.length;
|
|
305
|
-
this._highlightItem(next);
|
|
306
|
-
} else if (e.key === 'ArrowUp') {
|
|
307
|
-
e.preventDefault();
|
|
308
|
-
const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
|
|
309
|
-
this._highlightItem(prev);
|
|
310
|
-
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
311
|
-
if (this._activeIndex >= 0) {
|
|
312
|
-
e.preventDefault();
|
|
313
|
-
this._select(this._activeIndex);
|
|
314
|
-
}
|
|
315
|
-
} else if (e.key === 'Escape') {
|
|
316
|
-
this._hideDropdown();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
_onDocClick(e) {
|
|
321
|
-
if (!this._open) return;
|
|
322
|
-
if (this._dropdown?.contains(e.target)) return;
|
|
323
|
-
this._hideDropdown();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
// Insert
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
|
|
330
|
-
_select(index) {
|
|
331
|
-
const item = this._items[index];
|
|
332
|
-
if (!item) return;
|
|
333
|
-
|
|
334
|
-
// Delete the trigger + query text from the DOM
|
|
335
|
-
if (this._triggerNode) {
|
|
336
|
-
const node = this._triggerNode;
|
|
337
|
-
const before = node.textContent.slice(0, this._triggerOffset);
|
|
338
|
-
const after = node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
|
|
339
|
-
node.textContent = before + after;
|
|
340
|
-
|
|
341
|
-
// Move cursor to where the trigger was
|
|
342
|
-
const sel = globalThis.getSelection();
|
|
343
|
-
const range = document.createRange();
|
|
344
|
-
range.setStart(node, this._triggerOffset);
|
|
345
|
-
range.collapse(true);
|
|
346
|
-
sel.removeAllRanges();
|
|
347
|
-
sel.addRange(range);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Build the chip HTML
|
|
351
|
-
let html;
|
|
352
|
-
if (typeof this._cfg.onInsert === 'function') {
|
|
353
|
-
html = this._cfg.onInsert(item);
|
|
354
|
-
}
|
|
355
|
-
if (!html) {
|
|
356
|
-
const cls = this._cfg.mentionClass;
|
|
357
|
-
html = `<span class="${cls}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Insert a trailing space so the cursor lands outside the chip
|
|
361
|
-
this.context.invoke('editor.insertHTML', html + '​');
|
|
362
|
-
this._hideDropdown();
|
|
363
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
364
|
-
}
|
|
365
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Placeholder.js - Shows placeholder text when the editor is empty
|
|
3
|
-
* Inspired by Summernote's Placeholder module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { on } from '../core/dom.js';
|
|
7
|
-
|
|
8
|
-
export class Placeholder {
|
|
9
|
-
/**
|
|
10
|
-
* @param {import('../Context.js').Context} context
|
|
11
|
-
*/
|
|
12
|
-
constructor(context) {
|
|
13
|
-
this.context = context;
|
|
14
|
-
this.options = context.options;
|
|
15
|
-
this._disposers = [];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
initialize() {
|
|
19
|
-
const editable = this.context.layoutInfo.editable;
|
|
20
|
-
const placeholder = this.options.placeholder || '';
|
|
21
|
-
if (placeholder) {
|
|
22
|
-
editable.dataset.placeholder = placeholder;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const update = () => this._update();
|
|
26
|
-
const d1 = on(editable, 'input', update);
|
|
27
|
-
const d2 = on(editable, 'focus', update);
|
|
28
|
-
const d3 = on(editable, 'blur', update);
|
|
29
|
-
this._disposers.push(d1, d2, d3);
|
|
30
|
-
this._update();
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
destroy() {
|
|
35
|
-
this._disposers.forEach((d) => d());
|
|
36
|
-
this._disposers = [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
_update() {
|
|
40
|
-
const editable = this.context.layoutInfo.editable;
|
|
41
|
-
const isFocused = document.activeElement === editable;
|
|
42
|
-
// Strip ZWS (\u200B) cursor anchors used by checklist/icon insertion in
|
|
43
|
-
// addition to regular whitespace before deciding if the editor is empty.
|
|
44
|
-
// Without this, a freshly-created checklist item or icon leaves a ZWS in
|
|
45
|
-
// the DOM that causes the placeholder to overlap real content (A-1).
|
|
46
|
-
const hasText = editable.textContent.replaceAll('\u200B', '').trim().length > 0;
|
|
47
|
-
const isEmpty = !hasText &&
|
|
48
|
-
!editable.querySelector('img, table, hr, .an-video-wrapper');
|
|
49
|
-
editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ShortcutsDialog.js - Modal dialog listing all keyboard shortcuts.
|
|
3
|
-
* Opened via the toolbar '?' button or Shift+? inside the editor.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createElement, on } from '../core/dom.js';
|
|
7
|
-
import { BaseDialog } from './BaseDialog.js';
|
|
8
|
-
|
|
9
|
-
const SHORTCUTS = [
|
|
10
|
-
{
|
|
11
|
-
category: 'Text Formatting',
|
|
12
|
-
items: [
|
|
13
|
-
{ keys: 'Ctrl + B', action: 'Bold' },
|
|
14
|
-
{ keys: 'Ctrl + I', action: 'Italic' },
|
|
15
|
-
{ keys: 'Ctrl + U', action: 'Underline' },
|
|
16
|
-
{ keys: 'Ctrl + K', action: 'Insert / edit link' },
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
category: 'History',
|
|
21
|
-
items: [
|
|
22
|
-
{ keys: 'Ctrl + Z', action: 'Undo' },
|
|
23
|
-
{ keys: 'Ctrl + Y / Ctrl + Shift + Z', action: 'Redo' },
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
category: 'Selection & Navigation',
|
|
28
|
-
items: [
|
|
29
|
-
{ keys: 'Ctrl + A', action: 'Select all content' },
|
|
30
|
-
{ keys: 'Tab', action: 'Indent list item / insert spaces' },
|
|
31
|
-
{ keys: 'Shift + Tab', action: 'Outdent list item' },
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
category: 'Clipboard',
|
|
36
|
-
items: [
|
|
37
|
-
{ keys: 'Ctrl + Shift + V', action: 'Paste as plain text' },
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
category: 'Find & Replace',
|
|
42
|
-
items: [
|
|
43
|
-
{ keys: 'Ctrl + F', action: 'Find in document' },
|
|
44
|
-
{ keys: 'Ctrl + H', action: 'Find & Replace' },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
category: 'Editor',
|
|
49
|
-
items: [
|
|
50
|
-
{ keys: 'Ctrl + Shift + /', action: 'Show this keyboard shortcuts dialog' },
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M6 11h.01M10 11h.01M14 11h.01M18 11h.01M6 15h.01M18 15h.01M10 15h4"/><path d="M7 3l5 4 5-4"/></svg>`;
|
|
56
|
-
|
|
57
|
-
export class ShortcutsDialog extends BaseDialog {
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Public API
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
show() {
|
|
63
|
-
this._open();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Build dialog
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
_buildDialog() {
|
|
71
|
-
const L = this.context.locale.shortcutsDialog;
|
|
72
|
-
const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
|
|
73
|
-
box.classList.add('an-shortcuts-box');
|
|
74
|
-
|
|
75
|
-
// Close button pinned to the right of the header row
|
|
76
|
-
const closeBtn = createElement('button', {
|
|
77
|
-
type: 'button',
|
|
78
|
-
class: 'an-icon-close',
|
|
79
|
-
'aria-label': L.close,
|
|
80
|
-
style: 'margin-left:auto',
|
|
81
|
-
});
|
|
82
|
-
closeBtn.textContent = '×';
|
|
83
|
-
this._closeBtn = closeBtn;
|
|
84
|
-
this._firstInput = closeBtn;
|
|
85
|
-
box.querySelector('.an-dialog-header').appendChild(closeBtn);
|
|
86
|
-
|
|
87
|
-
const shortcuts = this.context.locale.shortcutsDialog.shortcuts || SHORTCUTS;
|
|
88
|
-
shortcuts.forEach(({ category, items }) => {
|
|
89
|
-
const catEl = createElement('div', { class: 'an-shortcuts-cat' });
|
|
90
|
-
catEl.textContent = category;
|
|
91
|
-
box.appendChild(catEl);
|
|
92
|
-
|
|
93
|
-
const table = createElement('div', { class: 'an-shortcuts-table' });
|
|
94
|
-
items.forEach(({ keys, action }) => {
|
|
95
|
-
const row = createElement('div', { class: 'an-shortcuts-row' });
|
|
96
|
-
const keyEl = createElement('span', { class: 'an-shortcuts-key' });
|
|
97
|
-
keyEl.textContent = keys;
|
|
98
|
-
const actEl = createElement('span', { class: 'an-shortcuts-action' });
|
|
99
|
-
actEl.textContent = action;
|
|
100
|
-
row.append(keyEl, actEl);
|
|
101
|
-
table.appendChild(row);
|
|
102
|
-
});
|
|
103
|
-
box.appendChild(table);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const d1 = on(closeBtn, 'click', () => this._close());
|
|
107
|
-
this._disposers.push(d1);
|
|
108
|
-
|
|
109
|
-
return overlay;
|
|
110
|
-
}
|
|
111
|
-
}
|