autumnnote 1.4.2 → 1.6.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 +102 -21
- package/dist/autumnnote.css +324 -2
- package/dist/autumnnote.es.js +700 -228
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +691 -235
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +8 -1
- package/src/js/Context.js +8 -3
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +62 -6
- package/src/js/core/markdown.js +14 -13
- package/src/js/core/range.js +2 -2
- package/src/js/editing/History.js +6 -6
- package/src/js/editing/Style.js +19 -19
- package/src/js/editing/Table.js +4 -6
- package/src/js/editing/Typing.js +6 -6
- package/src/js/i18n/en.js +2 -0
- package/src/js/i18n/vi.js +2 -0
- package/src/js/index.js +4 -3
- package/src/js/module/BubbleToolbar.js +30 -13
- package/src/js/module/Buttons.js +16 -14
- package/src/js/module/Clipboard.js +5 -5
- package/src/js/module/CodeTooltip.js +42 -16
- package/src/js/module/Codeview.js +2 -2
- package/src/js/module/ContextMenu.js +12 -12
- package/src/js/module/Editor.js +64 -12
- package/src/js/module/EmojiDialog.js +19 -13
- package/src/js/module/FindReplace.js +85 -59
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +26 -20
- package/src/js/module/ImageCropOverlay.js +6 -6
- package/src/js/module/ImageDialog.js +18 -12
- package/src/js/module/ImageResizer.js +1 -1
- package/src/js/module/ImageTooltip.js +8 -4
- package/src/js/module/LinkDialog.js +18 -12
- package/src/js/module/LinkTooltip.js +5 -2
- package/src/js/module/Mention.js +3 -3
- package/src/js/module/Statusbar.js +2 -2
- package/src/js/module/TableTooltip.js +180 -18
- package/src/js/module/Toolbar.js +16 -15
- package/src/js/module/VideoDialog.js +8 -2
- package/src/js/module/VideoResizer.js +3 -3
- package/src/js/module/VideoTooltip.js +11 -6
- package/src/js/renderer.js +4 -2
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Click an emoji to insert it directly at the caret — no extra "Insert" step.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
6
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
7
7
|
import { withSavedRange } from '../core/range.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -566,19 +566,23 @@ export class EmojiDialog {
|
|
|
566
566
|
|
|
567
567
|
// Title row
|
|
568
568
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
569
|
+
const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
|
|
570
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
|
|
571
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 13s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>`;
|
|
569
572
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
570
573
|
title.textContent = L.title;
|
|
574
|
+
titleGroup.append(iconEl, title);
|
|
571
575
|
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
|
|
572
576
|
closeBtn.innerHTML = '×';
|
|
573
|
-
titleRow.append(
|
|
577
|
+
titleRow.append(titleGroup, closeBtn);
|
|
574
578
|
|
|
575
579
|
// Search
|
|
576
|
-
const searchInput = createElement('input', {
|
|
580
|
+
const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
577
581
|
type: 'search',
|
|
578
582
|
class: 'an-input an-icon-search',
|
|
579
583
|
placeholder: L.searchPlaceholder,
|
|
580
584
|
autocomplete: 'off',
|
|
581
|
-
});
|
|
585
|
+
}));
|
|
582
586
|
this._searchInput = searchInput;
|
|
583
587
|
|
|
584
588
|
// Category tabs
|
|
@@ -617,6 +621,7 @@ export class EmojiDialog {
|
|
|
617
621
|
|
|
618
622
|
box.append(titleRow, searchInput, catBar, grid, btnRow);
|
|
619
623
|
overlay.appendChild(box);
|
|
624
|
+
makeDraggable(titleRow, box);
|
|
620
625
|
|
|
621
626
|
// Events
|
|
622
627
|
const d1 = on(closeBtn, 'click', () => this._close());
|
|
@@ -624,7 +629,7 @@ export class EmojiDialog {
|
|
|
624
629
|
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
625
630
|
const d4 = on(searchInput, 'input', () => this._filterEmojis(searchInput.value, this._activeCat));
|
|
626
631
|
const d5 = on(catBar, 'click', (e) => {
|
|
627
|
-
const tab = e.target
|
|
632
|
+
const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
|
|
628
633
|
if (tab) {
|
|
629
634
|
this._activeCat = tab.dataset.cat;
|
|
630
635
|
this._updateCatTabs();
|
|
@@ -632,7 +637,7 @@ export class EmojiDialog {
|
|
|
632
637
|
}
|
|
633
638
|
});
|
|
634
639
|
const d6 = on(grid, 'click', (e) => {
|
|
635
|
-
const cell = e.target
|
|
640
|
+
const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-emoji-cell'));
|
|
636
641
|
if (cell) this._onEmojiClick(cell.dataset.char);
|
|
637
642
|
});
|
|
638
643
|
this._disposers.push(d1, d2, d3, d4, d5, d6);
|
|
@@ -645,7 +650,7 @@ export class EmojiDialog {
|
|
|
645
650
|
|
|
646
651
|
_updateCatTabs() {
|
|
647
652
|
this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
|
|
648
|
-
tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
|
|
653
|
+
tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
|
|
649
654
|
});
|
|
650
655
|
}
|
|
651
656
|
|
|
@@ -653,10 +658,11 @@ export class EmojiDialog {
|
|
|
653
658
|
const q = (query || '').trim().toLowerCase();
|
|
654
659
|
let count = 0;
|
|
655
660
|
this._grid.querySelectorAll('.an-emoji-cell').forEach((cell) => {
|
|
656
|
-
const
|
|
657
|
-
const
|
|
661
|
+
const hCell = /** @type {HTMLElement} */ (cell);
|
|
662
|
+
const matchCat = !cat || cat === 'all' || hCell.dataset.cat === cat;
|
|
663
|
+
const matchQuery = !q || hCell.dataset.keywords.includes(q) || hCell.dataset.char === q;
|
|
658
664
|
const visible = matchCat && matchQuery;
|
|
659
|
-
|
|
665
|
+
hCell.style.display = visible ? '' : 'none';
|
|
660
666
|
if (visible) count++;
|
|
661
667
|
});
|
|
662
668
|
let empty = this._grid.querySelector('.an-icon-empty');
|
|
@@ -665,7 +671,7 @@ export class EmojiDialog {
|
|
|
665
671
|
empty.textContent = 'No emojis found';
|
|
666
672
|
this._grid.appendChild(empty);
|
|
667
673
|
}
|
|
668
|
-
empty.style.display = count > 0 ? 'none' : '';
|
|
674
|
+
/** @type {HTMLElement} */ (empty).style.display = count > 0 ? 'none' : '';
|
|
669
675
|
}
|
|
670
676
|
|
|
671
677
|
// ---------------------------------------------------------------------------
|
|
@@ -694,8 +700,8 @@ export class EmojiDialog {
|
|
|
694
700
|
// deleteContents() can drift the range endpoint outside the cell in some
|
|
695
701
|
// browsers. Save the cell reference first and re-anchor after deletion.
|
|
696
702
|
const _sc = range.startContainer;
|
|
697
|
-
const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
698
|
-
?.closest
|
|
703
|
+
const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
704
|
+
?.closest('td, th');
|
|
699
705
|
range.deleteContents();
|
|
700
706
|
if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
|
|
701
707
|
range.setStart(_tdAnchor, 0);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* single / replace-all replacement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
10
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
11
11
|
|
|
12
12
|
export class FindReplace {
|
|
13
13
|
/** @param {import('../Context.js').Context} context */
|
|
@@ -120,8 +120,8 @@ export class FindReplace {
|
|
|
120
120
|
const title = this._dialog.querySelector('.an-dialog-title');
|
|
121
121
|
|
|
122
122
|
const isReplace = this._mode === 'replace';
|
|
123
|
-
if (replaceRow) replaceRow.style.display = isReplace ? '' : 'none';
|
|
124
|
-
if (replaceActions) replaceActions.style.display = isReplace ? '' : 'none';
|
|
123
|
+
if (replaceRow) /** @type {HTMLElement} */ (replaceRow).style.display = isReplace ? '' : 'none';
|
|
124
|
+
if (replaceActions) /** @type {HTMLElement} */ (replaceActions).style.display = isReplace ? '' : 'none';
|
|
125
125
|
if (title) title.textContent = isReplace ? this.context.locale.findReplace.findReplaceTitle : this.context.locale.findReplace.findTitle;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -137,107 +137,133 @@ export class FindReplace {
|
|
|
137
137
|
'aria-modal': 'true',
|
|
138
138
|
'aria-label': L.findReplaceTitle,
|
|
139
139
|
});
|
|
140
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
140
|
+
const box = createElement('div', { class: 'an-dialog-box an-fr-box' });
|
|
141
141
|
|
|
142
|
-
// ----
|
|
143
|
-
const
|
|
142
|
+
// ---- Header: [icon][title] [×] ----
|
|
143
|
+
const header = createElement('div', { class: 'an-fr-header' });
|
|
144
|
+
const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
|
|
145
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
|
|
146
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
|
|
144
147
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
145
148
|
title.textContent = L.findTitle;
|
|
149
|
+
titleGroup.append(iconEl, title);
|
|
146
150
|
const closeBtn = createElement('button', {
|
|
147
151
|
type: 'button',
|
|
148
152
|
class: 'an-icon-close',
|
|
149
|
-
|
|
153
|
+
title: L.close,
|
|
154
|
+
'aria-label': L.close,
|
|
150
155
|
});
|
|
151
|
-
closeBtn.
|
|
156
|
+
closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
152
157
|
this._closeBtn = closeBtn;
|
|
153
|
-
|
|
154
|
-
box.appendChild(
|
|
158
|
+
header.append(titleGroup, closeBtn);
|
|
159
|
+
box.appendChild(header);
|
|
155
160
|
|
|
156
|
-
// ----
|
|
157
|
-
const
|
|
158
|
-
const findInput = createElement('input', {
|
|
161
|
+
// ---- Search bar: [input] [Aa] [↑] [↓] counter ----
|
|
162
|
+
const searchBar = createElement('div', { class: 'an-fr-search-bar' });
|
|
163
|
+
const findInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
159
164
|
type: 'text',
|
|
160
|
-
class: 'an-input',
|
|
165
|
+
class: 'an-input an-fr-input',
|
|
161
166
|
placeholder: L.findPlaceholder,
|
|
162
167
|
'aria-label': L.searchAriaLabel,
|
|
163
|
-
});
|
|
168
|
+
}));
|
|
164
169
|
this._findInput = findInput;
|
|
165
|
-
findRow.appendChild(findInput);
|
|
166
|
-
box.appendChild(findRow);
|
|
167
170
|
|
|
168
|
-
//
|
|
169
|
-
const
|
|
170
|
-
const caseLabel = createElement('label', { class: 'an-label an-label-inline' });
|
|
171
|
-
const caseCheckbox = createElement('input', {
|
|
171
|
+
// Case-sensitive toggle (visual button; hidden checkbox kept for state)
|
|
172
|
+
const caseCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
172
173
|
type: 'checkbox',
|
|
174
|
+
style: 'display:none',
|
|
175
|
+
'aria-hidden': 'true',
|
|
176
|
+
}));
|
|
177
|
+
this._caseCheckbox = caseCheckbox;
|
|
178
|
+
const caseBtn = createElement('button', {
|
|
179
|
+
type: 'button',
|
|
180
|
+
class: 'an-fr-icon-btn',
|
|
181
|
+
title: 'Case sensitive',
|
|
173
182
|
'aria-label': 'Case sensitive',
|
|
174
183
|
});
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
caseBtn.textContent = 'Aa';
|
|
185
|
+
|
|
186
|
+
const prevBtn = createElement('button', {
|
|
187
|
+
type: 'button',
|
|
188
|
+
class: 'an-fr-icon-btn',
|
|
189
|
+
title: 'Previous (Shift+Enter)',
|
|
190
|
+
'aria-label': 'Previous',
|
|
191
|
+
});
|
|
192
|
+
prevBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`;
|
|
193
|
+
|
|
194
|
+
const nextBtn = createElement('button', {
|
|
195
|
+
type: 'button',
|
|
196
|
+
class: 'an-fr-icon-btn',
|
|
197
|
+
title: 'Next (Enter)',
|
|
198
|
+
'aria-label': 'Next',
|
|
199
|
+
});
|
|
200
|
+
nextBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
|
|
201
|
+
|
|
177
202
|
const counter = createElement('span', { class: 'an-fr-counter' });
|
|
178
203
|
this._counterEl = counter;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const prevBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
185
|
-
prevBtn.textContent = L.prevBtn;
|
|
186
|
-
const nextBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
187
|
-
nextBtn.textContent = L.nextBtn;
|
|
188
|
-
findActions.append(prevBtn, nextBtn);
|
|
189
|
-
box.appendChild(findActions);
|
|
190
|
-
|
|
191
|
-
// ---- Replace row (hidden by default) ----
|
|
204
|
+
|
|
205
|
+
searchBar.append(findInput, caseCheckbox, caseBtn, prevBtn, nextBtn, counter);
|
|
206
|
+
box.appendChild(searchBar);
|
|
207
|
+
|
|
208
|
+
// ---- Replace row: [input] [Replace] [All] (hidden by default) ----
|
|
192
209
|
const replaceRow = createElement('div', { class: 'an-fr-replace-row' });
|
|
193
210
|
replaceRow.style.display = 'none';
|
|
194
|
-
const replaceInput = createElement('input', {
|
|
211
|
+
const replaceInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
195
212
|
type: 'text',
|
|
196
|
-
class: 'an-input',
|
|
213
|
+
class: 'an-input an-fr-input',
|
|
197
214
|
placeholder: L.replacePlaceholder,
|
|
198
215
|
'aria-label': L.replaceAriaLabel,
|
|
199
|
-
});
|
|
216
|
+
}));
|
|
200
217
|
this._replaceInput = replaceInput;
|
|
201
|
-
|
|
218
|
+
const replaceBtn = createElement('button', { type: 'button', class: 'an-btn an-fr-replace-btn' });
|
|
219
|
+
replaceBtn.textContent = L.replaceBtn;
|
|
220
|
+
const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary an-fr-replace-btn' });
|
|
221
|
+
replaceAllBtn.textContent = L.replaceAllBtn;
|
|
222
|
+
replaceRow.append(replaceInput, replaceBtn, replaceAllBtn);
|
|
202
223
|
box.appendChild(replaceRow);
|
|
203
224
|
|
|
204
|
-
//
|
|
205
|
-
const replaceActions = createElement('div', { class: 'an-
|
|
225
|
+
// Compat div — _updateMode toggles this; kept empty so existing logic works
|
|
226
|
+
const replaceActions = createElement('div', { class: 'an-fr-replace-actions' });
|
|
206
227
|
replaceActions.style.display = 'none';
|
|
207
|
-
const replaceBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
208
|
-
replaceBtn.textContent = L.replaceBtn;
|
|
209
|
-
const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
210
|
-
replaceAllBtn.textContent = L.replaceAllBtn;
|
|
211
|
-
replaceActions.append(replaceBtn, replaceAllBtn);
|
|
212
228
|
box.appendChild(replaceActions);
|
|
213
229
|
|
|
214
230
|
overlay.appendChild(box);
|
|
231
|
+
makeDraggable(header, box);
|
|
215
232
|
|
|
216
233
|
// ---- Event bindings ----
|
|
217
234
|
const d1 = on(closeBtn, 'click', () => this._close());
|
|
218
235
|
const d2 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
219
236
|
const d3 = on(findInput, 'input', () => this._onSearch());
|
|
220
|
-
const d4 = on(
|
|
237
|
+
const d4 = on(caseBtn, 'click', () => {
|
|
238
|
+
this._caseSensitive = !this._caseSensitive;
|
|
239
|
+
caseCheckbox.checked = this._caseSensitive;
|
|
240
|
+
caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
|
|
241
|
+
this._onSearch();
|
|
242
|
+
});
|
|
243
|
+
// Hidden checkbox change handler keeps backward-compat with programmatic toggles
|
|
244
|
+
const d5 = on(caseCheckbox, 'change', () => {
|
|
221
245
|
this._caseSensitive = caseCheckbox.checked;
|
|
246
|
+
caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
|
|
222
247
|
this._onSearch();
|
|
223
248
|
});
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
249
|
+
const d6 = on(nextBtn, 'click', () => this._next());
|
|
250
|
+
const d7 = on(prevBtn, 'click', () => this._prev());
|
|
251
|
+
const d8 = on(replaceBtn, 'click', () => this._replace());
|
|
252
|
+
const d9 = on(replaceAllBtn, 'click', () => this._replaceAll());
|
|
253
|
+
const d10 = on(findInput, 'keydown', (e) => {
|
|
254
|
+
const ke = /** @type {KeyboardEvent} */ (e);
|
|
255
|
+
if (ke.key === 'Enter') {
|
|
230
256
|
e.preventDefault();
|
|
231
|
-
|
|
257
|
+
ke.shiftKey ? this._prev() : this._next();
|
|
232
258
|
}
|
|
233
259
|
});
|
|
234
|
-
const
|
|
235
|
-
if (e.key === 'Enter') {
|
|
260
|
+
const d11 = on(replaceInput, 'keydown', (e) => {
|
|
261
|
+
if (/** @type {KeyboardEvent} */ (e).key === 'Enter') {
|
|
236
262
|
e.preventDefault();
|
|
237
263
|
this._replace();
|
|
238
264
|
}
|
|
239
265
|
});
|
|
240
|
-
this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10);
|
|
266
|
+
this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11);
|
|
241
267
|
|
|
242
268
|
return overlay;
|
|
243
269
|
}
|
|
@@ -331,7 +357,7 @@ export class FindReplace {
|
|
|
331
357
|
re.lastIndex = 0;
|
|
332
358
|
let m;
|
|
333
359
|
while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) {
|
|
334
|
-
results.push({ node, start: m.index, end: m.index + m[0].length });
|
|
360
|
+
results.push({ node: /** @type {Text} */ (node), start: m.index, end: m.index + m[0].length });
|
|
335
361
|
}
|
|
336
362
|
}
|
|
337
363
|
return results;
|
|
@@ -21,7 +21,7 @@ export class Fullscreen {
|
|
|
21
21
|
initialize() {
|
|
22
22
|
// Press Escape to exit fullscreen
|
|
23
23
|
const d = on(document, 'keydown', (event) => {
|
|
24
|
-
if (this._active && isKey(event, key.ESCAPE)) {
|
|
24
|
+
if (this._active && isKey(/** @type {KeyboardEvent} */ (event), key.ESCAPE)) {
|
|
25
25
|
this.deactivate();
|
|
26
26
|
}
|
|
27
27
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* IconDialog.js - Browse and insert FontAwesome Free icons
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
5
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
6
6
|
import { withSavedRange } from '../core/range.js';
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -281,7 +281,7 @@ export class IconDialog {
|
|
|
281
281
|
// Already present (icon element or stylesheet link)?
|
|
282
282
|
if (document.getElementById('an-fontawesome-css')) return;
|
|
283
283
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
284
|
-
.map((l) => l.href || '').join(' ');
|
|
284
|
+
.map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
|
|
285
285
|
if (/fontawesome|font-awesome/.test(links)) return;
|
|
286
286
|
if (document.querySelector('.fa-solid, .fas, .far, .fab')) return;
|
|
287
287
|
|
|
@@ -344,19 +344,23 @@ export class IconDialog {
|
|
|
344
344
|
|
|
345
345
|
// Title row
|
|
346
346
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
347
|
+
const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
|
|
348
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
|
|
349
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>`;
|
|
347
350
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
348
351
|
title.textContent = L.title;
|
|
352
|
+
titleGroup.append(iconEl, title);
|
|
349
353
|
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
|
|
350
354
|
closeBtn.innerHTML = '×';
|
|
351
|
-
titleRow.append(
|
|
355
|
+
titleRow.append(titleGroup, closeBtn);
|
|
352
356
|
|
|
353
357
|
// Search
|
|
354
|
-
const searchInput = createElement('input', {
|
|
358
|
+
const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
355
359
|
type: 'search',
|
|
356
360
|
class: 'an-input an-icon-search',
|
|
357
361
|
placeholder: L.searchPlaceholder,
|
|
358
362
|
autocomplete: 'off',
|
|
359
|
-
});
|
|
363
|
+
}));
|
|
360
364
|
this._searchInput = searchInput;
|
|
361
365
|
|
|
362
366
|
// Category tabs
|
|
@@ -388,7 +392,7 @@ export class IconDialog {
|
|
|
388
392
|
|
|
389
393
|
const styleLabel = createElement('label', { class: 'an-label' });
|
|
390
394
|
styleLabel.textContent = L.style;
|
|
391
|
-
const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
|
|
395
|
+
const styleSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
|
|
392
396
|
[['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
|
|
393
397
|
const opt = createElement('option', { value: v });
|
|
394
398
|
opt.textContent = t;
|
|
@@ -399,9 +403,9 @@ export class IconDialog {
|
|
|
399
403
|
|
|
400
404
|
const sizeLabel = createElement('label', { class: 'an-label' });
|
|
401
405
|
sizeLabel.textContent = L.size;
|
|
402
|
-
const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
|
|
406
|
+
const sizeSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
|
|
403
407
|
[['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
|
|
404
|
-
const opt = createElement('option', { value: v });
|
|
408
|
+
const opt = /** @type {HTMLOptionElement} */ (createElement('option', { value: v }));
|
|
405
409
|
if (v === '1em') opt.selected = true;
|
|
406
410
|
opt.textContent = t;
|
|
407
411
|
sizeSelect.appendChild(opt);
|
|
@@ -410,11 +414,11 @@ export class IconDialog {
|
|
|
410
414
|
|
|
411
415
|
const colorLabel = createElement('label', { class: 'an-label' });
|
|
412
416
|
colorLabel.textContent = L.color;
|
|
413
|
-
const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
|
|
417
|
+
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' }));
|
|
414
418
|
this._colorInput = colorInput;
|
|
415
419
|
|
|
416
420
|
const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
|
|
417
|
-
const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
|
|
421
|
+
const useColorCb = /** @type {HTMLInputElement} */ (createElement('input', { type: 'checkbox', checked: '' }));
|
|
418
422
|
this._useColorCb = useColorCb;
|
|
419
423
|
useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
|
|
420
424
|
|
|
@@ -438,6 +442,7 @@ export class IconDialog {
|
|
|
438
442
|
|
|
439
443
|
box.append(titleRow, searchInput, catBar, grid, optRow, preview, btnRow);
|
|
440
444
|
overlay.appendChild(box);
|
|
445
|
+
makeDraggable(titleRow, box);
|
|
441
446
|
|
|
442
447
|
// -------------------------------------------------------------------------
|
|
443
448
|
// Events
|
|
@@ -449,7 +454,7 @@ export class IconDialog {
|
|
|
449
454
|
const d4 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
450
455
|
const d5 = on(searchInput, 'input', () => this._filterIcons(searchInput.value, this._activeCat));
|
|
451
456
|
const d6 = on(catBar, 'click', (e) => {
|
|
452
|
-
const tab = e.target
|
|
457
|
+
const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
|
|
453
458
|
if (tab) {
|
|
454
459
|
this._activeCat = tab.dataset.cat;
|
|
455
460
|
this._updateCatTabs();
|
|
@@ -457,7 +462,7 @@ export class IconDialog {
|
|
|
457
462
|
}
|
|
458
463
|
});
|
|
459
464
|
const d7 = on(grid, 'click', (e) => {
|
|
460
|
-
const cell = e.target
|
|
465
|
+
const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-icon-cell'));
|
|
461
466
|
if (cell) this._selectIcon(cell.dataset.name);
|
|
462
467
|
});
|
|
463
468
|
const d8 = on(styleSelect, 'change', () => this._updatePreview(this._selectedIcon));
|
|
@@ -476,7 +481,7 @@ export class IconDialog {
|
|
|
476
481
|
|
|
477
482
|
_updateCatTabs() {
|
|
478
483
|
this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
|
|
479
|
-
tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
|
|
484
|
+
tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
|
|
480
485
|
});
|
|
481
486
|
}
|
|
482
487
|
|
|
@@ -484,12 +489,13 @@ export class IconDialog {
|
|
|
484
489
|
const q = (query || '').trim().toLowerCase();
|
|
485
490
|
let visibleCount = 0;
|
|
486
491
|
this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
|
|
487
|
-
const
|
|
488
|
-
const
|
|
492
|
+
const hCell = /** @type {HTMLElement} */ (cell);
|
|
493
|
+
const name = hCell.dataset.name;
|
|
494
|
+
const cellCat = hCell.dataset.cat;
|
|
489
495
|
const matchesCat = !cat || cat === 'all' || cellCat === cat;
|
|
490
496
|
const matchesQuery = !q || name.includes(q);
|
|
491
497
|
const visible = matchesCat && matchesQuery;
|
|
492
|
-
|
|
498
|
+
hCell.style.display = visible ? '' : 'none';
|
|
493
499
|
if (visible) visibleCount++;
|
|
494
500
|
});
|
|
495
501
|
// Show empty state if needed
|
|
@@ -499,14 +505,14 @@ export class IconDialog {
|
|
|
499
505
|
empty.textContent = 'No icons found';
|
|
500
506
|
this._grid.appendChild(empty);
|
|
501
507
|
}
|
|
502
|
-
empty.style.display = visibleCount > 0 ? 'none' : '';
|
|
508
|
+
/** @type {HTMLElement} */ (empty).style.display = visibleCount > 0 ? 'none' : '';
|
|
503
509
|
}
|
|
504
510
|
|
|
505
511
|
_selectIcon(name) {
|
|
506
512
|
this._selectedIcon = name;
|
|
507
513
|
// Highlight selected cell
|
|
508
514
|
this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
|
|
509
|
-
cell.classList.toggle('active', cell.dataset.name === name);
|
|
515
|
+
cell.classList.toggle('active', /** @type {HTMLElement} */ (cell).dataset.name === name);
|
|
510
516
|
});
|
|
511
517
|
// Enable insert button
|
|
512
518
|
this._insertBtn.removeAttribute('disabled');
|
|
@@ -581,8 +587,8 @@ export class IconDialog {
|
|
|
581
587
|
// deleteContents() can drift the range endpoint outside the cell in some
|
|
582
588
|
// browsers. Save the cell reference first and re-anchor after deletion.
|
|
583
589
|
const _sc = range.startContainer;
|
|
584
|
-
const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
585
|
-
?.closest
|
|
590
|
+
const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
591
|
+
?.closest('td, th');
|
|
586
592
|
range.deleteContents();
|
|
587
593
|
if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
|
|
588
594
|
range.setStart(_tdAnchor, 0);
|
|
@@ -230,7 +230,7 @@ export class ImageCropOverlay {
|
|
|
230
230
|
on(h, 'touchstart', (e) => {
|
|
231
231
|
e.preventDefault();
|
|
232
232
|
e.stopPropagation();
|
|
233
|
-
this._startHandleDrag({ clientX:
|
|
233
|
+
const te = /** @type {TouchEvent} */ (e); this._startHandleDrag({ clientX: te.touches[0].clientX, clientY: te.touches[0].clientY }, id);
|
|
234
234
|
}, { passive: false }),
|
|
235
235
|
);
|
|
236
236
|
this._handles[id] = h;
|
|
@@ -240,18 +240,18 @@ export class ImageCropOverlay {
|
|
|
240
240
|
/* ---- crop move drag ---- */
|
|
241
241
|
this._disposers.push(
|
|
242
242
|
on(cropBox, 'mousedown', (e) => {
|
|
243
|
-
if (e.target.classList.contains('an-crop-handle')) return;
|
|
243
|
+
if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
|
|
244
244
|
if (e.target !== cropBox && e.target !== grid) return;
|
|
245
245
|
e.preventDefault();
|
|
246
246
|
e.stopPropagation();
|
|
247
247
|
this._startBoxMove(e);
|
|
248
248
|
}),
|
|
249
249
|
on(cropBox, 'touchstart', (e) => {
|
|
250
|
-
if (e.target.classList.contains('an-crop-handle')) return;
|
|
250
|
+
if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
|
|
251
251
|
if (e.target !== cropBox && e.target !== grid) return;
|
|
252
252
|
e.preventDefault();
|
|
253
253
|
e.stopPropagation();
|
|
254
|
-
this._startBoxMove({ clientX:
|
|
254
|
+
const te2 = /** @type {TouchEvent} */ (e); this._startBoxMove({ clientX: te2.touches[0].clientX, clientY: te2.touches[0].clientY });
|
|
255
255
|
}, { passive: false }),
|
|
256
256
|
);
|
|
257
257
|
|
|
@@ -444,7 +444,7 @@ export class ImageCropOverlay {
|
|
|
444
444
|
_attachDocDrag(onMove) {
|
|
445
445
|
const onTouchMove = (e) => {
|
|
446
446
|
e.preventDefault();
|
|
447
|
-
onMove({ clientX:
|
|
447
|
+
const te3 = /** @type {TouchEvent} */ (e); onMove(/** @type {MouseEvent} */ (/** @type {unknown} */ ({ clientX: te3.touches[0].clientX, clientY: te3.touches[0].clientY })));
|
|
448
448
|
};
|
|
449
449
|
const cleanup = () => {
|
|
450
450
|
document.removeEventListener('mousemove', onMove);
|
|
@@ -500,7 +500,7 @@ export class ImageCropOverlay {
|
|
|
500
500
|
const outW = w;
|
|
501
501
|
const outH = h;
|
|
502
502
|
|
|
503
|
-
const naturalRect = { x: natX, y: natY, width: natW, height: natH };
|
|
503
|
+
const naturalRect = /** @type {DOMRect} */ (/** @type {unknown} */ ({ x: natX, y: natY, width: natW, height: natH }));
|
|
504
504
|
const canvas = await drawCropToCanvas(img, naturalRect, outW, outH);
|
|
505
505
|
|
|
506
506
|
if (!canvas) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Inspired by Summernote's ImageDialog — rewritten without jQuery
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
6
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
7
7
|
import { withSavedRange } from '../core/range.js';
|
|
8
8
|
|
|
9
9
|
export class ImageDialog {
|
|
@@ -66,32 +66,37 @@ export class ImageDialog {
|
|
|
66
66
|
});
|
|
67
67
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
68
68
|
|
|
69
|
+
const header = createElement('div', { class: 'an-dialog-header' });
|
|
70
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
71
|
+
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"><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"/></svg>`;
|
|
69
72
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
70
73
|
title.textContent = L.title;
|
|
74
|
+
header.appendChild(iconEl);
|
|
75
|
+
header.appendChild(title);
|
|
71
76
|
|
|
72
77
|
// URL tab
|
|
73
78
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
74
79
|
urlLabel.textContent = L.imageUrl;
|
|
75
|
-
const urlInput = createElement('input', {
|
|
80
|
+
const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
76
81
|
type: 'url',
|
|
77
82
|
class: 'an-input',
|
|
78
83
|
placeholder: L.urlPlaceholder,
|
|
79
84
|
autocomplete: 'off',
|
|
80
|
-
});
|
|
85
|
+
}));
|
|
81
86
|
this._urlInput = urlInput;
|
|
82
87
|
|
|
83
88
|
// Alt text
|
|
84
89
|
const altLabel = createElement('label', { class: 'an-label' });
|
|
85
90
|
altLabel.textContent = L.altText;
|
|
86
|
-
const altInput = createElement('input', {
|
|
91
|
+
const altInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
87
92
|
type: 'text',
|
|
88
93
|
class: 'an-input',
|
|
89
94
|
placeholder: L.altPlaceholder,
|
|
90
95
|
autocomplete: 'off',
|
|
91
|
-
});
|
|
96
|
+
}));
|
|
92
97
|
this._altInput = altInput;
|
|
93
98
|
|
|
94
|
-
box.append(
|
|
99
|
+
box.append(header, urlLabel, urlInput, altLabel, altInput);
|
|
95
100
|
|
|
96
101
|
// Alignment
|
|
97
102
|
const alignLabel = createElement('label', { class: 'an-label' });
|
|
@@ -105,7 +110,7 @@ export class ImageDialog {
|
|
|
105
110
|
];
|
|
106
111
|
alignOptions.forEach(({ value, label }) => {
|
|
107
112
|
const radioId = `an-align-${value || 'none'}`;
|
|
108
|
-
const radio = createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value });
|
|
113
|
+
const radio = /** @type {HTMLInputElement} */ (createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value }));
|
|
109
114
|
if (value === '') radio.checked = true;
|
|
110
115
|
const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
|
|
111
116
|
lbl.textContent = label;
|
|
@@ -116,11 +121,11 @@ export class ImageDialog {
|
|
|
116
121
|
if (this.options.allowImageUpload !== false) {
|
|
117
122
|
const fileLabel = createElement('label', { class: 'an-label' });
|
|
118
123
|
fileLabel.textContent = L.uploadLabel;
|
|
119
|
-
const fileInput = createElement('input', {
|
|
124
|
+
const fileInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
120
125
|
type: 'file',
|
|
121
126
|
class: 'an-input',
|
|
122
127
|
accept: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif',
|
|
123
|
-
});
|
|
128
|
+
}));
|
|
124
129
|
this._fileInput = fileInput;
|
|
125
130
|
// Hint line shown below the file input for format errors
|
|
126
131
|
const fileHint = createElement('p', { class: 'an-dialog-hint' });
|
|
@@ -141,12 +146,13 @@ export class ImageDialog {
|
|
|
141
146
|
|
|
142
147
|
box.append(btnRow);
|
|
143
148
|
overlay.appendChild(box);
|
|
149
|
+
makeDraggable(header, box);
|
|
144
150
|
|
|
145
151
|
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
146
152
|
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
147
153
|
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
148
|
-
const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
149
|
-
const d5 = on(altInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
154
|
+
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
155
|
+
const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
150
156
|
this._disposers.push(d1, d2, d3, d4, d5);
|
|
151
157
|
|
|
152
158
|
return overlay;
|
|
@@ -197,7 +203,7 @@ export class ImageDialog {
|
|
|
197
203
|
const src = this._urlInput.value.trim();
|
|
198
204
|
const alt = this._altInput.value.trim();
|
|
199
205
|
const alignRadio = this._alignRow && this._alignRow.querySelector('input[name="an-img-align"]:checked');
|
|
200
|
-
const align = alignRadio ? alignRadio.value : '';
|
|
206
|
+
const align = alignRadio ? /** @type {HTMLInputElement} */ (alignRadio).value : '';
|
|
201
207
|
|
|
202
208
|
if (!src) {
|
|
203
209
|
this._urlInput.focus();
|
|
@@ -51,7 +51,7 @@ export class ImageResizer {
|
|
|
51
51
|
// Also select on right-click so the highlight shows before the context menu
|
|
52
52
|
on(editable, 'contextmenu', (e) => {
|
|
53
53
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
54
|
-
const img = e.target
|
|
54
|
+
const img = /** @type {Element} */ (e.target)?.closest('img');
|
|
55
55
|
if (img) this._select(img);
|
|
56
56
|
}),
|
|
57
57
|
on(document, 'click', (e) => this._onDocClick(e)),
|