autumnnote 1.0.5 → 1.0.7

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.
@@ -74,6 +74,7 @@ const ICONS = {
74
74
  mergeCells: `<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="2" y="7" width="8" height="10" rx="1"/><rect x="14" y="7" width="8" height="10" rx="1"/><path d="M10 12h4"/><path d="M12 10l2 2-2 2"/></svg>`,
75
75
  colWidth: `<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"><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="7" y1="12" x2="17" y2="12"/><path d="M10 9l-3 3 3 3"/><path d="M14 9l3 3-3 3"/></svg>`,
76
76
  rowHeight: `<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"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="17" x2="20" y2="17"/><line x1="12" y1="7" x2="12" y2="17"/><path d="M9 10l3-3 3 3"/><path d="M9 14l3 3 3-3"/></svg>`,
77
+ tableBorder: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6" stroke-width="1"/><line x1="3" y1="13" x2="21" y2="13" stroke-width="2"/><line x1="3" y1="20" x2="21" y2="20" stroke-width="3"/></svg>`,
77
78
  deleteTable: `<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="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="16" y1="16" x2="22" y2="22" stroke="#ef4444"/><line x1="22" y1="16" x2="16" y2="22" stroke="#ef4444"/></svg>`,
78
79
  };
79
80
 
@@ -104,6 +105,7 @@ export class TableTooltip {
104
105
 
105
106
  this._disposers.push(
106
107
  on(editable, 'mouseover', (e) => {
108
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
107
109
  const table = e.target.closest('table');
108
110
  if (table && editable.contains(table)) {
109
111
  const cell = e.target.closest('td, th');
@@ -168,6 +170,10 @@ export class TableTooltip {
168
170
 
169
171
  const onEditorMove = (e) => {
170
172
  if (_resizing) return;
173
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) {
174
+ clearHover();
175
+ return;
176
+ }
171
177
  // Throttle to one check per animation frame — getBoundingClientRect forces reflow
172
178
  if (_rafEditorMove !== null) return;
173
179
  const target = e.target;
@@ -194,6 +200,7 @@ export class TableTooltip {
194
200
  };
195
201
 
196
202
  const onEditorDown = (e) => {
203
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
197
204
  if (!_nearCell || !_nearEdge) return;
198
205
  _resizing = true;
199
206
  _edge = _nearEdge;
@@ -320,8 +327,9 @@ export class TableTooltip {
320
327
  el.appendChild(this._sep());
321
328
 
322
329
  // Resize
323
- el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
324
- el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
330
+ el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
331
+ el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
332
+ el.appendChild(this._makeBtn(ICONS.tableBorder,'Table Border Width',() => this._openSizePopover('border')));
325
333
 
326
334
  el.appendChild(this._sep());
327
335
 
@@ -641,30 +649,59 @@ export class TableTooltip {
641
649
  _openSizePopover(type) {
642
650
  const cell = this._getCell();
643
651
  if (!cell || !this._sizePopover) return;
644
- const isCol = type === 'col';
645
- this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
646
- this._sizeInputEl.value = isCol
647
- ? (cell.offsetWidth || 120)
648
- : (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
649
-
650
- this._sizeApply = (val) => {
651
- if (isCol) {
652
- const table = cell.closest('table');
653
- const visualColIdx = getVisualColIndex(cell);
654
- const rows = Array.from(table.querySelectorAll('tr'));
655
- // Batch reads, then writes
656
- const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
657
- cells.forEach(c => {
658
- if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
659
- });
660
- } else {
661
- const row = cell.closest('tr');
662
- if (row) {
663
- for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
652
+
653
+ if (type === 'border') {
654
+ const table = cell.closest('table');
655
+ if (!table) return;
656
+ // Read current inline border-width, fall back to computed value, then default 1px
657
+ const firstCell = table.querySelector('td, th');
658
+ const currentPx = firstCell
659
+ ? (parseInt(firstCell.style.borderWidth, 10) ||
660
+ parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
661
+ : 1;
662
+ this._sizeTitleEl.textContent = 'Table Border Width (px)';
663
+ this._sizeInputEl.min = '0';
664
+ this._sizeInputEl.max = '10';
665
+ this._sizeInputEl.value = currentPx;
666
+ this._sizeApply = (val) => {
667
+ // Apply border-width to every cell; with border-collapse:collapse this
668
+ // controls all grid lines including the outer table border.
669
+ // Setting only borderWidth preserves the existing border-color from CSS.
670
+ const cells = Array.from(table.querySelectorAll('td, th'));
671
+ if (val === 0) {
672
+ cells.forEach(c => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
673
+ } else {
674
+ cells.forEach(c => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
664
675
  }
665
- }
666
- this.context.invoke('editor.afterCommand');
667
- };
676
+ this.context.invoke('editor.afterCommand');
677
+ };
678
+ } else {
679
+ const isCol = type === 'col';
680
+ this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
681
+ this._sizeInputEl.min = '1';
682
+ this._sizeInputEl.max = '2000';
683
+ this._sizeInputEl.value = isCol
684
+ ? (cell.offsetWidth || 120)
685
+ : (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
686
+ this._sizeApply = (val) => {
687
+ if (isCol) {
688
+ const table = cell.closest('table');
689
+ const visualColIdx = getVisualColIndex(cell);
690
+ const rows = Array.from(table.querySelectorAll('tr'));
691
+ // Batch reads, then writes
692
+ const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
693
+ cells.forEach(c => {
694
+ if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
695
+ });
696
+ } else {
697
+ const row = cell.closest('tr');
698
+ if (row) {
699
+ for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
700
+ }
701
+ }
702
+ this.context.invoke('editor.afterCommand');
703
+ };
704
+ }
668
705
 
669
706
  this._sizePopover.style.display = 'block';
670
707
  requestAnimationFrame(() => {
@@ -11,6 +11,97 @@ import { createElement, on } from '../core/dom.js';
11
11
  // auto-injects its own FA <link> for the icon-picker glyph rendering.
12
12
  let _faPageLevelReady = null;
13
13
 
14
+ // ---------------------------------------------------------------------------
15
+ // Module-level icon lookup tables — built once, shared across all instances.
16
+ // Previously these were re-created inside _createButton() on every button
17
+ // render, producing O(buttons × map-size) allocations per toolbar init.
18
+ // ---------------------------------------------------------------------------
19
+ const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
20
+ const _svgWrap = (paths) =>
21
+ `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${paths}</svg>`;
22
+
23
+ const _SVG_MAP = new Map([
24
+ // Format
25
+ ['bold', _svgWrap('<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>')],
26
+ ['italic', _svgWrap('<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>')],
27
+ ['underline', _svgWrap('<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/>')],
28
+ ['strikethrough', _svgWrap('<path d="M17.3 12H6.7"/><path d="M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2"/><path d="M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2"/>')],
29
+ ['superscript', _svgWrap('<path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7.25C20 6 19 5 17.5 5S15 6 15 7"/>')],
30
+ ['subscript', _svgWrap('<path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 21h-4c0-1.5.44-2 1.5-2.5S20 17.33 20 16.25C20 15 19 14 17.5 14S15 15 15 16"/>')],
31
+ // Alignment
32
+ ['align-left', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/>')],
33
+ ['align-center', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="18" y1="12" x2="6" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
34
+ ['align-right', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="9" y2="12"/><line x1="21" y1="18" x2="7" y2="18"/>')],
35
+ ['align-justify', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="3" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
36
+ // Lists
37
+ ['list-ul', _svgWrap('<line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>')],
38
+ ['list-ol', _svgWrap('<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1V3"/><path d="M4 10h2l-2 2h2"/><path d="M4 16.5A1.5 1.5 0 0 1 5.5 15a1.5 1.5 0 0 1 0 3H4"/>')],
39
+ ['indent', _svgWrap('<polyline points="3 8 7 12 3 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
40
+ ['outdent', _svgWrap('<polyline points="7 8 3 12 7 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
41
+ // History
42
+ ['undo', _svgWrap('<path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>')],
43
+ ['redo', _svgWrap('<path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/>')],
44
+ // Insert
45
+ ['minus', _svgWrap('<line x1="5" y1="12" x2="19" y2="12"/>')],
46
+ ['link', _svgWrap('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>')],
47
+ ['image', _svgWrap('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>')],
48
+ ['video', _svgWrap('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>')],
49
+ ['table', _svgWrap('<rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>')],
50
+ ['emoji', _svgWrap('<circle cx="12" cy="12" r="10"/><path d="M8.5 14.5s1.5 2.5 3.5 2.5 3.5-2.5 3.5-2.5"/><circle cx="9" cy="9" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="9" r="1.5" fill="currentColor" stroke="none"/>')],
51
+ ['icon', _svgWrap('<circle cx="8" cy="8" r="3"/><circle cx="16" cy="8" r="3"/><rect x="5" y="13" width="6" height="6" rx="1"/><rect x="13" y="13" width="6" height="6" rx="1"/>')],
52
+ // View
53
+ ['code', _svgWrap('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')],
54
+ ['expand', _svgWrap('<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>')],
55
+ // Color pickers
56
+ ['foreColor', _svgWrap('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>')],
57
+ ['backColor', _svgWrap('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/>')],
58
+ ['keyboard', _svgWrap('<rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="10" stroke-width="2.5"/><line x1="10" y1="10" x2="10" y2="10" stroke-width="2.5"/><line x1="14" y1="10" x2="14" y2="10" stroke-width="2.5"/><line x1="18" y1="10" x2="18" y2="10" stroke-width="2.5"/><line x1="8" y1="14" x2="16" y2="14" stroke-width="2"/>')],
59
+ ['caption', _svgWrap('<rect x="3" y="3" width="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/>')],
60
+ ['remove-format', _svgWrap('<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/>')],
61
+ ['direction', _svgWrap('<path d="M12 20V4"/><path d="m9 7-3 3 3 3"/><path d="M4 10h8"/><path d="m15 7 3 3-3 3"/><path d="M20 10h-8"/>')],
62
+ ['search', _svgWrap('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')],
63
+ ['find-replace', _svgWrap('<circle cx="10" cy="10" r="6"/><line x1="18" y1="18" x2="14.35" y2="14.35"/><path d="M16 19h6"/><path d="M19 16v6"/>')],
64
+ ['inline-code', _svgWrap('<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1"/>')],
65
+ ['checklist', _svgWrap('<rect x="3" y="4" width="5" height="5" rx="1"/><path d="m4 6.5 1 1 2-2"/><rect x="3" y="13" width="5" height="5" rx="1"/><line x1="10" y1="6.5" x2="21" y2="6.5"/><line x1="10" y1="15.5" x2="21" y2="15.5"/>')],
66
+ ['print', _svgWrap('<path d="M6 9V2h12v7"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>')],
67
+ ]);
68
+
69
+ const _FA_MAP = new Map([
70
+ ['bold', 'fa-bold'],
71
+ ['italic', 'fa-italic'],
72
+ ['underline', 'fa-underline'],
73
+ ['strikethrough', 'fa-strikethrough'],
74
+ ['superscript', 'fa-superscript'],
75
+ ['subscript', 'fa-subscript'],
76
+ ['align-left', 'fa-align-left'],
77
+ ['align-center', 'fa-align-center'],
78
+ ['align-right', 'fa-align-right'],
79
+ ['align-justify', 'fa-align-justify'],
80
+ ['list-ul', 'fa-list-ul'],
81
+ ['list-ol', 'fa-list-ol'],
82
+ ['indent', 'fa-indent'],
83
+ ['outdent', 'fa-outdent'],
84
+ ['undo', 'fa-rotate-left'],
85
+ ['redo', 'fa-rotate-right'],
86
+ ['minus', 'fa-minus'],
87
+ ['link', 'fa-link'],
88
+ ['image', 'fa-image'],
89
+ ['code', 'fa-code'],
90
+ ['expand', 'fa-expand'],
91
+ ['emoji', 'fa-face-smile'],
92
+ ['icon', 'fa-icons'],
93
+ ['foreColor', 'fa-font'],
94
+ ['backColor', 'fa-highlighter'],
95
+ ['keyboard', 'fa-keyboard'],
96
+ ['remove-format', 'fa-remove-format'],
97
+ ['direction', 'fa-arrow-right-arrow-left'],
98
+ ['search', 'fa-magnifying-glass'],
99
+ ['find-replace', 'fa-magnifying-glass-plus'],
100
+ ['inline-code', 'fa-code'],
101
+ ['checklist', 'fa-list-check'],
102
+ ['print', 'fa-print'],
103
+ ]);
104
+
14
105
  export class Toolbar {
15
106
  /**
16
107
  * @param {import('../Context.js').Context} context
@@ -55,6 +146,9 @@ export class Toolbar {
55
146
 
56
147
  _buildButtons() {
57
148
  const toolbar = this.options.toolbar || [];
149
+ // Build into a DocumentFragment so all groups are appended in a single
150
+ // DOM operation, avoiding one reflow per group.
151
+ const fragment = document.createDocumentFragment();
58
152
  toolbar.forEach((group) => {
59
153
  const groupEl = createElement('div', { class: 'an-btn-group' });
60
154
  group.forEach((btnDef) => {
@@ -65,8 +159,9 @@ export class Toolbar {
65
159
  else el = this._createButton(btnDef);
66
160
  groupEl.appendChild(el);
67
161
  });
68
- this.el.appendChild(groupEl);
162
+ fragment.appendChild(groupEl);
69
163
  });
164
+ this.el.appendChild(fragment);
70
165
  }
71
166
 
72
167
  /**
@@ -271,10 +366,13 @@ export class Toolbar {
271
366
 
272
367
  const restoreSelection = () => {
273
368
  if (!savedRange) return;
274
- const sel = window.getSelection();
275
- if (sel) {
369
+ try {
370
+ const sel = window.getSelection();
371
+ if (!sel) return;
276
372
  sel.removeAllRanges();
277
373
  sel.addRange(savedRange);
374
+ } catch (_) {
375
+ // Range target may be detached if DOM changed while popup was open
278
376
  }
279
377
  };
280
378
 
@@ -415,16 +513,34 @@ export class Toolbar {
415
513
  select.appendChild(opt);
416
514
  });
417
515
 
516
+ // Save the editor selection when the user starts interacting with the
517
+ // dropdown (mousedown fires before the editor loses focus). When the
518
+ // change handler runs, focus has moved to the <select>; we restore the
519
+ // saved range so execCommand / fontSize() act on the intended text.
520
+ /** @type {Range|null} */
521
+ let _savedRange = null;
522
+ const dMousedown = on(select, 'mousedown', () => {
523
+ const sel = window.getSelection();
524
+ _savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
525
+ });
526
+
418
527
  const disposer = on(select, 'change', (e) => {
419
- const value = e.target.value;
420
- const selectedOpt = e.target.options[e.target.selectedIndex];
528
+ const value = /** @type {HTMLSelectElement} */ (e.target).value;
529
+ const selectedOpt = /** @type {HTMLSelectElement} */ (e.target).options[/** @type {HTMLSelectElement} */ (e.target).selectedIndex];
421
530
  if (!value || selectedOpt.disabled) return;
422
531
  this.context.invoke('editor.focus');
532
+ // Restore selection saved on mousedown so the action targets the correct text.
533
+ if (_savedRange) {
534
+ try {
535
+ const sel = window.getSelection();
536
+ if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
537
+ } catch (_) { /* range may be stale if DOM changed */ }
538
+ }
423
539
  def.action(this.context, value);
424
540
  this.context.invoke('editor.afterCommand');
425
541
  });
426
542
 
427
- this._disposers.push(disposer);
543
+ this._disposers.push(dMousedown, disposer);
428
544
  return select;
429
545
  }
430
546
 
@@ -448,110 +564,23 @@ export class Toolbar {
448
564
  });
449
565
 
450
566
  // Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
451
-
452
- // Heroicons-style stroke SVGs — consistent with context menu icons
453
- const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
454
- const svgWrap = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block">${paths}</svg>`;
455
-
456
- const svgMap = new Map([
457
- // Format
458
- ['bold', svgWrap('<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>')],
459
- ['italic', svgWrap('<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>')],
460
- ['underline', svgWrap('<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/>')],
461
- ['strikethrough', svgWrap('<path d="M17.3 12H6.7"/><path d="M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2"/><path d="M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2"/>')],
462
- ['superscript', svgWrap('<path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7.25C20 6 19 5 17.5 5S15 6 15 7"/>')],
463
- ['subscript', svgWrap('<path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 21h-4c0-1.5.44-2 1.5-2.5S20 17.33 20 16.25C20 15 19 14 17.5 14S15 15 15 16"/>')],
464
- // Alignment
465
- ['align-left', svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/>')],
466
- ['align-center', svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="18" y1="12" x2="6" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
467
- ['align-right', svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="9" y2="12"/><line x1="21" y1="18" x2="7" y2="18"/>')],
468
- ['align-justify', svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="3" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
469
- // Lists
470
- ['list-ul', svgWrap('<line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>')],
471
- ['list-ol', svgWrap('<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1V3"/><path d="M4 10h2l-2 2h2"/><path d="M4 16.5A1.5 1.5 0 0 1 5.5 15a1.5 1.5 0 0 1 0 3H4"/>')],
472
- ['indent', svgWrap('<polyline points="3 8 7 12 3 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
473
- ['outdent', svgWrap('<polyline points="7 8 3 12 7 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
474
- // History
475
- ['undo', svgWrap('<path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>')],
476
- ['redo', svgWrap('<path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/>')],
477
- // Insert
478
- ['minus', svgWrap('<line x1="5" y1="12" x2="19" y2="12"/>')],
479
- ['link', svgWrap('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>')],
480
- ['image', svgWrap('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>')],
481
- ['video', svgWrap('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>')],
482
- ['table', svgWrap('<rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>')],
483
- ['emoji', svgWrap('<circle cx="12" cy="12" r="10"/><path d="M8.5 14.5s1.5 2.5 3.5 2.5 3.5-2.5 3.5-2.5"/><circle cx="9" cy="9" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="9" r="1.5" fill="currentColor" stroke="none"/>')],
484
- ['icon', svgWrap('<circle cx="8" cy="8" r="3"/><circle cx="16" cy="8" r="3"/><rect x="5" y="13" width="6" height="6" rx="1"/><rect x="13" y="13" width="6" height="6" rx="1"/>')],
485
- // View
486
- ['code', svgWrap('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')],
487
- ['expand', svgWrap('<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>')],
488
- // Color pickers
489
- ['foreColor', svgWrap('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>')],
490
- ['backColor', svgWrap('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/>')],
491
- ['keyboard', svgWrap('<rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="10" stroke-width="2.5"/><line x1="10" y1="10" x2="10" y2="10" stroke-width="2.5"/><line x1="14" y1="10" x2="14" y2="10" stroke-width="2.5"/><line x1="18" y1="10" x2="18" y2="10" stroke-width="2.5"/><line x1="8" y1="14" x2="16" y2="14" stroke-width="2"/>')],
492
- ['caption', svgWrap('<rect x="3" y="3" width="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/>')],
493
- ['remove-format', svgWrap('<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/>')],
494
- ['direction', svgWrap('<path d="M12 20V4"/><path d="m9 7-3 3 3 3"/><path d="M4 10h8"/><path d="m15 7 3 3-3 3"/><path d="M20 10h-8"/>')],
495
- ['search', svgWrap('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')],
496
- ['find-replace', svgWrap('<circle cx="10" cy="10" r="6"/><line x1="18" y1="18" x2="14.35" y2="14.35"/><path d="M16 19h6"/><path d="M19 16v6"/>')],
497
- ['inline-code', svgWrap('<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1"/>')],
498
- ['checklist', svgWrap('<rect x="3" y="4" width="5" height="5" rx="1"/><path d="m4 6.5 1 1 2-2"/><rect x="3" y="13" width="5" height="5" rx="1"/><line x1="10" y1="6.5" x2="21" y2="6.5"/><line x1="10" y1="15.5" x2="21" y2="15.5"/>')],
499
- ['print', svgWrap('<path d="M6 9V2h12v7"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>')],
500
- ]);
501
-
502
567
  const faPrefix = this.options.fontAwesomeClass || 'fas';
503
- const faMap = new Map([
504
- ['bold', 'fa-bold'],
505
- ['italic', 'fa-italic'],
506
- ['underline', 'fa-underline'],
507
- ['strikethrough', 'fa-strikethrough'],
508
- ['superscript', 'fa-superscript'],
509
- ['subscript', 'fa-subscript'],
510
- ['align-left', 'fa-align-left'],
511
- ['align-center', 'fa-align-center'],
512
- ['align-right', 'fa-align-right'],
513
- ['align-justify', 'fa-align-justify'],
514
- ['list-ul', 'fa-list-ul'],
515
- ['list-ol', 'fa-list-ol'],
516
- ['indent', 'fa-indent'],
517
- ['outdent', 'fa-outdent'],
518
- ['undo', 'fa-rotate-left'],
519
- ['redo', 'fa-rotate-right'],
520
- ['minus', 'fa-minus'],
521
- ['link', 'fa-link'],
522
- ['image', 'fa-image'],
523
- ['code', 'fa-code'],
524
- ['expand', 'fa-expand'],
525
- ['emoji', 'fa-face-smile'],
526
- ['icon', 'fa-icons'],
527
- ['foreColor', 'fa-font'],
528
- ['backColor', 'fa-highlighter'],
529
- ['keyboard', 'fa-keyboard'],
530
- ['remove-format', 'fa-remove-format'],
531
- ['direction', 'fa-arrow-right-arrow-left'],
532
- ['search', 'fa-magnifying-glass'],
533
- ['find-replace', 'fa-magnifying-glass-plus'],
534
- ['inline-code', 'fa-code'],
535
- ['checklist', 'fa-list-check'],
536
- ['print', 'fa-print'],
537
- ]);
538
-
539
568
  const useFaNow = this._faReady;
540
569
  if (useFaNow) {
541
- const faName = faMap.get(btnDef.icon) || faMap.get(btnDef.name) || null;
570
+ const faName = _FA_MAP.get(btnDef.icon) || _FA_MAP.get(btnDef.name) || null;
542
571
  if (faName) {
543
572
  btn.innerHTML = `<i class="${faPrefix} ${faName}" aria-hidden="true"></i>`;
544
- } else if (svgMap.has(btnDef.icon)) {
545
- btn.innerHTML = svgMap.get(btnDef.icon);
573
+ } else if (_SVG_MAP.has(btnDef.icon)) {
574
+ btn.innerHTML = _SVG_MAP.get(btnDef.icon);
546
575
  } else {
547
576
  btn.textContent = btnDef.icon || btnDef.name;
548
577
  }
549
578
  } else {
550
579
  // FontAwesome absent: use SVG fallback when available
551
- if (svgMap.has(btnDef.icon)) {
552
- btn.innerHTML = svgMap.get(btnDef.icon);
553
- } else if (svgMap.has(btnDef.name)) {
554
- btn.innerHTML = svgMap.get(btnDef.name);
580
+ if (_SVG_MAP.has(btnDef.icon)) {
581
+ btn.innerHTML = _SVG_MAP.get(btnDef.icon);
582
+ } else if (_SVG_MAP.has(btnDef.name)) {
583
+ btn.innerHTML = _SVG_MAP.get(btnDef.name);
555
584
  } else {
556
585
  btn.textContent = btnDef.icon || btnDef.name;
557
586
  }
@@ -73,12 +73,12 @@ export class VideoDialog {
73
73
  // URL input
74
74
  const urlLabel = createElement('label', { class: 'an-label' });
75
75
  urlLabel.textContent = 'Video URL';
76
- const urlInput = createElement('input', {
76
+ const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
77
77
  type: 'url',
78
78
  class: 'an-input',
79
79
  placeholder: 'YouTube, Vimeo, or direct .mp4 URL',
80
80
  autocomplete: 'off',
81
- });
81
+ }));
82
82
  this._urlInput = urlInput;
83
83
 
84
84
  // Hint (detected source)
@@ -88,14 +88,14 @@ export class VideoDialog {
88
88
  // Width
89
89
  const widthLabel = createElement('label', { class: 'an-label' });
90
90
  widthLabel.textContent = 'Width (px)';
91
- const widthInput = createElement('input', {
91
+ const widthInput = /** @type {HTMLInputElement} */ (createElement('input', {
92
92
  type: 'number',
93
93
  class: 'an-input',
94
94
  placeholder: '560',
95
95
  min: '80',
96
96
  max: '1920',
97
97
  value: '560',
98
- });
98
+ }));
99
99
  this._widthInput = widthInput;
100
100
 
101
101
  // Buttons
@@ -119,7 +119,7 @@ export class VideoDialog {
119
119
  const d1 = on(insertBtn, 'click', () => this._onInsert());
120
120
  const d2 = on(cancelBtn, 'click', () => this._close());
121
121
  const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
122
- const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
122
+ const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
123
123
  this._disposers.push(d0, d1, d2, d3, d4);
124
124
 
125
125
  return overlay;
@@ -41,6 +41,7 @@ export class VideoResizer {
41
41
  this._disposers.push(
42
42
  on(editable, 'click', (e) => this._onEditorClick(e)),
43
43
  on(editable, 'contextmenu', (e) => {
44
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
44
45
  const wrapper = this._findWrapper(e.target);
45
46
  if (wrapper) this._select(wrapper);
46
47
  }),
@@ -48,6 +49,15 @@ export class VideoResizer {
48
49
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
49
50
  on(window, 'resize', onWindowResize),
50
51
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
52
+ // D1: Prevent native browser drag of video wrappers. Without this, a user
53
+ // can hold-and-drag to produce a "copy" that lands outside .an-editable,
54
+ // where the .an-video-shield CSS loses its containing context so the
55
+ // copied video becomes directly playable.
56
+ on(editable, 'dragstart', (e) => {
57
+ if (e.target instanceof Element && e.target.closest('.an-video-wrapper')) {
58
+ e.preventDefault();
59
+ }
60
+ }),
51
61
  );
52
62
 
53
63
  return this;
@@ -131,6 +141,7 @@ export class VideoResizer {
131
141
  }
132
142
 
133
143
  _onEditorClick(e) {
144
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
134
145
  const wrapper = this._findWrapper(e.target);
135
146
  if (wrapper) {
136
147
  e.preventDefault();
@@ -37,6 +37,7 @@ export class VideoTooltip {
37
37
 
38
38
  this._disposers.push(
39
39
  on(editable, 'mouseover', (e) => {
40
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
40
41
  // The shield div sits on top of iframes — we detect hover via it or the wrapper
41
42
  const wrapper = e.target.closest('.an-video-wrapper');
42
43
  if (wrapper && editable.contains(wrapper)) {
@@ -78,6 +78,9 @@ export function renderLayout(targetEl, options) {
78
78
  // Read-only mode
79
79
  if (options.readOnly) {
80
80
  container.classList.add('an-disabled');
81
+ editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
82
+ cb.setAttribute('disabled', '');
83
+ });
81
84
  }
82
85
 
83
86
  // Text direction