autumnnote 1.0.5 → 1.0.6
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 +2 -2
- package/dist/autumnnote.css +1641 -1
- package/dist/autumnnote.es.js +11521 -6464
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +11214 -84
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +14 -7
- package/src/js/Context.js +2 -6
- package/src/js/editing/Typing.js +28 -3
- package/src/js/module/Clipboard.js +17 -20
- package/src/js/module/CodeTooltip.js +9 -6
- package/src/js/module/FindReplace.js +21 -7
- package/src/js/module/ImageResizer.js +1 -1
- package/src/js/module/ImageTooltip.js +3 -1
- package/src/js/module/TableTooltip.js +56 -25
- package/src/js/module/Toolbar.js +108 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A modern, lightweight WYSIWYG editor — built with vanilla JavaScript, no jQuery required.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
"prepublishOnly": "npm run build",
|
|
20
20
|
"test": "vitest run",
|
|
21
21
|
"test:watch": "vitest",
|
|
22
|
-
"lint": "eslint src"
|
|
22
|
+
"lint": "eslint src",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"build:cdn": "vite build --config vite.cdn.config.js",
|
|
25
|
+
"analyze": "cross-env ANALYZE=1 vite build",
|
|
26
|
+
"bench": "vitest bench"
|
|
23
27
|
},
|
|
24
28
|
"keywords": [
|
|
25
29
|
"wysiwyg",
|
|
@@ -40,12 +44,15 @@
|
|
|
40
44
|
"author": "Minh Pham",
|
|
41
45
|
"license": "MIT",
|
|
42
46
|
"devDependencies": {
|
|
43
|
-
"@vitest/browser": "^1.
|
|
44
|
-
"
|
|
47
|
+
"@vitest/browser": "^4.1.2",
|
|
48
|
+
"cross-env": "^10.1.0",
|
|
49
|
+
"eslint": "^10.2.0",
|
|
45
50
|
"jsdom": "^29.0.1",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
51
|
+
"rollup-plugin-visualizer": "^7.0.1",
|
|
52
|
+
"sass": "^1.99.0",
|
|
53
|
+
"typescript": "^6.0.2",
|
|
54
|
+
"vite": "^8.0.3",
|
|
55
|
+
"vitest": "^4.1.2"
|
|
49
56
|
},
|
|
50
57
|
"browserslist": [
|
|
51
58
|
"last 2 versions",
|
package/src/js/Context.js
CHANGED
|
@@ -201,15 +201,11 @@ export class Context {
|
|
|
201
201
|
const [moduleName, methodName] = path.split('.');
|
|
202
202
|
const module = this._modules.get(moduleName);
|
|
203
203
|
if (!module) {
|
|
204
|
-
|
|
205
|
-
console.warn(`[AutumnNote] invoke: module "${moduleName}" not found (path: "${path}")`);
|
|
206
|
-
}
|
|
204
|
+
console.warn(`[AutumnNote] invoke: module "${moduleName}" not found (path: "${path}")`);
|
|
207
205
|
return undefined;
|
|
208
206
|
}
|
|
209
207
|
if (typeof module[methodName] !== 'function') {
|
|
210
|
-
|
|
211
|
-
console.warn(`[AutumnNote] invoke: method "${methodName}" not found on module "${moduleName}" (path: "${path}")`);
|
|
212
|
-
}
|
|
208
|
+
console.warn(`[AutumnNote] invoke: method "${methodName}" not found on module "${moduleName}" (path: "${path}")`);
|
|
213
209
|
return undefined;
|
|
214
210
|
}
|
|
215
211
|
return module[methodName](...args);
|
package/src/js/editing/Typing.js
CHANGED
|
@@ -8,6 +8,15 @@ import { closestPara, isLi } from '../core/dom.js';
|
|
|
8
8
|
import { execCommand } from './Style.js';
|
|
9
9
|
import { currentRange } from '../core/range.js';
|
|
10
10
|
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Module-level predicates — defined once, not re-created on every keypress.
|
|
13
|
+
// Previously these were arrow functions inside handleKeydown() which fires
|
|
14
|
+
// at ~120+ events/sec during normal typing.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const _FA_PATTERN = /\bfa-/;
|
|
17
|
+
const isFAIcon = (n) => !!(n && n.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
|
|
18
|
+
const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* Handles special keydown behaviour inside the editor.
|
|
13
22
|
* @param {KeyboardEvent} event
|
|
@@ -16,8 +25,6 @@ import { currentRange } from '../core/range.js';
|
|
|
16
25
|
* @returns {boolean} true if the event was consumed
|
|
17
26
|
*/
|
|
18
27
|
export function handleKeydown(event, editable, options = {}) {
|
|
19
|
-
const isFAIcon = (n) => !!(n && n.nodeName === 'I' && /\bfa-/.test(n.className || ''));
|
|
20
|
-
const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
|
|
21
28
|
const moveCaret = (setFn) => {
|
|
22
29
|
const sel = window.getSelection();
|
|
23
30
|
if (!sel) return false;
|
|
@@ -51,8 +58,26 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
51
58
|
if (r.startOffset === 1 && textNode.textContent === '\u200B' &&
|
|
52
59
|
isFAIcon(textNode.previousSibling)) {
|
|
53
60
|
event.preventDefault();
|
|
54
|
-
textNode.
|
|
61
|
+
const parent = textNode.parentNode;
|
|
62
|
+
const icon = textNode.previousSibling;
|
|
63
|
+
const prevNode = icon.previousSibling; // node before the icon (e.g. ZWS of prior icon)
|
|
64
|
+
icon.remove();
|
|
55
65
|
textNode.remove();
|
|
66
|
+
// Explicitly restore the cursor to the node preceding the deleted icon.
|
|
67
|
+
// Without this, the browser collapses the selection to the parent element
|
|
68
|
+
// (not a text node), causing the next Backspace to miss Cases A/B and
|
|
69
|
+
// requiring an extra keypress when two icons are adjacent.
|
|
70
|
+
const nr = document.createRange();
|
|
71
|
+
if (prevNode && prevNode.nodeType === Node.TEXT_NODE) {
|
|
72
|
+
nr.setStart(prevNode, prevNode.textContent.length);
|
|
73
|
+
} else if (prevNode) {
|
|
74
|
+
nr.setStartAfter(prevNode);
|
|
75
|
+
} else if (parent) {
|
|
76
|
+
nr.setStart(parent, 0);
|
|
77
|
+
}
|
|
78
|
+
nr.collapse(true);
|
|
79
|
+
sel.removeAllRanges();
|
|
80
|
+
sel.addRange(nr);
|
|
56
81
|
return true;
|
|
57
82
|
}
|
|
58
83
|
}
|
|
@@ -124,23 +124,22 @@ export class Clipboard {
|
|
|
124
124
|
*/
|
|
125
125
|
_cleanSocialHtml(html) {
|
|
126
126
|
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
|
|
127
|
-
// Unwrap purely presentational wrapper spans/divs with no semantic meaning
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
127
|
+
// Unwrap purely presentational wrapper spans/divs with no semantic meaning.
|
|
128
|
+
// Single-pass reverse traversal: querySelectorAll returns elements in document
|
|
129
|
+
// order, so iterating backwards processes innermost elements first — once a
|
|
130
|
+
// child is unwrapped its parent may become unwrappable in the same pass.
|
|
131
|
+
// This replaces the previous O(n²) while-loop that re-queried the whole tree
|
|
132
|
+
// on every iteration.
|
|
133
|
+
const candidates = Array.from(doc.querySelectorAll('span, div'));
|
|
134
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
135
|
+
const el = candidates[i];
|
|
136
|
+
if (!el.parentNode) continue; // already detached by an earlier iteration
|
|
137
|
+
// Keep if it contains any semantic child element
|
|
138
|
+
if (el.querySelector('a, strong, em, b, i, ul, ol, li, table, img, blockquote, pre, code, h1, h2, h3, h4, h5, h6')) continue;
|
|
139
|
+
// Unwrap — replace el with its children
|
|
140
|
+
const parent = el.parentNode;
|
|
141
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
142
|
+
parent.removeChild(el);
|
|
144
143
|
}
|
|
145
144
|
// Strip class and all data-* attributes from every remaining element
|
|
146
145
|
doc.querySelectorAll('*').forEach((el) => {
|
|
@@ -321,9 +320,7 @@ export class Clipboard {
|
|
|
321
320
|
}).catch((err) => {
|
|
322
321
|
const message = `Image "${file.name}" could not be processed.`;
|
|
323
322
|
this.context.triggerEvent('imageError', { file, message, error: err });
|
|
324
|
-
|
|
325
|
-
console.warn('[AutumnNote]', message, err);
|
|
326
|
-
}
|
|
323
|
+
console.warn('[AutumnNote]', message, err);
|
|
327
324
|
});
|
|
328
325
|
});
|
|
329
326
|
}
|
|
@@ -6,6 +6,9 @@ import { createElement, on } from '../core/dom.js';
|
|
|
6
6
|
const SHOW_DELAY = 100;
|
|
7
7
|
const HIDE_DELAY = 180;
|
|
8
8
|
|
|
9
|
+
// Cached regex for extracting language class — defined once at module level.
|
|
10
|
+
const _LANG_CLASS_RE = /language-(\S+)/;
|
|
11
|
+
|
|
9
12
|
const ICONS = {
|
|
10
13
|
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
|
11
14
|
wrapOn: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><path d="M3 12h15a3 3 0 0 1 0 6H3"/><polyline points="6 15 3 18 6 21"/></svg>`,
|
|
@@ -185,7 +188,9 @@ export class CodeTooltip {
|
|
|
185
188
|
_scheduleHide() {
|
|
186
189
|
clearTimeout(this._showTimer);
|
|
187
190
|
this._showTimer = null;
|
|
188
|
-
|
|
191
|
+
// Always reset the hide timer so rapid mouseout→mouseover sequences
|
|
192
|
+
// don't leave a stale timer that hides the tooltip prematurely.
|
|
193
|
+
clearTimeout(this._hideTimer);
|
|
189
194
|
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
190
195
|
}
|
|
191
196
|
|
|
@@ -242,7 +247,7 @@ export class CodeTooltip {
|
|
|
242
247
|
if (!this._activePre || !this._langSelect) return;
|
|
243
248
|
const codeEl = this._activePre.querySelector('code');
|
|
244
249
|
const fromAttr = this._activePre.getAttribute('data-language') || '';
|
|
245
|
-
const fromClass = codeEl ? (codeEl.className
|
|
250
|
+
const fromClass = codeEl ? (_LANG_CLASS_RE.exec(codeEl.className) || [])[1] || '' : '';
|
|
246
251
|
this._langSelect.value = fromAttr || fromClass || '';
|
|
247
252
|
}
|
|
248
253
|
|
|
@@ -355,8 +360,7 @@ export class CodeTooltip {
|
|
|
355
360
|
*/
|
|
356
361
|
_ensurePrism() {
|
|
357
362
|
if (!this.context.options.codeHighlight || window.Prism) return;
|
|
358
|
-
const cdn = this.context.options.codeHighlightCDN
|
|
359
|
-
|| 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0';
|
|
363
|
+
const cdn = this.context.options.codeHighlightCDN;
|
|
360
364
|
const themeHref = `${cdn}/themes/prism-tomorrow.min.css`;
|
|
361
365
|
const scriptSrc = `${cdn}/prism.min.js`;
|
|
362
366
|
|
|
@@ -389,8 +393,7 @@ export class CodeTooltip {
|
|
|
389
393
|
* @param {Function} cb – called once the grammar is ready
|
|
390
394
|
*/
|
|
391
395
|
_loadPrismComponent(lang, cb) {
|
|
392
|
-
const cdn = this.context.options.codeHighlightCDN
|
|
393
|
-
|| 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0';
|
|
396
|
+
const cdn = this.context.options.codeHighlightCDN;
|
|
394
397
|
const src = `${cdn}/components/prism-${lang}.min.js`;
|
|
395
398
|
// Avoid loading the same component twice
|
|
396
399
|
if (document.querySelector(`script[src="${src}"]`)) {
|
|
@@ -34,6 +34,11 @@ export class FindReplace {
|
|
|
34
34
|
/** @type {'find'|'replace'} */
|
|
35
35
|
this._mode = 'find';
|
|
36
36
|
|
|
37
|
+
/** Cached compiled regex — reused when query and case-sensitivity are unchanged */
|
|
38
|
+
this._queryRegex = null;
|
|
39
|
+
this._lastQuery = null;
|
|
40
|
+
this._lastCaseSensitive = null;
|
|
41
|
+
|
|
37
42
|
this._disposers = [];
|
|
38
43
|
this._removeTrap = null;
|
|
39
44
|
this._focusTimer = null;
|
|
@@ -267,7 +272,10 @@ export class FindReplace {
|
|
|
267
272
|
|
|
268
273
|
this._currentIndex = 0;
|
|
269
274
|
|
|
270
|
-
// Wrap in reverse order so earlier
|
|
275
|
+
// Wrap matches in reverse order so earlier text offsets stay valid when
|
|
276
|
+
// later sections of the same text node are split by surroundContents().
|
|
277
|
+
// Use push() instead of unshift() to avoid O(n²) shifting on every insert;
|
|
278
|
+
// reverse() at the end restores forward document order in O(n).
|
|
271
279
|
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
272
280
|
const { node, start, end } = rawMatches[i];
|
|
273
281
|
try {
|
|
@@ -277,13 +285,14 @@ export class FindReplace {
|
|
|
277
285
|
const mark = document.createElement('mark');
|
|
278
286
|
mark.className = 'an-highlight';
|
|
279
287
|
range.surroundContents(mark);
|
|
280
|
-
this._matches.
|
|
288
|
+
this._matches.push({ mark });
|
|
281
289
|
} catch (_) {
|
|
282
290
|
// surroundContents fails when the range crosses element boundaries.
|
|
283
291
|
// This can happen with <br> inside matched text — skip safely.
|
|
284
|
-
this._matches.
|
|
292
|
+
this._matches.push({ mark: null });
|
|
285
293
|
}
|
|
286
294
|
}
|
|
295
|
+
this._matches.reverse(); // O(n) — restore forward document order
|
|
287
296
|
|
|
288
297
|
// Drop entries where wrapping failed so the counter and navigation are accurate
|
|
289
298
|
this._matches = this._matches.filter((m) => m.mark);
|
|
@@ -304,10 +313,15 @@ export class FindReplace {
|
|
|
304
313
|
*/
|
|
305
314
|
_findRawMatches(query, root) {
|
|
306
315
|
const results = [];
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
316
|
+
// Reuse compiled regex when query and case-sensitivity haven't changed
|
|
317
|
+
if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive) {
|
|
318
|
+
const flags = this._caseSensitive ? 'g' : 'gi';
|
|
319
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
320
|
+
this._queryRegex = new RegExp(escaped, flags);
|
|
321
|
+
this._lastQuery = query;
|
|
322
|
+
this._lastCaseSensitive = this._caseSensitive;
|
|
323
|
+
}
|
|
324
|
+
const re = this._queryRegex;
|
|
311
325
|
|
|
312
326
|
const walker = document.createTreeWalker(root, 0x4 /* NodeFilter.SHOW_TEXT */);
|
|
313
327
|
let node;
|
|
@@ -55,7 +55,7 @@ export class ImageResizer {
|
|
|
55
55
|
}),
|
|
56
56
|
on(document, 'click', (e) => this._onDocClick(e)),
|
|
57
57
|
on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
58
|
-
on(window, 'resize', onWindowResize),
|
|
58
|
+
on(window, 'resize', onWindowResize, { passive: true }),
|
|
59
59
|
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
60
60
|
);
|
|
61
61
|
|
|
@@ -173,7 +173,9 @@ export class ImageTooltip {
|
|
|
173
173
|
_scheduleHide() {
|
|
174
174
|
clearTimeout(this._showTimer);
|
|
175
175
|
this._showTimer = null;
|
|
176
|
-
|
|
176
|
+
// Always reset the hide timer so rapid mouseout→mouseover sequences
|
|
177
|
+
// don't leave a stale timer that hides the tooltip prematurely.
|
|
178
|
+
clearTimeout(this._hideTimer);
|
|
177
179
|
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -74,6 +74,7 @@ const ICONS = {
|
|
|
74
74
|
mergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="8" height="10" rx="1"/><rect x="14" y="7" width="8" height="10" rx="1"/><path d="M10 12h4"/><path d="M12 10l2 2-2 2"/></svg>`,
|
|
75
75
|
colWidth: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="7" y1="12" x2="17" y2="12"/><path d="M10 9l-3 3 3 3"/><path d="M14 9l3 3-3 3"/></svg>`,
|
|
76
76
|
rowHeight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="17" x2="20" y2="17"/><line x1="12" y1="7" x2="12" y2="17"/><path d="M9 10l3-3 3 3"/><path d="M9 14l3 3 3-3"/></svg>`,
|
|
77
|
+
tableBorder: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6" stroke-width="1"/><line x1="3" y1="13" x2="21" y2="13" stroke-width="2"/><line x1="3" y1="20" x2="21" y2="20" stroke-width="3"/></svg>`,
|
|
77
78
|
deleteTable: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="16" y1="16" x2="22" y2="22" stroke="#ef4444"/><line x1="22" y1="16" x2="16" y2="22" stroke="#ef4444"/></svg>`,
|
|
78
79
|
};
|
|
79
80
|
|
|
@@ -320,8 +321,9 @@ export class TableTooltip {
|
|
|
320
321
|
el.appendChild(this._sep());
|
|
321
322
|
|
|
322
323
|
// Resize
|
|
323
|
-
el.appendChild(this._makeBtn(ICONS.colWidth,
|
|
324
|
-
el.appendChild(this._makeBtn(ICONS.rowHeight,
|
|
324
|
+
el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
|
|
325
|
+
el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
|
|
326
|
+
el.appendChild(this._makeBtn(ICONS.tableBorder,'Table Border Width',() => this._openSizePopover('border')));
|
|
325
327
|
|
|
326
328
|
el.appendChild(this._sep());
|
|
327
329
|
|
|
@@ -641,30 +643,59 @@ export class TableTooltip {
|
|
|
641
643
|
_openSizePopover(type) {
|
|
642
644
|
const cell = this._getCell();
|
|
643
645
|
if (!cell || !this._sizePopover) return;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
646
|
+
|
|
647
|
+
if (type === 'border') {
|
|
648
|
+
const table = cell.closest('table');
|
|
649
|
+
if (!table) return;
|
|
650
|
+
// Read current inline border-width, fall back to computed value, then default 1px
|
|
651
|
+
const firstCell = table.querySelector('td, th');
|
|
652
|
+
const currentPx = firstCell
|
|
653
|
+
? (parseInt(firstCell.style.borderWidth, 10) ||
|
|
654
|
+
parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
|
|
655
|
+
: 1;
|
|
656
|
+
this._sizeTitleEl.textContent = 'Table Border Width (px)';
|
|
657
|
+
this._sizeInputEl.min = '0';
|
|
658
|
+
this._sizeInputEl.max = '10';
|
|
659
|
+
this._sizeInputEl.value = currentPx;
|
|
660
|
+
this._sizeApply = (val) => {
|
|
661
|
+
// Apply border-width to every cell; with border-collapse:collapse this
|
|
662
|
+
// controls all grid lines including the outer table border.
|
|
663
|
+
// Setting only borderWidth preserves the existing border-color from CSS.
|
|
664
|
+
const cells = Array.from(table.querySelectorAll('td, th'));
|
|
665
|
+
if (val === 0) {
|
|
666
|
+
cells.forEach(c => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
|
|
667
|
+
} else {
|
|
668
|
+
cells.forEach(c => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
|
|
664
669
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
670
|
+
this.context.invoke('editor.afterCommand');
|
|
671
|
+
};
|
|
672
|
+
} else {
|
|
673
|
+
const isCol = type === 'col';
|
|
674
|
+
this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
|
|
675
|
+
this._sizeInputEl.min = '1';
|
|
676
|
+
this._sizeInputEl.max = '2000';
|
|
677
|
+
this._sizeInputEl.value = isCol
|
|
678
|
+
? (cell.offsetWidth || 120)
|
|
679
|
+
: (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
|
|
680
|
+
this._sizeApply = (val) => {
|
|
681
|
+
if (isCol) {
|
|
682
|
+
const table = cell.closest('table');
|
|
683
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
684
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
685
|
+
// Batch reads, then writes
|
|
686
|
+
const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
|
|
687
|
+
cells.forEach(c => {
|
|
688
|
+
if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
|
|
689
|
+
});
|
|
690
|
+
} else {
|
|
691
|
+
const row = cell.closest('tr');
|
|
692
|
+
if (row) {
|
|
693
|
+
for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
this.context.invoke('editor.afterCommand');
|
|
697
|
+
};
|
|
698
|
+
}
|
|
668
699
|
|
|
669
700
|
this._sizePopover.style.display = 'block';
|
|
670
701
|
requestAnimationFrame(() => {
|