autumnnote 2.0.0 → 2.2.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 +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
package/src/js/module/Toolbar.js
DELETED
|
@@ -1,750 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toolbar.js - Builds and manages the editor toolbar UI
|
|
3
|
-
* Inspired by Summernote's Toolbar module — rewritten without jQuery
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createElement, on } from '../core/dom.js';
|
|
7
|
-
import { getButton } from './Buttons.js';
|
|
8
|
-
|
|
9
|
-
/** Resolve a toolbar item: string → registry lookup, object → pass-through. */
|
|
10
|
-
const _resolveBtn = (item) => (typeof item === 'string') ? getButton(item) : item;
|
|
11
|
-
|
|
12
|
-
// Module-level cache for FontAwesome detection.
|
|
13
|
-
// Evaluated once per page load so all Toolbar instances on the same page agree
|
|
14
|
-
// on whether the HOST PAGE included FA — regardless of whether IconDialog later
|
|
15
|
-
// auto-injects its own FA <link> for the icon-picker glyph rendering.
|
|
16
|
-
let _faPageLevelReady = null;
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Module-level icon lookup tables — built once, shared across all instances.
|
|
20
|
-
// Previously these were re-created inside _createButton() on every button
|
|
21
|
-
// render, producing O(buttons × map-size) allocations per toolbar init.
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
24
|
-
const _svgWrap = (paths) =>
|
|
25
|
-
`<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>`;
|
|
26
|
-
|
|
27
|
-
const _SVG_MAP = new Map([
|
|
28
|
-
// Format
|
|
29
|
-
['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"/>')],
|
|
30
|
-
['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"/>')],
|
|
31
|
-
['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"/>')],
|
|
32
|
-
['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"/>')],
|
|
33
|
-
['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"/>')],
|
|
34
|
-
['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"/>')],
|
|
35
|
-
// Alignment
|
|
36
|
-
['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"/>')],
|
|
37
|
-
['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"/>')],
|
|
38
|
-
['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"/>')],
|
|
39
|
-
['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"/>')],
|
|
40
|
-
// Lists
|
|
41
|
-
['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"/>')],
|
|
42
|
-
['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"/>')],
|
|
43
|
-
['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"/>')],
|
|
44
|
-
['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"/>')],
|
|
45
|
-
// History
|
|
46
|
-
['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"/>')],
|
|
47
|
-
['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"/>')],
|
|
48
|
-
// Insert
|
|
49
|
-
['minus', _svgWrap('<line x1="5" y1="12" x2="19" y2="12"/>')],
|
|
50
|
-
['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"/>')],
|
|
51
|
-
['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"/>')],
|
|
52
|
-
['video', _svgWrap('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>')],
|
|
53
|
-
['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"/>')],
|
|
54
|
-
['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"/>')],
|
|
55
|
-
['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"/>')],
|
|
56
|
-
// View
|
|
57
|
-
['code', _svgWrap('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')],
|
|
58
|
-
['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"/>')],
|
|
59
|
-
// Color pickers
|
|
60
|
-
['foreColor', _svgWrap('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>')],
|
|
61
|
-
['backColor', _svgWrap('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/>')],
|
|
62
|
-
['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"/>')],
|
|
63
|
-
['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"/>')],
|
|
64
|
-
['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"/>')],
|
|
65
|
-
['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"/>')],
|
|
66
|
-
['search', _svgWrap('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')],
|
|
67
|
-
['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"/>')],
|
|
68
|
-
['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"/>')],
|
|
69
|
-
['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"/>')],
|
|
70
|
-
['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"/>')],
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
const _FA_MAP = new Map([
|
|
74
|
-
['bold', 'fa-bold'],
|
|
75
|
-
['italic', 'fa-italic'],
|
|
76
|
-
['underline', 'fa-underline'],
|
|
77
|
-
['strikethrough', 'fa-strikethrough'],
|
|
78
|
-
['superscript', 'fa-superscript'],
|
|
79
|
-
['subscript', 'fa-subscript'],
|
|
80
|
-
['align-left', 'fa-align-left'],
|
|
81
|
-
['align-center', 'fa-align-center'],
|
|
82
|
-
['align-right', 'fa-align-right'],
|
|
83
|
-
['align-justify', 'fa-align-justify'],
|
|
84
|
-
['list-ul', 'fa-list-ul'],
|
|
85
|
-
['list-ol', 'fa-list-ol'],
|
|
86
|
-
['indent', 'fa-indent'],
|
|
87
|
-
['outdent', 'fa-outdent'],
|
|
88
|
-
['undo', 'fa-rotate-left'],
|
|
89
|
-
['redo', 'fa-rotate-right'],
|
|
90
|
-
['minus', 'fa-minus'],
|
|
91
|
-
['link', 'fa-link'],
|
|
92
|
-
['image', 'fa-image'],
|
|
93
|
-
['code', 'fa-code'],
|
|
94
|
-
['expand', 'fa-expand'],
|
|
95
|
-
['emoji', 'fa-face-smile'],
|
|
96
|
-
['icon', 'fa-icons'],
|
|
97
|
-
['foreColor', 'fa-font'],
|
|
98
|
-
['backColor', 'fa-highlighter'],
|
|
99
|
-
['keyboard', 'fa-keyboard'],
|
|
100
|
-
['remove-format', 'fa-remove-format'],
|
|
101
|
-
['direction', 'fa-arrow-right-arrow-left'],
|
|
102
|
-
['search', 'fa-magnifying-glass'],
|
|
103
|
-
['find-replace', 'fa-magnifying-glass-plus'],
|
|
104
|
-
['inline-code', 'fa-code'],
|
|
105
|
-
['checklist', 'fa-list-check'],
|
|
106
|
-
['print', 'fa-print'],
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
export class Toolbar {
|
|
110
|
-
/**
|
|
111
|
-
* @param {import('../Context.js').Context} context
|
|
112
|
-
*/
|
|
113
|
-
constructor(context) {
|
|
114
|
-
this.context = context;
|
|
115
|
-
this.options = context.options;
|
|
116
|
-
/** @type {HTMLElement|null} */
|
|
117
|
-
this.el = null;
|
|
118
|
-
/** @type {Array<() => void>} disposers */
|
|
119
|
-
this._disposers = [];
|
|
120
|
-
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
121
|
-
this._colorPickerClosers = [];
|
|
122
|
-
/** @type {number|null} rAF handle for debounced refresh */
|
|
123
|
-
this._refreshRaf = null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Lifecycle
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
initialize() {
|
|
131
|
-
this.el = createElement('div', { class: 'an-toolbar' });
|
|
132
|
-
// Detect FontAwesome once at toolbar build time to avoid re-querying the DOM
|
|
133
|
-
// for every button rendered.
|
|
134
|
-
this._faReady = this._detectFontAwesome();
|
|
135
|
-
this._buildButtons();
|
|
136
|
-
this._btnMap = new Map(
|
|
137
|
-
(this.options.toolbar || []).flat()
|
|
138
|
-
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
139
|
-
);
|
|
140
|
-
return this;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
destroy() {
|
|
144
|
-
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
145
|
-
this._refreshRaf = null;
|
|
146
|
-
this._disposers.forEach((d) => d());
|
|
147
|
-
this._disposers = [];
|
|
148
|
-
if (this.el?.parentNode) {
|
|
149
|
-
this.el.remove();
|
|
150
|
-
}
|
|
151
|
-
this.el = null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Build
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
_buildButtons() {
|
|
159
|
-
const toolbar = this.options.toolbar || [];
|
|
160
|
-
// Build into a DocumentFragment so all groups are appended in a single
|
|
161
|
-
// DOM operation, avoiding one reflow per group.
|
|
162
|
-
const fragment = document.createDocumentFragment();
|
|
163
|
-
toolbar.forEach((group) => {
|
|
164
|
-
const groupEl = createElement('div', { class: 'an-btn-group' });
|
|
165
|
-
group.forEach((item) => {
|
|
166
|
-
const btnDef = _resolveBtn(item);
|
|
167
|
-
if (!btnDef) {
|
|
168
|
-
console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
let el;
|
|
172
|
-
if (btnDef.type === 'select') el = this._createSelect(btnDef);
|
|
173
|
-
else if (btnDef.type === 'grid') el = this._createGridPicker(btnDef);
|
|
174
|
-
else if (btnDef.type === 'colorpicker') el = this._createColorPicker(btnDef);
|
|
175
|
-
else el = this._createButton(btnDef);
|
|
176
|
-
groupEl.appendChild(el);
|
|
177
|
-
});
|
|
178
|
-
fragment.appendChild(groupEl);
|
|
179
|
-
});
|
|
180
|
-
this.el.appendChild(fragment);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Creates a table-grid picker button with a hoverable row/col selector popup.
|
|
185
|
-
* @param {import('./Buttons.js').ButtonDef} def
|
|
186
|
-
* @returns {HTMLDivElement}
|
|
187
|
-
*/
|
|
188
|
-
_createGridPicker(def) {
|
|
189
|
-
const ROWS = 10;
|
|
190
|
-
const COLS = 10;
|
|
191
|
-
|
|
192
|
-
const wrap = createElement('div', { class: 'an-table-picker-wrap' });
|
|
193
|
-
|
|
194
|
-
const useBootstrap = !!this.options.useBootstrap;
|
|
195
|
-
const baseClass = useBootstrap
|
|
196
|
-
? (this.options.toolbarButtonClass || 'btn btn-sm btn-light')
|
|
197
|
-
: 'an-btn';
|
|
198
|
-
const btn = createElement('button', {
|
|
199
|
-
type: 'button',
|
|
200
|
-
class: baseClass,
|
|
201
|
-
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
202
|
-
'data-btn': def.name,
|
|
203
|
-
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
204
|
-
'aria-haspopup': 'true',
|
|
205
|
-
'aria-expanded': 'false',
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Set icon — inline SVG (table) with optional FontAwesome fallback
|
|
209
|
-
if (this._faReady) {
|
|
210
|
-
const faPrefix = this.options.fontAwesomeClass || 'fas';
|
|
211
|
-
btn.innerHTML = `<i class="${faPrefix} fa-table" aria-hidden="true"></i>`;
|
|
212
|
-
} else {
|
|
213
|
-
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
214
|
-
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><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"/></svg>`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Popup
|
|
218
|
-
const popup = createElement('div', {
|
|
219
|
-
class: 'an-table-picker-popup',
|
|
220
|
-
role: 'dialog',
|
|
221
|
-
'aria-label': 'Select table size',
|
|
222
|
-
});
|
|
223
|
-
const grid = createElement('div', { class: 'an-table-grid' });
|
|
224
|
-
const label = createElement('div', { class: 'an-table-label' });
|
|
225
|
-
label.textContent = this.context.locale.toolbar.insertTableLabel || 'Insert Table';
|
|
226
|
-
|
|
227
|
-
const cells = [];
|
|
228
|
-
for (let r = 1; r <= ROWS; r++) {
|
|
229
|
-
for (let c = 1; c <= COLS; c++) {
|
|
230
|
-
const cell = createElement('div', {
|
|
231
|
-
class: 'an-table-cell',
|
|
232
|
-
'data-row': String(r),
|
|
233
|
-
'data-col': String(c),
|
|
234
|
-
});
|
|
235
|
-
cells.push(cell);
|
|
236
|
-
grid.appendChild(cell);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
popup.appendChild(grid);
|
|
241
|
-
popup.appendChild(label);
|
|
242
|
-
|
|
243
|
-
let isOpen = false;
|
|
244
|
-
|
|
245
|
-
const setHighlight = (rows, cols) => {
|
|
246
|
-
cells.forEach((cell) => {
|
|
247
|
-
const r = +cell.dataset.row;
|
|
248
|
-
const c = +cell.dataset.col;
|
|
249
|
-
cell.classList.toggle('active', r <= rows && c <= cols);
|
|
250
|
-
});
|
|
251
|
-
label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const openPopup = () => {
|
|
255
|
-
isOpen = true;
|
|
256
|
-
const rect = btn.getBoundingClientRect();
|
|
257
|
-
|
|
258
|
-
// Measure popup dimensions while invisible so we can set the correct
|
|
259
|
-
// position before the browser paints (matches the color-picker pattern).
|
|
260
|
-
popup.style.visibility = 'hidden';
|
|
261
|
-
popup.style.display = 'block';
|
|
262
|
-
const pw = popup.offsetWidth;
|
|
263
|
-
const ph = popup.offsetHeight;
|
|
264
|
-
|
|
265
|
-
let left = rect.left;
|
|
266
|
-
let top = rect.bottom + 4;
|
|
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
|
-
|
|
270
|
-
popup.style.left = `${left}px`;
|
|
271
|
-
popup.style.top = `${top}px`;
|
|
272
|
-
popup.style.visibility = '';
|
|
273
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const closePopup = () => {
|
|
277
|
-
isOpen = false;
|
|
278
|
-
popup.style.display = 'none';
|
|
279
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
280
|
-
setHighlight(0, 0);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const d1 = on(btn, 'click', (e) => {
|
|
284
|
-
e.stopPropagation();
|
|
285
|
-
if (isOpen) closePopup(); else openPopup();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const d2 = on(grid, 'mouseover', (e) => {
|
|
289
|
-
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
290
|
-
if (!cell) return;
|
|
291
|
-
setHighlight(+cell.dataset.row, +cell.dataset.col);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
|
|
295
|
-
|
|
296
|
-
const d4 = on(grid, 'click', (e) => {
|
|
297
|
-
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
298
|
-
if (!cell) return;
|
|
299
|
-
const rows = +cell.dataset.row;
|
|
300
|
-
const cols = +cell.dataset.col;
|
|
301
|
-
closePopup();
|
|
302
|
-
this.context.invoke('editor.focus');
|
|
303
|
-
def.action(this.context, rows, cols);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const d5 = on(document, 'click', () => { if (isOpen) closePopup(); });
|
|
307
|
-
|
|
308
|
-
// Append popup to body so position:fixed is truly viewport-relative,
|
|
309
|
-
// unaffected by any ancestor transform / filter (same pattern as color picker).
|
|
310
|
-
this._disposers.push(d1, d2, d3, d4, d5, () => {
|
|
311
|
-
if (popup.parentNode) popup.remove();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
wrap.appendChild(btn);
|
|
315
|
-
document.body.appendChild(popup);
|
|
316
|
-
return /** @type {HTMLDivElement} */ (wrap);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Creates a split color-picker widget:
|
|
321
|
-
* [icon + strip | ▾] — left applies current color, right opens swatch popup.
|
|
322
|
-
* @param {{ name: string, type: 'colorpicker', tooltip: string, defaultColor: string, action: Function }} def
|
|
323
|
-
* @returns {HTMLDivElement}
|
|
324
|
-
*/
|
|
325
|
-
_createColorPicker(def) {
|
|
326
|
-
const PRESETS = [
|
|
327
|
-
// Grayscale
|
|
328
|
-
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
|
|
329
|
-
// Saturated
|
|
330
|
-
'#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
|
|
331
|
-
// Pastel
|
|
332
|
-
'#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
|
|
333
|
-
];
|
|
334
|
-
|
|
335
|
-
let currentColor = def.defaultColor || '#000000';
|
|
336
|
-
|
|
337
|
-
const wrap = createElement('div', { class: 'an-color-picker-wrap' });
|
|
338
|
-
|
|
339
|
-
const useBootstrap = !!this.options.useBootstrap;
|
|
340
|
-
const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : 'an-btn';
|
|
341
|
-
|
|
342
|
-
// ---- Apply button (icon + color strip) ----
|
|
343
|
-
const applyBtn = createElement('button', {
|
|
344
|
-
type: 'button',
|
|
345
|
-
class: `${baseClass} an-color-btn`,
|
|
346
|
-
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
347
|
-
'data-btn': def.name,
|
|
348
|
-
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
352
|
-
const iconSvg = def.name === 'foreColor'
|
|
353
|
-
? `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/></svg>`
|
|
354
|
-
: `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/></svg>`;
|
|
355
|
-
|
|
356
|
-
applyBtn.innerHTML = iconSvg;
|
|
357
|
-
const strip = createElement('span', { class: 'an-color-strip' });
|
|
358
|
-
strip.style.background = currentColor;
|
|
359
|
-
applyBtn.appendChild(strip);
|
|
360
|
-
|
|
361
|
-
// ---- Arrow button (open popup) ----
|
|
362
|
-
const arrowBtn = createElement('button', {
|
|
363
|
-
type: 'button',
|
|
364
|
-
class: `${baseClass} an-color-arrow`,
|
|
365
|
-
title: def.name === 'foreColor'
|
|
366
|
-
? (this.context.locale.toolbar.chooseTextColor || 'Choose text color')
|
|
367
|
-
: (this.context.locale.toolbar.chooseHighlightColor || 'Choose highlight color'),
|
|
368
|
-
'aria-haspopup': 'true',
|
|
369
|
-
'aria-expanded': 'false',
|
|
370
|
-
});
|
|
371
|
-
arrowBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display:block"><path d="M7 10l5 5 5-5H7z"/></svg>`;
|
|
372
|
-
|
|
373
|
-
// ---- Popup ----
|
|
374
|
-
const popup = createElement('div', { class: 'an-color-popup' });
|
|
375
|
-
popup.style.display = 'none';
|
|
376
|
-
|
|
377
|
-
const swatches = createElement('div', { class: 'an-color-swatches' });
|
|
378
|
-
const userSwatches = Array.isArray(this.options.colorSwatches) ? this.options.colorSwatches : [];
|
|
379
|
-
const allColors = [...new Set([...userSwatches, ...PRESETS])];
|
|
380
|
-
allColors.forEach((color) => {
|
|
381
|
-
const sw = createElement('div', { class: 'an-color-swatch', title: color, 'data-color': color });
|
|
382
|
-
sw.style.background = color;
|
|
383
|
-
swatches.appendChild(sw);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const customRow = createElement('div', { class: 'an-color-custom' });
|
|
387
|
-
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' }));
|
|
388
|
-
const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
|
|
389
|
-
customRow.appendChild(colorInput);
|
|
390
|
-
customRow.appendChild(customLabel);
|
|
391
|
-
|
|
392
|
-
popup.appendChild(swatches);
|
|
393
|
-
popup.appendChild(customRow);
|
|
394
|
-
|
|
395
|
-
// ---- State ----
|
|
396
|
-
let isOpen = false;
|
|
397
|
-
/** @type {Range|null} saved selection range before popup opens */
|
|
398
|
-
let savedRange = null;
|
|
399
|
-
|
|
400
|
-
const saveSelection = () => {
|
|
401
|
-
const sel = globalThis.getSelection();
|
|
402
|
-
savedRange = sel?.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const restoreSelection = () => {
|
|
406
|
-
if (!savedRange) return;
|
|
407
|
-
try {
|
|
408
|
-
const sel = globalThis.getSelection();
|
|
409
|
-
if (!sel) return;
|
|
410
|
-
sel.removeAllRanges();
|
|
411
|
-
sel.addRange(savedRange);
|
|
412
|
-
} catch (_) {
|
|
413
|
-
void _; // range may be stale if DOM changed while popup was open
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const openPopup = () => {
|
|
418
|
-
// Close any other open color picker before opening this one
|
|
419
|
-
this._colorPickerClosers.forEach((fn) => { if (fn !== closePopup) fn(); });
|
|
420
|
-
saveSelection();
|
|
421
|
-
isOpen = true;
|
|
422
|
-
// Use fixed positioning so the popup escapes any overflow-clipping ancestor
|
|
423
|
-
// (notably toolbar scroll mode, where overflow-x:auto coerces overflow-y)
|
|
424
|
-
const rect = arrowBtn.getBoundingClientRect();
|
|
425
|
-
const popupMinW = 184;
|
|
426
|
-
let left = rect.left;
|
|
427
|
-
if (left + popupMinW > globalThis.innerWidth) left = rect.right - popupMinW;
|
|
428
|
-
popup.style.top = `${rect.bottom + 4}px`;
|
|
429
|
-
popup.style.left = `${Math.max(4, left)}px`;
|
|
430
|
-
popup.style.display = 'block';
|
|
431
|
-
arrowBtn.setAttribute('aria-expanded', 'true');
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const closePopup = () => {
|
|
435
|
-
isOpen = false;
|
|
436
|
-
popup.style.display = 'none';
|
|
437
|
-
popup.style.top = '';
|
|
438
|
-
popup.style.left = '';
|
|
439
|
-
arrowBtn.setAttribute('aria-expanded', 'false');
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
const applyColor = (color) => {
|
|
443
|
-
currentColor = color;
|
|
444
|
-
strip.style.background = color;
|
|
445
|
-
colorInput.value = color;
|
|
446
|
-
restoreSelection();
|
|
447
|
-
def.action(this.context, color);
|
|
448
|
-
this.context.invoke('editor.afterCommand');
|
|
449
|
-
closePopup();
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const d1 = on(applyBtn, 'click', (e) => {
|
|
453
|
-
e.preventDefault();
|
|
454
|
-
restoreSelection();
|
|
455
|
-
def.action(this.context, currentColor);
|
|
456
|
-
this.context.invoke('editor.afterCommand');
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const d2 = on(arrowBtn, 'mousedown', (e) => {
|
|
460
|
-
// Prevent editor blur so selection is preserved when the popup opens
|
|
461
|
-
e.preventDefault();
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
const d2b = on(arrowBtn, 'click', (e) => {
|
|
465
|
-
e.stopPropagation();
|
|
466
|
-
if (isOpen) closePopup(); else openPopup();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
const d3 = on(swatches, 'mousedown', (e) => {
|
|
470
|
-
// Prevent blur before the click handler fires
|
|
471
|
-
e.preventDefault();
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
const d3b = on(swatches, 'click', (e) => {
|
|
475
|
-
const sw = /** @type {Element} */ (e.target)?.closest('.an-color-swatch');
|
|
476
|
-
if (sw) applyColor(/** @type {HTMLElement} */ (sw).dataset.color);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const d4 = on(colorInput, 'change', (e) => {
|
|
480
|
-
applyColor(/** @type {HTMLInputElement} */ (e.target).value);
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const d5 = on(document, 'click', (e) => {
|
|
484
|
-
// popup is in document.body, not inside wrap — check both
|
|
485
|
-
if (isOpen && !wrap.contains(/** @type {Node} */ (e.target)) && !popup.contains(/** @type {Node} */ (e.target))) closePopup();
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const d6 = on(popup, 'click', (e) => e.stopPropagation());
|
|
489
|
-
|
|
490
|
-
// Close the popup when the viewport scrolls or resizes so the fixed-position
|
|
491
|
-
// popup doesn't drift away from the button it belongs to.
|
|
492
|
-
const onScrollResize = () => { if (isOpen) closePopup(); };
|
|
493
|
-
document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
|
|
494
|
-
globalThis.addEventListener('resize', onScrollResize, { passive: true });
|
|
495
|
-
|
|
496
|
-
this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
|
|
497
|
-
() => document.removeEventListener('scroll', onScrollResize, { capture: true }),
|
|
498
|
-
() => globalThis.removeEventListener('resize', onScrollResize),
|
|
499
|
-
// Remove popup from body on editor destroy
|
|
500
|
-
() => { if (popup.parentNode) popup.remove(); },
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
// Register this popup's closer so other color pickers can close it
|
|
504
|
-
this._colorPickerClosers.push(closePopup);
|
|
505
|
-
this._disposers.push(() => {
|
|
506
|
-
const idx = this._colorPickerClosers.indexOf(closePopup);
|
|
507
|
-
if (idx !== -1) this._colorPickerClosers.splice(idx, 1);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Append popup to document.body so it escapes all overflow-clipping and
|
|
511
|
-
// contain:layout ancestors (contain:layout makes the container a fixed-pos
|
|
512
|
-
// containing block per the CSS Contain spec, breaking viewport coordinates).
|
|
513
|
-
wrap.appendChild(applyBtn);
|
|
514
|
-
wrap.appendChild(arrowBtn);
|
|
515
|
-
document.body.appendChild(popup);
|
|
516
|
-
return /** @type {HTMLDivElement} */ (wrap);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Creates a <select> dropdown for font-family (or similar) options.
|
|
521
|
-
* @param {import('./Buttons.js').DropdownDef} def
|
|
522
|
-
* @returns {HTMLSelectElement}
|
|
523
|
-
*/
|
|
524
|
-
_createSelect(def) {
|
|
525
|
-
const items = (def.name === 'fontFamily')
|
|
526
|
-
? (this.options.fontFamilies || [])
|
|
527
|
-
: (def.items || []);
|
|
528
|
-
|
|
529
|
-
const cls = def.selectClass ? `an-select ${def.selectClass}` : 'an-select';
|
|
530
|
-
const select = createElement('select', {
|
|
531
|
-
class: cls,
|
|
532
|
-
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
533
|
-
'data-btn': def.name,
|
|
534
|
-
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Blank "placeholder" option (non-selectable header)
|
|
538
|
-
const placeholderText = this.context.locale.toolbar[def.name + 'Placeholder'] || def.placeholder || 'Font';
|
|
539
|
-
const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
|
|
540
|
-
select.appendChild(placeholder);
|
|
541
|
-
|
|
542
|
-
items.forEach((item) => {
|
|
543
|
-
const value = (typeof item === 'object') ? item.value : item;
|
|
544
|
-
let label;
|
|
545
|
-
if (typeof item !== 'object') {
|
|
546
|
-
label = item;
|
|
547
|
-
} else if (def.name === 'paragraphStyle') {
|
|
548
|
-
label = this.context.locale.toolbar.paragraphItems?.[item.value] || item.label;
|
|
549
|
-
} else {
|
|
550
|
-
label = item.label;
|
|
551
|
-
}
|
|
552
|
-
const isHeader = (typeof item === 'object') && !!item.disabled;
|
|
553
|
-
const attrs = { value };
|
|
554
|
-
if (isHeader) attrs.disabled = '';
|
|
555
|
-
const opt = createElement('option', attrs, [label]);
|
|
556
|
-
// Only apply fontFamily face preview on real (non-header) entries
|
|
557
|
-
if (def.name === 'fontFamily' && !isHeader) opt.style.fontFamily = value;
|
|
558
|
-
select.appendChild(opt);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// Save the editor selection when the user starts interacting with the
|
|
562
|
-
// dropdown (mousedown fires before the editor loses focus). When the
|
|
563
|
-
// change handler runs, focus has moved to the <select>; we restore the
|
|
564
|
-
// saved range so execCommand / fontSize() act on the intended text.
|
|
565
|
-
/** @type {Range|null} */
|
|
566
|
-
let _savedRange = null;
|
|
567
|
-
const dMousedown = on(select, 'mousedown', () => {
|
|
568
|
-
const sel = globalThis.getSelection();
|
|
569
|
-
_savedRange = sel?.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const disposer = on(select, 'change', (e) => {
|
|
573
|
-
const value = /** @type {HTMLSelectElement} */ (e.target).value;
|
|
574
|
-
const selectedOpt = /** @type {HTMLSelectElement} */ (e.target).options[/** @type {HTMLSelectElement} */ (e.target).selectedIndex];
|
|
575
|
-
if (!value || selectedOpt.disabled) return;
|
|
576
|
-
this.context.invoke('editor.focus');
|
|
577
|
-
// Restore selection saved on mousedown so the action targets the correct text.
|
|
578
|
-
if (_savedRange) {
|
|
579
|
-
try {
|
|
580
|
-
const sel = globalThis.getSelection();
|
|
581
|
-
if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
|
|
582
|
-
} catch (_) { void _; /* range may be stale if DOM changed */ }
|
|
583
|
-
}
|
|
584
|
-
def.action(this.context, value);
|
|
585
|
-
this.context.invoke('editor.afterCommand');
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
this._disposers.push(dMousedown, disposer);
|
|
589
|
-
return /** @type {HTMLSelectElement} */ (select);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* @param {import('./Buttons.js').ButtonDef} btnDef
|
|
594
|
-
* @returns {HTMLButtonElement}
|
|
595
|
-
*/
|
|
596
|
-
_createButton(btnDef) {
|
|
597
|
-
// Determine classes based on whether the consumer wants Bootstrap styling
|
|
598
|
-
const useBootstrap = !!this.options.useBootstrap;
|
|
599
|
-
const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : `an-btn`;
|
|
600
|
-
const extra = btnDef.className ? ` ${btnDef.className}` : '';
|
|
601
|
-
const classAttr = `${baseClass}${extra}`;
|
|
602
|
-
|
|
603
|
-
const btn = createElement('button', {
|
|
604
|
-
type: 'button',
|
|
605
|
-
class: classAttr,
|
|
606
|
-
title: this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || '',
|
|
607
|
-
'data-btn': btnDef.name,
|
|
608
|
-
'aria-label': this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || btnDef.name,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
|
|
612
|
-
const faPrefix = this.options.fontAwesomeClass || 'fas';
|
|
613
|
-
const useFaNow = this._faReady;
|
|
614
|
-
if (useFaNow) {
|
|
615
|
-
const faName = _FA_MAP.get(btnDef.icon) || _FA_MAP.get(btnDef.name) || null;
|
|
616
|
-
if (faName) {
|
|
617
|
-
btn.innerHTML = `<i class="${faPrefix} ${faName}" aria-hidden="true"></i>`;
|
|
618
|
-
} else if (_SVG_MAP.has(btnDef.icon)) {
|
|
619
|
-
btn.innerHTML = _SVG_MAP.get(btnDef.icon);
|
|
620
|
-
} else {
|
|
621
|
-
btn.textContent = btnDef.icon || btnDef.name;
|
|
622
|
-
}
|
|
623
|
-
} else if (_SVG_MAP.has(btnDef.icon)) {
|
|
624
|
-
// FontAwesome absent: use SVG fallback when available
|
|
625
|
-
btn.innerHTML = _SVG_MAP.get(btnDef.icon);
|
|
626
|
-
} else if (_SVG_MAP.has(btnDef.name)) {
|
|
627
|
-
btn.innerHTML = _SVG_MAP.get(btnDef.name);
|
|
628
|
-
} else {
|
|
629
|
-
btn.textContent = btnDef.icon || btnDef.name;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const disposer = on(btn, 'click', (event) => {
|
|
633
|
-
event.preventDefault();
|
|
634
|
-
// Restore focus to the editor before executing the action
|
|
635
|
-
this.context.invoke('editor.focus');
|
|
636
|
-
btnDef.action(this.context);
|
|
637
|
-
this.context.invoke('editor.afterCommand');
|
|
638
|
-
this.refresh();
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
this._disposers.push(disposer);
|
|
642
|
-
return /** @type {HTMLButtonElement} */ (btn);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// ---------------------------------------------------------------------------
|
|
646
|
-
// FontAwesome detection (run once at initialize time)
|
|
647
|
-
// ---------------------------------------------------------------------------
|
|
648
|
-
|
|
649
|
-
_detectFontAwesome() {
|
|
650
|
-
if (!this.options.useFontAwesome) return false;
|
|
651
|
-
// Return cached result when available. This ensures that a later-initialised
|
|
652
|
-
// toolbar sees the same detection state as the first one — even if IconDialog
|
|
653
|
-
// has since injected its own FA <link> into <head> for the icon-picker UI.
|
|
654
|
-
if (_faPageLevelReady !== null) return _faPageLevelReady;
|
|
655
|
-
if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) {
|
|
656
|
-
_faPageLevelReady = true;
|
|
657
|
-
return true;
|
|
658
|
-
}
|
|
659
|
-
// Exclude the editor-self-injected link (id='an-fontawesome-css') so it doesn't
|
|
660
|
-
// count as "the host page loaded FA" for toolbar icon rendering purposes.
|
|
661
|
-
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
662
|
-
.filter((l) => l.id !== 'an-fontawesome-css')
|
|
663
|
-
.map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
|
|
664
|
-
_faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
|
|
665
|
-
return _faPageLevelReady;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// ---------------------------------------------------------------------------
|
|
669
|
-
// State refresh (active / disabled states)
|
|
670
|
-
// ---------------------------------------------------------------------------
|
|
671
|
-
|
|
672
|
-
refresh() {
|
|
673
|
-
// Debounce via rAF — multiple rapid calls (e.g. afterCommand + button click)
|
|
674
|
-
// collapse into a single update per animation frame.
|
|
675
|
-
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
676
|
-
this._refreshRaf = requestAnimationFrame(() => {
|
|
677
|
-
this._refreshRaf = null;
|
|
678
|
-
this._doRefresh();
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
_doRefresh() {
|
|
683
|
-
if (!this.el) return;
|
|
684
|
-
const btnMap = this._btnMap || new Map();
|
|
685
|
-
|
|
686
|
-
// Sync button active states
|
|
687
|
-
this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
|
|
688
|
-
const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
|
|
689
|
-
if (def && typeof def.isActive === 'function') {
|
|
690
|
-
btn.classList.toggle('active', !!def.isActive(this.context));
|
|
691
|
-
}
|
|
692
|
-
if (def && typeof def.isDisabled === 'function') {
|
|
693
|
-
/** @type {HTMLButtonElement} */ (btn).disabled = !!def.isDisabled(this.context);
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
// Sync select dropdowns (e.g. font family) with current cursor position
|
|
698
|
-
this.el.querySelectorAll('select[data-btn]').forEach((select) => {
|
|
699
|
-
const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
|
|
700
|
-
if (!def || typeof def.getValue !== 'function') return;
|
|
701
|
-
// queryCommandValue returns the font name, possibly quoted — strip quotes
|
|
702
|
-
let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
|
|
703
|
-
// Fallback: when no selection/font set, use the configured default font
|
|
704
|
-
if (!raw) {
|
|
705
|
-
raw = this.options.defaultFontFamily
|
|
706
|
-
|| this.options.fontFamilies?.[0]
|
|
707
|
-
|| '';
|
|
708
|
-
}
|
|
709
|
-
// Try to match against available options (case-insensitive)
|
|
710
|
-
const sel = /** @type {HTMLSelectElement} */ (select);
|
|
711
|
-
const matched = Array.from(sel.options).find(
|
|
712
|
-
(opt) => opt.value?.toLowerCase() === raw.toLowerCase()
|
|
713
|
-
);
|
|
714
|
-
sel.value = matched ? matched.value : '';
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Shows the toolbar.
|
|
720
|
-
*/
|
|
721
|
-
show() {
|
|
722
|
-
if (this.el) this.el.style.display = '';
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Hides the toolbar.
|
|
727
|
-
*/
|
|
728
|
-
hide() {
|
|
729
|
-
if (this.el) this.el.style.display = 'none';
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Tears down and re-renders the toolbar in-place.
|
|
734
|
-
* Call after registering new buttons post-create via context.use(plugin)
|
|
735
|
-
* or AutumnNote.registerButton() to make them appear in the toolbar.
|
|
736
|
-
*/
|
|
737
|
-
rebuild() {
|
|
738
|
-
if (this._refreshRaf) { cancelAnimationFrame(this._refreshRaf); this._refreshRaf = null; }
|
|
739
|
-
this._disposers.forEach((d) => d());
|
|
740
|
-
this._disposers = [];
|
|
741
|
-
if (this.el) this.el.innerHTML = '';
|
|
742
|
-
this._faReady = this._detectFontAwesome();
|
|
743
|
-
this._buildButtons();
|
|
744
|
-
this._btnMap = new Map(
|
|
745
|
-
(this.options.toolbar || []).flat()
|
|
746
|
-
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
747
|
-
);
|
|
748
|
-
this.refresh();
|
|
749
|
-
}
|
|
750
|
-
}
|