autumnnote 1.6.0 → 1.6.2
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 +4 -4
- package/dist/autumnnote.css +0 -2
- package/dist/autumnnote.es.js +468 -552
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +461 -546
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +21 -3
- package/src/js/Context.js +21 -16
- package/src/js/core/dom.js +9 -9
- package/src/js/core/env.js +1 -1
- package/src/js/core/func.js +1 -1
- package/src/js/core/lists.js +1 -1
- package/src/js/core/markdown.js +18 -18
- package/src/js/core/range.js +6 -6
- package/src/js/editing/History.js +4 -4
- package/src/js/editing/Style.js +32 -32
- package/src/js/editing/Table.js +2 -2
- package/src/js/editing/Typing.js +15 -15
- package/src/js/index.js +1 -1
- package/src/js/module/AutoSaveRestore.js +2 -4
- package/src/js/module/BaseDialog.js +126 -0
- package/src/js/module/BubbleToolbar.js +25 -25
- package/src/js/module/Buttons.js +4 -4
- package/src/js/module/Clipboard.js +12 -13
- package/src/js/module/CodeTooltip.js +14 -16
- package/src/js/module/Codeview.js +3 -5
- package/src/js/module/ContextMenu.js +27 -27
- package/src/js/module/Editor.js +17 -18
- package/src/js/module/EmojiDialog.js +5 -5
- package/src/js/module/FindReplace.js +8 -7
- package/src/js/module/IconDialog.js +13 -15
- package/src/js/module/ImageCropOverlay.js +7 -7
- package/src/js/module/ImageDialog.js +12 -82
- package/src/js/module/ImageResizer.js +4 -4
- package/src/js/module/ImageTooltip.js +14 -14
- package/src/js/module/LinkDialog.js +13 -79
- package/src/js/module/LinkTooltip.js +12 -12
- package/src/js/module/MarkdownShortcuts.js +11 -14
- package/src/js/module/Mention.js +8 -10
- package/src/js/module/Placeholder.js +1 -1
- package/src/js/module/ShortcutsDialog.js +2 -4
- package/src/js/module/Statusbar.js +3 -5
- package/src/js/module/TableTooltip.js +30 -32
- package/src/js/module/Toolbar.js +21 -21
- package/src/js/module/VideoDialog.js +17 -87
- package/src/js/module/VideoResizer.js +5 -7
- package/src/js/module/VideoTooltip.js +7 -7
- package/src/js/renderer.js +1 -1
- package/src/styles/autumnnote.scss +0 -3
|
@@ -109,9 +109,7 @@ export class Statusbar {
|
|
|
109
109
|
this._dragDisposers.forEach((d) => d());
|
|
110
110
|
this._dragDisposers = null;
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
this.el.parentNode.removeChild(this.el);
|
|
114
|
-
}
|
|
112
|
+
this.el?.remove();
|
|
115
113
|
this.el = null;
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -217,7 +215,7 @@ export class Statusbar {
|
|
|
217
215
|
// textContent is faster than innerText (no layout flush, no CSS visibility check)
|
|
218
216
|
const text = editable.textContent || '';
|
|
219
217
|
const words = _countWords(text);
|
|
220
|
-
const chars = text.
|
|
218
|
+
const chars = text.replaceAll('\n', '').length;
|
|
221
219
|
const maxWords = this.options.maxWords || 0;
|
|
222
220
|
const maxChars = this.options.maxChars || 0;
|
|
223
221
|
|
|
@@ -249,6 +247,6 @@ export class Statusbar {
|
|
|
249
247
|
*/
|
|
250
248
|
getCharCount() {
|
|
251
249
|
const editable = this.context.layoutInfo.editable;
|
|
252
|
-
return (
|
|
250
|
+
return (editable.innerText || '').replaceAll('\n', '').length;
|
|
253
251
|
}
|
|
254
252
|
}
|
|
@@ -212,26 +212,26 @@ export class TableTooltip {
|
|
|
212
212
|
if (!to || (
|
|
213
213
|
!editable.contains(to) &&
|
|
214
214
|
!this._el.contains(to) &&
|
|
215
|
-
!
|
|
215
|
+
!this._sizePopover?.contains(to)
|
|
216
216
|
)) {
|
|
217
217
|
this._scheduleHide();
|
|
218
218
|
}
|
|
219
219
|
}, { passive: true }),
|
|
220
220
|
on(document, 'click', (e) => {
|
|
221
221
|
const et = /** @type {Node} */ (e.target);
|
|
222
|
-
if (this._selectMode && this._activeTable
|
|
222
|
+
if (this._selectMode && this._activeTable?.contains(et)) return;
|
|
223
223
|
if (this._activeTable &&
|
|
224
224
|
!this._activeTable.contains(et) &&
|
|
225
225
|
!this._el.contains(et) &&
|
|
226
|
-
!
|
|
226
|
+
!this._sizePopover?.contains(et)) {
|
|
227
227
|
this._hide();
|
|
228
228
|
}
|
|
229
229
|
}),
|
|
230
230
|
// Sync shade strip whenever selection moves to a different cell
|
|
231
231
|
on(document, 'selectionchange', () => this._syncShadeStrip()),
|
|
232
232
|
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
233
|
-
on(
|
|
234
|
-
on(
|
|
233
|
+
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
234
|
+
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
235
235
|
);
|
|
236
236
|
|
|
237
237
|
this._initResize();
|
|
@@ -386,14 +386,14 @@ export class TableTooltip {
|
|
|
386
386
|
this._clearTimers();
|
|
387
387
|
this._disposers.forEach((d) => d());
|
|
388
388
|
this._disposers = [];
|
|
389
|
-
if (this._el && this._el.parentNode) this._el.
|
|
389
|
+
if (this._el && this._el.parentNode) this._el.remove();
|
|
390
390
|
this._el = null;
|
|
391
391
|
if (this._sizePopover && this._sizePopover.parentNode) {
|
|
392
|
-
this._sizePopover.
|
|
392
|
+
this._sizePopover.remove();
|
|
393
393
|
}
|
|
394
394
|
this._sizePopover = null;
|
|
395
395
|
if (this._shadePopover && this._shadePopover.parentNode) {
|
|
396
|
-
this._shadePopover.
|
|
396
|
+
this._shadePopover.remove();
|
|
397
397
|
}
|
|
398
398
|
this._shadePopover = null;
|
|
399
399
|
}
|
|
@@ -553,7 +553,7 @@ export class TableTooltip {
|
|
|
553
553
|
_syncShadeStrip() {
|
|
554
554
|
if (!this._shadeColorStrip || !this._el || this._el.style.display === 'none') return;
|
|
555
555
|
const cell = this._getCell();
|
|
556
|
-
this._shadeColorStrip.style.background =
|
|
556
|
+
this._shadeColorStrip.style.background = cell?.style.backgroundColor || 'transparent';
|
|
557
557
|
}
|
|
558
558
|
|
|
559
559
|
_hide() {
|
|
@@ -590,7 +590,7 @@ export class TableTooltip {
|
|
|
590
590
|
let top = rect.top - tipH - margin;
|
|
591
591
|
|
|
592
592
|
if (top < margin) top = rect.bottom + margin;
|
|
593
|
-
if (left + tipW >
|
|
593
|
+
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
594
594
|
if (left < margin) left = margin;
|
|
595
595
|
|
|
596
596
|
this._el.style.left = `${left}px`;
|
|
@@ -603,17 +603,17 @@ export class TableTooltip {
|
|
|
603
603
|
|
|
604
604
|
_getCell() {
|
|
605
605
|
// Prefer the cell under the current text cursor (most intuitive for operations)
|
|
606
|
-
const sel =
|
|
606
|
+
const sel = globalThis.getSelection();
|
|
607
607
|
if (sel && sel.rangeCount) {
|
|
608
608
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
609
609
|
if (container.nodeType === 3) container = container.parentElement;
|
|
610
|
-
const cellFromSel =
|
|
611
|
-
if (cellFromSel && this._activeTable
|
|
610
|
+
const cellFromSel = /** @type {Element} */ (container)?.closest('td, th');
|
|
611
|
+
if (cellFromSel && this._activeTable?.contains(cellFromSel)) {
|
|
612
612
|
return cellFromSel;
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
615
|
return this._activeCell
|
|
616
|
-
||
|
|
616
|
+
|| this._activeTable?.querySelector('td, th');
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
// ---------------------------------------------------------------------------
|
|
@@ -735,7 +735,7 @@ export class TableTooltip {
|
|
|
735
735
|
const bi = allRows.indexOf(best);
|
|
736
736
|
const ri = allRows.indexOf(r);
|
|
737
737
|
return position === 'above' ? (ri < bi ? r : best) : (ri > bi ? r : best);
|
|
738
|
-
});
|
|
738
|
+
}, selectedRows[0]);
|
|
739
739
|
const colCount = Array.from(refRow.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
740
740
|
const newRow = document.createElement('tr');
|
|
741
741
|
const refCells = Array.from(refRow.cells);
|
|
@@ -786,7 +786,7 @@ export class TableTooltip {
|
|
|
786
786
|
if (bodyRowsToDelete.length >= totalBodyRows) return;
|
|
787
787
|
this._activeCell = null;
|
|
788
788
|
this._clearSelection();
|
|
789
|
-
selectedRows.forEach((r) => r.
|
|
789
|
+
selectedRows.forEach((r) => r.remove());
|
|
790
790
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
791
791
|
this.context.invoke('editor.afterCommand');
|
|
792
792
|
}
|
|
@@ -810,7 +810,7 @@ export class TableTooltip {
|
|
|
810
810
|
});
|
|
811
811
|
this._activeCell = null;
|
|
812
812
|
this._clearSelection();
|
|
813
|
-
cellsToDelete.forEach((c) => c.
|
|
813
|
+
cellsToDelete.forEach((c) => c.remove());
|
|
814
814
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
815
815
|
this.context.invoke('editor.afterCommand');
|
|
816
816
|
}
|
|
@@ -824,7 +824,7 @@ export class TableTooltip {
|
|
|
824
824
|
// Prefer user panel-selected cells; fall back to text-selection range
|
|
825
825
|
let selected = this._getSelectedCells().filter((c) => table.contains(c));
|
|
826
826
|
if (selected.length < 2) {
|
|
827
|
-
const sel =
|
|
827
|
+
const sel = globalThis.getSelection();
|
|
828
828
|
if (!sel || sel.rangeCount === 0) return;
|
|
829
829
|
const range = sel.getRangeAt(0);
|
|
830
830
|
const allCells = Array.from(table.querySelectorAll('td, th'));
|
|
@@ -866,7 +866,7 @@ export class TableTooltip {
|
|
|
866
866
|
first.rowSpan = maxR - minR + 1;
|
|
867
867
|
first.style.verticalAlign = 'middle';
|
|
868
868
|
first.innerHTML = rectCells.map((c) => c.innerHTML).join('');
|
|
869
|
-
rectCells.slice(1).forEach((c) => c.
|
|
869
|
+
rectCells.slice(1).forEach((c) => c.remove());
|
|
870
870
|
|
|
871
871
|
this._clearSelection();
|
|
872
872
|
this.context.invoke('editor.afterCommand');
|
|
@@ -876,7 +876,7 @@ export class TableTooltip {
|
|
|
876
876
|
const table = this._activeTable;
|
|
877
877
|
if (!table) return;
|
|
878
878
|
this._hide();
|
|
879
|
-
if (table.parentNode) table.
|
|
879
|
+
if (table.parentNode) table.remove();
|
|
880
880
|
this.context.invoke('editor.afterCommand');
|
|
881
881
|
}
|
|
882
882
|
|
|
@@ -981,7 +981,7 @@ export class TableTooltip {
|
|
|
981
981
|
this._sizeApply = null;
|
|
982
982
|
|
|
983
983
|
const d1 = on(applyBtn, 'click', () => {
|
|
984
|
-
const val = parseInt(this._sizeInputEl.value, 10);
|
|
984
|
+
const val = Number.parseInt(this._sizeInputEl.value, 10);
|
|
985
985
|
if (val > 0 && typeof this._sizeApply === 'function') this._sizeApply(val);
|
|
986
986
|
this._hideSizePopover();
|
|
987
987
|
});
|
|
@@ -1016,8 +1016,8 @@ export class TableTooltip {
|
|
|
1016
1016
|
if (!table) return;
|
|
1017
1017
|
const firstCell = table.querySelector('td, th');
|
|
1018
1018
|
const currentPx = firstCell
|
|
1019
|
-
? (parseInt(firstCell.style.borderWidth, 10) ||
|
|
1020
|
-
parseInt(
|
|
1019
|
+
? (Number.parseInt(firstCell.style.borderWidth, 10) ||
|
|
1020
|
+
Number.parseInt(globalThis.getComputedStyle(firstCell).borderWidth, 10) || 1)
|
|
1021
1021
|
: 1;
|
|
1022
1022
|
this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
|
|
1023
1023
|
this._sizeInputEl.min = '0';
|
|
@@ -1079,8 +1079,8 @@ export class TableTooltip {
|
|
|
1079
1079
|
const ph = this._sizePopover.offsetHeight || 110;
|
|
1080
1080
|
let left = tipRect.left;
|
|
1081
1081
|
let top = tipRect.bottom + 6;
|
|
1082
|
-
if (left + pw >
|
|
1083
|
-
if (top + ph >
|
|
1082
|
+
if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
|
|
1083
|
+
if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
|
|
1084
1084
|
this._sizePopover.style.left = `${left}px`;
|
|
1085
1085
|
this._sizePopover.style.top = `${top}px`;
|
|
1086
1086
|
if (this._sizeInputEl) { this._sizeInputEl.focus(); this._sizeInputEl.select(); }
|
|
@@ -1135,11 +1135,9 @@ export class TableTooltip {
|
|
|
1135
1135
|
customRow.appendChild(customLabel);
|
|
1136
1136
|
pop.appendChild(customRow);
|
|
1137
1137
|
|
|
1138
|
-
// Prevent mousedown from collapsing editor selection
|
|
1139
|
-
this._disposers.push(on(pop, 'mousedown', (e) => e.preventDefault()));
|
|
1140
|
-
|
|
1141
|
-
// Keep tooltip alive while hovering popover
|
|
1138
|
+
// Prevent mousedown from collapsing editor selection; keep tooltip alive while hovering
|
|
1142
1139
|
this._disposers.push(
|
|
1140
|
+
on(pop, 'mousedown', (e) => e.preventDefault()),
|
|
1143
1141
|
on(pop, 'mouseenter', () => this._clearTimers()),
|
|
1144
1142
|
on(pop, 'mouseleave', () => this._scheduleHide()),
|
|
1145
1143
|
);
|
|
@@ -1150,7 +1148,7 @@ export class TableTooltip {
|
|
|
1150
1148
|
if (this._shadePopover &&
|
|
1151
1149
|
this._shadePopover.style.display !== 'none' &&
|
|
1152
1150
|
!this._shadePopover.contains(et) &&
|
|
1153
|
-
!
|
|
1151
|
+
!this._el?.contains(et)) {
|
|
1154
1152
|
this._hideCellShadePopover();
|
|
1155
1153
|
}
|
|
1156
1154
|
}));
|
|
@@ -1172,8 +1170,8 @@ export class TableTooltip {
|
|
|
1172
1170
|
const tipRect = this._el.getBoundingClientRect();
|
|
1173
1171
|
let left = tipRect.left;
|
|
1174
1172
|
let top = tipRect.bottom + 6;
|
|
1175
|
-
if (left + pw >
|
|
1176
|
-
if (top + ph >
|
|
1173
|
+
if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
|
|
1174
|
+
if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
|
|
1177
1175
|
this._shadePopover.style.left = `${Math.max(8, left)}px`;
|
|
1178
1176
|
this._shadePopover.style.top = `${Math.max(8, top)}px`;
|
|
1179
1177
|
});
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -146,7 +146,7 @@ export class Toolbar {
|
|
|
146
146
|
this._disposers.forEach((d) => d());
|
|
147
147
|
this._disposers = [];
|
|
148
148
|
if (this.el && this.el.parentNode) {
|
|
149
|
-
this.el.
|
|
149
|
+
this.el.remove();
|
|
150
150
|
}
|
|
151
151
|
this.el = null;
|
|
152
152
|
}
|
|
@@ -244,8 +244,8 @@ export class Toolbar {
|
|
|
244
244
|
|
|
245
245
|
const setHighlight = (rows, cols) => {
|
|
246
246
|
cells.forEach((cell) => {
|
|
247
|
-
const r = +cell.
|
|
248
|
-
const c = +cell.
|
|
247
|
+
const r = +cell.dataset.row;
|
|
248
|
+
const c = +cell.dataset.col;
|
|
249
249
|
cell.classList.toggle('active', r <= rows && c <= cols);
|
|
250
250
|
});
|
|
251
251
|
label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
|
|
@@ -264,8 +264,8 @@ export class Toolbar {
|
|
|
264
264
|
|
|
265
265
|
let left = rect.left;
|
|
266
266
|
let top = rect.bottom + 4;
|
|
267
|
-
if (left + pw >
|
|
268
|
-
if (top + ph >
|
|
267
|
+
if (left + pw > globalThis.innerWidth - 8) left = Math.max(8, globalThis.innerWidth - pw - 8);
|
|
268
|
+
if (top + ph > globalThis.innerHeight - 8) top = rect.top - ph - 4;
|
|
269
269
|
|
|
270
270
|
popup.style.left = `${left}px`;
|
|
271
271
|
popup.style.top = `${top}px`;
|
|
@@ -286,18 +286,18 @@ export class Toolbar {
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
const d2 = on(grid, 'mouseover', (e) => {
|
|
289
|
-
const cell = /** @type {Element} */ (e.target)?.closest('.an-table-cell');
|
|
289
|
+
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
290
290
|
if (!cell) return;
|
|
291
|
-
setHighlight(+cell.
|
|
291
|
+
setHighlight(+cell.dataset.row, +cell.dataset.col);
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
|
|
295
295
|
|
|
296
296
|
const d4 = on(grid, 'click', (e) => {
|
|
297
|
-
const cell = /** @type {Element} */ (e.target)?.closest('.an-table-cell');
|
|
297
|
+
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
298
298
|
if (!cell) return;
|
|
299
|
-
const rows = +cell.
|
|
300
|
-
const cols = +cell.
|
|
299
|
+
const rows = +cell.dataset.row;
|
|
300
|
+
const cols = +cell.dataset.col;
|
|
301
301
|
closePopup();
|
|
302
302
|
this.context.invoke('editor.focus');
|
|
303
303
|
def.action(this.context, rows, cols);
|
|
@@ -308,7 +308,7 @@ export class Toolbar {
|
|
|
308
308
|
// Append popup to body so position:fixed is truly viewport-relative,
|
|
309
309
|
// unaffected by any ancestor transform / filter (same pattern as color picker).
|
|
310
310
|
this._disposers.push(d1, d2, d3, d4, d5, () => {
|
|
311
|
-
if (popup.parentNode) popup.
|
|
311
|
+
if (popup.parentNode) popup.remove();
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
wrap.appendChild(btn);
|
|
@@ -398,14 +398,14 @@ export class Toolbar {
|
|
|
398
398
|
let savedRange = null;
|
|
399
399
|
|
|
400
400
|
const saveSelection = () => {
|
|
401
|
-
const sel =
|
|
401
|
+
const sel = globalThis.getSelection();
|
|
402
402
|
savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
403
403
|
};
|
|
404
404
|
|
|
405
405
|
const restoreSelection = () => {
|
|
406
406
|
if (!savedRange) return;
|
|
407
407
|
try {
|
|
408
|
-
const sel =
|
|
408
|
+
const sel = globalThis.getSelection();
|
|
409
409
|
if (!sel) return;
|
|
410
410
|
sel.removeAllRanges();
|
|
411
411
|
sel.addRange(savedRange);
|
|
@@ -424,7 +424,7 @@ export class Toolbar {
|
|
|
424
424
|
const rect = arrowBtn.getBoundingClientRect();
|
|
425
425
|
const popupMinW = 184;
|
|
426
426
|
let left = rect.left;
|
|
427
|
-
if (left + popupMinW >
|
|
427
|
+
if (left + popupMinW > globalThis.innerWidth) left = rect.right - popupMinW;
|
|
428
428
|
popup.style.top = `${rect.bottom + 4}px`;
|
|
429
429
|
popup.style.left = `${Math.max(4, left)}px`;
|
|
430
430
|
popup.style.display = 'block';
|
|
@@ -491,13 +491,13 @@ export class Toolbar {
|
|
|
491
491
|
// popup doesn't drift away from the button it belongs to.
|
|
492
492
|
const onScrollResize = () => { if (isOpen) closePopup(); };
|
|
493
493
|
document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
|
|
494
|
-
|
|
494
|
+
globalThis.addEventListener('resize', onScrollResize, { passive: true });
|
|
495
495
|
|
|
496
496
|
this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
|
|
497
497
|
() => document.removeEventListener('scroll', onScrollResize, { capture: true }),
|
|
498
|
-
() =>
|
|
498
|
+
() => globalThis.removeEventListener('resize', onScrollResize),
|
|
499
499
|
// Remove popup from body on editor destroy
|
|
500
|
-
() => { if (popup.parentNode) popup.
|
|
500
|
+
() => { if (popup.parentNode) popup.remove(); },
|
|
501
501
|
);
|
|
502
502
|
|
|
503
503
|
// Register this popup's closer so other color pickers can close it
|
|
@@ -562,7 +562,7 @@ export class Toolbar {
|
|
|
562
562
|
/** @type {Range|null} */
|
|
563
563
|
let _savedRange = null;
|
|
564
564
|
const dMousedown = on(select, 'mousedown', () => {
|
|
565
|
-
const sel =
|
|
565
|
+
const sel = globalThis.getSelection();
|
|
566
566
|
_savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
567
567
|
});
|
|
568
568
|
|
|
@@ -574,7 +574,7 @@ export class Toolbar {
|
|
|
574
574
|
// Restore selection saved on mousedown so the action targets the correct text.
|
|
575
575
|
if (_savedRange) {
|
|
576
576
|
try {
|
|
577
|
-
const sel =
|
|
577
|
+
const sel = globalThis.getSelection();
|
|
578
578
|
if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
|
|
579
579
|
} catch (_) { /* range may be stale if DOM changed */ }
|
|
580
580
|
}
|
|
@@ -684,7 +684,7 @@ export class Toolbar {
|
|
|
684
684
|
|
|
685
685
|
// Sync button active states
|
|
686
686
|
this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
|
|
687
|
-
const def = btnMap.get(btn.
|
|
687
|
+
const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
|
|
688
688
|
if (def && typeof def.isActive === 'function') {
|
|
689
689
|
btn.classList.toggle('active', !!def.isActive(this.context));
|
|
690
690
|
}
|
|
@@ -695,7 +695,7 @@ export class Toolbar {
|
|
|
695
695
|
|
|
696
696
|
// Sync select dropdowns (e.g. font family) with current cursor position
|
|
697
697
|
this.el.querySelectorAll('select[data-btn]').forEach((select) => {
|
|
698
|
-
const def = btnMap.get(select.
|
|
698
|
+
const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
|
|
699
699
|
if (!def || typeof def.getValue !== 'function') return;
|
|
700
700
|
// queryCommandValue returns the font name, possibly quoted — strip quotes
|
|
701
701
|
let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
|
|
@@ -7,47 +7,18 @@
|
|
|
7
7
|
* • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createElement, on
|
|
11
|
-
import {
|
|
10
|
+
import { createElement, on } from '../core/dom.js';
|
|
11
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
/** @param {import('../Context.js').Context} context */
|
|
15
|
-
constructor(context) {
|
|
16
|
-
this.context = context;
|
|
17
|
-
this.options = context.options;
|
|
18
|
-
/** @type {HTMLElement|null} */
|
|
19
|
-
this._dialog = null;
|
|
20
|
-
this._disposers = [];
|
|
21
|
-
this._savedRange = null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Lifecycle
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
initialize() {
|
|
29
|
-
this._dialog = this._buildDialog();
|
|
30
|
-
document.body.appendChild(this._dialog);
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
destroy() {
|
|
35
|
-
this._disposers.forEach((d) => d());
|
|
36
|
-
this._disposers = [];
|
|
37
|
-
if (this._dialog && this._dialog.parentNode) {
|
|
38
|
-
this._dialog.parentNode.removeChild(this._dialog);
|
|
39
|
-
}
|
|
40
|
-
this._dialog = null;
|
|
41
|
-
}
|
|
13
|
+
const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
|
|
42
14
|
|
|
15
|
+
export class VideoDialog extends BaseDialog {
|
|
43
16
|
// ---------------------------------------------------------------------------
|
|
44
17
|
// Public API
|
|
45
18
|
// ---------------------------------------------------------------------------
|
|
46
19
|
|
|
47
20
|
show() {
|
|
48
|
-
|
|
49
|
-
this._savedRange = range;
|
|
50
|
-
});
|
|
21
|
+
this._saveRange();
|
|
51
22
|
this._urlInput.value = '';
|
|
52
23
|
this._widthInput.value = '560';
|
|
53
24
|
this._hintEl.textContent = '';
|
|
@@ -60,21 +31,7 @@ export class VideoDialog {
|
|
|
60
31
|
|
|
61
32
|
_buildDialog() {
|
|
62
33
|
const L = this.context.locale.videoDialog;
|
|
63
|
-
const overlay =
|
|
64
|
-
class: 'an-dialog-overlay',
|
|
65
|
-
role: 'dialog',
|
|
66
|
-
'aria-modal': 'true',
|
|
67
|
-
'aria-label': L.ariaLabel,
|
|
68
|
-
});
|
|
69
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
70
|
-
|
|
71
|
-
const header = createElement('div', { class: 'an-dialog-header' });
|
|
72
|
-
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
73
|
-
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
|
|
74
|
-
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
75
|
-
title.textContent = L.title;
|
|
76
|
-
header.appendChild(iconEl);
|
|
77
|
-
header.appendChild(title);
|
|
34
|
+
const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
|
|
78
35
|
|
|
79
36
|
// URL input
|
|
80
37
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
@@ -86,6 +43,7 @@ export class VideoDialog {
|
|
|
86
43
|
autocomplete: 'off',
|
|
87
44
|
}));
|
|
88
45
|
this._urlInput = urlInput;
|
|
46
|
+
this._firstInput = urlInput;
|
|
89
47
|
|
|
90
48
|
// Hint (detected source)
|
|
91
49
|
const hintEl = createElement('p', { class: 'an-dialog-hint' });
|
|
@@ -104,30 +62,16 @@ export class VideoDialog {
|
|
|
104
62
|
}));
|
|
105
63
|
this._widthInput = widthInput;
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
110
|
-
insertBtn.textContent = L.insertBtn;
|
|
111
|
-
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
112
|
-
cancelBtn.textContent = L.cancelBtn;
|
|
113
|
-
btnRow.appendChild(insertBtn);
|
|
114
|
-
btnRow.appendChild(cancelBtn);
|
|
115
|
-
|
|
116
|
-
box.append(header, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
117
|
-
overlay.appendChild(box);
|
|
118
|
-
makeDraggable(header, box);
|
|
65
|
+
const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
|
|
66
|
+
box.append(urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
119
67
|
|
|
120
68
|
// Live URL hint
|
|
121
69
|
const d0 = on(urlInput, 'input', () => {
|
|
122
70
|
const info = this._parseVideoUrl(urlInput.value.trim());
|
|
123
71
|
hintEl.textContent = info ? this.context.locale.videoDialog.detected(info.type) : (urlInput.value ? this.context.locale.videoDialog.unknownFormat : '');
|
|
124
72
|
});
|
|
125
|
-
|
|
126
|
-
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
127
|
-
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
128
|
-
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
129
73
|
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
130
|
-
this._disposers.push(d0,
|
|
74
|
+
this._disposers.push(d0, d4);
|
|
131
75
|
|
|
132
76
|
return overlay;
|
|
133
77
|
}
|
|
@@ -138,7 +82,7 @@ export class VideoDialog {
|
|
|
138
82
|
|
|
139
83
|
_onInsert() {
|
|
140
84
|
const rawUrl = this._urlInput.value.trim();
|
|
141
|
-
const width = Math.max(80, parseInt(this._widthInput.value, 10) || 560);
|
|
85
|
+
const width = Math.max(80, Number.parseInt(this._widthInput.value, 10) || 560);
|
|
142
86
|
|
|
143
87
|
if (!rawUrl) {
|
|
144
88
|
this._urlInput.focus();
|
|
@@ -157,20 +101,6 @@ export class VideoDialog {
|
|
|
157
101
|
this._close();
|
|
158
102
|
}
|
|
159
103
|
|
|
160
|
-
_open() {
|
|
161
|
-
if (this._dialog) {
|
|
162
|
-
this._dialog.style.display = 'flex';
|
|
163
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
164
|
-
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
_close() {
|
|
169
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
170
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
171
|
-
this._savedRange = null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
104
|
// ---------------------------------------------------------------------------
|
|
175
105
|
// URL parsing & HTML building
|
|
176
106
|
// ---------------------------------------------------------------------------
|
|
@@ -190,19 +120,19 @@ export class VideoDialog {
|
|
|
190
120
|
} catch { return null; }
|
|
191
121
|
|
|
192
122
|
// YouTube watch: https://www.youtube.com/watch?v=ID
|
|
193
|
-
const ytWatch =
|
|
123
|
+
const ytWatch = /(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/.exec(url);
|
|
194
124
|
if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
|
|
195
125
|
|
|
196
126
|
// YouTube short: https://youtu.be/ID
|
|
197
|
-
const ytShort =
|
|
127
|
+
const ytShort = /youtu\.be\/([a-zA-Z0-9_-]{11})/.exec(url);
|
|
198
128
|
if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
|
|
199
129
|
|
|
200
130
|
// YouTube Shorts: https://www.youtube.com/shorts/ID
|
|
201
|
-
const ytShorts =
|
|
131
|
+
const ytShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/.exec(url);
|
|
202
132
|
if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
|
|
203
133
|
|
|
204
134
|
// Vimeo: https://vimeo.com/ID
|
|
205
|
-
const vimeo =
|
|
135
|
+
const vimeo = /vimeo\.com\/(\d+)/.exec(url);
|
|
206
136
|
if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
|
|
207
137
|
|
|
208
138
|
// Direct video file
|
|
@@ -238,7 +168,7 @@ export class VideoDialog {
|
|
|
238
168
|
}
|
|
239
169
|
|
|
240
170
|
if (info && info.type === 'Direct video') {
|
|
241
|
-
const src = info.embedUrl.
|
|
171
|
+
const src = info.embedUrl.replaceAll('"', '%22');
|
|
242
172
|
return (
|
|
243
173
|
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
244
174
|
`<video src="${src}" width="${width}" height="${height}" controls ` +
|
|
@@ -258,7 +188,7 @@ export class VideoDialog {
|
|
|
258
188
|
})();
|
|
259
189
|
if (!safeSrc) return null;
|
|
260
190
|
|
|
261
|
-
const escapedSrc = safeSrc.
|
|
191
|
+
const escapedSrc = safeSrc.replaceAll('"', '%22');
|
|
262
192
|
return (
|
|
263
193
|
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
264
194
|
`<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
|
|
@@ -46,8 +46,8 @@ export class VideoResizer {
|
|
|
46
46
|
if (wrapper) this._select(wrapper);
|
|
47
47
|
}),
|
|
48
48
|
on(document, 'click', (e) => this._onDocClick(e)),
|
|
49
|
-
on(
|
|
50
|
-
on(
|
|
49
|
+
on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
50
|
+
on(globalThis, 'resize', onWindowResize),
|
|
51
51
|
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
52
52
|
// D1: Prevent native browser drag of video wrappers. Without this, a user
|
|
53
53
|
// can hold-and-drag to produce a "copy" that lands outside .an-editable,
|
|
@@ -75,9 +75,7 @@ export class VideoResizer {
|
|
|
75
75
|
this._positionRaf = null;
|
|
76
76
|
}
|
|
77
77
|
this._deselect();
|
|
78
|
-
|
|
79
|
-
this._overlay.parentNode.removeChild(this._overlay);
|
|
80
|
-
}
|
|
78
|
+
this._overlay?.remove();
|
|
81
79
|
this._overlay = null;
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -111,7 +109,7 @@ export class VideoResizer {
|
|
|
111
109
|
_findWrapper(el) {
|
|
112
110
|
if (!el || !(el instanceof Element)) return null;
|
|
113
111
|
// Direct hit on wrapper
|
|
114
|
-
if (el.classList
|
|
112
|
+
if (el.classList?.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
|
|
115
113
|
// Child element (iframe, video, or nested)
|
|
116
114
|
const w = el.closest('.an-video-wrapper');
|
|
117
115
|
if (w) return /** @type {HTMLElement} */ (w);
|
|
@@ -152,7 +150,7 @@ export class VideoResizer {
|
|
|
152
150
|
_onDocClick(e) {
|
|
153
151
|
if (!this._activeWrapper) return;
|
|
154
152
|
if (this._activeWrapper.contains(e.target)) return;
|
|
155
|
-
if (this._overlay
|
|
153
|
+
if (this._overlay?.contains(e.target)) return;
|
|
156
154
|
if (e.target.closest('.an-contextmenu')) return;
|
|
157
155
|
this._deselect();
|
|
158
156
|
}
|
|
@@ -40,7 +40,7 @@ export class VideoTooltip {
|
|
|
40
40
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
41
41
|
// The shield div sits on top of iframes — we detect hover via it or the wrapper
|
|
42
42
|
const target = /** @type {Element} */ (e.target);
|
|
43
|
-
const wrapper = target
|
|
43
|
+
const wrapper = target?.closest('.an-video-wrapper');
|
|
44
44
|
if (wrapper && editable.contains(wrapper)) {
|
|
45
45
|
this._scheduleShow(wrapper);
|
|
46
46
|
}
|
|
@@ -62,8 +62,8 @@ export class VideoTooltip {
|
|
|
62
62
|
}
|
|
63
63
|
}),
|
|
64
64
|
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
65
|
-
on(
|
|
66
|
-
on(
|
|
65
|
+
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
66
|
+
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
return this;
|
|
@@ -74,7 +74,7 @@ export class VideoTooltip {
|
|
|
74
74
|
this._clearTimers();
|
|
75
75
|
this._disposers.forEach((d) => d());
|
|
76
76
|
this._disposers = [];
|
|
77
|
-
|
|
77
|
+
this._el?.remove();
|
|
78
78
|
this._el = null;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -210,8 +210,8 @@ export class VideoTooltip {
|
|
|
210
210
|
let top = rect.bottom + margin;
|
|
211
211
|
let left = rect.left + (rect.width - tipW) / 2;
|
|
212
212
|
|
|
213
|
-
if (top + tipH >
|
|
214
|
-
if (left + tipW >
|
|
213
|
+
if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
|
|
214
|
+
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
215
215
|
if (left < margin) left = margin;
|
|
216
216
|
|
|
217
217
|
this._el.style.top = `${top}px`;
|
|
@@ -268,7 +268,7 @@ export class VideoTooltip {
|
|
|
268
268
|
if (!wrapper) return;
|
|
269
269
|
this._hide();
|
|
270
270
|
this.context.invoke('videoResizer.deselect');
|
|
271
|
-
|
|
271
|
+
wrapper.remove();
|
|
272
272
|
this.context.invoke('editor.afterCommand');
|
|
273
273
|
}
|
|
274
274
|
|
package/src/js/renderer.js
CHANGED
|
@@ -46,7 +46,7 @@ export function renderLayout(targetEl, options) {
|
|
|
46
46
|
editable.innerHTML = sanitiseHTML(initialContent, { allowIframes: true });
|
|
47
47
|
|
|
48
48
|
// Apply default font family so the editable renders in the configured font
|
|
49
|
-
const defaultFont = options.defaultFontFamily ||
|
|
49
|
+
const defaultFont = options.defaultFontFamily || options.fontFamilies?.[0];
|
|
50
50
|
if (defaultFont) {
|
|
51
51
|
editable.style.fontFamily = defaultFont;
|
|
52
52
|
}
|