autumnnote 1.15.0 → 1.16.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 +22 -0
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +543 -62
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +61 -0
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +5 -5
- package/src/js/Context.js +154 -15
- package/src/js/editing/History.js +38 -6
- package/src/js/i18n/de.js +15 -0
- package/src/js/i18n/en.js +15 -0
- package/src/js/i18n/es.js +15 -0
- package/src/js/i18n/fr.js +15 -0
- package/src/js/i18n/ja.js +15 -0
- package/src/js/i18n/ko.js +15 -0
- package/src/js/i18n/vi.js +15 -0
- package/src/js/i18n/zh.js +15 -0
- package/src/js/index.js +13 -1
- package/src/js/module/Clipboard.js +20 -6
- package/src/js/module/Editor.js +15 -1
- package/src/js/module/ImageDialog.js +34 -6
- package/src/js/module/MarkdownShortcuts.js +2 -2
- package/src/js/module/SlashMenu.js +376 -0
- package/src/js/module/Statusbar.js +3 -9
- package/src/js/settings.js +27 -0
- package/src/styles/autumnnote.scss +49 -0
- package/types/index.d.ts +84 -1
|
@@ -167,13 +167,13 @@ export class MarkdownShortcuts {
|
|
|
167
167
|
|
|
168
168
|
_convertToChecklist() {
|
|
169
169
|
this._selectLineAndDelete();
|
|
170
|
-
this.context.invoke('editor.
|
|
170
|
+
this.context.invoke('editor.toggleChecklist');
|
|
171
171
|
this.context.triggerEvent('change', this.context.getHTML());
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
_convertToHr() {
|
|
175
175
|
this._selectLineAndDelete();
|
|
176
|
-
this.context.invoke('editor.
|
|
176
|
+
this.context.invoke('editor.insertHr');
|
|
177
177
|
this.context.triggerEvent('change', this.context.getHTML());
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlashMenu.js — Notion-style "/" command palette for quick block insertion.
|
|
3
|
+
*
|
|
4
|
+
* Typing "/" as the very first character of an otherwise-empty block opens a
|
|
5
|
+
* filterable list of quick-insert commands (headings, lists, table, image, …).
|
|
6
|
+
* Arrow keys navigate, Enter/Tab selects, Escape or deleting back past the
|
|
7
|
+
* "/" closes it. Disabled via `slashMenu: false`.
|
|
8
|
+
*
|
|
9
|
+
* Positioning follows the same synchronous-caret-rect technique as Mention.js:
|
|
10
|
+
* getBoundingClientRect() on a zero-width Range over the trigger character is
|
|
11
|
+
* reliable while the triggering DOM event is still live, but often returns an
|
|
12
|
+
* empty rect once deferred into a callback — so the rect is captured eagerly
|
|
13
|
+
* in `_onInput`, not lazily when the menu is rendered.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { on } from '../core/dom.js';
|
|
17
|
+
|
|
18
|
+
export class SlashMenu {
|
|
19
|
+
/** @param {import('../Context.js').Context} context */
|
|
20
|
+
constructor(context) {
|
|
21
|
+
this.context = context;
|
|
22
|
+
this.options = context.options;
|
|
23
|
+
this._disposers = [];
|
|
24
|
+
|
|
25
|
+
/** @type {HTMLElement|null} */
|
|
26
|
+
this._menu = null;
|
|
27
|
+
/** @type {Array<{id: string, label: string, keywords: string, run: () => void}>} */
|
|
28
|
+
this._filtered = [];
|
|
29
|
+
this._activeIndex = -1;
|
|
30
|
+
this._open = false;
|
|
31
|
+
this._query = '';
|
|
32
|
+
/** @type {Text|null} */
|
|
33
|
+
this._textNode = null;
|
|
34
|
+
this._triggerOffset = 0;
|
|
35
|
+
/** @type {DOMRect|null} */
|
|
36
|
+
this._caretRect = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
initialize() {
|
|
40
|
+
if (this.options.slashMenu === false) return this;
|
|
41
|
+
const editable = this.context.layoutInfo.editable;
|
|
42
|
+
const d1 = on(editable, 'input', () => this._onInput());
|
|
43
|
+
const d2 = on(editable, 'keydown', (e) => this._onKeydown(e));
|
|
44
|
+
const d3 = on(document, 'click', (e) => this._onDocClick(e));
|
|
45
|
+
this._disposers.push(d1, d2, d3);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
destroy() {
|
|
50
|
+
this._menu?.remove();
|
|
51
|
+
this._menu = null;
|
|
52
|
+
this._disposers.forEach((d) => d());
|
|
53
|
+
this._disposers = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Command list
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
_commands() {
|
|
61
|
+
const L = this.context.locale.slashMenu;
|
|
62
|
+
const ctx = this.context;
|
|
63
|
+
const builtIns = [
|
|
64
|
+
{ id: 'h1', label: L.heading1, keywords: 'h1 heading title', run: () => ctx.invoke('editor.formatBlock', 'h1') },
|
|
65
|
+
{ id: 'h2', label: L.heading2, keywords: 'h2 heading subtitle', run: () => ctx.invoke('editor.formatBlock', 'h2') },
|
|
66
|
+
{ id: 'h3', label: L.heading3, keywords: 'h3 heading', run: () => ctx.invoke('editor.formatBlock', 'h3') },
|
|
67
|
+
{ id: 'ul', label: L.bulletList, keywords: 'ul bullet list unordered', run: () => ctx.invoke('editor.insertUL') },
|
|
68
|
+
{ id: 'ol', label: L.numberedList, keywords: 'ol numbered list ordered', run: () => ctx.invoke('editor.insertOL') },
|
|
69
|
+
{ id: 'checklist', label: L.checklist, keywords: 'checklist todo checkbox task', run: () => ctx.invoke('editor.toggleChecklist') },
|
|
70
|
+
{ id: 'quote', label: L.blockquote, keywords: 'quote blockquote', run: () => ctx.invoke('editor.formatBlock', 'blockquote') },
|
|
71
|
+
{ id: 'code', label: L.codeBlock, keywords: 'code pre block', run: () => ctx.invoke('editor.formatBlock', 'pre') },
|
|
72
|
+
{ id: 'hr', label: L.horizontalRule, keywords: 'hr divider rule line', run: () => ctx.invoke('editor.insertHr') },
|
|
73
|
+
{ id: 'table', label: L.table, keywords: 'table grid', run: () => ctx.invoke('editor.insertTable', 3, 3) },
|
|
74
|
+
{ id: 'image', label: L.image, keywords: 'image picture photo upload', run: () => ctx.invoke('imageDialog.show') },
|
|
75
|
+
];
|
|
76
|
+
const custom = (this.options.slashCommands || []).map((command) => ({
|
|
77
|
+
id: command.id,
|
|
78
|
+
label: command.label || command.id,
|
|
79
|
+
keywords: command.keywords || '',
|
|
80
|
+
run: () => command.run(ctx),
|
|
81
|
+
}));
|
|
82
|
+
return [...builtIns, ...custom];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
refresh() {
|
|
86
|
+
if (this._open) this._filterAndRender();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Menu DOM
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
_buildMenu() {
|
|
94
|
+
const el = document.createElement('div');
|
|
95
|
+
el.className = 'an-slash-menu';
|
|
96
|
+
el.setAttribute('role', 'listbox');
|
|
97
|
+
el.style.display = 'none';
|
|
98
|
+
|
|
99
|
+
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
100
|
+
el.addEventListener('click', (e) => {
|
|
101
|
+
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-slash-menu-item'));
|
|
102
|
+
if (item) this._select(+item.dataset.index);
|
|
103
|
+
});
|
|
104
|
+
el.addEventListener('mousemove', (e) => {
|
|
105
|
+
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-slash-menu-item'));
|
|
106
|
+
if (item) this._highlight(+item.dataset.index);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
document.body.appendChild(el);
|
|
110
|
+
this._menu = el;
|
|
111
|
+
return el;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_renderItems() {
|
|
115
|
+
if (!this._menu) this._buildMenu();
|
|
116
|
+
const menu = this._menu;
|
|
117
|
+
const L = this.context.locale.slashMenu;
|
|
118
|
+
|
|
119
|
+
if (this._filtered.length === 0) {
|
|
120
|
+
menu.innerHTML = '';
|
|
121
|
+
const empty = document.createElement('div');
|
|
122
|
+
empty.className = 'an-slash-menu-empty';
|
|
123
|
+
empty.textContent = L.noResults;
|
|
124
|
+
menu.appendChild(empty);
|
|
125
|
+
this._activeIndex = -1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const frag = document.createDocumentFragment();
|
|
130
|
+
this._filtered.forEach((item, i) => {
|
|
131
|
+
const row = document.createElement('div');
|
|
132
|
+
row.className = 'an-slash-menu-item';
|
|
133
|
+
row.setAttribute('role', 'option');
|
|
134
|
+
row.id = `an-slash-option-${item.id}`;
|
|
135
|
+
row.dataset.index = String(i);
|
|
136
|
+
row.textContent = item.label;
|
|
137
|
+
frag.appendChild(row);
|
|
138
|
+
});
|
|
139
|
+
menu.innerHTML = '';
|
|
140
|
+
menu.appendChild(frag);
|
|
141
|
+
this._highlight(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_highlight(index) {
|
|
145
|
+
if (!this._menu) return;
|
|
146
|
+
this._menu.querySelectorAll('.an-slash-menu-item').forEach((el, i) => {
|
|
147
|
+
const active = i === index;
|
|
148
|
+
el.classList.toggle('an-slash-menu-active', active);
|
|
149
|
+
el.setAttribute('aria-selected', String(active));
|
|
150
|
+
if (active) this._menu.setAttribute('aria-activedescendant', el.id);
|
|
151
|
+
});
|
|
152
|
+
this._activeIndex = index;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_position() {
|
|
156
|
+
const menu = this._menu;
|
|
157
|
+
const rect = this._caretRect;
|
|
158
|
+
if (!menu || !rect || rect.height === 0) return;
|
|
159
|
+
|
|
160
|
+
menu.style.visibility = 'hidden';
|
|
161
|
+
menu.style.display = 'block';
|
|
162
|
+
const mh = menu.offsetHeight;
|
|
163
|
+
const mw = menu.offsetWidth;
|
|
164
|
+
|
|
165
|
+
let top = rect.bottom + 4;
|
|
166
|
+
let left = rect.left;
|
|
167
|
+
if (rect.bottom + mh + 8 > globalThis.innerHeight) {
|
|
168
|
+
top = rect.top - mh - 4;
|
|
169
|
+
}
|
|
170
|
+
left = Math.max(8, Math.min(left, globalThis.innerWidth - mw - 8));
|
|
171
|
+
|
|
172
|
+
menu.style.top = `${top}px`;
|
|
173
|
+
menu.style.left = `${left}px`;
|
|
174
|
+
menu.style.visibility = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_open_() {
|
|
178
|
+
this._open = true;
|
|
179
|
+
this._filterAndRender();
|
|
180
|
+
this._position();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_close() {
|
|
184
|
+
if (this._menu) this._menu.style.display = 'none';
|
|
185
|
+
this._open = false;
|
|
186
|
+
this._filtered = [];
|
|
187
|
+
this._activeIndex = -1;
|
|
188
|
+
this._menu?.removeAttribute('aria-activedescendant');
|
|
189
|
+
this._textNode = null;
|
|
190
|
+
this._caretRect = null;
|
|
191
|
+
this._query = '';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Trigger detection
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
_isBlock(node) {
|
|
199
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
200
|
+
const display = globalThis.getComputedStyle(node).display;
|
|
201
|
+
return display === 'block' || display === 'list-item' || display === 'table-cell';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Detects a "/" trigger at the caret, requiring it to be the only content
|
|
206
|
+
* typed so far in its block (i.e. the block's text is exactly "/" + query,
|
|
207
|
+
* nothing before or after). This deliberately avoids firing mid-sentence
|
|
208
|
+
* (e.g. "10/20", "and/or").
|
|
209
|
+
* @returns {{ query: string, textNode: Text, triggerOffset: number } | null}
|
|
210
|
+
*/
|
|
211
|
+
_getTriggerContext() {
|
|
212
|
+
const sel = globalThis.getSelection();
|
|
213
|
+
if (!sel?.rangeCount) return null;
|
|
214
|
+
const range = sel.getRangeAt(0);
|
|
215
|
+
if (!range.collapsed) return null;
|
|
216
|
+
|
|
217
|
+
const editable = this.context.layoutInfo.editable;
|
|
218
|
+
if (!editable.contains(range.startContainer)) return null;
|
|
219
|
+
if (range.startContainer.nodeType !== Node.TEXT_NODE) return null;
|
|
220
|
+
|
|
221
|
+
let block = range.startContainer.parentNode;
|
|
222
|
+
while (block && block !== editable && !this._isBlock(block)) block = block.parentNode;
|
|
223
|
+
if (!block || block === editable) return null;
|
|
224
|
+
|
|
225
|
+
const fullText = block.textContent || '';
|
|
226
|
+
const beforeRange = document.createRange();
|
|
227
|
+
beforeRange.setStart(block, 0);
|
|
228
|
+
beforeRange.setEnd(range.startContainer, range.startOffset);
|
|
229
|
+
const before = beforeRange.toString();
|
|
230
|
+
|
|
231
|
+
const m = /^\/(\S*)$/.exec(before);
|
|
232
|
+
if (!m) return null;
|
|
233
|
+
if (fullText.length > before.length) return null; // content after the caret
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
query: m[1],
|
|
237
|
+
textNode: /** @type {Text} */ (range.startContainer),
|
|
238
|
+
triggerOffset: range.startOffset - m[0].length,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Captures the caret rect synchronously — see file header for why this
|
|
244
|
+
* can't be deferred to a later tick.
|
|
245
|
+
*/
|
|
246
|
+
_captureCaretRect(textNode, triggerOffset) {
|
|
247
|
+
try {
|
|
248
|
+
const r = document.createRange();
|
|
249
|
+
const end = Math.min(triggerOffset + 1, textNode.textContent.length);
|
|
250
|
+
r.setStart(textNode, triggerOffset);
|
|
251
|
+
r.setEnd(textNode, end);
|
|
252
|
+
const candidate = r.getBoundingClientRect();
|
|
253
|
+
if (candidate.height > 0) return candidate;
|
|
254
|
+
} catch (_) { void _; }
|
|
255
|
+
|
|
256
|
+
const sel = globalThis.getSelection();
|
|
257
|
+
if (!sel?.rangeCount) return null;
|
|
258
|
+
const rects = sel.getRangeAt(0).getClientRects();
|
|
259
|
+
return rects.length > 0 ? rects[rects.length - 1] : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_filterAndRender() {
|
|
263
|
+
const q = this._query.toLowerCase();
|
|
264
|
+
this._filtered = q
|
|
265
|
+
? this._commands().filter((c) => c.keywords.includes(q) || c.label.toLowerCase().includes(q))
|
|
266
|
+
: this._commands();
|
|
267
|
+
this._renderItems();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Events
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
_onInput() {
|
|
275
|
+
const ctx = this._getTriggerContext();
|
|
276
|
+
if (!ctx) {
|
|
277
|
+
if (this._open) this._close();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this._textNode = ctx.textNode;
|
|
282
|
+
this._triggerOffset = ctx.triggerOffset;
|
|
283
|
+
this._query = ctx.query;
|
|
284
|
+
this._caretRect = this._captureCaretRect(ctx.textNode, ctx.triggerOffset);
|
|
285
|
+
|
|
286
|
+
if (!this._open) this._open_();
|
|
287
|
+
else { this._filterAndRender(); this._position(); }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_onKeydown(e) {
|
|
291
|
+
if (!this._open) return;
|
|
292
|
+
|
|
293
|
+
if (e.key === 'ArrowDown') {
|
|
294
|
+
e.preventDefault();
|
|
295
|
+
if (this._filtered.length === 0) return;
|
|
296
|
+
this._highlight((this._activeIndex + 1) % this._filtered.length);
|
|
297
|
+
} else if (e.key === 'ArrowUp') {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
if (this._filtered.length === 0) return;
|
|
300
|
+
this._highlight((this._activeIndex - 1 + this._filtered.length) % this._filtered.length);
|
|
301
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
302
|
+
if (this._activeIndex >= 0) {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
this._select(this._activeIndex);
|
|
305
|
+
}
|
|
306
|
+
} else if (e.key === 'Escape') {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
this._close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_onDocClick(e) {
|
|
313
|
+
if (!this._open) return;
|
|
314
|
+
if (this._menu?.contains(e.target)) return;
|
|
315
|
+
this._close();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Selection
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
_deleteTriggerText() {
|
|
323
|
+
const node = this._textNode;
|
|
324
|
+
if (!node?.isConnected) return;
|
|
325
|
+
const before = node.textContent.slice(0, this._triggerOffset);
|
|
326
|
+
const after = node.textContent.slice(this._triggerOffset + 1 + this._query.length);
|
|
327
|
+
node.textContent = before + after;
|
|
328
|
+
|
|
329
|
+
const sel = globalThis.getSelection();
|
|
330
|
+
if (!sel) return;
|
|
331
|
+
const range = document.createRange();
|
|
332
|
+
range.setStart(node, this._triggerOffset);
|
|
333
|
+
range.collapse(true);
|
|
334
|
+
sel.removeAllRanges();
|
|
335
|
+
sel.addRange(range);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_makeTriggerRemover() {
|
|
339
|
+
const node = this._textNode;
|
|
340
|
+
const triggerOffset = this._triggerOffset;
|
|
341
|
+
const query = this._query;
|
|
342
|
+
return () => {
|
|
343
|
+
if (!node?.isConnected) return;
|
|
344
|
+
const before = node.textContent.slice(0, triggerOffset);
|
|
345
|
+
const after = node.textContent.slice(triggerOffset + 1 + query.length);
|
|
346
|
+
node.textContent = before + after;
|
|
347
|
+
|
|
348
|
+
const sel = globalThis.getSelection();
|
|
349
|
+
if (!sel) return;
|
|
350
|
+
const range = document.createRange();
|
|
351
|
+
range.setStart(node, triggerOffset);
|
|
352
|
+
range.collapse(true);
|
|
353
|
+
sel.removeAllRanges();
|
|
354
|
+
sel.addRange(range);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_select(index) {
|
|
359
|
+
const item = this._filtered[index];
|
|
360
|
+
if (!item) return;
|
|
361
|
+
|
|
362
|
+
if (item.id === 'image') {
|
|
363
|
+
const beforeInsert = this._makeTriggerRemover();
|
|
364
|
+
this._close();
|
|
365
|
+
this.context.invoke('imageDialog.show', { beforeInsert });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._deleteTriggerText();
|
|
370
|
+
this._close();
|
|
371
|
+
// Editor.* methods invoked by commands already call afterCommand()
|
|
372
|
+
// internally (toolbar/statusbar refresh + debounced undo snapshot + the
|
|
373
|
+
// 'change' event), so no manual triggerEvent here — see Editor.js.
|
|
374
|
+
item.run();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createElement, on } from '../core/dom.js';
|
|
7
|
-
import { debounce } from '../core/func.js';
|
|
8
7
|
|
|
9
8
|
// Cache the segmenter instance at module level to avoid per-call allocation
|
|
10
9
|
const _segmenter =
|
|
@@ -97,7 +96,6 @@ export class Statusbar {
|
|
|
97
96
|
info.appendChild(this._charCountEl);
|
|
98
97
|
this.el.appendChild(info);
|
|
99
98
|
|
|
100
|
-
this._bindContentEvents();
|
|
101
99
|
this.update();
|
|
102
100
|
return this;
|
|
103
101
|
}
|
|
@@ -202,13 +200,9 @@ export class Statusbar {
|
|
|
202
200
|
// Counter update
|
|
203
201
|
// ---------------------------------------------------------------------------
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const d = on(editable, 'input', /** @type {EventListener} */ (updateDebounced));
|
|
209
|
-
this._disposers.push(d);
|
|
210
|
-
}
|
|
211
|
-
|
|
203
|
+
// Editor.afterCommand() already invokes 'statusbar.update' on every native
|
|
204
|
+
// 'input' event and after every toolbar/formatting command, so a separate
|
|
205
|
+
// content listener here would just re-run this on the same keystroke.
|
|
212
206
|
update() {
|
|
213
207
|
if (!this._wordCountEl || !this._charCountEl) return;
|
|
214
208
|
const editable = this.context.layoutInfo.editable;
|
package/src/js/settings.js
CHANGED
|
@@ -25,6 +25,7 @@ import { defaultToolbar } from './module/Buttons.js';
|
|
|
25
25
|
* @property {number} [maxImageSize] - Max upload size in MB
|
|
26
26
|
* @property {number} [tabSize] - Spaces per tab in non-list context
|
|
27
27
|
* @property {number} [historyLimit] - Maximum undo/redo history steps
|
|
28
|
+
* @property {number} [historyMaxBytes] - Maximum combined size (chars) of all stacked undo/redo snapshots
|
|
28
29
|
* @property {string} [defaultFontFamily] - Default font family applied to the editable area on init
|
|
29
30
|
* @property {string} [defaultFontSize] - Default font size applied to the editable area on init (e.g. '14px')
|
|
30
31
|
* @property {string[]} [fontFamilies] - Font families shown in the font-family toolbar dropdown
|
|
@@ -46,6 +47,8 @@ import { defaultToolbar } from './module/Buttons.js';
|
|
|
46
47
|
* @property {string} [toolbarOverflow] - Toolbar overflow strategy: 'wrap' (default) | 'scroll'
|
|
47
48
|
* @property {boolean} [autoSave] - Auto-save content to localStorage on change
|
|
48
49
|
* @property {string} [autoSaveKey] - localStorage key used for auto-save (default: 'autumnnote-autosave')
|
|
50
|
+
* @property {number} [autoSaveDelay] - Debounce delay for auto-save writes in milliseconds
|
|
51
|
+
* @property {object|null} [autoSaveAdapter] - Optional async persistence adapter with save/load/remove methods
|
|
49
52
|
* @property {number} [maxChars] - Maximum character count (0 = unlimited). Shows warning in statusbar.
|
|
50
53
|
* @property {number} [maxWords] - Maximum word count (0 = unlimited). Shows warning in statusbar.
|
|
51
54
|
* @property {boolean} [tableHeaderRow] - Insert a header row (<thead><th>) when creating tables
|
|
@@ -64,6 +67,7 @@ import { defaultToolbar } from './module/Buttons.js';
|
|
|
64
67
|
* @property {boolean} [bubbleToolbar] - Show a mini floating toolbar above text selections
|
|
65
68
|
* @property {string[]} [bubbleToolbarItems] - Button names for the bubble toolbar
|
|
66
69
|
* @property {object|null} [mention] - @mention configuration (onSearch, minChars, ...)
|
|
70
|
+
* @property {boolean} [slashMenu] - Show a "/" command palette for quick block insertion (default true)
|
|
67
71
|
* @property {string} [lang] - Display language or partial locale object override
|
|
68
72
|
*/
|
|
69
73
|
|
|
@@ -103,6 +107,10 @@ export const defaultOptions = {
|
|
|
103
107
|
codeHighlightCDN: 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0',
|
|
104
108
|
markdownPaste: true,
|
|
105
109
|
historyLimit: 100,
|
|
110
|
+
// Max combined size (chars) of all stacked undo/redo snapshots. Guards
|
|
111
|
+
// against documents with many large embedded images holding dozens of
|
|
112
|
+
// full-size copies in memory despite historyLimit.
|
|
113
|
+
historyMaxBytes: 10 * 1024 * 1024,
|
|
106
114
|
// Default font family applied to the editor and shown in the dropdown when no explicit font is set
|
|
107
115
|
defaultFontFamily: 'Arial',
|
|
108
116
|
// Default font size applied to the editor and shown in the size dropdown when no explicit size is set
|
|
@@ -132,6 +140,8 @@ export const defaultOptions = {
|
|
|
132
140
|
autoSave: false,
|
|
133
141
|
// localStorage key used when autoSave is enabled
|
|
134
142
|
autoSaveKey: 'autumnnote-autosave',
|
|
143
|
+
autoSaveDelay: 400,
|
|
144
|
+
autoSaveAdapter: null,
|
|
135
145
|
// Maximum character count (0 = unlimited)
|
|
136
146
|
maxChars: 0,
|
|
137
147
|
// Maximum word count (0 = unlimited)
|
|
@@ -171,6 +181,23 @@ export const defaultOptions = {
|
|
|
171
181
|
// e.g. "## " at line start → <h2>, "**bold**" → <strong>
|
|
172
182
|
markdownShortcuts: true,
|
|
173
183
|
|
|
184
|
+
// "/" command palette for quick block insertion (headings, lists, table, image, ...).
|
|
185
|
+
// Triggers only when "/" is the first character typed on an otherwise-empty line.
|
|
186
|
+
slashMenu: true,
|
|
187
|
+
// Additional slash-menu commands supplied by applications/plugins.
|
|
188
|
+
slashCommands: [],
|
|
189
|
+
|
|
190
|
+
// Optional import/export adapters keyed by format name.
|
|
191
|
+
documentAdapters: {},
|
|
192
|
+
// Optional collaboration bridge notified with local HTML changes.
|
|
193
|
+
collaborationAdapter: null,
|
|
194
|
+
|
|
195
|
+
// Optional image processor (for example a Web Worker bridge). Receives a
|
|
196
|
+
// File and returns a data URL; Clipboard's canvas pipeline remains fallback.
|
|
197
|
+
imageProcessor: null,
|
|
198
|
+
// Add stable data-an-block-id attributes to top-level document blocks.
|
|
199
|
+
blockIds: false,
|
|
200
|
+
|
|
174
201
|
// Maximum paste size in bytes (default 5 MB). Pastes larger than this are silently dropped.
|
|
175
202
|
maxPasteSize: 5 * 1024 * 1024,
|
|
176
203
|
// Minimum image dimension in px during resize (width and height). Prevents images from being
|
|
@@ -97,6 +97,17 @@
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
@media (prefers-reduced-motion: reduce) {
|
|
101
|
+
.an-container *,
|
|
102
|
+
.an-dialog-overlay *,
|
|
103
|
+
.an-slash-menu * {
|
|
104
|
+
scroll-behavior: auto !important;
|
|
105
|
+
animation-duration: 0.01ms !important;
|
|
106
|
+
animation-iteration-count: 1 !important;
|
|
107
|
+
transition-duration: 0.01ms !important;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
// =============================================================================
|
|
101
112
|
// Toolbar
|
|
102
113
|
// =============================================================================
|
|
@@ -2815,3 +2826,41 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
2815
2826
|
color: #93c5fd;
|
|
2816
2827
|
}
|
|
2817
2828
|
}
|
|
2829
|
+
|
|
2830
|
+
// =============================================================================
|
|
2831
|
+
// "/" command palette
|
|
2832
|
+
// =============================================================================
|
|
2833
|
+
|
|
2834
|
+
.an-slash-menu {
|
|
2835
|
+
display: none;
|
|
2836
|
+
position: fixed;
|
|
2837
|
+
z-index: 9998;
|
|
2838
|
+
background: $an-bg;
|
|
2839
|
+
border: 1px solid $an-border;
|
|
2840
|
+
border-radius: $an-radius;
|
|
2841
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
2842
|
+
min-width: 180px;
|
|
2843
|
+
max-width: 260px;
|
|
2844
|
+
max-height: 260px;
|
|
2845
|
+
overflow-y: auto;
|
|
2846
|
+
padding: 4px 0;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
.an-slash-menu-item {
|
|
2850
|
+
padding: 6px 12px;
|
|
2851
|
+
cursor: pointer;
|
|
2852
|
+
font-size: 13px;
|
|
2853
|
+
color: $an-text;
|
|
2854
|
+
transition: background $an-transition;
|
|
2855
|
+
|
|
2856
|
+
&:hover,
|
|
2857
|
+
&.an-slash-menu-active {
|
|
2858
|
+
background: $an-bg-btn-hover;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
.an-slash-menu-empty {
|
|
2863
|
+
padding: 6px 12px;
|
|
2864
|
+
font-size: 13px;
|
|
2865
|
+
color: $an-muted;
|
|
2866
|
+
}
|