autumnnote 1.0.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/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,562 @@
|
|
|
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
|
+
|
|
8
|
+
export class Toolbar {
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('../Context.js').Context} context
|
|
11
|
+
*/
|
|
12
|
+
constructor(context) {
|
|
13
|
+
this.context = context;
|
|
14
|
+
this.options = context.options;
|
|
15
|
+
/** @type {HTMLElement|null} */
|
|
16
|
+
this.el = null;
|
|
17
|
+
/** @type {Array<() => void>} disposers */
|
|
18
|
+
this._disposers = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Lifecycle
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
initialize() {
|
|
26
|
+
this.el = createElement('div', { class: 'an-toolbar' });
|
|
27
|
+
// Detect FontAwesome once at toolbar build time to avoid re-querying the DOM
|
|
28
|
+
// for every button rendered.
|
|
29
|
+
this._faReady = this._detectFontAwesome();
|
|
30
|
+
this._buildButtons();
|
|
31
|
+
this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
destroy() {
|
|
36
|
+
this._disposers.forEach((d) => d());
|
|
37
|
+
this._disposers = [];
|
|
38
|
+
if (this.el && this.el.parentNode) {
|
|
39
|
+
this.el.parentNode.removeChild(this.el);
|
|
40
|
+
}
|
|
41
|
+
this.el = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Build
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
_buildButtons() {
|
|
49
|
+
const toolbar = this.options.toolbar || [];
|
|
50
|
+
toolbar.forEach((group) => {
|
|
51
|
+
const groupEl = createElement('div', { class: 'an-btn-group' });
|
|
52
|
+
group.forEach((btnDef) => {
|
|
53
|
+
let el;
|
|
54
|
+
if (btnDef.type === 'select') el = this._createSelect(btnDef);
|
|
55
|
+
else if (btnDef.type === 'grid') el = this._createGridPicker(btnDef);
|
|
56
|
+
else if (btnDef.type === 'colorpicker') el = this._createColorPicker(btnDef);
|
|
57
|
+
else el = this._createButton(btnDef);
|
|
58
|
+
groupEl.appendChild(el);
|
|
59
|
+
});
|
|
60
|
+
this.el.appendChild(groupEl);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a table-grid picker button with a hoverable row/col selector popup.
|
|
66
|
+
* @param {import('./Buttons.js').ButtonDef} def
|
|
67
|
+
* @returns {HTMLDivElement}
|
|
68
|
+
*/
|
|
69
|
+
_createGridPicker(def) {
|
|
70
|
+
const ROWS = 10;
|
|
71
|
+
const COLS = 10;
|
|
72
|
+
|
|
73
|
+
const wrap = createElement('div', { class: 'an-table-picker-wrap' });
|
|
74
|
+
|
|
75
|
+
const useBootstrap = !!this.options.useBootstrap;
|
|
76
|
+
const baseClass = useBootstrap
|
|
77
|
+
? (this.options.toolbarButtonClass || 'btn btn-sm btn-light')
|
|
78
|
+
: 'an-btn';
|
|
79
|
+
const btn = createElement('button', {
|
|
80
|
+
type: 'button',
|
|
81
|
+
class: baseClass,
|
|
82
|
+
title: def.tooltip || '',
|
|
83
|
+
'data-btn': def.name,
|
|
84
|
+
'aria-label': def.tooltip || def.name,
|
|
85
|
+
'aria-haspopup': 'true',
|
|
86
|
+
'aria-expanded': 'false',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Set icon — inline SVG (table) with optional FontAwesome fallback
|
|
90
|
+
if (this._faReady) {
|
|
91
|
+
const faPrefix = this.options.fontAwesomeClass || 'fas';
|
|
92
|
+
btn.innerHTML = `<i class="${faPrefix} fa-table" aria-hidden="true"></i>`;
|
|
93
|
+
} else {
|
|
94
|
+
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
95
|
+
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>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Popup
|
|
99
|
+
const popup = createElement('div', {
|
|
100
|
+
class: 'an-table-picker-popup',
|
|
101
|
+
role: 'dialog',
|
|
102
|
+
'aria-label': 'Select table size',
|
|
103
|
+
});
|
|
104
|
+
const grid = createElement('div', { class: 'an-table-grid' });
|
|
105
|
+
const label = createElement('div', { class: 'an-table-label' });
|
|
106
|
+
label.textContent = 'Insert Table';
|
|
107
|
+
|
|
108
|
+
const cells = [];
|
|
109
|
+
for (let r = 1; r <= ROWS; r++) {
|
|
110
|
+
for (let c = 1; c <= COLS; c++) {
|
|
111
|
+
const cell = createElement('div', {
|
|
112
|
+
class: 'an-table-cell',
|
|
113
|
+
'data-row': String(r),
|
|
114
|
+
'data-col': String(c),
|
|
115
|
+
});
|
|
116
|
+
cells.push(cell);
|
|
117
|
+
grid.appendChild(cell);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
popup.appendChild(grid);
|
|
122
|
+
popup.appendChild(label);
|
|
123
|
+
|
|
124
|
+
let isOpen = false;
|
|
125
|
+
|
|
126
|
+
const setHighlight = (rows, cols) => {
|
|
127
|
+
cells.forEach((cell) => {
|
|
128
|
+
const r = +cell.getAttribute('data-row');
|
|
129
|
+
const c = +cell.getAttribute('data-col');
|
|
130
|
+
cell.classList.toggle('active', r <= rows && c <= cols);
|
|
131
|
+
});
|
|
132
|
+
label.textContent = (rows && cols) ? `${rows} × ${cols}` : 'Insert Table';
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const openPopup = () => {
|
|
136
|
+
isOpen = true;
|
|
137
|
+
popup.style.display = 'block';
|
|
138
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const closePopup = () => {
|
|
142
|
+
isOpen = false;
|
|
143
|
+
popup.style.display = 'none';
|
|
144
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
145
|
+
setHighlight(0, 0);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const d1 = on(btn, 'click', (e) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
if (isOpen) closePopup(); else openPopup();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const d2 = on(grid, 'mouseover', (e) => {
|
|
154
|
+
const cell = e.target.closest('.an-table-cell');
|
|
155
|
+
if (!cell) return;
|
|
156
|
+
setHighlight(+cell.getAttribute('data-row'), +cell.getAttribute('data-col'));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
|
|
160
|
+
|
|
161
|
+
const d4 = on(grid, 'click', (e) => {
|
|
162
|
+
const cell = e.target.closest('.an-table-cell');
|
|
163
|
+
if (!cell) return;
|
|
164
|
+
const rows = +cell.getAttribute('data-row');
|
|
165
|
+
const cols = +cell.getAttribute('data-col');
|
|
166
|
+
closePopup();
|
|
167
|
+
this.context.invoke('editor.focus');
|
|
168
|
+
def.action(this.context, rows, cols);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const d5 = on(document, 'click', () => { if (isOpen) closePopup(); });
|
|
172
|
+
|
|
173
|
+
this._disposers.push(d1, d2, d3, d4, d5);
|
|
174
|
+
|
|
175
|
+
wrap.appendChild(btn);
|
|
176
|
+
wrap.appendChild(popup);
|
|
177
|
+
return wrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates a split color-picker widget:
|
|
182
|
+
* [icon + strip | ▾] — left applies current color, right opens swatch popup.
|
|
183
|
+
* @param {{ name: string, type: 'colorpicker', tooltip: string, defaultColor: string, action: Function }} def
|
|
184
|
+
* @returns {HTMLDivElement}
|
|
185
|
+
*/
|
|
186
|
+
_createColorPicker(def) {
|
|
187
|
+
const PRESETS = [
|
|
188
|
+
// Grayscale
|
|
189
|
+
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
|
|
190
|
+
// Saturated
|
|
191
|
+
'#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
|
|
192
|
+
// Pastel
|
|
193
|
+
'#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
let currentColor = def.defaultColor || '#000000';
|
|
197
|
+
|
|
198
|
+
const wrap = createElement('div', { class: 'an-color-picker-wrap' });
|
|
199
|
+
|
|
200
|
+
const useBootstrap = !!this.options.useBootstrap;
|
|
201
|
+
const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : 'an-btn';
|
|
202
|
+
|
|
203
|
+
// ---- Apply button (icon + color strip) ----
|
|
204
|
+
const applyBtn = createElement('button', {
|
|
205
|
+
type: 'button',
|
|
206
|
+
class: `${baseClass} an-color-btn`,
|
|
207
|
+
title: def.tooltip || '',
|
|
208
|
+
'data-btn': def.name,
|
|
209
|
+
'aria-label': def.tooltip || def.name,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
213
|
+
const iconSvg = def.name === 'foreColor'
|
|
214
|
+
? `<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>`
|
|
215
|
+
: `<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>`;
|
|
216
|
+
|
|
217
|
+
applyBtn.innerHTML = iconSvg;
|
|
218
|
+
const strip = createElement('span', { class: 'an-color-strip' });
|
|
219
|
+
strip.style.background = currentColor;
|
|
220
|
+
applyBtn.appendChild(strip);
|
|
221
|
+
|
|
222
|
+
// ---- Arrow button (open popup) ----
|
|
223
|
+
const arrowBtn = createElement('button', {
|
|
224
|
+
type: 'button',
|
|
225
|
+
class: `${baseClass} an-color-arrow`,
|
|
226
|
+
title: `Choose ${def.name === 'foreColor' ? 'text' : 'highlight'} color`,
|
|
227
|
+
'aria-haspopup': 'true',
|
|
228
|
+
'aria-expanded': 'false',
|
|
229
|
+
});
|
|
230
|
+
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>`;
|
|
231
|
+
|
|
232
|
+
// ---- Popup ----
|
|
233
|
+
const popup = createElement('div', { class: 'an-color-popup' });
|
|
234
|
+
popup.style.display = 'none';
|
|
235
|
+
|
|
236
|
+
const swatches = createElement('div', { class: 'an-color-swatches' });
|
|
237
|
+
const userSwatches = Array.isArray(this.options.colorSwatches) ? this.options.colorSwatches : [];
|
|
238
|
+
const allColors = [...new Set([...userSwatches, ...PRESETS])];
|
|
239
|
+
allColors.forEach((color) => {
|
|
240
|
+
const sw = createElement('div', { class: 'an-color-swatch', title: color, 'data-color': color });
|
|
241
|
+
sw.style.background = color;
|
|
242
|
+
swatches.appendChild(sw);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const customRow = createElement('div', { class: 'an-color-custom' });
|
|
246
|
+
const colorInput = createElement('input', { type: 'color', value: currentColor, title: 'Custom color' });
|
|
247
|
+
const customLabel = createElement('span', {}, ['Custom color']);
|
|
248
|
+
customRow.appendChild(colorInput);
|
|
249
|
+
customRow.appendChild(customLabel);
|
|
250
|
+
|
|
251
|
+
popup.appendChild(swatches);
|
|
252
|
+
popup.appendChild(customRow);
|
|
253
|
+
|
|
254
|
+
// ---- State ----
|
|
255
|
+
let isOpen = false;
|
|
256
|
+
|
|
257
|
+
const openPopup = () => {
|
|
258
|
+
isOpen = true;
|
|
259
|
+
popup.style.display = 'block';
|
|
260
|
+
arrowBtn.setAttribute('aria-expanded', 'true');
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const closePopup = () => {
|
|
264
|
+
isOpen = false;
|
|
265
|
+
popup.style.display = 'none';
|
|
266
|
+
arrowBtn.setAttribute('aria-expanded', 'false');
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const applyColor = (color) => {
|
|
270
|
+
currentColor = color;
|
|
271
|
+
strip.style.background = color;
|
|
272
|
+
colorInput.value = color;
|
|
273
|
+
this.context.invoke('editor.focus');
|
|
274
|
+
def.action(this.context, color);
|
|
275
|
+
this.context.invoke('editor.afterCommand');
|
|
276
|
+
closePopup();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const d1 = on(applyBtn, 'click', (e) => {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
this.context.invoke('editor.focus');
|
|
282
|
+
def.action(this.context, currentColor);
|
|
283
|
+
this.context.invoke('editor.afterCommand');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const d2 = on(arrowBtn, 'click', (e) => {
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
if (isOpen) closePopup(); else openPopup();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const d3 = on(swatches, 'click', (e) => {
|
|
292
|
+
const sw = e.target.closest('.an-color-swatch');
|
|
293
|
+
if (sw) applyColor(sw.dataset.color);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const d4 = on(colorInput, 'change', (e) => {
|
|
297
|
+
applyColor(e.target.value);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const d5 = on(document, 'click', (e) => {
|
|
301
|
+
if (isOpen && !wrap.contains(e.target)) closePopup();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const d6 = on(popup, 'click', (e) => e.stopPropagation());
|
|
305
|
+
|
|
306
|
+
this._disposers.push(d1, d2, d3, d4, d5, d6);
|
|
307
|
+
|
|
308
|
+
wrap.appendChild(applyBtn);
|
|
309
|
+
wrap.appendChild(arrowBtn);
|
|
310
|
+
wrap.appendChild(popup);
|
|
311
|
+
return wrap;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Creates a <select> dropdown for font-family (or similar) options.
|
|
316
|
+
* @param {import('./Buttons.js').DropdownDef} def
|
|
317
|
+
* @returns {HTMLSelectElement}
|
|
318
|
+
*/
|
|
319
|
+
_createSelect(def) {
|
|
320
|
+
const items = (def.name === 'fontFamily')
|
|
321
|
+
? (this.options.fontFamilies || [])
|
|
322
|
+
: (def.items || []);
|
|
323
|
+
|
|
324
|
+
const cls = def.selectClass ? `an-select ${def.selectClass}` : 'an-select';
|
|
325
|
+
const select = createElement('select', {
|
|
326
|
+
class: cls,
|
|
327
|
+
title: def.tooltip || '',
|
|
328
|
+
'data-btn': def.name,
|
|
329
|
+
'aria-label': def.tooltip || def.name,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Blank "placeholder" option
|
|
333
|
+
const placeholderText = def.placeholder || 'Font';
|
|
334
|
+
const placeholder = createElement('option', { value: '' }, [placeholderText]);
|
|
335
|
+
select.appendChild(placeholder);
|
|
336
|
+
|
|
337
|
+
items.forEach((item) => {
|
|
338
|
+
const value = (typeof item === 'object') ? item.value : item;
|
|
339
|
+
const label = (typeof item === 'object') ? item.label : item;
|
|
340
|
+
const opt = createElement('option', { value }, [label]);
|
|
341
|
+
if (def.name === 'fontFamily') opt.style.fontFamily = value;
|
|
342
|
+
select.appendChild(opt);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const disposer = on(select, 'change', (e) => {
|
|
346
|
+
const value = e.target.value;
|
|
347
|
+
if (!value) return;
|
|
348
|
+
this.context.invoke('editor.focus');
|
|
349
|
+
def.action(this.context, value);
|
|
350
|
+
this.context.invoke('editor.afterCommand');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
this._disposers.push(disposer);
|
|
354
|
+
return select;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @param {import('./Buttons.js').ButtonDef} btnDef
|
|
359
|
+
* @returns {HTMLButtonElement}
|
|
360
|
+
*/
|
|
361
|
+
_createButton(btnDef) {
|
|
362
|
+
// Determine classes based on whether the consumer wants Bootstrap styling
|
|
363
|
+
const useBootstrap = !!this.options.useBootstrap;
|
|
364
|
+
const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : `an-btn`;
|
|
365
|
+
const extra = btnDef.className ? ` ${btnDef.className}` : '';
|
|
366
|
+
const classAttr = `${baseClass}${extra}`;
|
|
367
|
+
|
|
368
|
+
const btn = createElement('button', {
|
|
369
|
+
type: 'button',
|
|
370
|
+
class: classAttr,
|
|
371
|
+
title: btnDef.tooltip || '',
|
|
372
|
+
'data-btn': btnDef.name,
|
|
373
|
+
'aria-label': btnDef.tooltip || btnDef.name,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
|
|
377
|
+
|
|
378
|
+
// Heroicons-style stroke SVGs — consistent with context menu icons
|
|
379
|
+
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
380
|
+
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>`;
|
|
381
|
+
|
|
382
|
+
const svgMap = new Map([
|
|
383
|
+
// Format
|
|
384
|
+
['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"/>')],
|
|
385
|
+
['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"/>')],
|
|
386
|
+
['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"/>')],
|
|
387
|
+
['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"/>')],
|
|
388
|
+
['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"/>')],
|
|
389
|
+
['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"/>')],
|
|
390
|
+
// Alignment
|
|
391
|
+
['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"/>')],
|
|
392
|
+
['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"/>')],
|
|
393
|
+
['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"/>')],
|
|
394
|
+
['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"/>')],
|
|
395
|
+
// Lists
|
|
396
|
+
['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"/>')],
|
|
397
|
+
['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"/>')],
|
|
398
|
+
['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"/>')],
|
|
399
|
+
['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"/>')],
|
|
400
|
+
// History
|
|
401
|
+
['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"/>')],
|
|
402
|
+
['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"/>')],
|
|
403
|
+
// Insert
|
|
404
|
+
['minus', svgWrap('<line x1="5" y1="12" x2="19" y2="12"/>')],
|
|
405
|
+
['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"/>')],
|
|
406
|
+
['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"/>')],
|
|
407
|
+
['video', svgWrap('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>')],
|
|
408
|
+
['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"/>')],
|
|
409
|
+
['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"/>')],
|
|
410
|
+
['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"/>')],
|
|
411
|
+
// View
|
|
412
|
+
['code', svgWrap('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')],
|
|
413
|
+
['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"/>')],
|
|
414
|
+
// Color pickers
|
|
415
|
+
['foreColor', svgWrap('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>')],
|
|
416
|
+
['backColor', svgWrap('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/>')],
|
|
417
|
+
['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"/>')],
|
|
418
|
+
['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"/>')],
|
|
419
|
+
['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"/>')],
|
|
420
|
+
['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"/>')],
|
|
421
|
+
['search', svgWrap('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')],
|
|
422
|
+
['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"/>')],
|
|
423
|
+
['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"/>')],
|
|
424
|
+
['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"/>')],
|
|
425
|
+
['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"/>')],
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
const faPrefix = this.options.fontAwesomeClass || 'fas';
|
|
429
|
+
const faMap = new Map([
|
|
430
|
+
['bold', 'fa-bold'],
|
|
431
|
+
['italic', 'fa-italic'],
|
|
432
|
+
['underline', 'fa-underline'],
|
|
433
|
+
['strikethrough', 'fa-strikethrough'],
|
|
434
|
+
['superscript', 'fa-superscript'],
|
|
435
|
+
['subscript', 'fa-subscript'],
|
|
436
|
+
['align-left', 'fa-align-left'],
|
|
437
|
+
['align-center', 'fa-align-center'],
|
|
438
|
+
['align-right', 'fa-align-right'],
|
|
439
|
+
['align-justify', 'fa-align-justify'],
|
|
440
|
+
['list-ul', 'fa-list-ul'],
|
|
441
|
+
['list-ol', 'fa-list-ol'],
|
|
442
|
+
['indent', 'fa-indent'],
|
|
443
|
+
['outdent', 'fa-outdent'],
|
|
444
|
+
['undo', 'fa-rotate-left'],
|
|
445
|
+
['redo', 'fa-rotate-right'],
|
|
446
|
+
['minus', 'fa-minus'],
|
|
447
|
+
['link', 'fa-link'],
|
|
448
|
+
['image', 'fa-image'],
|
|
449
|
+
['code', 'fa-code'],
|
|
450
|
+
['expand', 'fa-expand'],
|
|
451
|
+
['emoji', 'fa-face-smile'],
|
|
452
|
+
['icon', 'fa-icons'],
|
|
453
|
+
['foreColor', 'fa-font'],
|
|
454
|
+
['backColor', 'fa-highlighter'],
|
|
455
|
+
['keyboard', 'fa-keyboard'],
|
|
456
|
+
['remove-format', 'fa-remove-format'],
|
|
457
|
+
['direction', 'fa-arrow-right-arrow-left'],
|
|
458
|
+
['search', 'fa-magnifying-glass'],
|
|
459
|
+
['find-replace', 'fa-magnifying-glass-plus'],
|
|
460
|
+
['inline-code', 'fa-code'],
|
|
461
|
+
['checklist', 'fa-list-check'],
|
|
462
|
+
['print', 'fa-print'],
|
|
463
|
+
]);
|
|
464
|
+
|
|
465
|
+
const useFaNow = this._faReady;
|
|
466
|
+
if (useFaNow) {
|
|
467
|
+
const faName = faMap.get(btnDef.icon) || faMap.get(btnDef.name) || null;
|
|
468
|
+
if (faName) {
|
|
469
|
+
btn.innerHTML = `<i class="${faPrefix} ${faName}" aria-hidden="true"></i>`;
|
|
470
|
+
} else if (svgMap.has(btnDef.icon)) {
|
|
471
|
+
btn.innerHTML = svgMap.get(btnDef.icon);
|
|
472
|
+
} else {
|
|
473
|
+
btn.textContent = btnDef.icon || btnDef.name;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
// FontAwesome absent: use SVG fallback when available
|
|
477
|
+
if (svgMap.has(btnDef.icon)) {
|
|
478
|
+
btn.innerHTML = svgMap.get(btnDef.icon);
|
|
479
|
+
} else if (svgMap.has(btnDef.name)) {
|
|
480
|
+
btn.innerHTML = svgMap.get(btnDef.name);
|
|
481
|
+
} else {
|
|
482
|
+
btn.textContent = btnDef.icon || btnDef.name;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const disposer = on(btn, 'click', (event) => {
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
// Restore focus to the editor before executing the action
|
|
489
|
+
this.context.invoke('editor.focus');
|
|
490
|
+
btnDef.action(this.context);
|
|
491
|
+
this.context.invoke('editor.afterCommand');
|
|
492
|
+
this.refresh();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
this._disposers.push(disposer);
|
|
496
|
+
return btn;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// FontAwesome detection (run once at initialize time)
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
_detectFontAwesome() {
|
|
504
|
+
if (!this.options.useFontAwesome) return false;
|
|
505
|
+
if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) return true;
|
|
506
|
+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map((l) => l.href || '').join(' ');
|
|
507
|
+
return /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// State refresh (active / disabled states)
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
refresh() {
|
|
515
|
+
if (!this.el) return;
|
|
516
|
+
const btnMap = this._btnMap || new Map();
|
|
517
|
+
|
|
518
|
+
// Sync button active states
|
|
519
|
+
this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
|
|
520
|
+
const def = btnMap.get(btn.getAttribute('data-btn'));
|
|
521
|
+
if (def && typeof def.isActive === 'function') {
|
|
522
|
+
btn.classList.toggle('active', !!def.isActive(this.context));
|
|
523
|
+
}
|
|
524
|
+
if (def && typeof def.isDisabled === 'function') {
|
|
525
|
+
btn.disabled = !!def.isDisabled(this.context);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Sync select dropdowns (e.g. font family) with current cursor position
|
|
530
|
+
this.el.querySelectorAll('select[data-btn]').forEach((select) => {
|
|
531
|
+
const def = btnMap.get(select.getAttribute('data-btn'));
|
|
532
|
+
if (!def || typeof def.getValue !== 'function') return;
|
|
533
|
+
// queryCommandValue returns the font name, possibly quoted — strip quotes
|
|
534
|
+
let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
|
|
535
|
+
// Fallback: when no selection/font set, use the configured default font
|
|
536
|
+
if (!raw) {
|
|
537
|
+
raw = this.options.defaultFontFamily
|
|
538
|
+
|| (this.options.fontFamilies && this.options.fontFamilies[0])
|
|
539
|
+
|| '';
|
|
540
|
+
}
|
|
541
|
+
// Try to match against available options (case-insensitive)
|
|
542
|
+
const matched = Array.from(select.options).find(
|
|
543
|
+
(opt) => opt.value && opt.value.toLowerCase() === raw.toLowerCase()
|
|
544
|
+
);
|
|
545
|
+
select.value = matched ? matched.value : '';
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Shows the toolbar.
|
|
551
|
+
*/
|
|
552
|
+
show() {
|
|
553
|
+
if (this.el) this.el.style.display = '';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Hides the toolbar.
|
|
558
|
+
*/
|
|
559
|
+
hide() {
|
|
560
|
+
if (this.el) this.el.style.display = 'none';
|
|
561
|
+
}
|
|
562
|
+
}
|