@richscripts/richtexteditor 2.6.3 → 2.7.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 +21 -1
- package/angular/component.ts +16 -3
- package/integrations/shared.js +27 -0
- package/package.json +2 -2
- package/react/index.d.ts +7 -0
- package/react/index.js +25 -3
- package/richtexteditor/CHANGELOG.md +87 -0
- package/richtexteditor/all_plugins.js +721 -53
- package/richtexteditor/all_plugins.min.js +59 -59
- package/richtexteditor/lang/rte-lang-de.js +96 -0
- package/richtexteditor/lang/rte-lang-zh-Hans.js +181 -0
- package/richtexteditor/lang/rte-lang-zh-Hant.js +181 -0
- package/richtexteditor/plugins/all_plugins.js +721 -53
- package/richtexteditor/plugins/all_plugins.min.js +59 -59
- package/richtexteditor/plugins/comments.js +11 -1
- package/richtexteditor/plugins/crdt-engine.js +244 -17
- package/richtexteditor/plugins/crdt-engine.min.js +5 -5
- package/richtexteditor/plugins/documentimport.js +31 -13
- package/richtexteditor/plugins/docxexport.js +31 -0
- package/richtexteditor/plugins/inlinecode.js +270 -0
- package/richtexteditor/plugins/insertcode.js +2 -2
- package/richtexteditor/plugins/pdfexport.js +25 -2
- package/richtexteditor/plugins/readabilitystats.js +6 -11
- package/richtexteditor/plugins/sanitizer.js +136 -10
- package/richtexteditor/plugins/shadowdom.js +11 -1
- package/richtexteditor/plugins/slashcommand.js +78 -7
- package/richtexteditor/plugins/wordexport.js +3 -1
- package/richtexteditor/plugins/yjscollab.js +116 -5
- package/richtexteditor/rte-webcomponent.js +15 -0
- package/richtexteditor/rte.js +15 -3
- package/richtexteditor/rte_theme_default.css +13 -4
- package/richtexteditor/rte_theme_default.min.css +1 -1
- package/vue/index.js +12 -2
|
@@ -21718,7 +21718,17 @@ function RTE_Plugin_Comments() {
|
|
|
21718
21718
|
|
|
21719
21719
|
var id = "cmt-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
21720
21720
|
var user = getCurrentUser();
|
|
21721
|
-
var anchor = options.anchor;
|
|
21721
|
+
var anchor = options.anchor || options.range || null;
|
|
21722
|
+
// Accept a plain DOM Range (or Range-like) as the anchor: it has the
|
|
21723
|
+
// container/offset fields but no `.text`, so until 2026-09-02 the common
|
|
21724
|
+
// call add({ anchor: range }) silently recorded the comment WITHOUT a
|
|
21725
|
+
// highlight. Derive the text from the range instead.
|
|
21726
|
+
if (anchor && anchor.startContainer && anchor.endContainer && !anchor.text) {
|
|
21727
|
+
var derived = "";
|
|
21728
|
+
try { derived = typeof anchor.toString === "function" ? String(anchor.toString()) : ""; } catch (e) { derived = ""; }
|
|
21729
|
+
if (!derived) { try { derived = anchor.cloneContents ? (anchor.cloneContents().textContent || "") : ""; } catch (e2) { derived = ""; } }
|
|
21730
|
+
anchor = { startContainer: anchor.startContainer, startOffset: anchor.startOffset, endContainer: anchor.endContainer, endOffset: anchor.endOffset, text: derived };
|
|
21731
|
+
}
|
|
21722
21732
|
var wrappedText = "";
|
|
21723
21733
|
|
|
21724
21734
|
if (anchor && anchor.startContainer && anchor.endContainer && anchor.text) {
|
|
@@ -23437,17 +23447,12 @@ function RTE_Plugin_DocumentImport() {
|
|
|
23437
23447
|
openPicker({});
|
|
23438
23448
|
});
|
|
23439
23449
|
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
keywords: ["import", "open", "file", "word", "markdown", "upload"],
|
|
23447
|
-
action: function () { openPicker({}); }
|
|
23448
|
-
});
|
|
23449
|
-
} catch (e) {}
|
|
23450
|
-
}
|
|
23450
|
+
// NOTE: do NOT call editor.slashCommands.register() from here. Plugins
|
|
23451
|
+
// initialise in bundle (alphabetical) order and this file sorts before
|
|
23452
|
+
// "slashcommand", so editor.slashCommands does not exist yet and the guarded
|
|
23453
|
+
// call silently did nothing - the "import-document" slash entry never appeared for any
|
|
23454
|
+
// customer until 2026-09-02. The entry now lives in slashcommand.js, gated on
|
|
23455
|
+
// typeof editor.openImportDialog === "function" (same pattern as exportToPdf).
|
|
23451
23456
|
};
|
|
23452
23457
|
|
|
23453
23458
|
function kindFromName(name) {
|
|
@@ -24515,6 +24520,19 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24515
24520
|
// matches the old behaviour rather than inventing <ol>s.
|
|
24516
24521
|
info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
|
|
24517
24522
|
}
|
|
24523
|
+
// Word paragraph indentation. Nothing read this before 2026-09-04,
|
|
24524
|
+
// so every indented paragraph in every imported .docx arrived
|
|
24525
|
+
// flush left and the indentation was lost silently.
|
|
24526
|
+
// w:left is in twips; 15 twips = 1px.
|
|
24527
|
+
var ind = firstChildEl(ppr, "ind");
|
|
24528
|
+
if (ind) {
|
|
24529
|
+
var lft = ind.getAttributeNS(W, "left") || ind.getAttribute("w:left") || "";
|
|
24530
|
+
var tw = parseInt(lft, 10);
|
|
24531
|
+
// 720 twips alongside the Quote style is the blockquote's own
|
|
24532
|
+
// indent, already carried by the blockquote element itself.
|
|
24533
|
+
if (tw > 0 && !(info.quote && tw === 720))
|
|
24534
|
+
info.indentPx = Math.round(tw / 15);
|
|
24535
|
+
}
|
|
24518
24536
|
var jc = firstChildEl(ppr, "jc");
|
|
24519
24537
|
if (jc) {
|
|
24520
24538
|
var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
|
|
@@ -24581,10 +24599,20 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24581
24599
|
continue;
|
|
24582
24600
|
}
|
|
24583
24601
|
closeListsTo(0);
|
|
24584
|
-
if (st.heading)
|
|
24602
|
+
if (st.heading) {
|
|
24603
|
+
// Headings carry w:ind too; without this an indented
|
|
24604
|
+
// heading imported flush left.
|
|
24605
|
+
var hs = st.indentPx ? ' style="margin-left:' + st.indentPx + 'px"' : "";
|
|
24606
|
+
out.push("<h" + st.heading + hs + ">" + (inner || "") + "</h" + st.heading + ">");
|
|
24607
|
+
}
|
|
24585
24608
|
else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
|
|
24586
24609
|
else {
|
|
24587
|
-
var
|
|
24610
|
+
var decl = [];
|
|
24611
|
+
if (st.align) decl.push("text-align:" + st.align);
|
|
24612
|
+
// indentUseMargin defaults to margin, and margin is what
|
|
24613
|
+
// w:ind means, so import writes margin-left to match.
|
|
24614
|
+
if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
|
|
24615
|
+
var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
|
|
24588
24616
|
// A paragraph that contained ONLY a text box has no text of
|
|
24589
24617
|
// its own; emitting an empty <p> before the box adds a blank
|
|
24590
24618
|
// line that was never in the document.
|
|
@@ -25607,6 +25635,31 @@ function RTE_Plugin_DocxExport() {
|
|
|
25607
25635
|
var revSeq = { n: 1 };
|
|
25608
25636
|
var base = { bold: false, italic: false, underline: false, strike: false, sizeHalfPoints: 0, color: null, highlight: null, vertAlign: null, mono: false };
|
|
25609
25637
|
|
|
25638
|
+
|
|
25639
|
+
// A block indented in the editor carries margin-left (or padding-left,
|
|
25640
|
+
// per indentUseMargin). Word expresses the same thing as w:ind, in
|
|
25641
|
+
// TWIPS. Before 2026-09-04 indentation reached Word only when it was
|
|
25642
|
+
// spelled <blockquote>, which dragged the Quote style along with it -
|
|
25643
|
+
// so indented body text arrived italic and accent-coloured.
|
|
25644
|
+
// 1px = 0.75pt and 1pt = 20 twips, hence px * 15.
|
|
25645
|
+
function indentTwips(node) {
|
|
25646
|
+
if (!node || !node.style) return 0;
|
|
25647
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
25648
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
25649
|
+
if (!m) return 0;
|
|
25650
|
+
var n = parseFloat(m[1]);
|
|
25651
|
+
if (!n || n <= 0) return 0;
|
|
25652
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
25653
|
+
var px = unit === "px" ? n
|
|
25654
|
+
: unit === "pt" ? n * (4 / 3)
|
|
25655
|
+
: unit === "in" ? n * 96
|
|
25656
|
+
: unit === "cm" ? n * 37.795
|
|
25657
|
+
: unit === "mm" ? n * 3.7795
|
|
25658
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
25659
|
+
: 0;
|
|
25660
|
+
return Math.round(px * 15);
|
|
25661
|
+
}
|
|
25662
|
+
|
|
25610
25663
|
function para(node, style, opts) {
|
|
25611
25664
|
opts = opts || {};
|
|
25612
25665
|
var pr = "";
|
|
@@ -25615,6 +25668,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
25615
25668
|
if (opts.pre) pr += '<w:pStyle w:val="Code"/>';
|
|
25616
25669
|
if (opts.numId) pr += "<w:numPr><w:ilvl w:val=\"" + (opts.level || 0) + "\"/><w:numId w:val=\"" + opts.numId + "\"/></w:numPr>";
|
|
25617
25670
|
if (opts.pageBreakBefore) pr += "<w:pageBreakBefore/>";
|
|
25671
|
+
// Independent of the Quote style: a real blockquote already got
|
|
25672
|
+
// its w:ind above, so only emit this for everything else.
|
|
25673
|
+
if (!opts.quote) {
|
|
25674
|
+
var indTw = indentTwips(node);
|
|
25675
|
+
if (indTw > 0) pr += '<w:ind w:left="' + indTw + '"/>';
|
|
25676
|
+
}
|
|
25618
25677
|
var align = node && node.style ? node.style.textAlign : "";
|
|
25619
25678
|
if (align === "center" || align === "right") pr += '<w:jc w:val="' + align + '"/>';
|
|
25620
25679
|
else if (align === "justify") pr += '<w:jc w:val="both"/>';
|
|
@@ -29256,6 +29315,277 @@ function RTE_Plugin_ImportStyles() {
|
|
|
29256
29315
|
}
|
|
29257
29316
|
}
|
|
29258
29317
|
|
|
29318
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
29319
|
+
|
|
29320
|
+
// 2026-09-04 Inline code — CKEditor 5's "Code" feature (`<code>`), the one
|
|
29321
|
+
// basic-style it ships that this editor had no command for. Markdown `x` was
|
|
29322
|
+
// the only way to produce inline code here, which meant there was no way to
|
|
29323
|
+
// REMOVE it and no toolbar affordance at all.
|
|
29324
|
+
//
|
|
29325
|
+
// The command is "inlinecode", NOT "code". "code" is already taken by the
|
|
29326
|
+
// HTML-source-view toggle in the core (__Is_Cmd_Active case "code" ->
|
|
29327
|
+
// ___Is_CodeMode()), so registering "code" here would have collided with source
|
|
29328
|
+
// view. keyboarda11y.js has carried "inlinecode" in its TOGGLE_CMD list all
|
|
29329
|
+
// along, with a comment that it matched nothing in this build; a core
|
|
29330
|
+
// __Is_Cmd_Active case added alongside this plugin is what makes it match, so
|
|
29331
|
+
// the button reports aria-pressed and not just a highlight.
|
|
29332
|
+
//
|
|
29333
|
+
// Design notes:
|
|
29334
|
+
// - Toggling is done by SPLITTING the <code> element around the selection
|
|
29335
|
+
// rather than by lifting text nodes out of it. Splitting preserves nested
|
|
29336
|
+
// inline markup: un-coding the middle of <code>a <em>b</em> c</code> keeps
|
|
29337
|
+
// the <em>. Lifting text nodes would have dropped it.
|
|
29338
|
+
// - A collapsed selection acts on the word under the caret. That is the same
|
|
29339
|
+
// choice changecase.js makes, and for the same reason: it makes the command
|
|
29340
|
+
// usable from a keyboard shortcut without selecting first. The alternative -
|
|
29341
|
+
// inserting an empty <code> and putting the caret inside - is the caret trap
|
|
29342
|
+
// documented in the footnotes work, so it is deliberately not done.
|
|
29343
|
+
// - Never nests inside <pre> or an existing <code>: both already mean
|
|
29344
|
+
// "preformatted", and nesting produces markup no serializer round-trips.
|
|
29345
|
+
// - Emits a bare <code> element and no rte-* class. That keeps the styling in
|
|
29346
|
+
// the content stylesheet where authors can override it, and keeps the plugin
|
|
29347
|
+
// out of check-css-contract.mjs, which only scans the four theme sheets.
|
|
29348
|
+
RTE_DefaultConfig.plugin_inlinecode = RTE_Plugin_InlineCode;
|
|
29349
|
+
|
|
29350
|
+
if (!RTE_DefaultConfig.text_inlinecode) RTE_DefaultConfig.text_inlinecode = "Inline code";
|
|
29351
|
+
|
|
29352
|
+
if (!RTE_DefaultConfig.svgCode_inlinecode) {
|
|
29353
|
+
RTE_DefaultConfig.svgCode_inlinecode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/></svg>';
|
|
29354
|
+
}
|
|
29355
|
+
|
|
29356
|
+
function RTE_Plugin_InlineCode() {
|
|
29357
|
+
var obj = this;
|
|
29358
|
+
var config, editor;
|
|
29359
|
+
|
|
29360
|
+
obj.PluginName = "InlineCode";
|
|
29361
|
+
|
|
29362
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
29363
|
+
|
|
29364
|
+
obj.InitEditor = function (argeditor) {
|
|
29365
|
+
editor = argeditor;
|
|
29366
|
+
|
|
29367
|
+
editor.attachEvent("exec_command_inlinecode", function (state) {
|
|
29368
|
+
state.returnValue = true;
|
|
29369
|
+
obj.Toggle();
|
|
29370
|
+
});
|
|
29371
|
+
|
|
29372
|
+
// Public API, matching the shape the other toggle plugins expose.
|
|
29373
|
+
editor.toggleInlineCode = function () { return obj.Toggle(); };
|
|
29374
|
+
editor.isInlineCode = function () { return obj.IsActive(); };
|
|
29375
|
+
};
|
|
29376
|
+
|
|
29377
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
29378
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
29379
|
+
|
|
29380
|
+
function getRange(doc) {
|
|
29381
|
+
var sel = doc && doc.getSelection ? doc.getSelection() : null;
|
|
29382
|
+
if (!sel || !sel.rangeCount) return null;
|
|
29383
|
+
return sel.getRangeAt(0);
|
|
29384
|
+
}
|
|
29385
|
+
|
|
29386
|
+
// Nearest <code> ancestor that is still inside the editable area.
|
|
29387
|
+
function codeAncestor(node, editable) {
|
|
29388
|
+
while (node && node !== editable) {
|
|
29389
|
+
if (node.nodeType === 1 && node.nodeName === "CODE") return node;
|
|
29390
|
+
node = node.parentNode;
|
|
29391
|
+
}
|
|
29392
|
+
return null;
|
|
29393
|
+
}
|
|
29394
|
+
|
|
29395
|
+
function inPre(node, editable) {
|
|
29396
|
+
while (node && node !== editable) {
|
|
29397
|
+
if (node.nodeType === 1 && node.nodeName === "PRE") return true;
|
|
29398
|
+
node = node.parentNode;
|
|
29399
|
+
}
|
|
29400
|
+
return false;
|
|
29401
|
+
}
|
|
29402
|
+
|
|
29403
|
+
obj.IsActive = function () {
|
|
29404
|
+
var doc = getDoc(), editable = getEditable();
|
|
29405
|
+
if (!doc || !editable) return false;
|
|
29406
|
+
var range = getRange(doc);
|
|
29407
|
+
if (!range) return false;
|
|
29408
|
+
return !!codeAncestor(range.startContainer, editable);
|
|
29409
|
+
};
|
|
29410
|
+
|
|
29411
|
+
// ---- selection helpers -----------------------------------------------
|
|
29412
|
+
|
|
29413
|
+
// Expand a collapsed caret to the word under it. Returns false when the
|
|
29414
|
+
// caret is not sitting in a word, in which case the command does nothing
|
|
29415
|
+
// rather than creating an empty element.
|
|
29416
|
+
function expandToWord(range) {
|
|
29417
|
+
var node = range.startContainer;
|
|
29418
|
+
if (!node || node.nodeType !== 3) return false;
|
|
29419
|
+
var text = node.data || "";
|
|
29420
|
+
var offset = range.startOffset;
|
|
29421
|
+
var start = offset, end = offset;
|
|
29422
|
+
while (start > 0 && /\S/.test(text.charAt(start - 1))) start--;
|
|
29423
|
+
while (end < text.length && /\S/.test(text.charAt(end))) end++;
|
|
29424
|
+
if (start === end) return false;
|
|
29425
|
+
range.setStart(node, start);
|
|
29426
|
+
range.setEnd(node, end);
|
|
29427
|
+
return true;
|
|
29428
|
+
}
|
|
29429
|
+
|
|
29430
|
+
// Every text node the range actually covers. Boundary text nodes are split
|
|
29431
|
+
// first so a partially-selected node is not treated as fully selected.
|
|
29432
|
+
function selectedTextNodes(doc, range, editable) {
|
|
29433
|
+
if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
|
|
29434
|
+
var s = range.startContainer;
|
|
29435
|
+
if (range.startOffset < s.data.length) {
|
|
29436
|
+
var rest = s.splitText(range.startOffset);
|
|
29437
|
+
var endSame = (range.endContainer === s);
|
|
29438
|
+
var endOff = range.endOffset;
|
|
29439
|
+
range.setStart(rest, 0);
|
|
29440
|
+
if (endSame) range.setEnd(rest, endOff - s.data.length);
|
|
29441
|
+
}
|
|
29442
|
+
}
|
|
29443
|
+
if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.data.length && range.endOffset > 0) {
|
|
29444
|
+
range.endContainer.splitText(range.endOffset);
|
|
29445
|
+
}
|
|
29446
|
+
|
|
29447
|
+
var out = [];
|
|
29448
|
+
var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
|
|
29449
|
+
var n;
|
|
29450
|
+
while ((n = walker.nextNode())) {
|
|
29451
|
+
if (!n.data) continue;
|
|
29452
|
+
if (range.intersectsNode ? range.intersectsNode(n) : true) {
|
|
29453
|
+
// intersectsNode is true for merely-touching nodes, so confirm
|
|
29454
|
+
// the node is genuinely within the boundaries.
|
|
29455
|
+
var r = doc.createRange();
|
|
29456
|
+
r.selectNodeContents(n);
|
|
29457
|
+
if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
|
|
29458
|
+
range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
|
|
29459
|
+
out.push(n);
|
|
29460
|
+
}
|
|
29461
|
+
}
|
|
29462
|
+
}
|
|
29463
|
+
return out;
|
|
29464
|
+
}
|
|
29465
|
+
|
|
29466
|
+
// ---- apply / remove ---------------------------------------------------
|
|
29467
|
+
|
|
29468
|
+
// Split `code` around the selection and unwrap only the overlapping part,
|
|
29469
|
+
// so partial removal keeps the untouched halves coded.
|
|
29470
|
+
function unwrapPart(doc, code, range) {
|
|
29471
|
+
var parent = code.parentNode;
|
|
29472
|
+
if (!parent) return;
|
|
29473
|
+
|
|
29474
|
+
var startsInside = code.contains(range.startContainer);
|
|
29475
|
+
var endsInside = code.contains(range.endContainer);
|
|
29476
|
+
|
|
29477
|
+
// Tail first: extracting it cannot disturb the head's boundaries.
|
|
29478
|
+
var after = doc.createRange();
|
|
29479
|
+
if (endsInside) after.setStart(range.endContainer, range.endOffset);
|
|
29480
|
+
else after.setStart(code, code.childNodes.length);
|
|
29481
|
+
after.setEnd(code, code.childNodes.length);
|
|
29482
|
+
var afterFrag = after.extractContents();
|
|
29483
|
+
|
|
29484
|
+
var before = doc.createRange();
|
|
29485
|
+
before.setStart(code, 0);
|
|
29486
|
+
if (startsInside) before.setEnd(range.startContainer, range.startOffset);
|
|
29487
|
+
else before.setEnd(code, 0);
|
|
29488
|
+
var beforeFrag = before.extractContents();
|
|
29489
|
+
|
|
29490
|
+
if (beforeFrag && beforeFrag.textContent !== "") {
|
|
29491
|
+
var head = doc.createElement("code");
|
|
29492
|
+
head.appendChild(beforeFrag);
|
|
29493
|
+
parent.insertBefore(head, code);
|
|
29494
|
+
}
|
|
29495
|
+
|
|
29496
|
+
// What is left in `code` is exactly the selected part: unwrap it.
|
|
29497
|
+
while (code.firstChild) parent.insertBefore(code.firstChild, code);
|
|
29498
|
+
|
|
29499
|
+
if (afterFrag && afterFrag.textContent !== "") {
|
|
29500
|
+
var tail = doc.createElement("code");
|
|
29501
|
+
tail.appendChild(afterFrag);
|
|
29502
|
+
parent.insertBefore(tail, code);
|
|
29503
|
+
}
|
|
29504
|
+
|
|
29505
|
+
parent.removeChild(code);
|
|
29506
|
+
}
|
|
29507
|
+
|
|
29508
|
+
function wrapNodes(doc, nodes, editable) {
|
|
29509
|
+
// Group contiguous siblings so a run of text under one parent becomes a
|
|
29510
|
+
// single <code>, not one per text node.
|
|
29511
|
+
var group = [], groups = [];
|
|
29512
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
29513
|
+
var n = nodes[i];
|
|
29514
|
+
if (inPre(n, editable) || codeAncestor(n, editable)) continue;
|
|
29515
|
+
if (group.length && group[group.length - 1].parentNode === n.parentNode &&
|
|
29516
|
+
group[group.length - 1].nextSibling === n) {
|
|
29517
|
+
group.push(n);
|
|
29518
|
+
} else {
|
|
29519
|
+
if (group.length) groups.push(group);
|
|
29520
|
+
group = [n];
|
|
29521
|
+
}
|
|
29522
|
+
}
|
|
29523
|
+
if (group.length) groups.push(group);
|
|
29524
|
+
|
|
29525
|
+
for (var g = 0; g < groups.length; g++) {
|
|
29526
|
+
var run = groups[g];
|
|
29527
|
+
// Whitespace-only runs would produce a <code> containing nothing but
|
|
29528
|
+
// a space, which is invisible formatting debris.
|
|
29529
|
+
var joined = "";
|
|
29530
|
+
for (var k = 0; k < run.length; k++) joined += run[k].data;
|
|
29531
|
+
if (!/\S/.test(joined)) continue;
|
|
29532
|
+
|
|
29533
|
+
var code = doc.createElement("code");
|
|
29534
|
+
run[0].parentNode.insertBefore(code, run[0]);
|
|
29535
|
+
for (var j = 0; j < run.length; j++) code.appendChild(run[j]);
|
|
29536
|
+
}
|
|
29537
|
+
}
|
|
29538
|
+
|
|
29539
|
+
// Adjacent <code><code> pairs read as one to the user but serialize as two.
|
|
29540
|
+
function mergeAdjacent(editable) {
|
|
29541
|
+
var all = editable.querySelectorAll("code");
|
|
29542
|
+
for (var i = 0; i < all.length; i++) {
|
|
29543
|
+
var code = all[i];
|
|
29544
|
+
var next = code.nextSibling;
|
|
29545
|
+
while (next && next.nodeType === 1 && next.nodeName === "CODE") {
|
|
29546
|
+
while (next.firstChild) code.appendChild(next.firstChild);
|
|
29547
|
+
next.parentNode.removeChild(next);
|
|
29548
|
+
next = code.nextSibling;
|
|
29549
|
+
}
|
|
29550
|
+
}
|
|
29551
|
+
}
|
|
29552
|
+
|
|
29553
|
+
obj.Toggle = function () {
|
|
29554
|
+
var doc = getDoc(), editable = getEditable();
|
|
29555
|
+
if (!doc || !editable) return false;
|
|
29556
|
+
var range = getRange(doc);
|
|
29557
|
+
if (!range) return false;
|
|
29558
|
+
if (!editable.contains(range.startContainer)) return false;
|
|
29559
|
+
|
|
29560
|
+
if (range.collapsed && !expandToWord(range)) return false;
|
|
29561
|
+
|
|
29562
|
+
var nodes = selectedTextNodes(doc, range, editable);
|
|
29563
|
+
if (!nodes.length) return false;
|
|
29564
|
+
|
|
29565
|
+
// Remove when every selected text node is already coded; otherwise apply.
|
|
29566
|
+
var allCoded = true;
|
|
29567
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
29568
|
+
if (!/\S/.test(nodes[i].data)) continue;
|
|
29569
|
+
if (!codeAncestor(nodes[i], editable)) { allCoded = false; break; }
|
|
29570
|
+
}
|
|
29571
|
+
|
|
29572
|
+
if (allCoded) {
|
|
29573
|
+
var codes = [], seen = [];
|
|
29574
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
29575
|
+
var c = codeAncestor(nodes[n], editable);
|
|
29576
|
+
if (c && seen.indexOf(c) < 0) { seen.push(c); codes.push(c); }
|
|
29577
|
+
}
|
|
29578
|
+
for (var x = 0; x < codes.length; x++) unwrapPart(doc, codes[x], range);
|
|
29579
|
+
} else {
|
|
29580
|
+
wrapNodes(doc, nodes, editable);
|
|
29581
|
+
mergeAdjacent(editable);
|
|
29582
|
+
}
|
|
29583
|
+
|
|
29584
|
+
try { editor.updateToolbarStatus && editor.updateToolbarStatus(); } catch (e) { }
|
|
29585
|
+
return true;
|
|
29586
|
+
};
|
|
29587
|
+
}
|
|
29588
|
+
|
|
29259
29589
|
|
|
29260
29590
|
if (!RTE_DefaultConfig.svgCode_insertcode) {
|
|
29261
29591
|
RTE_DefaultConfig.svgCode_insertcode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 7 3 12 8 17"/><polyline points="16 7 21 12 16 17"/><line x1="14" y1="5" x2="10" y2="19"/></svg>';
|
|
@@ -29588,7 +29918,7 @@ function RTE_Plugin_InsertCode() {
|
|
|
29588
29918
|
sel_lang.options.add(new Option(aliases, brush));
|
|
29589
29919
|
|
|
29590
29920
|
|
|
29591
|
-
var b = sessionStorage.getItem("rte-insertcode-lang")
|
|
29921
|
+
var b = null; try { b = sessionStorage.getItem("rte-insertcode-lang"); } catch (e) { } // storage throws in sandboxed / opaque-origin embeds
|
|
29592
29922
|
if (b) sel_lang.value = b;
|
|
29593
29923
|
}
|
|
29594
29924
|
|
|
@@ -29608,7 +29938,7 @@ function RTE_Plugin_InsertCode() {
|
|
|
29608
29938
|
btn.onclick = function () {
|
|
29609
29939
|
dialoginner.close();
|
|
29610
29940
|
|
|
29611
|
-
sessionStorage.setItem("rte-insertcode-lang", sel_lang.value)
|
|
29941
|
+
try { sessionStorage.setItem("rte-insertcode-lang", sel_lang.value); } catch (e) { }
|
|
29612
29942
|
|
|
29613
29943
|
if (sel_lang.value != "") {
|
|
29614
29944
|
var b = dp.sh.Brushes[sel_lang.value];
|
|
@@ -35445,6 +35775,28 @@ function RTE_Plugin_PdfExport() {
|
|
|
35445
35775
|
var self = this;
|
|
35446
35776
|
var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
|
|
35447
35777
|
|
|
35778
|
+
// A block indented in the editor carries margin-left (or padding-left, per
|
|
35779
|
+
// indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
|
|
35780
|
+
// it was spelled <blockquote>, because that was the only way the editor
|
|
35781
|
+
// could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
|
|
35782
|
+
function cssIndentPt(node) {
|
|
35783
|
+
if (!node || !node.style) return 0;
|
|
35784
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
35785
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
35786
|
+
if (!m) return 0;
|
|
35787
|
+
var n = parseFloat(m[1]);
|
|
35788
|
+
if (!n || n <= 0) return 0;
|
|
35789
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
35790
|
+
var px = unit === "px" ? n
|
|
35791
|
+
: unit === "pt" ? n * (4 / 3)
|
|
35792
|
+
: unit === "in" ? n * 96
|
|
35793
|
+
: unit === "cm" ? n * 37.795
|
|
35794
|
+
: unit === "mm" ? n * 3.7795
|
|
35795
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
35796
|
+
: 0;
|
|
35797
|
+
return px * 0.75;
|
|
35798
|
+
}
|
|
35799
|
+
|
|
35448
35800
|
function block(node, style, indent, listMarker, listId, listOrdered) {
|
|
35449
35801
|
var tag = node.tagName.toLowerCase();
|
|
35450
35802
|
var s = styleOf(node, style);
|
|
@@ -35517,8 +35869,9 @@ function RTE_Plugin_PdfExport() {
|
|
|
35517
35869
|
continue;
|
|
35518
35870
|
}
|
|
35519
35871
|
if (isBlockTag(tag)) {
|
|
35520
|
-
|
|
35521
|
-
|
|
35872
|
+
var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
|
|
35873
|
+
if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own); continue; }
|
|
35874
|
+
block(c, style, indent + own, null);
|
|
35522
35875
|
continue;
|
|
35523
35876
|
}
|
|
35524
35877
|
// Inline content sitting directly under a container.
|
|
@@ -36441,17 +36794,12 @@ function RTE_Plugin_ReadabilityStats() {
|
|
|
36441
36794
|
openDialog();
|
|
36442
36795
|
});
|
|
36443
36796
|
|
|
36444
|
-
|
|
36445
|
-
|
|
36446
|
-
|
|
36447
|
-
|
|
36448
|
-
|
|
36449
|
-
|
|
36450
|
-
keywords: ["readability", "flesch", "grade", "reading", "stats", "score"],
|
|
36451
|
-
action: function () { openDialog(); }
|
|
36452
|
-
});
|
|
36453
|
-
} catch (e) {}
|
|
36454
|
-
}
|
|
36797
|
+
// NOTE: do NOT call editor.slashCommands.register() from here. Plugins
|
|
36798
|
+
// initialise in bundle (alphabetical) order and this file sorts before
|
|
36799
|
+
// "slashcommand", so editor.slashCommands does not exist yet and the guarded
|
|
36800
|
+
// call silently did nothing - the "readability-stats" slash entry never appeared for any
|
|
36801
|
+
// customer until 2026-09-02. The entry now lives in slashcommand.js, gated on
|
|
36802
|
+
// typeof editor.getReadabilityStats === "function" (same pattern as exportToPdf).
|
|
36455
36803
|
};
|
|
36456
36804
|
|
|
36457
36805
|
function currentText() {
|
|
@@ -37877,8 +38225,9 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
37877
38225
|
// config.sanitizerAllowStyleTags = true // keep <style> in content
|
|
37878
38226
|
// config.sanitizerAllowIframes = false // drop <iframe> outright
|
|
37879
38227
|
// config.sanitizerAllowedIframeHosts = ["www.youtube.com"]
|
|
37880
|
-
// config.sanitizerAllowTags = ["custom-el"]
|
|
37881
|
-
// config.sanitizerAllowAttributes = ["my-attr"]
|
|
38228
|
+
// config.sanitizerAllowTags = ["custom-el", "x-*", /^ui-/]
|
|
38229
|
+
// config.sanitizerAllowAttributes = ["my-attr", "data-cms-*"]
|
|
38230
|
+
// exact names, "*" wildcards, or RegExp -- see buildMatcher()
|
|
37882
38231
|
RTE_DefaultConfig.plugin_sanitizer = RTE_Plugin_Sanitizer;
|
|
37883
38232
|
if (typeof RTE_DefaultConfig.contentSanitizer === "undefined") RTE_DefaultConfig.contentSanitizer = true;
|
|
37884
38233
|
|
|
@@ -38065,12 +38414,63 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38065
38414
|
return false;
|
|
38066
38415
|
}
|
|
38067
38416
|
|
|
38417
|
+
// Build a name matcher from a config list. Entries may be:
|
|
38418
|
+
// "my-widget" an exact name
|
|
38419
|
+
// "my-*" a wildcard, where * matches any run of characters
|
|
38420
|
+
// /^my-/ a RegExp
|
|
38421
|
+
// The wildcard form is what makes this a workable answer to CKEditor's General
|
|
38422
|
+
// HTML Support: without it an integrator has to enumerate every tag and
|
|
38423
|
+
// attribute a CMS might emit, by name, which nobody does -- so the content gets
|
|
38424
|
+
// unwrapped instead and the integrator concludes the editor destroys it.
|
|
38425
|
+
//
|
|
38426
|
+
// A pattern widens what is KEPT, never what is executed: on* handlers,
|
|
38427
|
+
// javascript: URLs, <script> and iframe[srcdoc] are all handled before this is
|
|
38428
|
+
// consulted, so allowing a tag or attribute by pattern cannot re-open those.
|
|
38429
|
+
//
|
|
38430
|
+
// Wildcards are matched by hand rather than compiled to a RegExp, so a config
|
|
38431
|
+
// value can never be a regex-injection vector and no escaping table has to be
|
|
38432
|
+
// kept correct.
|
|
38433
|
+
function buildMatcher(list) {
|
|
38434
|
+
var literal = {};
|
|
38435
|
+
var globs = [];
|
|
38436
|
+
var regexps = [];
|
|
38437
|
+
(list || []).forEach(function (entry) {
|
|
38438
|
+
if (entry instanceof RegExp) { regexps.push(entry); return; }
|
|
38439
|
+
var t = String(entry).toLowerCase();
|
|
38440
|
+
if (t.indexOf("*") === -1) { literal[t] = true; return; }
|
|
38441
|
+
globs.push(t.split("*"));
|
|
38442
|
+
});
|
|
38443
|
+
function globMatches(parts, name) {
|
|
38444
|
+
// parts came from splitting on "*": they must appear in order, the first
|
|
38445
|
+
// anchored at the start and the last at the end.
|
|
38446
|
+
var first = parts[0];
|
|
38447
|
+
if (name.lastIndexOf(first, 0) !== 0) return false;
|
|
38448
|
+
var last = parts[parts.length - 1];
|
|
38449
|
+
if (last) {
|
|
38450
|
+
if (name.length < last.length) return false;
|
|
38451
|
+
if (name.indexOf(last, name.length - last.length) === -1) return false;
|
|
38452
|
+
}
|
|
38453
|
+
var pos = first.length;
|
|
38454
|
+
for (var i = 1; i < parts.length; i++) {
|
|
38455
|
+
if (!parts[i]) continue;
|
|
38456
|
+
var at = name.indexOf(parts[i], pos);
|
|
38457
|
+
if (at === -1) return false;
|
|
38458
|
+
pos = at + parts[i].length;
|
|
38459
|
+
}
|
|
38460
|
+
return true;
|
|
38461
|
+
}
|
|
38462
|
+
return function (name) {
|
|
38463
|
+
if (literal[name]) return true;
|
|
38464
|
+
for (var i = 0; i < globs.length; i++) if (globMatches(globs[i], name)) return true;
|
|
38465
|
+
for (var j = 0; j < regexps.length; j++) if (regexps[j].test(name)) return true;
|
|
38466
|
+
return false;
|
|
38467
|
+
};
|
|
38468
|
+
}
|
|
38469
|
+
|
|
38068
38470
|
// --------------------------------------------------------------- engine
|
|
38069
38471
|
function sanitizeNode(root, report) {
|
|
38070
|
-
var allowExtra =
|
|
38071
|
-
|
|
38072
|
-
var attrExtra = {};
|
|
38073
|
-
(config.sanitizerAllowAttributes || []).forEach(function (a) { attrExtra[String(a).toLowerCase()] = true; });
|
|
38472
|
+
var allowExtra = buildMatcher(config.sanitizerAllowTags);
|
|
38473
|
+
var attrExtra = buildMatcher(config.sanitizerAllowAttributes);
|
|
38074
38474
|
var allowStyleTag = config.sanitizerAllowStyleTags === true;
|
|
38075
38475
|
|
|
38076
38476
|
// Collect first: removing nodes while walking a live list skips siblings.
|
|
@@ -38079,7 +38479,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38079
38479
|
for (var i = 0; i < n.childNodes.length; i++) {
|
|
38080
38480
|
var c = n.childNodes[i];
|
|
38081
38481
|
if (c.nodeType === 1) { all.push(c); collect(c); }
|
|
38082
|
-
else if (c.nodeType === 8) all.push(c); //
|
|
38482
|
+
else if (c.nodeType === 8) all.push(c); // checked, see commentEscapes()
|
|
38083
38483
|
}
|
|
38084
38484
|
})(root);
|
|
38085
38485
|
|
|
@@ -38087,7 +38487,33 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38087
38487
|
var el = all[i];
|
|
38088
38488
|
if (!el.parentNode) continue; // already removed with an ancestor
|
|
38089
38489
|
|
|
38090
|
-
if (el.nodeType === 8) {
|
|
38490
|
+
if (el.nodeType === 8) {
|
|
38491
|
+
// Comments used to be removed wholesale, on the stated grounds that
|
|
38492
|
+
// "comments can hide markup". That reason does not survive testing:
|
|
38493
|
+
// serialise <!--<script>alert(1)</script>--> and re-parse it and the
|
|
38494
|
+
// script is inert -- a well-formed comment is just text.
|
|
38495
|
+
//
|
|
38496
|
+
// What IS unsafe is a narrow, enumerable set of comment DATA that
|
|
38497
|
+
// cannot survive serialisation, because the serialiser writes the
|
|
38498
|
+
// data verbatim between <!-- and -->. Re-parsing then ends the
|
|
38499
|
+
// comment early and the remainder becomes live markup:
|
|
38500
|
+
//
|
|
38501
|
+
// "--> <img src=x onerror=...>" -> <!----> <img ...>--> ESCAPES
|
|
38502
|
+
// "--!> <img ...>" -> <!----!> <img ...>--> ESCAPES
|
|
38503
|
+
// "> <img ...>" -> <!--> <img ...>--> ESCAPES
|
|
38504
|
+
// "-> <img ...>" -> <!---> <img ...>--> ESCAPES
|
|
38505
|
+
//
|
|
38506
|
+
// That is the WHATWG serialisation constraint, not a heuristic, so
|
|
38507
|
+
// it can be enumerated rather than guessed at. Everything else is
|
|
38508
|
+
// kept, which is what lets a CMS round-trip its own markers and lets
|
|
38509
|
+
// an email template keep its Outlook conditional comments -- the
|
|
38510
|
+
// mechanism emailtoolkit.js needs and could not previously rely on.
|
|
38511
|
+
if (commentEscapes(el.data)) {
|
|
38512
|
+
note(report.removedTags, "#comment");
|
|
38513
|
+
el.parentNode.removeChild(el);
|
|
38514
|
+
}
|
|
38515
|
+
continue;
|
|
38516
|
+
}
|
|
38091
38517
|
|
|
38092
38518
|
var tag = (el.localName || el.nodeName || "").toLowerCase();
|
|
38093
38519
|
|
|
@@ -38115,7 +38541,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38115
38541
|
note(report.removedTags, tag);
|
|
38116
38542
|
el.parentNode.removeChild(el);
|
|
38117
38543
|
continue;
|
|
38118
|
-
} else if (!ALLOWED_TAGS[tag] && !allowExtra
|
|
38544
|
+
} else if (!ALLOWED_TAGS[tag] && !allowExtra(tag)) {
|
|
38119
38545
|
// Unknown element: unwrap rather than delete, so the words a
|
|
38120
38546
|
// user typed inside a stray tag are not silently lost.
|
|
38121
38547
|
note(report.removedTags, tag);
|
|
@@ -38144,7 +38570,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38144
38570
|
if (!isSafeUrl(value, tag, name)) { note(report.removedAttributes, tag + "[" + name + "]"); el.removeAttribute(attrs[k].name); }
|
|
38145
38571
|
continue;
|
|
38146
38572
|
}
|
|
38147
|
-
if (!ALLOWED_ATTRS[name] && !attrExtra
|
|
38573
|
+
if (!ALLOWED_ATTRS[name] && !attrExtra(name)) {
|
|
38148
38574
|
note(report.removedAttributes, name);
|
|
38149
38575
|
el.removeAttribute(attrs[k].name);
|
|
38150
38576
|
}
|
|
@@ -38257,6 +38683,44 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38257
38683
|
// Measured: the payload ran locally AND replicated to the peer with its
|
|
38258
38684
|
// onerror attribute intact.
|
|
38259
38685
|
//
|
|
38686
|
+
// THREAT MODEL -- read this before tightening or loosening the rule below.
|
|
38687
|
+
//
|
|
38688
|
+
// An escaping comment CANNOT BE AUTHORED AS AN HTML STRING. Feed
|
|
38689
|
+
// `<!--x--> <img src=q onerror=...>-->` to setHTMLCode and the parser yields
|
|
38690
|
+
// comment("x") + text + a normal <img> element, which the element/attribute
|
|
38691
|
+
// filters already handle. The ONLY way to obtain a comment node whose data
|
|
38692
|
+
// escapes is programmatic DOM construction -- document.createComment(), or a
|
|
38693
|
+
// sync layer building nodes directly.
|
|
38694
|
+
//
|
|
38695
|
+
// That is why the guard lives on the DOM-level paths (the sanitise pass and the
|
|
38696
|
+
// live-DOM MutationObserver) and why the string paths need nothing extra. Both
|
|
38697
|
+
// string directions were measured: sender getHTMLCode, receiver setHTMLCode, and
|
|
38698
|
+
// the receiver's live DOM all yield zero executable attributes, because
|
|
38699
|
+
// wrapSerializers() and wrapSetters() filter on the way out and on the way in.
|
|
38700
|
+
//
|
|
38701
|
+
// The previous rule here deleted EVERY comment, justified only by the remark
|
|
38702
|
+
// "comments can hide markup". That is false -- serialise
|
|
38703
|
+
// <!--<script>alert(1)</script>--> and re-parse it and the script is inert -- and
|
|
38704
|
+
// it cost the product CMS round-tripping and Outlook conditional comments (the
|
|
38705
|
+
// mechanism emailtoolkit.js needs) for as long as nobody re-checked it. An
|
|
38706
|
+
// unexplained restriction outlives its reason, so this one is written down.
|
|
38707
|
+
//
|
|
38708
|
+
// A comment's data is written verbatim between <!-- and -->, so data that
|
|
38709
|
+
// contains a terminator (or opens with one) breaks out of the comment when the
|
|
38710
|
+
// serialised output is re-parsed, turning the remainder into live markup.
|
|
38711
|
+
// Module scope on purpose: both the sanitiser pass and the live-DOM
|
|
38712
|
+
// MutationObserver below need it, and they must agree exactly on what is unsafe.
|
|
38713
|
+
// Deliberately conservative -- it rejects rather than rewrites, because silently
|
|
38714
|
+
// editing someone's comment text is its own kind of surprise.
|
|
38715
|
+
function commentEscapes(data) {
|
|
38716
|
+
var d = String(data == null ? "" : data);
|
|
38717
|
+
if (d.indexOf("-->") !== -1) return true;
|
|
38718
|
+
if (d.indexOf("--!>") !== -1) return true;
|
|
38719
|
+
if (d.charAt(0) === ">") return true;
|
|
38720
|
+
if (d.charAt(0) === "-" && d.charAt(1) === ">") return true;
|
|
38721
|
+
return false;
|
|
38722
|
+
}
|
|
38723
|
+
|
|
38260
38724
|
// A MutationObserver is the only place to catch every arrival path — sync,
|
|
38261
38725
|
// drop, a third-party plugin, host-page code. It is honest to say this
|
|
38262
38726
|
// narrows the window rather than closing it completely: the observer runs
|
|
@@ -38307,6 +38771,16 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38307
38771
|
}
|
|
38308
38772
|
for (var a = 0; a < rec.addedNodes.length; a++) {
|
|
38309
38773
|
var node = rec.addedNodes[a];
|
|
38774
|
+
// Comments are preserved now, so an injected comment whose data
|
|
38775
|
+
// escapes on re-parse has to wake the sanitiser. This loop used
|
|
38776
|
+
// to skip every non-element node, which was safe only while
|
|
38777
|
+
// comments were being deleted unconditionally. The CRDT sync
|
|
38778
|
+
// path is exactly how a node arrives here without passing the
|
|
38779
|
+
// input filter -- that is the path that produced the stored XSS.
|
|
38780
|
+
if (node.nodeType === 8) {
|
|
38781
|
+
if (commentEscapes(node.data)) { suspect = true; break; }
|
|
38782
|
+
continue;
|
|
38783
|
+
}
|
|
38310
38784
|
if (node.nodeType !== 1) continue; // typing inserts text: free
|
|
38311
38785
|
if (looksDangerous(node)) { suspect = true; break; }
|
|
38312
38786
|
}
|
|
@@ -38495,7 +38969,17 @@ function RTE_Plugin_ShadowDom() {
|
|
|
38495
38969
|
var id = String(node.id || "");
|
|
38496
38970
|
if (/^rte[-_]|^__rte/.test(id)) return true;
|
|
38497
38971
|
var href = node.href || "";
|
|
38498
|
-
if (!href)
|
|
38972
|
+
if (!href) {
|
|
38973
|
+
// An inline <style> with no id and no href is what bundlers (Vite,
|
|
38974
|
+
// webpack style-loader) inject for an imported theme. Recognise it by
|
|
38975
|
+
// its content, or a shadow-root editor is unstyled (39,726 px toolbar,
|
|
38976
|
+
// Times New Roman) exactly as before this plugin existed. 2026-09-02.
|
|
38977
|
+
if (node.tagName === "STYLE") {
|
|
38978
|
+
var css = node.textContent || "";
|
|
38979
|
+
return /[.]richtexteditor|rte-toolbar|rte-editor|rte-dialog|[.]rte-a11y/.test(css);
|
|
38980
|
+
}
|
|
38981
|
+
return false;
|
|
38982
|
+
}
|
|
38499
38983
|
var base = assetBase();
|
|
38500
38984
|
if (base && href.indexOf(base) === 0) return true;
|
|
38501
38985
|
// Fall back to the file names this product actually ships, so an
|
|
@@ -38643,7 +39127,7 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38643
39127
|
editor.slashCommands = {
|
|
38644
39128
|
register: registerCommand,
|
|
38645
39129
|
remove: removeCommand,
|
|
38646
|
-
list: function () { return commands.slice(); },
|
|
39130
|
+
list: function () { refreshDefaults(); return commands.slice(); },
|
|
38647
39131
|
open: function () { openPopup(true); },
|
|
38648
39132
|
close: closePopup,
|
|
38649
39133
|
isOpen: function () { return !!popupEl; }
|
|
@@ -38655,6 +39139,15 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38655
39139
|
registerCommand(config.slashCommands[i]);
|
|
38656
39140
|
}
|
|
38657
39141
|
}
|
|
39142
|
+
// Plugins initialise synchronously in bundle (alphabetical) order, so at
|
|
39143
|
+
// this point every plugin sorting AFTER "slashcommand" (tabletools,
|
|
39144
|
+
// textdirection, watermark, tableofcontents, typewriter, ...) has not
|
|
39145
|
+
// defined its editor.* API yet and the typeof gates in
|
|
39146
|
+
// buildDefaultCommands() were false: nine entries (sort asc/desc, rtl/
|
|
39147
|
+
// ltr/autodir, watermark, toc, typewriter, focusmode) were missing for
|
|
39148
|
+
// every customer until 2026-09-02. Rebuild once the constructor has
|
|
39149
|
+
// finished (setTimeout 0 runs after all plugin inits) and again on open.
|
|
39150
|
+
setTimeout(refreshDefaults, 0);
|
|
38658
39151
|
|
|
38659
39152
|
injectStyles();
|
|
38660
39153
|
|
|
@@ -38677,7 +39170,26 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38677
39170
|
else commands.push(normalizeCommand(def));
|
|
38678
39171
|
}
|
|
38679
39172
|
|
|
39173
|
+
var removedIds = {};
|
|
39174
|
+
// Rebuild the gated default entries (their typeof checks may only be true
|
|
39175
|
+
// now) while keeping every custom registration and honouring remove().
|
|
39176
|
+
function refreshDefaults() {
|
|
39177
|
+
if (!editor) return;
|
|
39178
|
+
var custom = [];
|
|
39179
|
+
for (var i = 0; i < commands.length; i++) if (!commands[i]._default) custom.push(commands[i]);
|
|
39180
|
+
var defaults = buildDefaultCommands();
|
|
39181
|
+
var next = [];
|
|
39182
|
+
for (var d = 0; d < defaults.length; d++) {
|
|
39183
|
+
var id = defaults[d].id;
|
|
39184
|
+
if (removedIds[id]) continue;
|
|
39185
|
+
var overridden = false;
|
|
39186
|
+
for (var c = 0; c < custom.length; c++) if (custom[c].id === id) { overridden = true; break; }
|
|
39187
|
+
if (!overridden) next.push(defaults[d]);
|
|
39188
|
+
}
|
|
39189
|
+
commands = next.concat(custom);
|
|
39190
|
+
}
|
|
38680
39191
|
function removeCommand(id) {
|
|
39192
|
+
removedIds[id] = true;
|
|
38681
39193
|
var idx = findCommandIndex(id);
|
|
38682
39194
|
if (idx >= 0) commands.splice(idx, 1);
|
|
38683
39195
|
}
|
|
@@ -38687,12 +39199,25 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38687
39199
|
return -1;
|
|
38688
39200
|
}
|
|
38689
39201
|
|
|
39202
|
+
// Localization with an English fallback for EVERY entry (defaults, plugin
|
|
39203
|
+
// register() calls, AI actions, config.slashCommands): the lang packs set
|
|
39204
|
+
// config.text_<key>; __GetLangText returns the raw KEY when one is missing,
|
|
39205
|
+
// so resolve only when a translation exists. Keys: text_slash_<id> (title),
|
|
39206
|
+
// text_slash_<id>_desc (description), text_slash_section_<section>.
|
|
39207
|
+
function L(key, fallback) {
|
|
39208
|
+
try {
|
|
39209
|
+
var k = String(key).toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
39210
|
+
if (config && config["text_" + k] && editor && typeof editor.getLangText === "function") return editor.getLangText(k);
|
|
39211
|
+
} catch (e) { }
|
|
39212
|
+
return fallback;
|
|
39213
|
+
}
|
|
38690
39214
|
function normalizeCommand(def) {
|
|
39215
|
+
var section = def.section || "Blocks";
|
|
38691
39216
|
return {
|
|
38692
39217
|
id: def.id,
|
|
38693
|
-
section:
|
|
38694
|
-
title: def.title || def.id,
|
|
38695
|
-
description: def.description || "",
|
|
39218
|
+
section: L("slash_section_" + section, section),
|
|
39219
|
+
title: L("slash_" + def.id, def.title || def.id),
|
|
39220
|
+
description: L("slash_" + def.id + "_desc", def.description || ""),
|
|
38696
39221
|
keywords: (def.keywords || []).slice(),
|
|
38697
39222
|
icon: def.icon || "",
|
|
38698
39223
|
iconSvg: def.iconSvg || "",
|
|
@@ -38743,11 +39268,13 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38743
39268
|
var list = [];
|
|
38744
39269
|
|
|
38745
39270
|
function push(section, id, title, description, keywords, iconSvg, run) {
|
|
38746
|
-
|
|
39271
|
+
var cmd = normalizeCommand({ // normalizeCommand localizes title/description/section
|
|
38747
39272
|
id: id, section: section, title: title,
|
|
38748
39273
|
description: description, keywords: keywords,
|
|
38749
39274
|
iconSvg: iconSvg, run: run
|
|
38750
|
-
})
|
|
39275
|
+
});
|
|
39276
|
+
cmd._default = true;
|
|
39277
|
+
list.push(cmd);
|
|
38751
39278
|
}
|
|
38752
39279
|
|
|
38753
39280
|
push("Blocks", "heading1", "Heading 1", "Large section heading", ["h1", "title"], iconHeading("1"),
|
|
@@ -38963,7 +39490,8 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38963
39490
|
// Gated on the API rather than registered by the plugin itself: plugins
|
|
38964
39491
|
// initialise in bundle order, so anything sorting before "slashcommand"
|
|
38965
39492
|
// would call slashCommands.register() before it exists and vanish from
|
|
38966
|
-
// this menu without any error.
|
|
39493
|
+
// this menu without any error. These checks are re-run by refreshDefaults()
|
|
39494
|
+
// after all plugins have initialised and again each time the menu opens.
|
|
38967
39495
|
if (typeof editor.exportToPdf === "function") {
|
|
38968
39496
|
push("Tools", "exportpdf", "Export to PDF", "Real text, not a screenshot — selectable, searchable and screen-reader friendly", ["pdf", "export", "download", "save", "print", "text", "accessible", "searchable"], iconPdfText(),
|
|
38969
39497
|
function () { editor.exportToPdf(); });
|
|
@@ -38972,6 +39500,32 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38972
39500
|
push("Tools", "exportdocx", "Export to Word (.docx)", "A real OOXML document, built in the browser with no upload", ["word", "docx", "ooxml", "export", "download", "save", "office"], iconWordFile(),
|
|
38973
39501
|
function () { editor.exportToDocx(); });
|
|
38974
39502
|
}
|
|
39503
|
+
// documentimport.js and readabilitystats.js sort before this file, so they
|
|
39504
|
+
// cannot register at init (editor.slashCommands does not exist yet); their
|
|
39505
|
+
// guarded calls silently did nothing and these two entries were missing for
|
|
39506
|
+
// every customer until 2026-09-02. Declared here, gated on the plugin API.
|
|
39507
|
+
if (typeof editor.openImportDialog === "function") {
|
|
39508
|
+
push("Tools", "import-document", "Import document", "Open a Markdown, HTML, text, or Word file into the editor", ["import", "open", "file", "word", "docx", "markdown", "upload"], iconWordFile(),
|
|
39509
|
+
function () { editor.openImportDialog({}); });
|
|
39510
|
+
}
|
|
39511
|
+
if (typeof editor.getReadabilityStats === "function") {
|
|
39512
|
+
push("Tools", "readability-stats", "Readability statistics", "Flesch reading ease, grade level, and reading time", ["readability", "flesch", "grade", "reading", "stats", "score"], iconSpell(),
|
|
39513
|
+
function () { editor.execCommand("readability"); });
|
|
39514
|
+
}
|
|
39515
|
+
// Marketed features that had NO toolbar button and NO slash entry (reachable
|
|
39516
|
+
// only via the API) until 2026-09-02. All gated on the owning plugin.
|
|
39517
|
+
if (typeof editor.insertCrossReference === "function") {
|
|
39518
|
+
push("Insert", "crossreference", "Cross-reference", "Insert a live reference to a heading, figure, table, or bookmark", ["cross", "reference", "xref", "see", "figure", "heading"], iconXref(),
|
|
39519
|
+
function () { editor.execCommand("insertcrossreference"); });
|
|
39520
|
+
}
|
|
39521
|
+
if (editor.restrictedEditing) {
|
|
39522
|
+
push("Tools", "restrictedediting", "Restricted editing", "Lock the document and mark only the regions others may edit", ["restrict", "lock", "protect", "editable", "region", "permission"], iconPen(),
|
|
39523
|
+
function () { editor.execCommand("restrictedediting"); });
|
|
39524
|
+
}
|
|
39525
|
+
if (editor.dictation) {
|
|
39526
|
+
push("Tools", "dictation", "Dictation", "Speak and have your words typed into the document", ["dictate", "speech", "voice", "microphone", "talk"], iconReadAloud(),
|
|
39527
|
+
function () { editor.execCommand("dictation"); });
|
|
39528
|
+
}
|
|
38975
39529
|
if (typeof editor.copyAsMarkdown === "function") {
|
|
38976
39530
|
push("Tools", "copymarkdown", "Copy as Markdown", "Copy the whole document to the clipboard as Markdown", ["markdown", "md", "copy", "clipboard", "export"], iconMarkdown(),
|
|
38977
39531
|
function () { editor.copyAsMarkdown(); });
|
|
@@ -39103,6 +39657,7 @@ function RTE_Plugin_SlashCommand() {
|
|
|
39103
39657
|
}
|
|
39104
39658
|
|
|
39105
39659
|
function openPopup(manual) {
|
|
39660
|
+
refreshDefaults();
|
|
39106
39661
|
closePopup();
|
|
39107
39662
|
var sel = editor.getSelection();
|
|
39108
39663
|
if (!sel || sel.rangeCount === 0) return;
|
|
@@ -42819,7 +43374,9 @@ function RTE_Plugin_WordExport() {
|
|
|
42819
43374
|
"table { border-collapse: collapse; }\r\n" +
|
|
42820
43375
|
"td, th { border: 1px solid #999; padding: 4px 8px; }\r\n" +
|
|
42821
43376
|
"img { max-width: 100%; height: auto; }\r\n" +
|
|
42822
|
-
|
|
43377
|
+
// margin-left was zeroed here, which also flattened any indentation
|
|
43378
|
+
// expressed as a margin. Keep the quote bar, keep the offset.
|
|
43379
|
+
"blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #444; }\r\n" +
|
|
42823
43380
|
"pre { font-family: Consolas, 'Courier New', monospace; background: #f4f4f4; padding: 8px; }\r\n" +
|
|
42824
43381
|
"</style>\r\n" +
|
|
42825
43382
|
"</head>\r\n" +
|
|
@@ -43018,9 +43575,42 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43018
43575
|
// and the review ledger bridge.
|
|
43019
43576
|
if (session.textSyncMode === "crdt") {
|
|
43020
43577
|
try {
|
|
43578
|
+
// 2026-09-02: the engine bundles its OWN Yjs and builds nodes with it.
|
|
43579
|
+
// A doc built by the host's Yjs (npm/esm "yjs" + y-websocket - the
|
|
43580
|
+
// documented path) is a FOREIGN instance: inserting engine-built nodes
|
|
43581
|
+
// into it throws "Unexpected content type in insert operation" and text
|
|
43582
|
+
// never syncs, while presence still works and the status still said
|
|
43583
|
+
// "crdt". Yjs updates are instance-independent bytes, so bind the
|
|
43584
|
+
// engine to an inner doc built with ITS Yjs and bridge updates both
|
|
43585
|
+
// ways: host->inner with the engine's applyUpdate, inner->host with the
|
|
43586
|
+
// host's module (options.Y or window.Y). Without the host module the
|
|
43587
|
+
// outbound leg cannot be applied safely, so fail LOUDLY instead.
|
|
43588
|
+
var engineY = window.RichTextEditorCrdt.Y || null;
|
|
43589
|
+
var hostY = options.Y || (typeof window !== "undefined" ? window.Y : null) || null;
|
|
43590
|
+
var bindDoc = doc;
|
|
43591
|
+
var foreign = !!(engineY && engineY.Doc && !(doc instanceof engineY.Doc));
|
|
43592
|
+
if (foreign) {
|
|
43593
|
+
if (!hostY || typeof hostY.applyUpdate !== "function" || typeof hostY.encodeStateAsUpdate !== "function") {
|
|
43594
|
+
throw new Error("yjscollab: the Y.Doc was created by a different Yjs instance than crdt-engine.js bundles. Pass your Yjs module to attach({ Y }) (or build the doc with window.RichTextEditorCrdt.Y) so text can be bridged.");
|
|
43595
|
+
}
|
|
43596
|
+
var BRIDGE = { bridge: "rte-yjs-bridge" };
|
|
43597
|
+
var inner = new engineY.Doc();
|
|
43598
|
+
engineY.applyUpdate(inner, hostY.encodeStateAsUpdate(doc), BRIDGE);
|
|
43599
|
+
var LOCAL = window.RichTextEditorCrdt.LOCAL_ORIGIN;
|
|
43600
|
+
// A host-doc write the plugin tagged as LOCAL (ledger entries) must not
|
|
43601
|
+
// make the engine re-render the editable (see flush race in the engine).
|
|
43602
|
+
var hostToInner = function (update, origin) { if (origin === BRIDGE) return; try { engineY.applyUpdate(inner, update, (LOCAL && origin === LOCAL) ? LOCAL : BRIDGE); } catch (e) { if (window.console) console.error("yjscollab bridge host->inner:", e); } };
|
|
43603
|
+
var innerToHost = function (update, origin) { if (origin === BRIDGE) return; try { hostY.applyUpdate(doc, update, BRIDGE); } catch (e) { if (window.console) console.error("yjscollab bridge inner->host:", e); } };
|
|
43604
|
+
doc.on("update", hostToInner);
|
|
43605
|
+
inner.on("update", innerToHost);
|
|
43606
|
+
session.cleanup.push(function () { try { doc.off("update", hostToInner); } catch (e) { } try { inner.off("update", innerToHost); } catch (e) { } try { inner.destroy(); } catch (e) { } });
|
|
43607
|
+
session.yjsBridge = true;
|
|
43608
|
+
bindDoc = inner;
|
|
43609
|
+
if (window.console && console.info) console.info("yjscollab: bridging a foreign Yjs doc into the CRDT engine (updates relayed both ways).");
|
|
43610
|
+
}
|
|
43021
43611
|
session.crdtBinding = window.RichTextEditorCrdt.attachCrdtBinding({
|
|
43022
43612
|
editable: editor.getEditable(),
|
|
43023
|
-
ydoc:
|
|
43613
|
+
ydoc: bindDoc,
|
|
43024
43614
|
provider: provider,
|
|
43025
43615
|
awareness: provider.awareness,
|
|
43026
43616
|
fragmentName: options.fragmentName || "default",
|
|
@@ -43033,6 +43623,24 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43033
43623
|
try { session.crdtBinding && session.crdtBinding.dispose(); }
|
|
43034
43624
|
catch (ignore) { }
|
|
43035
43625
|
});
|
|
43626
|
+
// Per-author undo. The engine ships a Y.UndoManager scoped to LOCAL_ORIGIN
|
|
43627
|
+
// (undo.ts) but nothing attached it, so Ctrl+Z ran the core DOM-snapshot
|
|
43628
|
+
// undo and reverted OTHER people's edits on every peer (2026-09-02). Route
|
|
43629
|
+
// undo/redo to the engine while crdt mode is active.
|
|
43630
|
+
try {
|
|
43631
|
+
if (typeof window.RichTextEditorCrdt.attachUndoManager === "function" && session.crdtBinding && session.crdtBinding.fragment) {
|
|
43632
|
+
session.undoManager = window.RichTextEditorCrdt.attachUndoManager({ fragment: session.crdtBinding.fragment });
|
|
43633
|
+
var undoHook = function (state) { if (!session || !session.undoManager) return; state.returnValue = true; state.stopBubble = true; try { session.undoManager.undo(); } catch (e) { } };
|
|
43634
|
+
var redoHook = function (state) { if (!session || !session.undoManager) return; state.returnValue = true; state.stopBubble = true; try { session.undoManager.redo(); } catch (e) { } };
|
|
43635
|
+
editor.attachEvent("exec_command_undo", undoHook);
|
|
43636
|
+
editor.attachEvent("exec_command_redo", redoHook);
|
|
43637
|
+
session.cleanup.push(function () {
|
|
43638
|
+
try { if (typeof editor.detachEvent === "function") { editor.detachEvent("exec_command_undo", undoHook); editor.detachEvent("exec_command_redo", redoHook); } } catch (e) { }
|
|
43639
|
+
try { session.undoManager && session.undoManager.destroy(); } catch (e) { }
|
|
43640
|
+
session.undoManager = null;
|
|
43641
|
+
});
|
|
43642
|
+
}
|
|
43643
|
+
} catch (undoErr) { if (window.console) console.warn("yjscollab: undo manager not attached:", undoErr); }
|
|
43036
43644
|
} catch (err) {
|
|
43037
43645
|
session.fallbackReason = "crdt-attach-failed";
|
|
43038
43646
|
session.crdtError = err;
|
|
@@ -43065,7 +43673,7 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43065
43673
|
}
|
|
43066
43674
|
return {
|
|
43067
43675
|
attached: true,
|
|
43068
|
-
textSyncMode: session.textSyncMode,
|
|
43676
|
+
textSyncMode: session.textSyncMode, yjsBridge: !!session.yjsBridge,
|
|
43069
43677
|
requestedTextSync: session.requestedTextSync,
|
|
43070
43678
|
fallbackReason: session.fallbackReason || null,
|
|
43071
43679
|
peerCount: getRemotePeers().length
|
|
@@ -43310,6 +43918,52 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43310
43918
|
lastKnownHtml = localInitial;
|
|
43311
43919
|
}
|
|
43312
43920
|
|
|
43921
|
+
// --- IME composition gate (legacy snapshot mode) ---------------------
|
|
43922
|
+
// setEditorHtml() replaces the whole document. Doing that while an IME
|
|
43923
|
+
// is composing swaps the live text node out from under it, so the
|
|
43924
|
+
// commit lands at a stale offset and the raw buffer survives: composing
|
|
43925
|
+
// "shi" and committing a character while a collaborator typed produced
|
|
43926
|
+
// "Xbasesh<char>i" on BOTH peers (2026-09-03 — the same corruption the
|
|
43927
|
+
// crdt binding had, on the deprecated-but-supported path v2.0 customers
|
|
43928
|
+
// still run). Hold the repaint until the composition commits.
|
|
43929
|
+
var composing = false;
|
|
43930
|
+
var pendingRemoteHtml = null;
|
|
43931
|
+
var compositionDoc = (editor.getDocument && editor.getDocument()) || null;
|
|
43932
|
+
if (compositionDoc) {
|
|
43933
|
+
var onCompStart = function () { composing = true; };
|
|
43934
|
+
var onCompEnd = function () {
|
|
43935
|
+
if (!composing) return;
|
|
43936
|
+
composing = false;
|
|
43937
|
+
setTimeout(function () {
|
|
43938
|
+
if (pendingRemoteHtml !== null) {
|
|
43939
|
+
// Deliberately NOT repainting: that would discard the
|
|
43940
|
+
// character the IME just committed. Adopt the remote text as
|
|
43941
|
+
// the diff baseline instead, so the local push that follows
|
|
43942
|
+
// is computed against what the peers actually hold. Legacy
|
|
43943
|
+
// mode is documented last-write-wins on same-paragraph
|
|
43944
|
+
// conflicts, so the local commit winning is correct here —
|
|
43945
|
+
// corrupted text was not.
|
|
43946
|
+
lastKnownHtml = pendingRemoteHtml;
|
|
43947
|
+
pendingRemoteHtml = null;
|
|
43948
|
+
}
|
|
43949
|
+
// Push explicitly. The commit's DOM mutation is delivered to
|
|
43950
|
+
// the MutationObserver BEFORE compositionend fires, so it was
|
|
43951
|
+
// dropped by the `composing` guard — without this the
|
|
43952
|
+
// committed character never reaches the peers at all
|
|
43953
|
+
// (measured: A "base<char>", B "base").
|
|
43954
|
+
onEditorMutation();
|
|
43955
|
+
}, 0);
|
|
43956
|
+
};
|
|
43957
|
+
// Bound on the DOCUMENT, not the body: some skins remount the body
|
|
43958
|
+
// (see attachMutationObserver below) and composition events bubble.
|
|
43959
|
+
compositionDoc.addEventListener("compositionstart", onCompStart, true);
|
|
43960
|
+
compositionDoc.addEventListener("compositionend", onCompEnd, true);
|
|
43961
|
+
session.cleanup.push(function () {
|
|
43962
|
+
compositionDoc.removeEventListener("compositionstart", onCompStart, true);
|
|
43963
|
+
compositionDoc.removeEventListener("compositionend", onCompEnd, true);
|
|
43964
|
+
});
|
|
43965
|
+
}
|
|
43966
|
+
|
|
43313
43967
|
// Y.Text → editor.
|
|
43314
43968
|
var onTextChange = function (event, transaction) {
|
|
43315
43969
|
if (pushing) return;
|
|
@@ -43317,6 +43971,7 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43317
43971
|
if (transaction && transaction.local) return;
|
|
43318
43972
|
var fresh = session.textMap.toString();
|
|
43319
43973
|
if (fresh === lastKnownHtml) return;
|
|
43974
|
+
if (composing) { pendingRemoteHtml = fresh; return; }
|
|
43320
43975
|
setEditorHtml(fresh);
|
|
43321
43976
|
lastKnownHtml = fresh;
|
|
43322
43977
|
// 2026-05-28 setEditorHtml writes into editable.innerHTML; if that
|
|
@@ -43331,6 +43986,9 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43331
43986
|
// Editor DOM → Y.Text. Debounced so bursts collapse to one CRDT diff.
|
|
43332
43987
|
var onEditorMutation = function () {
|
|
43333
43988
|
if (applying) return;
|
|
43989
|
+
// Intermediate IME buffers are not real edits; the commit fires its
|
|
43990
|
+
// own mutation. Pushing them also races the composition gate above.
|
|
43991
|
+
if (composing) return;
|
|
43334
43992
|
if (pendingPushTimer) return;
|
|
43335
43993
|
// 2026-05-28 Re-attach observer if the body was remounted since last
|
|
43336
43994
|
// mutation. Cheap no-op when body is unchanged.
|
|
@@ -43418,17 +44076,17 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43418
44076
|
|
|
43419
44077
|
ledger.add = function (entry) {
|
|
43420
44078
|
var result = originalAdd.call(ledger, entry);
|
|
43421
|
-
if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
|
|
44079
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.set(result.id, cloneEntry(result)); });
|
|
43422
44080
|
return result;
|
|
43423
44081
|
};
|
|
43424
44082
|
ledger.update = function (id, patch) {
|
|
43425
44083
|
var result = originalUpdate.call(ledger, id, patch);
|
|
43426
|
-
if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
|
|
44084
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.set(result.id, cloneEntry(result)); });
|
|
43427
44085
|
return result;
|
|
43428
44086
|
};
|
|
43429
44087
|
ledger.remove = function (id) {
|
|
43430
44088
|
var result = originalRemove.call(ledger, id);
|
|
43431
|
-
if (result && !echoGuard) session.ledgerMap.delete(id);
|
|
44089
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.delete(id); });
|
|
43432
44090
|
return result;
|
|
43433
44091
|
};
|
|
43434
44092
|
|
|
@@ -43462,6 +44120,16 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43462
44120
|
});
|
|
43463
44121
|
}
|
|
43464
44122
|
|
|
44123
|
+
// Ledger writes are the plugin's own (non-text) Y.Doc transactions. In crdt
|
|
44124
|
+
// mode they are tagged with the engine's LOCAL_ORIGIN so the engine does not
|
|
44125
|
+
// re-render the editable for them - a re-render between a DOM mutation and
|
|
44126
|
+
// the observer flush discarded that mutation (2026-09-02: comment anchors
|
|
44127
|
+
// vanished the instant the ledger entry was written).
|
|
44128
|
+
function ledgerWrite(fn) {
|
|
44129
|
+
var origin = (session && session.textSyncMode === "crdt" && window.RichTextEditorCrdt) ? window.RichTextEditorCrdt.LOCAL_ORIGIN : null;
|
|
44130
|
+
if (origin && session && session.doc && typeof session.doc.transact === "function") { session.doc.transact(fn, origin); }
|
|
44131
|
+
else { fn(); }
|
|
44132
|
+
}
|
|
43465
44133
|
function seedLedgerFromRemote() {
|
|
43466
44134
|
if (!editor.reviewLedger || !session.ledgerMap) return;
|
|
43467
44135
|
echoGuard = true;
|