@richscripts/richtexteditor 2.14.0 → 2.14.1
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/package.json +1 -1
- package/richtexteditor/all_plugins.js +113 -34
- package/richtexteditor/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/all_plugins.js +113 -34
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/documentoutline.js +41 -30
- package/richtexteditor/plugins/documentoutline.min.js +1 -1
- package/richtexteditor/plugins/formattingmarks.js +48 -3
- package/richtexteditor/plugins/formattingmarks.min.js +1 -1
- package/richtexteditor/plugins/sanitizer.js +24 -1
- package/richtexteditor/plugins/sanitizer.min.js +1 -1
- package/richtexteditor/rte-config.js +1 -0
- package/richtexteditor/rte.js +3 -2
- package/richtexteditor/runtime/colorpicker_more_ns.htm +93 -16
|
@@ -25526,36 +25526,47 @@ function RTE_Plugin_DocumentOutline() {
|
|
|
25526
25526
|
// valid and the pasted copy becomes "slug-2". Ids on non-heading elements are
|
|
25527
25527
|
// reserved too, so a heading never takes one. Every heading is checked, including
|
|
25528
25528
|
// levels the outline does not display, since they share one id namespace.
|
|
25529
|
-
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
//
|
|
25535
|
-
//
|
|
25536
|
-
//
|
|
25537
|
-
// and
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
25555
|
-
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25529
|
+
//
|
|
25530
|
+
// 2026-09-25 ...but only when the core does not own heading ids. With
|
|
25531
|
+
// autoHeadingAnchors on (the default) the core pass gives every heading a unique id
|
|
25532
|
+
// after each change, with the same first-keeps rule, and marks the ids it generated so
|
|
25533
|
+
// they can follow the text. This block ran 80 ms after each change instead: a heading
|
|
25534
|
+
// a person was typing got the slug of its FIRST letter ("Typed heading" -> id="t"),
|
|
25535
|
+
// unmarked, so the core took it for the author's own id and kept it for good. The
|
|
25536
|
+
// outline needs no id to work (it navigates by element), so it now leaves ids to the
|
|
25537
|
+
// core and keeps this pass for integrations that switched core anchors off.
|
|
25538
|
+
if (config.autoHeadingAnchors === false) {
|
|
25539
|
+
var used = {};
|
|
25540
|
+
var withId = editable.querySelectorAll("[id]");
|
|
25541
|
+
for (i = 0; i < withId.length; i++) {
|
|
25542
|
+
if (!/^H[1-6]$/.test(withId[i].nodeName)) used[withId[i].id] = true;
|
|
25543
|
+
}
|
|
25544
|
+
// Headings with text claim ids first. An EMPTY heading is usually the shell a cut
|
|
25545
|
+
// leaves behind, still carrying the moved heading's id; letting it claim first would
|
|
25546
|
+
// hand the moved heading "-2" and break every link to it. So empty headings go last
|
|
25547
|
+
// and simply drop an id that a real heading already holds.
|
|
25548
|
+
var textOf = function (el) {
|
|
25549
|
+
return (el.innerText || el.textContent || "").replace(/\s+/g, " ").replace(/^\s+|\s+$/g, "");
|
|
25550
|
+
};
|
|
25551
|
+
var empties = [];
|
|
25552
|
+
for (i = 0; i < headings.length; i++) {
|
|
25553
|
+
var h = headings[i];
|
|
25554
|
+
var t = textOf(h);
|
|
25555
|
+
if (!t) { empties.push(h); continue; }
|
|
25556
|
+
var base = h.id;
|
|
25557
|
+
if (!base) base = slugifyHeading(t);
|
|
25558
|
+
else if (!used[base]) { used[base] = true; continue; }
|
|
25559
|
+
var unique = base;
|
|
25560
|
+
var n = 2;
|
|
25561
|
+
while (used[unique]) { unique = base + "-" + n; n++; }
|
|
25562
|
+
used[unique] = true;
|
|
25563
|
+
if (h.id !== unique) h.id = unique;
|
|
25564
|
+
}
|
|
25565
|
+
for (i = 0; i < empties.length; i++) {
|
|
25566
|
+
if (!empties[i].id) continue;
|
|
25567
|
+
if (used[empties[i].id]) empties[i].removeAttribute("id");
|
|
25568
|
+
else used[empties[i].id] = true;
|
|
25569
|
+
}
|
|
25559
25570
|
}
|
|
25560
25571
|
for (i = 0; i < headings.length; i++) {
|
|
25561
25572
|
var heading = headings[i];
|
|
@@ -28477,22 +28488,67 @@ function RTE_Plugin_FormattingMarks() {
|
|
|
28477
28488
|
// may be deliberate.
|
|
28478
28489
|
if (parent.nodeName === "CODE" || parent.nodeName === "PRE") return;
|
|
28479
28490
|
|
|
28491
|
+
// Replacing the text node moves a caret inside it to the node's old place in its
|
|
28492
|
+
// parent - usually the paragraph start. This runs on every keyup, and the first
|
|
28493
|
+
// space typed at a line end is always a non-breaking one, so with marks on every
|
|
28494
|
+
// later character landed at the start, reversed: " edited" became "detide".
|
|
28495
|
+
// Remember where the selection was inside this node and put it back.
|
|
28496
|
+
var sel = doc.getSelection ? doc.getSelection() : null;
|
|
28497
|
+
var range = sel && sel.rangeCount ? sel.getRangeAt(0) : null;
|
|
28498
|
+
var keepStart = range && range.startContainer === node ? range.startOffset : -1;
|
|
28499
|
+
var keepEnd = range && range.endContainer === node ? range.endOffset : -1;
|
|
28500
|
+
|
|
28480
28501
|
var frag = doc.createDocumentFragment();
|
|
28502
|
+
var pieces = []; // in order: { node, marker } - every marker stands for ONE character
|
|
28481
28503
|
var buf = "";
|
|
28482
28504
|
for (var i = 0; i < text.length; i++) {
|
|
28483
28505
|
var def = chars[text[i]];
|
|
28506
|
+
// A space typed at a line end is inserted as a non-breaking placeholder that the
|
|
28507
|
+
// browser turns back into a space when the next character arrives. Wrapped in a
|
|
28508
|
+
// non-editable marker it never could, and saved as a hard . Leave the one
|
|
28509
|
+
// right before the caret alone; it is marked once the caret moves on.
|
|
28510
|
+
if (def && text[i] === " " && i === keepStart - 1 && keepStart === keepEnd) def = null;
|
|
28484
28511
|
if (!def) { buf += text[i]; continue; }
|
|
28485
|
-
if (buf) { frag.appendChild(doc.createTextNode(buf)); buf = ""; }
|
|
28512
|
+
if (buf) { pieces.push({ node: frag.appendChild(doc.createTextNode(buf)), marker: false }); buf = ""; }
|
|
28486
28513
|
var span = doc.createElement("span");
|
|
28487
28514
|
span.setAttribute("data-rte-fm", "1");
|
|
28488
28515
|
span.setAttribute("data-rte-fm-char", text[i]);
|
|
28489
28516
|
span.className = def.cls;
|
|
28490
28517
|
span.setAttribute("contenteditable", "false");
|
|
28491
28518
|
span.textContent = def.label;
|
|
28492
|
-
frag.appendChild(span);
|
|
28519
|
+
pieces.push({ node: frag.appendChild(span), marker: true });
|
|
28493
28520
|
}
|
|
28494
|
-
if (buf) frag.appendChild(doc.createTextNode(buf));
|
|
28521
|
+
if (buf) pieces.push({ node: frag.appendChild(doc.createTextNode(buf)), marker: false });
|
|
28495
28522
|
parent.replaceChild(frag, node);
|
|
28523
|
+
|
|
28524
|
+
if (keepStart < 0 && keepEnd < 0) return;
|
|
28525
|
+
// Offset k in the old text -> a point in the pieces. Inside text where possible;
|
|
28526
|
+
// never inside a marker (it is not editable): just before or after it.
|
|
28527
|
+
function pointAt(k) {
|
|
28528
|
+
var pos = 0;
|
|
28529
|
+
for (var p = 0; p < pieces.length; p++) {
|
|
28530
|
+
var piece = pieces[p];
|
|
28531
|
+
if (piece.marker) {
|
|
28532
|
+
if (k <= pos) return { node: parent, offset: Array.prototype.indexOf.call(parent.childNodes, piece.node) };
|
|
28533
|
+
pos += 1;
|
|
28534
|
+
} else {
|
|
28535
|
+
var len = piece.node.nodeValue.length;
|
|
28536
|
+
if (k <= pos + len) return { node: piece.node, offset: k - pos };
|
|
28537
|
+
pos += len;
|
|
28538
|
+
}
|
|
28539
|
+
}
|
|
28540
|
+
var last = pieces[pieces.length - 1].node;
|
|
28541
|
+
return { node: parent, offset: Array.prototype.indexOf.call(parent.childNodes, last) + 1 };
|
|
28542
|
+
}
|
|
28543
|
+
try {
|
|
28544
|
+
var r = doc.createRange();
|
|
28545
|
+
var a = pointAt(keepStart >= 0 ? keepStart : 0);
|
|
28546
|
+
var b = keepEnd >= 0 ? pointAt(keepEnd) : a;
|
|
28547
|
+
if (keepStart < 0) { r.setStart(range.startContainer, range.startOffset); } else r.setStart(a.node, a.offset);
|
|
28548
|
+
if (keepEnd < 0) { r.setEnd(range.endContainer, range.endOffset); } else r.setEnd(b.node, b.offset);
|
|
28549
|
+
sel.removeAllRanges();
|
|
28550
|
+
sel.addRange(r);
|
|
28551
|
+
} catch (e) { /* a selection we cannot restore is no reason to lose the marks */ }
|
|
28496
28552
|
}
|
|
28497
28553
|
|
|
28498
28554
|
// ---- serialization safety -------------------------------------------
|
|
@@ -40277,6 +40333,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
40277
40333
|
}
|
|
40278
40334
|
|
|
40279
40335
|
var attrs = [];
|
|
40336
|
+
var strippedSrc = false;
|
|
40280
40337
|
for (var a = 0; a < el.attributes.length; a++) attrs.push(el.attributes[a]);
|
|
40281
40338
|
for (var k = 0; k < attrs.length; k++) {
|
|
40282
40339
|
var name = attrs[k].name.toLowerCase();
|
|
@@ -40294,7 +40351,11 @@ function RTE_Plugin_Sanitizer() {
|
|
|
40294
40351
|
if (name.indexOf("data-") === 0 || name.indexOf("aria-") === 0) continue;
|
|
40295
40352
|
|
|
40296
40353
|
if (URL_ATTRS[name]) {
|
|
40297
|
-
if (!isSafeUrl(value, tag, name)) {
|
|
40354
|
+
if (!isSafeUrl(value, tag, name)) {
|
|
40355
|
+
note(report.removedAttributes, tag + "[" + name + "]");
|
|
40356
|
+
el.removeAttribute(attrs[k].name);
|
|
40357
|
+
if (name === "src") strippedSrc = true;
|
|
40358
|
+
}
|
|
40298
40359
|
continue;
|
|
40299
40360
|
}
|
|
40300
40361
|
if (!ALLOWED_ATTRS[name] && !attrExtra(name)) {
|
|
@@ -40303,6 +40364,18 @@ function RTE_Plugin_Sanitizer() {
|
|
|
40303
40364
|
}
|
|
40304
40365
|
}
|
|
40305
40366
|
|
|
40367
|
+
// An <img> whose only source was just stripped is an empty box that
|
|
40368
|
+
// saves as <img> with no src -- dragging a page image into the editor
|
|
40369
|
+
// from a page opened on disk did exactly that (its file: src is not a
|
|
40370
|
+
// safe scheme). Drop the element when nothing is left to show. Keep it
|
|
40371
|
+
// when srcset or a lazy-load data-*src still carries an image, and never
|
|
40372
|
+
// touch an <img> that arrived without a src (an app placeholder).
|
|
40373
|
+
if (tag === "img" && strippedSrc && !el.hasAttribute("srcset") && !hasDataSource(el)) {
|
|
40374
|
+
note(report.removedTags, "img");
|
|
40375
|
+
el.parentNode.removeChild(el);
|
|
40376
|
+
continue;
|
|
40377
|
+
}
|
|
40378
|
+
|
|
40306
40379
|
// A link that opens a new tab without rel="noopener" hands the
|
|
40307
40380
|
// opener window to the destination.
|
|
40308
40381
|
if (tag === "a" && (el.getAttribute("target") || "").toLowerCase() === "_blank") {
|
|
@@ -40319,6 +40392,12 @@ function RTE_Plugin_Sanitizer() {
|
|
|
40319
40392
|
parent.removeChild(el);
|
|
40320
40393
|
}
|
|
40321
40394
|
function note(list, what) { if (list.indexOf(what) < 0) list.push(what); }
|
|
40395
|
+
function hasDataSource(el) {
|
|
40396
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
40397
|
+
if (/^data-(.*-)?(src|srcset|original)$/i.test(el.attributes[i].name)) return true;
|
|
40398
|
+
}
|
|
40399
|
+
return false;
|
|
40400
|
+
}
|
|
40322
40401
|
|
|
40323
40402
|
function sanitizeHtml(html) {
|
|
40324
40403
|
var out = html;
|