tosijs-styled-editor 0.4.2 → 0.4.4
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/CHANGELOG.md +275 -0
- package/README.md +61 -6
- package/dist/commands.d.ts +11 -1
- package/dist/dom-utils.d.ts +24 -0
- package/dist/index.js +9 -9126
- package/dist/module.js +170 -34
- package/dist/tosijs-styled-editor.d.ts +58 -2
- package/dist/version.d.ts +1 -1
- package/package.json +3 -1
package/dist/module.js
CHANGED
|
@@ -250,6 +250,99 @@ function caretGeometryAt(marker, root) {
|
|
|
250
250
|
}
|
|
251
251
|
return null;
|
|
252
252
|
}
|
|
253
|
+
function forSchemeTest(value) {
|
|
254
|
+
return value.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, "").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
255
|
+
}
|
|
256
|
+
function isSafeUrl(value) {
|
|
257
|
+
const trimmed = forSchemeTest(value);
|
|
258
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(trimmed))
|
|
259
|
+
return true;
|
|
260
|
+
if (/^data:image\/(png|jpeg|jpg|gif|webp|avif|bmp|svg\+xml);/i.test(trimmed)) {
|
|
261
|
+
return !/^data:image\/svg/i.test(trimmed);
|
|
262
|
+
}
|
|
263
|
+
return /^(https?|mailto|tel):/i.test(trimmed);
|
|
264
|
+
}
|
|
265
|
+
var FORBIDDEN_TAGS = new Set([
|
|
266
|
+
"script",
|
|
267
|
+
"iframe",
|
|
268
|
+
"object",
|
|
269
|
+
"embed",
|
|
270
|
+
"link",
|
|
271
|
+
"meta",
|
|
272
|
+
"base",
|
|
273
|
+
"style",
|
|
274
|
+
"form",
|
|
275
|
+
"noscript",
|
|
276
|
+
"template",
|
|
277
|
+
"animate",
|
|
278
|
+
"animatetransform",
|
|
279
|
+
"animatemotion",
|
|
280
|
+
"set"
|
|
281
|
+
]);
|
|
282
|
+
var URL_ATTRIBUTES = [
|
|
283
|
+
"href",
|
|
284
|
+
"src",
|
|
285
|
+
"srcset",
|
|
286
|
+
"xlink:href",
|
|
287
|
+
"action",
|
|
288
|
+
"formaction",
|
|
289
|
+
"ping",
|
|
290
|
+
"poster",
|
|
291
|
+
"background",
|
|
292
|
+
"cite",
|
|
293
|
+
"data",
|
|
294
|
+
"longdesc",
|
|
295
|
+
"profile",
|
|
296
|
+
"usemap",
|
|
297
|
+
"manifest"
|
|
298
|
+
];
|
|
299
|
+
var DANGEROUS_SCHEME = /^(javascript|vbscript|livescript|mocha|data:text\/html)/i;
|
|
300
|
+
var FORBIDDEN_ATTRIBUTES = new Set(["ping"]);
|
|
301
|
+
var localNameGetter = Object.getOwnPropertyDescriptor(Element.prototype, "localName")?.get;
|
|
302
|
+
var attributesGetter = Object.getOwnPropertyDescriptor(Element.prototype, "attributes")?.get;
|
|
303
|
+
function getLocalName(el) {
|
|
304
|
+
return localNameGetter ? localNameGetter.call(el) : el.localName;
|
|
305
|
+
}
|
|
306
|
+
function getAttributes(el) {
|
|
307
|
+
return attributesGetter ? attributesGetter.call(el) : el.attributes;
|
|
308
|
+
}
|
|
309
|
+
function sanitizeInPlace(root) {
|
|
310
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
311
|
+
const doomed = [];
|
|
312
|
+
let node;
|
|
313
|
+
while (node = walker.nextNode()) {
|
|
314
|
+
const el = node;
|
|
315
|
+
const localName = String(getLocalName(el) ?? "").toLowerCase();
|
|
316
|
+
if (FORBIDDEN_TAGS.has(localName)) {
|
|
317
|
+
doomed.push(el);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
for (const attr of Array.from(getAttributes(el) ?? [])) {
|
|
321
|
+
const name = attr.name.toLowerCase();
|
|
322
|
+
if (name.startsWith("on")) {
|
|
323
|
+
el.removeAttribute(attr.name);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (FORBIDDEN_ATTRIBUTES.has(name)) {
|
|
327
|
+
el.removeAttribute(attr.name);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const normalized = forSchemeTest(attr.value);
|
|
331
|
+
if (DANGEROUS_SCHEME.test(normalized)) {
|
|
332
|
+
el.removeAttribute(attr.name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (URL_ATTRIBUTES.includes(name) && !isSafeUrl(attr.value)) {
|
|
336
|
+
el.removeAttribute(attr.name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const el of doomed)
|
|
341
|
+
el.remove();
|
|
342
|
+
}
|
|
343
|
+
function isSafeNavigationUrl(value) {
|
|
344
|
+
return isSafeUrl(value) && !/^data:/i.test(forSchemeTest(value));
|
|
345
|
+
}
|
|
253
346
|
// src/selection.ts
|
|
254
347
|
function characters(text) {
|
|
255
348
|
const Segmenter = Intl.Segmenter;
|
|
@@ -1173,6 +1266,10 @@ var commands = {
|
|
|
1173
1266
|
setLink(ctx, url, target = "_blank") {
|
|
1174
1267
|
if (!url)
|
|
1175
1268
|
return;
|
|
1269
|
+
if (!isSafeNavigationUrl(url)) {
|
|
1270
|
+
console.error("setLink: refusing unsafe URL scheme", url);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1176
1273
|
ctx.selectable.resetBounds();
|
|
1177
1274
|
spanify(ctx.root, false);
|
|
1178
1275
|
ctx.selectable.markBounds();
|
|
@@ -1456,6 +1553,15 @@ var commands = {
|
|
|
1456
1553
|
ctx.updateUndo("new");
|
|
1457
1554
|
}
|
|
1458
1555
|
};
|
|
1556
|
+
function runCommand(ctx, name, ...args) {
|
|
1557
|
+
const registry = ctx.commands ?? commands;
|
|
1558
|
+
const fn = registry[name];
|
|
1559
|
+
if (fn) {
|
|
1560
|
+
fn(ctx, ...args);
|
|
1561
|
+
} else {
|
|
1562
|
+
console.error("unrecognized command", name);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1459
1565
|
function executeCommand(ctx, commandString) {
|
|
1460
1566
|
const registry = ctx.commands ?? commands;
|
|
1461
1567
|
const commandList = commandString.split(/;\s*/);
|
|
@@ -1669,7 +1775,7 @@ function insertMenu(editor) {
|
|
|
1669
1775
|
if (!url)
|
|
1670
1776
|
return;
|
|
1671
1777
|
const target = prompt("Target (blank for a new tab)", "_blank");
|
|
1672
|
-
editor.
|
|
1778
|
+
editor.doCommandWith("setLink", url, target || "_blank");
|
|
1673
1779
|
}
|
|
1674
1780
|
},
|
|
1675
1781
|
{
|
|
@@ -1684,7 +1790,7 @@ function insertMenu(editor) {
|
|
|
1684
1790
|
if (!url)
|
|
1685
1791
|
return;
|
|
1686
1792
|
const alt = prompt("Alt text (describe the image)") || "";
|
|
1687
|
-
editor.
|
|
1793
|
+
editor.doCommandWith("insertImage", url, alt);
|
|
1688
1794
|
}
|
|
1689
1795
|
},
|
|
1690
1796
|
null,
|
|
@@ -1821,6 +1927,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
1821
1927
|
cursor: "text",
|
|
1822
1928
|
overflowY: "auto",
|
|
1823
1929
|
position: "relative",
|
|
1930
|
+
touchAction: "manipulation",
|
|
1824
1931
|
transition: "padding-top 0.15s ease-out, padding-bottom 0.15s ease-out"
|
|
1825
1932
|
},
|
|
1826
1933
|
':host [part="doc"] > *': {
|
|
@@ -1870,6 +1977,8 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
1870
1977
|
margin: "0",
|
|
1871
1978
|
border: "0",
|
|
1872
1979
|
outline: "none",
|
|
1980
|
+
fontSize: "16px",
|
|
1981
|
+
lineHeight: "1",
|
|
1873
1982
|
background: "var(--editor-text)",
|
|
1874
1983
|
color: "transparent",
|
|
1875
1984
|
caretColor: "transparent",
|
|
@@ -2149,6 +2258,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2149
2258
|
}
|
|
2150
2259
|
}
|
|
2151
2260
|
commands = { ...commands };
|
|
2261
|
+
sanitize = sanitizeInPlace;
|
|
2152
2262
|
content = [
|
|
2153
2263
|
slot({
|
|
2154
2264
|
name: "menubar",
|
|
@@ -2247,6 +2357,16 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2247
2357
|
this.selectable.focusTarget = this.parts.caret;
|
|
2248
2358
|
this.selectable.onBoundsChanged = () => this.syncCaret();
|
|
2249
2359
|
this.syncCaret();
|
|
2360
|
+
this.parts.doc.addEventListener("scroll", this.handleGeometryChange, {
|
|
2361
|
+
capture: true,
|
|
2362
|
+
passive: true
|
|
2363
|
+
});
|
|
2364
|
+
window.addEventListener("resize", this.handleGeometryChange, {
|
|
2365
|
+
passive: true
|
|
2366
|
+
});
|
|
2367
|
+
this.geometryObserver = new ResizeObserver(this.handleGeometryChange);
|
|
2368
|
+
this.geometryObserver.observe(this.parts.doc, { box: "content-box" });
|
|
2369
|
+
this.geometryObserver.observe(this, { box: "border-box" });
|
|
2250
2370
|
this.applyWidgets();
|
|
2251
2371
|
this.trackBar(this.parts.menubar, "has-menubar");
|
|
2252
2372
|
this.trackBar(this.parts.toolbar, "has-toolbar");
|
|
@@ -2280,7 +2400,19 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2280
2400
|
disconnectedCallback() {
|
|
2281
2401
|
super.disconnectedCallback();
|
|
2282
2402
|
this.selectable?.destroy();
|
|
2403
|
+
this.parts?.doc?.removeEventListener("scroll", this.handleGeometryChange, {
|
|
2404
|
+
capture: true
|
|
2405
|
+
});
|
|
2406
|
+
window.removeEventListener("resize", this.handleGeometryChange);
|
|
2407
|
+
this.geometryObserver?.disconnect();
|
|
2283
2408
|
}
|
|
2409
|
+
geometryObserver = null;
|
|
2410
|
+
handleGeometryChange = () => {
|
|
2411
|
+
this.syncCaret();
|
|
2412
|
+
if (this.touchAffordances?.style.display === "block") {
|
|
2413
|
+
this.positionAffordances();
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2284
2416
|
markerRect(marker) {
|
|
2285
2417
|
const geometry = caretGeometryAt(marker, this.parts.doc);
|
|
2286
2418
|
if (geometry) {
|
|
@@ -2303,7 +2435,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2303
2435
|
const start = doc.querySelector(".sel-start");
|
|
2304
2436
|
const end = doc.querySelector(".sel-end");
|
|
2305
2437
|
const anchor = end || start;
|
|
2306
|
-
const collapsed = doc.
|
|
2438
|
+
const collapsed = doc.querySelector(".selected") === null;
|
|
2307
2439
|
const host = this.getBoundingClientRect();
|
|
2308
2440
|
const view = doc.getBoundingClientRect();
|
|
2309
2441
|
const hostStyle = getComputedStyle(this);
|
|
@@ -2352,6 +2484,9 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2352
2484
|
doCommand(commandString) {
|
|
2353
2485
|
executeCommand(this.getContext(), commandString);
|
|
2354
2486
|
}
|
|
2487
|
+
doCommandWith(name, ...args) {
|
|
2488
|
+
runCommand(this.getContext(), name, ...args);
|
|
2489
|
+
}
|
|
2355
2490
|
focus() {
|
|
2356
2491
|
this.selectable?.focus();
|
|
2357
2492
|
}
|
|
@@ -2368,7 +2503,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2368
2503
|
return Array.from(this.parts.doc.querySelectorAll(".selected-block"));
|
|
2369
2504
|
}
|
|
2370
2505
|
insertionPoint() {
|
|
2371
|
-
return this.parts.doc.querySelector("
|
|
2506
|
+
return this.parts.doc.querySelector(".sel-end");
|
|
2372
2507
|
}
|
|
2373
2508
|
block(node) {
|
|
2374
2509
|
let current = node;
|
|
@@ -2394,10 +2529,9 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
2394
2529
|
moveCaretToCell(cell) {
|
|
2395
2530
|
this.selectable.unmark();
|
|
2396
2531
|
this.selectable.removeBounds();
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
const
|
|
2400
|
-
end.className = "sel-end caret";
|
|
2532
|
+
const bounds = this.selectable.createBounds();
|
|
2533
|
+
const end = bounds.lastChild;
|
|
2534
|
+
const start = bounds.firstChild;
|
|
2401
2535
|
cell.insertBefore(start, cell.firstChild);
|
|
2402
2536
|
cell.appendChild(end);
|
|
2403
2537
|
this.selectable.normalize();
|
|
@@ -3171,14 +3305,18 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3171
3305
|
if (!link || !this.parts.doc.contains(link))
|
|
3172
3306
|
return;
|
|
3173
3307
|
if (evt.metaKey || evt.ctrlKey) {
|
|
3174
|
-
|
|
3308
|
+
const href = link.getAttribute("href") || "";
|
|
3309
|
+
if (isSafeNavigationUrl(href)) {
|
|
3310
|
+
window.open(href, "_blank", "noopener");
|
|
3311
|
+
}
|
|
3175
3312
|
}
|
|
3176
3313
|
evt.preventDefault();
|
|
3177
3314
|
};
|
|
3178
3315
|
handleShortcut(evt) {
|
|
3179
3316
|
const key = evt.key.toLowerCase();
|
|
3180
3317
|
const shortcutStr = `${evt.ctrlKey || evt.metaKey ? "ctrl+" : ""}${key}`;
|
|
3181
|
-
const
|
|
3318
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(shortcutStr) : shortcutStr.replace(/["\\]/g, "\\$&");
|
|
3319
|
+
const btn = this.parts.toolbar.querySelector(`[data-shortcut="${escaped}"]`);
|
|
3182
3320
|
if (btn) {
|
|
3183
3321
|
const value = btn.getAttribute("value");
|
|
3184
3322
|
if (value) {
|
|
@@ -3388,6 +3526,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3388
3526
|
} else {
|
|
3389
3527
|
const temp = document.createElement("div");
|
|
3390
3528
|
temp.innerHTML = html;
|
|
3529
|
+
this.sanitize(temp);
|
|
3391
3530
|
while (temp.firstChild) {
|
|
3392
3531
|
ip.before(temp.firstChild);
|
|
3393
3532
|
}
|
|
@@ -3481,7 +3620,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3481
3620
|
reader.readAsDataURL(file);
|
|
3482
3621
|
});
|
|
3483
3622
|
if (url)
|
|
3484
|
-
this.
|
|
3623
|
+
this.doCommandWith("insertImage", url, file.name);
|
|
3485
3624
|
}
|
|
3486
3625
|
}
|
|
3487
3626
|
cellEdgeAt(evt) {
|
|
@@ -3511,6 +3650,7 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3511
3650
|
this.touchAffordances.style.display = "none";
|
|
3512
3651
|
this.parts.doc.style.paddingTop = "";
|
|
3513
3652
|
this.parts.doc.style.paddingBottom = "";
|
|
3653
|
+
this.syncCaret();
|
|
3514
3654
|
return;
|
|
3515
3655
|
}
|
|
3516
3656
|
const selStart = this.parts.doc.querySelector(".sel-start");
|
|
@@ -3519,12 +3659,12 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3519
3659
|
this.touchAffordances.style.display = "none";
|
|
3520
3660
|
this.parts.doc.style.paddingTop = "";
|
|
3521
3661
|
this.parts.doc.style.paddingBottom = "";
|
|
3662
|
+
this.syncCaret();
|
|
3522
3663
|
return;
|
|
3523
3664
|
}
|
|
3524
3665
|
const docRect = this.parts.doc.getBoundingClientRect();
|
|
3525
3666
|
const startRect = this.markerRect(selStart);
|
|
3526
3667
|
const endRect = this.markerRect(selEnd);
|
|
3527
|
-
let needsPaddingChange = false;
|
|
3528
3668
|
if (this.touchDrags.size === 0) {
|
|
3529
3669
|
const existingTopPad = parseFloat(this.parts.doc.style.paddingTop) || 0;
|
|
3530
3670
|
const existingBottomPad = parseFloat(this.parts.doc.style.paddingBottom) || 0;
|
|
@@ -3535,23 +3675,13 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3535
3675
|
const hadTop = this.parts.doc.style.paddingTop;
|
|
3536
3676
|
const hadBottom = this.parts.doc.style.paddingBottom;
|
|
3537
3677
|
if (wantTop !== hadTop || wantBottom !== hadBottom) {
|
|
3538
|
-
needsPaddingChange = true;
|
|
3539
3678
|
this.parts.doc.style.paddingTop = wantTop;
|
|
3540
3679
|
this.parts.doc.style.paddingBottom = wantBottom;
|
|
3541
3680
|
}
|
|
3542
3681
|
}
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
setTimeout(() => {
|
|
3547
|
-
this.positionAffordances();
|
|
3548
|
-
this.touchAffordances.style.opacity = "1";
|
|
3549
|
-
}, 160);
|
|
3550
|
-
} else {
|
|
3551
|
-
this.positionAffordances();
|
|
3552
|
-
this.touchAffordances.style.display = "block";
|
|
3553
|
-
this.touchAffordances.style.opacity = "1";
|
|
3554
|
-
}
|
|
3682
|
+
this.positionAffordances();
|
|
3683
|
+
this.touchAffordances.style.display = "block";
|
|
3684
|
+
this.touchAffordances.style.opacity = "1";
|
|
3555
3685
|
}
|
|
3556
3686
|
positionAffordances() {
|
|
3557
3687
|
const selStart = this.parts.doc.querySelector(".sel-start");
|
|
@@ -3672,9 +3802,9 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3672
3802
|
this.hideTouchMenu();
|
|
3673
3803
|
return;
|
|
3674
3804
|
}
|
|
3675
|
-
this.showTouchMenu();
|
|
3805
|
+
this.showTouchMenu(evt);
|
|
3676
3806
|
};
|
|
3677
|
-
showTouchMenu() {
|
|
3807
|
+
showTouchMenu(openedBy) {
|
|
3678
3808
|
this.hideTouchMenu();
|
|
3679
3809
|
const menu = document.createElement("div");
|
|
3680
3810
|
menu.className = "touch-menu not-selectable do-not-spanify";
|
|
@@ -3771,13 +3901,13 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3771
3901
|
this.touchMenuEl = menu;
|
|
3772
3902
|
this.touchAffordances.appendChild(menu);
|
|
3773
3903
|
this.touchMenuDismiss = (e) => {
|
|
3904
|
+
if (e === openedBy)
|
|
3905
|
+
return;
|
|
3774
3906
|
if (this.touchMenuEl && !this.touchMenuEl.contains(e.target)) {
|
|
3775
3907
|
this.hideTouchMenu();
|
|
3776
3908
|
}
|
|
3777
3909
|
};
|
|
3778
|
-
|
|
3779
|
-
this.parts.doc.addEventListener("pointerdown", this.touchMenuDismiss);
|
|
3780
|
-
}, 0);
|
|
3910
|
+
this.parts.doc.addEventListener("pointerdown", this.touchMenuDismiss);
|
|
3781
3911
|
}
|
|
3782
3912
|
touchMenuDismiss = null;
|
|
3783
3913
|
hideTouchMenu() {
|
|
@@ -3848,14 +3978,17 @@ class TosijsStyledEditor extends WebComponent {
|
|
|
3848
3978
|
if (!select)
|
|
3849
3979
|
return;
|
|
3850
3980
|
const value = select.value;
|
|
3851
|
-
if (value)
|
|
3852
|
-
|
|
3853
|
-
|
|
3981
|
+
if (!value)
|
|
3982
|
+
return;
|
|
3983
|
+
const name = value.trim().split(/[\s;]/)[0];
|
|
3984
|
+
if (!name || !(name in this.commands))
|
|
3985
|
+
return;
|
|
3986
|
+
this.doCommand(value);
|
|
3854
3987
|
};
|
|
3855
3988
|
}
|
|
3856
3989
|
var tosijsStyledEditor = TosijsStyledEditor.elementCreator();
|
|
3857
3990
|
// src/version.ts
|
|
3858
|
-
var version = "0.4.
|
|
3991
|
+
var version = "0.4.4";
|
|
3859
3992
|
export {
|
|
3860
3993
|
MENU_CLASS,
|
|
3861
3994
|
Selectable,
|
|
@@ -3889,6 +4022,7 @@ export {
|
|
|
3889
4022
|
insertMenu,
|
|
3890
4023
|
isBefore,
|
|
3891
4024
|
isHeaderCell,
|
|
4025
|
+
isSafeNavigationUrl,
|
|
3892
4026
|
isTableCell,
|
|
3893
4027
|
justificationMenu,
|
|
3894
4028
|
lastLeafNode,
|
|
@@ -3906,6 +4040,8 @@ export {
|
|
|
3906
4040
|
previousLeafNode,
|
|
3907
4041
|
renumberFootnotes,
|
|
3908
4042
|
rowOfCell,
|
|
4043
|
+
runCommand,
|
|
4044
|
+
sanitizeInPlace,
|
|
3909
4045
|
setColumnWidths,
|
|
3910
4046
|
siblingOrder,
|
|
3911
4047
|
spanify,
|
|
@@ -59,6 +59,35 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
59
59
|
set value(html: string);
|
|
60
60
|
/** The editable commands — extend this object to add custom commands */
|
|
61
61
|
commands: Record<string, Command>;
|
|
62
|
+
/**
|
|
63
|
+
* Sanitizer applied to pasted and dropped content, before it enters the
|
|
64
|
+
* document. Replace it to use a different one:
|
|
65
|
+
*
|
|
66
|
+
* ```js
|
|
67
|
+
* editor.sanitize = (root) => {
|
|
68
|
+
* DOMPurify.sanitize(root, {
|
|
69
|
+
* IN_PLACE: true,
|
|
70
|
+
* FORBID_TAGS: ['style'],
|
|
71
|
+
* CUSTOM_ELEMENT_HANDLING: {
|
|
72
|
+
* tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,
|
|
73
|
+
* attributeNameCheck: /^data-|^slot$|^dir$/,
|
|
74
|
+
* },
|
|
75
|
+
* })
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* It takes a detached ELEMENT and mutates it, rather than taking and
|
|
80
|
+
* returning an HTML string, deliberately: a string signature would force a
|
|
81
|
+
* serialize-and-reparse round trip, and that round trip is where mutation
|
|
82
|
+
* XSS lives — markup that is inert when parsed once can become executable
|
|
83
|
+
* when re-parsed from its own serialization. DOMPurify's `IN_PLACE: true`
|
|
84
|
+
* has the same shape for the same reason.
|
|
85
|
+
*
|
|
86
|
+
* The default is `sanitizeInPlace`. Setting this to a no-op disables
|
|
87
|
+
* sanitization; see the security section of the README for what is and is
|
|
88
|
+
* not covered.
|
|
89
|
+
*/
|
|
90
|
+
sanitize: (root: Element) => void;
|
|
62
91
|
content: (HTMLDivElement | HTMLSlotElement)[];
|
|
63
92
|
formResetCallback(): void;
|
|
64
93
|
connectedCallback(): void;
|
|
@@ -70,6 +99,17 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
70
99
|
*/
|
|
71
100
|
private applyWidgets;
|
|
72
101
|
disconnectedCallback(): void;
|
|
102
|
+
private geometryObserver;
|
|
103
|
+
/**
|
|
104
|
+
* Repaint everything positioned over the document.
|
|
105
|
+
*
|
|
106
|
+
* Deliberately NOT coalesced with requestAnimationFrame. Measured: one
|
|
107
|
+
* repaint costs 0.015ms — 0.09% of a frame — and the browser already
|
|
108
|
+
* delivers scroll at most once per frame (60 events across 60 frames). So
|
|
109
|
+
* rAF bought no work reduction and cost a frame of latency, which shows up
|
|
110
|
+
* as the caret trailing the text while scrolling.
|
|
111
|
+
*/
|
|
112
|
+
private handleGeometryChange;
|
|
73
113
|
/**
|
|
74
114
|
* Position the caret (and, for an expanded selection, its two edges) over the
|
|
75
115
|
* document from the inert markers' rects. Nothing here is in the text, so
|
|
@@ -90,14 +130,30 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
90
130
|
private getContext;
|
|
91
131
|
/** Execute a command string */
|
|
92
132
|
doCommand(commandString: string): void;
|
|
133
|
+
/**
|
|
134
|
+
* Run one command with pre-separated arguments.
|
|
135
|
+
*
|
|
136
|
+
* Use this whenever an argument is a runtime value (a URL, a filename): the
|
|
137
|
+
* string form splits on `;` and whitespace, so those values can inject a
|
|
138
|
+
* second command — and a data URI contains `;` by spec.
|
|
139
|
+
*/
|
|
140
|
+
doCommandWith(name: string, ...args: string[]): void;
|
|
93
141
|
focus(): void;
|
|
94
142
|
normalize(): void;
|
|
95
143
|
/** Get all leaf nodes in the current selection */
|
|
96
144
|
selectedLeafNodes(): Node[];
|
|
97
145
|
/** Get the selected blocks */
|
|
98
146
|
selectedBlocks(): Element[];
|
|
99
|
-
/**
|
|
100
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The caret marker, if the doc has one.
|
|
149
|
+
*
|
|
150
|
+
* This selected `input.caret` until the markers stopped being `<input>`
|
|
151
|
+
* elements, after which it matched nothing and returned null forever —
|
|
152
|
+
* silently disabling every command that inserts at the caret
|
|
153
|
+
* (insertFootnote, insertTable, insertImage, setLink and the rest), because
|
|
154
|
+
* they all bail on a null insertion point.
|
|
155
|
+
*/
|
|
156
|
+
insertionPoint(): HTMLElement | null;
|
|
101
157
|
/** Get the top-level block containing a node */
|
|
102
158
|
block(node: Node): Element | null;
|
|
103
159
|
/** Find the table cell containing the caret, if any */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.4.
|
|
1
|
+
export declare const version = "0.4.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tosijs-styled-editor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Rich text editor web component — no contentEditable, no browser selection APIs",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
"dist",
|
|
27
27
|
"!dist/*.tsbuildinfo",
|
|
28
28
|
"README.md",
|
|
29
|
+
"CHANGELOG.md",
|
|
29
30
|
"LICENSE"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"start": "bun bin/site.ts",
|
|
33
34
|
"make": "bun bin/site.ts --build",
|
|
35
|
+
"prepublishOnly": "bun run make && bun test && bun run lint",
|
|
34
36
|
"test": "bun test",
|
|
35
37
|
"lint": "bun tsc --noEmit --noUnusedLocals --noUnusedParameters",
|
|
36
38
|
"format": "bun prettier --write .",
|