@richscripts/richtexteditor 2.8.0 → 2.10.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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/richtexteditor/all_plugins.js +1032 -719
  3. package/richtexteditor/all_plugins.min.js +41 -41
  4. package/richtexteditor/plugins/accessibilitychecker.js +7 -0
  5. package/richtexteditor/plugins/all_plugins.js +1032 -719
  6. package/richtexteditor/plugins/all_plugins.min.js +41 -41
  7. package/richtexteditor/plugins/autoembed.js +7 -0
  8. package/richtexteditor/plugins/blocktypes.js +7 -0
  9. package/richtexteditor/plugins/bookmarkcard.js +7 -0
  10. package/richtexteditor/plugins/charlimit.js +7 -0
  11. package/richtexteditor/plugins/comments.js +14 -0
  12. package/richtexteditor/plugins/contentminimap.js +7 -0
  13. package/richtexteditor/plugins/crossreference.js +7 -0
  14. package/richtexteditor/plugins/dictation.js +7 -0
  15. package/richtexteditor/plugins/documentoutline.js +9 -3
  16. package/richtexteditor/plugins/draghandle.js +7 -0
  17. package/richtexteditor/plugins/foldheadings.js +7 -0
  18. package/richtexteditor/plugins/footnotes.js +7 -0
  19. package/richtexteditor/plugins/formatpainter.js +7 -0
  20. package/richtexteditor/plugins/formattingmarks.js +7 -0
  21. package/richtexteditor/plugins/gapcursor.js +7 -0
  22. package/richtexteditor/plugins/ghostcomplete.js +7 -0
  23. package/richtexteditor/plugins/layouttable.js +7 -0
  24. package/richtexteditor/plugins/linenumbers.js +7 -0
  25. package/richtexteditor/plugins/linkchecker.js +7 -0
  26. package/richtexteditor/plugins/mentionplugin.js +14 -0
  27. package/richtexteditor/plugins/mergefields.js +7 -0
  28. package/richtexteditor/plugins/mermaiddiagram.js +7 -0
  29. package/richtexteditor/plugins/multilevellist.js +7 -0
  30. package/richtexteditor/plugins/pagination.js +7 -0
  31. package/richtexteditor/plugins/restrictedediting.js +7 -0
  32. package/richtexteditor/plugins/revisionhistory.js +7 -0
  33. package/richtexteditor/plugins/rtlui.js +7 -0
  34. package/richtexteditor/plugins/rubyannotation.js +6 -0
  35. package/richtexteditor/plugins/slashcommand.js +7 -0
  36. package/richtexteditor/plugins/smartchips.js +7 -0
  37. package/richtexteditor/plugins/spellcheck.js +7 -0
  38. package/richtexteditor/plugins/stateinspector.js +6 -0
  39. package/richtexteditor/plugins/tableofcontents.js +7 -0
  40. package/richtexteditor/plugins/textdirection.js +7 -0
  41. package/richtexteditor/plugins/textpartlanguage.js +7 -0
  42. package/richtexteditor/plugins/todolist.js +7 -0
  43. package/richtexteditor/plugins/trackedchanges.js +730 -716
  44. package/richtexteditor/plugins/typewriter.js +7 -0
  45. package/richtexteditor/plugins/watermark.js +15 -0
  46. package/richtexteditor/plugins/yjscollab.js +7 -0
  47. package/richtexteditor/rte.js +2 -2
@@ -208,6 +208,13 @@ function RTE_Plugin_AccessibilityChecker() {
208
208
  "@media (max-width: 1420px){.rte-a11y-shell{display:block;}.rte-a11y-panel{margin-top:12px;max-width:none;width:100%;}.rte-a11y-body{max-height:360px;}}"
209
209
  ].join("");
210
210
  hostDoc.head.appendChild(style);
211
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
212
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
213
+ // the path everyone else takes; this re-injects through the CSSOM only when
214
+ // the policy actually emptied it.
215
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
216
+ editor.ensureStyleSheetLive(hostDoc, style, "rte-accessibility-checker-style", style.textContent);
217
+ }
211
218
  }
212
219
 
213
220
  function ensureShell() {
@@ -20910,6 +20917,13 @@ function RTE_Plugin_AutoEmbed() {
20910
20917
  st.id = "rte-autoembed-styles";
20911
20918
  st.appendChild(doc.createTextNode(css));
20912
20919
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
20920
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
20921
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
20922
+ // the path everyone else takes; this re-injects through the CSSOM only when
20923
+ // the policy actually emptied it.
20924
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
20925
+ editor.ensureStyleSheetLive(doc, st, "rte-autoembed-styles", st.textContent);
20926
+ }
20913
20927
  }
20914
20928
 
20915
20929
  // Programmatic insert at caret (public editor.embedUrl API).
@@ -21091,6 +21105,13 @@ function RTE_Plugin_BlockTypes() {
21091
21105
  st.setAttribute("data-rte-blocktypes", "1");
21092
21106
  st.textContent = css;
21093
21107
  head.appendChild(st);
21108
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
21109
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
21110
+ // the path everyone else takes; this re-injects through the CSSOM only when
21111
+ // the policy actually emptied it.
21112
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
21113
+ editor.ensureStyleSheetLive(editdoc, st, "rte-blocktypes-st", st.textContent);
21114
+ }
21094
21115
  }
21095
21116
  } catch (e) { /* ignore */ }
21096
21117
  }
@@ -21329,6 +21350,13 @@ function RTE_Plugin_BookmarkCard() {
21329
21350
  st.setAttribute("data-rte-bookmarkcard", "1");
21330
21351
  st.textContent = css;
21331
21352
  head.appendChild(st);
21353
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
21354
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
21355
+ // the path everyone else takes; this re-injects through the CSSOM only when
21356
+ // the policy actually emptied it.
21357
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
21358
+ editor.ensureStyleSheetLive(editdoc, st, "rte-bookmarkcard-st", st.textContent);
21359
+ }
21332
21360
  }
21333
21361
  } catch (e) { /* ignore */ }
21334
21362
  }
@@ -21742,6 +21770,13 @@ function RTE_Plugin_CharLimit() {
21742
21770
  st.textContent = ".rte-charlimit-counter{position:absolute;right:10px;bottom:6px;z-index:5;font:11px -apple-system,Segoe UI,sans-serif;color:#64748b;background:rgba(255,255,255,.85);padding:1px 7px;border-radius:9px;pointer-events:none}" +
21743
21771
  ".rte-charlimit-counter.is-near{color:#b45309}.rte-charlimit-counter.is-over{color:#dc2626;font-weight:600}";
21744
21772
  (doc.head || doc.documentElement).appendChild(st);
21773
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
21774
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
21775
+ // the path everyone else takes; this re-injects through the CSSOM only when
21776
+ // the policy actually emptied it.
21777
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
21778
+ editor.ensureStyleSheetLive(doc, st, "rte-charlimit-st", st.textContent);
21779
+ }
21745
21780
  }
21746
21781
 
21747
21782
  function refresh() {
@@ -22398,6 +22433,13 @@ function RTE_Plugin_Comments() {
22398
22433
  ".rte-comment-dark .rte-comment-quote{background:rgba(245,158,11,.12);color:#fcd34d}"
22399
22434
  ].join("\n");
22400
22435
  host.head.appendChild(style);
22436
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
22437
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
22438
+ // the path everyone else takes; this re-injects through the CSSOM only when
22439
+ // the policy actually emptied it.
22440
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
22441
+ editor.ensureStyleSheetLive(host, style, "rte-comments-style", style.textContent);
22442
+ }
22401
22443
  }
22402
22444
 
22403
22445
  var editdoc = editor.getDocument();
@@ -22411,6 +22453,13 @@ function RTE_Plugin_Comments() {
22411
22453
  "@keyframes rte-comment-flash{0%{background:rgba(251,191,36,.9)}100%{background:" + config.commentHighlightBg + "}}"
22412
22454
  ].join("\n");
22413
22455
  editdoc.head.appendChild(iStyle);
22456
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
22457
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
22458
+ // the path everyone else takes; this re-injects through the CSSOM only when
22459
+ // the policy actually emptied it.
22460
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
22461
+ editor.ensureStyleSheetLive(editdoc, iStyle, "rte-comments-iStyle", iStyle.textContent);
22462
+ }
22414
22463
  }
22415
22464
  }
22416
22465
 
@@ -22545,6 +22594,13 @@ function RTE_Plugin_ContentMinimap() {
22545
22594
  "@media (max-width: 1100px){.rte-content-minimap-shell{display:block;}.rte-content-minimap-panel{margin-top:12px;max-width:none;width:100%;}.rte-content-minimap-body{max-height:280px;}}"
22546
22595
  ].join("");
22547
22596
  hostDoc.head.appendChild(style);
22597
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
22598
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
22599
+ // the path everyone else takes; this re-injects through the CSSOM only when
22600
+ // the policy actually emptied it.
22601
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
22602
+ editor.ensureStyleSheetLive(hostDoc, style, "rte-content-minimap-style", style.textContent);
22603
+ }
22548
22604
  }
22549
22605
 
22550
22606
  function ensureShell() {
@@ -23216,6 +23272,13 @@ function RTE_Plugin_CrossReference() {
23216
23272
  st.setAttribute("data-css", text);
23217
23273
  st.appendChild(doc.createTextNode(text));
23218
23274
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
23275
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
23276
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
23277
+ // the path everyone else takes; this re-injects through the CSSOM only when
23278
+ // the policy actually emptied it.
23279
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
23280
+ editor.ensureStyleSheetLive(doc, st, "rte-xref-styles", st.textContent);
23281
+ }
23219
23282
  }
23220
23283
  }
23221
23284
 
@@ -23671,6 +23734,13 @@ function RTE_Plugin_Dictation() {
23671
23734
  style.setAttribute("data-rte-dictation", "1");
23672
23735
  style.appendChild(document.createTextNode(css));
23673
23736
  document.head.appendChild(style);
23737
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
23738
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
23739
+ // the path everyone else takes; this re-injects through the CSSOM only when
23740
+ // the policy actually emptied it.
23741
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
23742
+ editor.ensureStyleSheetLive(document, style, "rte-dictation-style", style.textContent);
23743
+ }
23674
23744
  }
23675
23745
 
23676
23746
  function normalize(text, isFinal) {
@@ -25116,9 +25186,7 @@ function RTE_Plugin_DocumentOutline() {
25116
25186
  function injectStyles() {
25117
25187
  var hostDoc = config.container.ownerDocument;
25118
25188
  if (hostDoc.getElementById("rte-document-outline-style")) return;
25119
- var style = hostDoc.createElement("style");
25120
- style.id = "rte-document-outline-style";
25121
- style.innerHTML = [
25189
+ var csstext = [
25122
25190
  ".rte-document-outline-shell{display:flex;align-items:stretch;gap:10px;}",
25123
25191
  ".rte-document-outline-shell>.rte-document-outline-host{flex:1 1 auto;min-width:0;}",
25124
25192
  "/* 2026-07-12 navigation panel precision */",
@@ -25141,7 +25209,15 @@ function RTE_Plugin_DocumentOutline() {
25141
25209
  ".rte-document-outline-empty{margin:6px;padding:14px;color:#52657e;font-size:13px;font-weight:700;line-height:1.55;background:#fff;border:1px dashed rgba(148,163,184,.34);border-radius:8px;}",
25142
25210
  "@media (max-width: 1100px){.rte-document-outline-shell{display:block;}.rte-document-outline-panel{margin-top:12px;max-width:none;width:100%;}.rte-document-outline-body{max-height:320px;}}"
25143
25211
  ].join("");
25212
+ var style = hostDoc.createElement("style");
25213
+ style.id = "rte-document-outline-style";
25214
+ style.appendChild(hostDoc.createTextNode(csstext));
25144
25215
  hostDoc.head.appendChild(style);
25216
+ // A <style> element's rules are dropped under a strict style-src; this
25217
+ // re-injects them through the CSSOM in that case only.
25218
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
25219
+ editor.ensureStyleSheetLive(hostDoc, style, "rte-document-outline-style", csstext);
25220
+ }
25145
25221
  }
25146
25222
 
25147
25223
  function ensureShell() {
@@ -26704,6 +26780,13 @@ function RTE_Plugin_DragHandle() {
26704
26780
  ".rte-drag-active *{cursor:grabbing !important;}"
26705
26781
  ));
26706
26782
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
26783
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
26784
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
26785
+ // the path everyone else takes; this re-injects through the CSSOM only when
26786
+ // the policy actually emptied it.
26787
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
26788
+ editor.ensureStyleSheetLive(doc, st, "rte-drag-handle-styles", st.textContent);
26789
+ }
26707
26790
  }
26708
26791
  }
26709
26792
 
@@ -27263,6 +27346,13 @@ function RTE_Plugin_FoldHeadings() {
27263
27346
  st.id = "rte-foldheadings-styles";
27264
27347
  st.appendChild(doc.createTextNode(css));
27265
27348
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
27349
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
27350
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
27351
+ // the path everyone else takes; this re-injects through the CSSOM only when
27352
+ // the policy actually emptied it.
27353
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
27354
+ editor.ensureStyleSheetLive(doc, st, "rte-foldheadings-styles", st.textContent);
27355
+ }
27266
27356
  }
27267
27357
 
27268
27358
  // Strip fold classes for the duration of a serialization call so saved markup
@@ -27693,6 +27783,13 @@ function RTE_Plugin_Footnotes() {
27693
27783
  st.setAttribute("data-css", text);
27694
27784
  st.appendChild(doc.createTextNode(text));
27695
27785
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
27786
+ // A <style> ELEMENT is refused under `style-src 'self'` and its rules are
27787
+ // dropped, so footnote markers and the separator rendered unstyled for any
27788
+ // customer on a strict CSP. This re-injects through the CSSOM only in that
27789
+ // case; the element above stays the path everyone else takes.
27790
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
27791
+ editor.ensureStyleSheetLive(doc, st, "rte-footnotes-styles", text);
27792
+ }
27696
27793
  }
27697
27794
  }
27698
27795
 
@@ -27992,6 +28089,13 @@ function RTE_Plugin_FormatPainter() {
27992
28089
  st.id = "rte-format-painter-styles";
27993
28090
  st.appendChild(doc.createTextNode(css()));
27994
28091
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
28092
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
28093
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
28094
+ // the path everyone else takes; this re-injects through the CSSOM only when
28095
+ // the policy actually emptied it.
28096
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
28097
+ editor.ensureStyleSheetLive(doc, st, "rte-format-painter-styles", st.textContent);
28098
+ }
27995
28099
  }
27996
28100
  }
27997
28101
 
@@ -28239,6 +28343,13 @@ function RTE_Plugin_FormattingMarks() {
28239
28343
  st.setAttribute("data-css", text);
28240
28344
  st.appendChild(doc.createTextNode(text));
28241
28345
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
28346
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
28347
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
28348
+ // the path everyone else takes; this re-injects through the CSSOM only when
28349
+ // the policy actually emptied it.
28350
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
28351
+ editor.ensureStyleSheetLive(doc, st, "rte-formatting-marks-styles", st.textContent);
28352
+ }
28242
28353
  }
28243
28354
  }
28244
28355
 
@@ -28605,6 +28716,13 @@ function RTE_Plugin_GapCursor() {
28605
28716
  st.setAttribute("data-css", text);
28606
28717
  st.appendChild(doc.createTextNode(text));
28607
28718
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
28719
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
28720
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
28721
+ // the path everyone else takes; this re-injects through the CSSOM only when
28722
+ // the policy actually emptied it.
28723
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
28724
+ editor.ensureStyleSheetLive(doc, st, "rte-gapcursor-styles", st.textContent);
28725
+ }
28608
28726
  }
28609
28727
  }
28610
28728
 
@@ -28874,6 +28992,13 @@ function RTE_Plugin_GhostComplete() {
28874
28992
  st.id = "rte-ghost-styles";
28875
28993
  st.appendChild(doc.createTextNode(css));
28876
28994
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
28995
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
28996
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
28997
+ // the path everyone else takes; this re-injects through the CSSOM only when
28998
+ // the policy actually emptied it.
28999
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
29000
+ editor.ensureStyleSheetLive(doc, st, "rte-ghost-styles", st.textContent);
29001
+ }
28877
29002
  }
28878
29003
 
28879
29004
  function stripFor() {
@@ -32561,6 +32686,13 @@ function RTE_Plugin_LayoutTable() {
32561
32686
  st.setAttribute("data-css", text);
32562
32687
  st.appendChild(doc.createTextNode(text));
32563
32688
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
32689
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
32690
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
32691
+ // the path everyone else takes; this re-injects through the CSSOM only when
32692
+ // the policy actually emptied it.
32693
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
32694
+ editor.ensureStyleSheetLive(doc, st, "rte-layout-table-styles", st.textContent);
32695
+ }
32564
32696
  }
32565
32697
 
32566
32698
  function fireChange() {
@@ -32919,6 +33051,13 @@ function RTE_Plugin_LineNumbers() {
32919
33051
  st.setAttribute("data-css", text);
32920
33052
  st.appendChild(doc.createTextNode(text));
32921
33053
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
33054
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
33055
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
33056
+ // the path everyone else takes; this re-injects through the CSSOM only when
33057
+ // the policy actually emptied it.
33058
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
33059
+ editor.ensureStyleSheetLive(doc, st, "rte-linenumber-styles", st.textContent);
33060
+ }
32922
33061
  }
32923
33062
  }
32924
33063
 
@@ -33316,6 +33455,13 @@ function RTE_Plugin_LinkChecker() {
33316
33455
  st.setAttribute("data-css", text);
33317
33456
  st.appendChild(doc.createTextNode(text));
33318
33457
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
33458
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
33459
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
33460
+ // the path everyone else takes; this re-injects through the CSSOM only when
33461
+ // the policy actually emptied it.
33462
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
33463
+ editor.ensureStyleSheetLive(doc, st, "rte-link-checker-styles", st.textContent);
33464
+ }
33319
33465
  }
33320
33466
  }
33321
33467
 
@@ -34319,6 +34465,13 @@ function RTE_Plugin_Mention() {
34319
34465
  ".rte-mention:hover{background:#e4edff}"
34320
34466
  ].join("\n");
34321
34467
  host.head.appendChild(style);
34468
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
34469
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
34470
+ // the path everyone else takes; this re-injects through the CSSOM only when
34471
+ // the policy actually emptied it.
34472
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
34473
+ editor.ensureStyleSheetLive(host, style, "rte-mentionplugin-style", style.textContent);
34474
+ }
34322
34475
 
34323
34476
  // Also inject mention-span styles inside the iframe so the pill looks right while editing.
34324
34477
  var editdoc = editor.getDocument();
@@ -34330,6 +34483,13 @@ function RTE_Plugin_Mention() {
34330
34483
  ".rte-mention:hover{background:#e4edff}"
34331
34484
  ].join("\n");
34332
34485
  editdoc.head.appendChild(iStyle);
34486
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
34487
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
34488
+ // the path everyone else takes; this re-injects through the CSSOM only when
34489
+ // the policy actually emptied it.
34490
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
34491
+ editor.ensureStyleSheetLive(editdoc, iStyle, "rte-mentionplugin-iStyle", iStyle.textContent);
34492
+ }
34333
34493
  }
34334
34494
  }
34335
34495
  }
@@ -34660,6 +34820,13 @@ function RTE_Plugin_MergeFields() {
34660
34820
  st.setAttribute("data-css", text);
34661
34821
  st.appendChild(doc.createTextNode(text));
34662
34822
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
34823
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
34824
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
34825
+ // the path everyone else takes; this re-injects through the CSSOM only when
34826
+ // the policy actually emptied it.
34827
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
34828
+ editor.ensureStyleSheetLive(doc, st, "rte-merge-field-styles", st.textContent);
34829
+ }
34663
34830
  }
34664
34831
  }
34665
34832
 
@@ -34793,6 +34960,13 @@ function RTE_Plugin_MermaidDiagram() {
34793
34960
  style.id = "rte-mermaid-styles";
34794
34961
  style.appendChild(doc.createTextNode(css));
34795
34962
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(style);
34963
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
34964
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
34965
+ // the path everyone else takes; this re-injects through the CSSOM only when
34966
+ // the policy actually emptied it.
34967
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
34968
+ editor.ensureStyleSheetLive(doc, style, "rte-mermaid-styles", style.textContent);
34969
+ }
34796
34970
  }
34797
34971
 
34798
34972
  function getMermaid(win) {
@@ -35093,6 +35267,13 @@ function RTE_Plugin_MultiLevelList() {
35093
35267
  st.setAttribute("data-css", text);
35094
35268
  st.appendChild(doc.createTextNode(text));
35095
35269
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
35270
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
35271
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
35272
+ // the path everyone else takes; this re-injects through the CSSOM only when
35273
+ // the policy actually emptied it.
35274
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
35275
+ editor.ensureStyleSheetLive(doc, st, "rte-legal-list-styles", st.textContent);
35276
+ }
35096
35277
  }
35097
35278
  }
35098
35279
 
@@ -36266,6 +36447,13 @@ function RTE_Plugin_Pagination() {
36266
36447
  st.id = "rte-pagination-styles";
36267
36448
  st.appendChild(doc.createTextNode(css));
36268
36449
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
36450
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
36451
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
36452
+ // the path everyone else takes; this re-injects through the CSSOM only when
36453
+ // the policy actually emptied it.
36454
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
36455
+ editor.ensureStyleSheetLive(doc, st, "rte-pagination-styles", st.textContent);
36456
+ }
36269
36457
  }
36270
36458
 
36271
36459
  // ---- serialization guard --------------------------------------------
@@ -38165,6 +38353,13 @@ function RTE_Plugin_RestrictedEditing() {
38165
38353
  "body.rte-restricted [" + editableAttr + "='true']{background:rgba(34,197,94,.16)}"
38166
38354
  ].join("\n");
38167
38355
  (doc.head || doc.getElementsByTagName("head")[0]).appendChild(s);
38356
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
38357
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
38358
+ // the path everyone else takes; this re-injects through the CSSOM only when
38359
+ // the policy actually emptied it.
38360
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
38361
+ editor.ensureStyleSheetLive(doc, s, "__rte_restrictedediting_styles", s.textContent);
38362
+ }
38168
38363
  }
38169
38364
 
38170
38365
  function enable() {
@@ -38975,6 +39170,13 @@ function RTE_Plugin_RevisionHistory() {
38975
39170
  ".rte-rev-dark .rte-rev-diff-del{background:rgba(244,63,94,.16);color:#fda4af}"
38976
39171
  ].join("\n");
38977
39172
  host.head.appendChild(style);
39173
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
39174
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
39175
+ // the path everyone else takes; this re-injects through the CSSOM only when
39176
+ // the policy actually emptied it.
39177
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
39178
+ editor.ensureStyleSheetLive(host, style, "rte-revisionhistory-style", style.textContent);
39179
+ }
38978
39180
  }
38979
39181
  }
38980
39182
 
@@ -39126,6 +39328,13 @@ function RTE_Plugin_RtlUi() {
39126
39328
  st.id = "rte-rtl-ui-styles";
39127
39329
  st.appendChild(doc.createTextNode(css()));
39128
39330
  (doc.head || doc.documentElement).appendChild(st);
39331
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
39332
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
39333
+ // the path everyone else takes; this re-injects through the CSSOM only when
39334
+ // the policy actually emptied it.
39335
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
39336
+ editor.ensureStyleSheetLive(doc, st, "rte-rtl-ui-styles", st.textContent);
39337
+ }
39129
39338
  }
39130
39339
  }
39131
39340
 
@@ -39231,6 +39440,12 @@ function RTE_Plugin_RubyAnnotation() {
39231
39440
  "@supports (display:ruby){.rte-ruby-annotated>rp{display:none;}}"
39232
39441
  ].join("");
39233
39442
  hostDoc.head.appendChild(style);
39443
+ // A <style> element's rules are dropped under a strict style-src; this
39444
+ // re-injects them through the CSSOM in that case only. Missed in 2.9.0,
39445
+ // found by auditing the FULL bundle rather than a sample of plugins.
39446
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
39447
+ editor.ensureStyleSheetLive(hostDoc, style, "rte-ruby-style", style.textContent);
39448
+ }
39234
39449
  }
39235
39450
 
39236
39451
  function editableRoot() {
@@ -41137,6 +41352,13 @@ function RTE_Plugin_SlashCommand() {
41137
41352
  ".rte-slash-popup-dark .rte-slash-empty{color:#94a3b8}"
41138
41353
  ].join("\n");
41139
41354
  host.head.appendChild(style);
41355
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
41356
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
41357
+ // the path everyone else takes; this re-injects through the CSSOM only when
41358
+ // the policy actually emptied it.
41359
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
41360
+ editor.ensureStyleSheetLive(host, style, "rte-slashcommand-style", style.textContent);
41361
+ }
41140
41362
  }
41141
41363
 
41142
41364
  // --- Icons (minimal inline SVGs) ---
@@ -41441,6 +41663,13 @@ function RTE_Plugin_SmartChips() {
41441
41663
  st.setAttribute("data-rte-smartchips", "1");
41442
41664
  st.textContent = css;
41443
41665
  head.appendChild(st);
41666
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
41667
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
41668
+ // the path everyone else takes; this re-injects through the CSSOM only when
41669
+ // the policy actually emptied it.
41670
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
41671
+ editor.ensureStyleSheetLive(editdoc, st, "rte-smartchips-st", st.textContent);
41672
+ }
41444
41673
  }
41445
41674
  } catch (e) { /* ignore */ }
41446
41675
  }
@@ -41626,6 +41855,13 @@ function RTE_Plugin_SpellCheck() {
41626
41855
  st.setAttribute("data-rte-spellcheck", "1");
41627
41856
  st.textContent = css;
41628
41857
  (host.head || host.documentElement).appendChild(st);
41858
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
41859
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
41860
+ // the path everyone else takes; this re-injects through the CSSOM only when
41861
+ // the policy actually emptied it.
41862
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
41863
+ editor.ensureStyleSheetLive(host, st, "rte-spellcheck-st", st.textContent);
41864
+ }
41629
41865
  }
41630
41866
  } catch (e) { /* ignore */ }
41631
41867
  }
@@ -42185,6 +42421,12 @@ function RTE_Plugin_StateInspector() {
42185
42421
  "@media (max-width: 1100px){.rte-state-inspector-shell{display:block;}.rte-state-inspector-panel{margin-top:12px;max-width:none;width:100%;}.rte-state-inspector-body{max-height:340px;}}"
42186
42422
  ].join("");
42187
42423
  hostDoc.head.appendChild(style);
42424
+ // A <style> element's rules are dropped under a strict style-src; this
42425
+ // re-injects them through the CSSOM in that case only. Missed in 2.9.0,
42426
+ // found by auditing the FULL bundle rather than a sample of plugins.
42427
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
42428
+ editor.ensureStyleSheetLive(hostDoc, style, "rte-state-inspector-style", style.textContent);
42429
+ }
42188
42430
  }
42189
42431
 
42190
42432
  function ensureShell() {
@@ -42849,6 +43091,13 @@ function RTE_Plugin_TableOfContents() {
42849
43091
  st.setAttribute("data-css", text);
42850
43092
  st.appendChild(doc.createTextNode(text));
42851
43093
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
43094
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
43095
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
43096
+ // the path everyone else takes; this re-injects through the CSSOM only when
43097
+ // the policy actually emptied it.
43098
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
43099
+ editor.ensureStyleSheetLive(doc, st, "rte-toc-styles", st.textContent);
43100
+ }
42852
43101
  }
42853
43102
  }
42854
43103
 
@@ -43506,6 +43755,13 @@ function RTE_Plugin_TextDirection() {
43506
43755
  st.setAttribute("data-css", text);
43507
43756
  st.appendChild(doc.createTextNode(text));
43508
43757
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
43758
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
43759
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
43760
+ // the path everyone else takes; this re-injects through the CSSOM only when
43761
+ // the policy actually emptied it.
43762
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
43763
+ editor.ensureStyleSheetLive(doc, st, "rte-textdirection-styles", st.textContent);
43764
+ }
43509
43765
  }
43510
43766
  }
43511
43767
 
@@ -44011,6 +44267,13 @@ function RTE_Plugin_TextPartLanguage() {
44011
44267
  st.setAttribute("data-css", text);
44012
44268
  st.appendChild(doc.createTextNode(text));
44013
44269
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
44270
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
44271
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
44272
+ // the path everyone else takes; this re-injects through the CSSOM only when
44273
+ // the policy actually emptied it.
44274
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
44275
+ editor.ensureStyleSheetLive(doc, st, "rte-textpartlanguage-styles", st.textContent);
44276
+ }
44014
44277
  }
44015
44278
  }
44016
44279
 
@@ -44099,6 +44362,13 @@ function RTE_Plugin_TodoList() {
44099
44362
  st.id = "rte-todolist-styles";
44100
44363
  st.appendChild(doc.createTextNode(css));
44101
44364
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
44365
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
44366
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
44367
+ // the path everyone else takes; this re-injects through the CSSOM only when
44368
+ // the policy actually emptied it.
44369
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
44370
+ editor.ensureStyleSheetLive(doc, st, "rte-todolist-styles", st.textContent);
44371
+ }
44102
44372
  }
44103
44373
 
44104
44374
  function makeCheckbox() {
@@ -44152,722 +44422,736 @@ function RTE_Plugin_TodoList() {
44152
44422
  };
44153
44423
  }
44154
44424
 
44155
- if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
44156
-
44157
- if (!RTE_DefaultConfig.svgCode_trackchanges) {
44158
- RTE_DefaultConfig.svgCode_trackchanges = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l4-1 10-10-3-3L5 13l-1 4z"/><path d="M14 4l3 3"/><path d="M4 21h10"/></svg>';
44159
- }
44160
-
44161
- RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
44162
-
44163
- function RTE_Plugin_TrackedChanges() {
44164
- var obj = this;
44165
- var config;
44166
- var editor;
44167
- var enabled = false;
44168
-
44169
- obj.PluginName = "TrackedChanges";
44170
-
44171
- obj.InitConfig = function (argconfig) {
44172
- config = argconfig;
44173
- if (config.trackChangesEnabled === false) return;
44174
-
44175
- config.currentUser = config.currentUser || null;
44176
- config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
44177
- config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
44178
- // Optional application policy for accept/reject. Return false to keep a
44179
- // suggestion pending. The default remains permissive for compatibility.
44180
- config.reviewCanDecide = config.reviewCanDecide || null;
44181
- config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
44182
-
44183
- // Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
44184
- config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
44185
- appendToolbarCommand("toolbar_default", "#{trackchanges}");
44186
- appendToolbarCommand("toolbar_full", "#{trackchanges}");
44187
- appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
44188
-
44189
- if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
44190
- config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
44191
- }
44192
- };
44193
-
44194
- obj.InitEditor = function (argeditor) {
44195
- editor = argeditor;
44196
- if (config.trackChangesEnabled === false) return;
44197
-
44198
- editor.trackedChanges = {
44199
- enable: function (user) { enable(user); },
44200
- disable: function () { disable(); },
44201
- isEnabled: function () { return enabled; },
44202
- acceptAll: function (filter) { return acceptAll(filter); },
44203
- rejectAll: function (filter) { return rejectAll(filter); },
44204
- accept: function (id) { return acceptEntry(id); },
44205
- reject: function (id) { return rejectEntry(id); },
44206
- canDecide: function (id, action) {
44207
- var entry = editor.reviewLedger && editor.reviewLedger.get(id);
44208
- return !!entry && canDecide(entry, action || "accept");
44209
- },
44210
- list: function (opts) {
44211
- if (!editor.reviewLedger) return [];
44212
- // Default to the PENDING review queue (matches the internal
44213
- // accept/reject semantics). Pass {all:true} for full history
44214
- // including accepted/rejected entries (also via reviewLedger.list()).
44215
- var includeAll = opts && opts.all === true;
44216
- return editor.reviewLedger.list().filter(function (e) {
44217
- if (e.changeType !== "insert" && e.changeType !== "delete") return false;
44218
- return includeAll || e.status === "pending";
44219
- });
44220
- }
44221
- };
44222
-
44223
- injectStyles();
44224
-
44225
- editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
44226
- editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
44227
- var btn = editor.createToolbarButton(cmd);
44228
- btn.__tcSync = function () {
44229
- if (enabled) btn.classList.add("rte-ui-active");
44230
- else btn.classList.remove("rte-ui-active");
44231
- btn.setAttribute("aria-pressed", enabled ? "true" : "false");
44232
- // WCAG 2.5.3 Label in Name: the visible label (this tooltip) must be
44233
- // contained in the accessible name. "Turn on suggesting mode" was not
44234
- // — the accessible name is "Suggesting mode" — so a speech-input user
44235
- // saying what they see did not match the control. State belongs in
44236
- // aria-pressed and the active styling, not in the visible label.
44237
- btn.setAttribute("title", config.text_trackchanges || "Suggesting mode");
44238
- };
44239
- btn.__tcSync();
44240
- return btn;
44241
- };
44242
-
44243
- editor.attachEvent("exec_command_trackchanges", function (state) {
44244
- state.returnValue = true;
44245
- state.stopBubble = true;
44246
- if (enabled) disable();
44247
- else enable();
44248
- });
44249
-
44250
- editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
44251
- };
44252
-
44253
- function appendToolbarCommand(toolbar, item) {
44254
- if (!config[toolbar]) return;
44255
- if (config[toolbar].indexOf(item) !== -1) return;
44256
- config[toolbar] = config[toolbar] + item;
44257
- }
44258
-
44259
- function enable(user) {
44260
- if (user) config.currentUser = user;
44261
- if (!config.currentUser) {
44262
- config.currentUser = { id: "user", name: "User", color: "#2563eb" };
44263
- console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
44264
- }
44265
- enabled = true;
44266
- syncToolbarButtons();
44267
- }
44268
-
44269
- function disable() {
44270
- enabled = false;
44271
- syncToolbarButtons();
44272
- }
44273
-
44274
- function syncToolbarButtons() {
44275
- var doc = editor.iframe && editor.iframe.ownerDocument || document;
44276
- var btns = doc.querySelectorAll(".rte_command_trackchanges");
44277
- for (var i = 0; i < btns.length; i++) {
44278
- if (btns[i].__tcSync) btns[i].__tcSync();
44279
- }
44280
- }
44281
-
44282
- // --- beforeinput intercept ---
44283
-
44284
- function onBeforeInput(e) {
44285
- if (!enabled) return;
44286
- if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
44287
-
44288
- var type = e.inputType;
44289
- if (!type) return;
44290
-
44291
- if (type === "insertText" || type === "insertReplacementText") {
44292
- e.preventDefault();
44293
- handleInsertText(e.data || "");
44294
- return;
44295
- }
44296
- if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
44297
- e.preventDefault();
44298
- handleDelete(type);
44299
- return;
44300
- }
44301
- // insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
44302
- }
44303
-
44304
- // --- insert tracking ---
44305
-
44306
- function handleInsertText(text) {
44307
- if (!text) return;
44308
- var sel = editor.getSelection();
44309
- if (!sel || sel.rangeCount === 0) return;
44310
- var range = sel.getRangeAt(0);
44311
- if (!range.collapsed) {
44312
- // Selection replace: wrap selection as delete, then insert new text.
44313
- if (!wrapRangeAsDelete(range)) return;
44314
- // After wrapping, caret should be past the delete span; re-fetch selection.
44315
- range = sel.getRangeAt(0);
44316
- createInsertSpan(text, range);
44317
- return;
44318
- }
44319
-
44320
- var mergeTarget = adjacentInsertSpan(range);
44321
- if (mergeTarget) {
44322
- appendToInsertSpan(mergeTarget, text);
44323
- } else {
44324
- createInsertSpan(text, range);
44325
- }
44326
- }
44327
-
44328
- function adjacentInsertSpan(range) {
44329
- // If caret sits inside an insert span by the current user, or immediately after one, merge.
44330
- var node = range.startContainer;
44331
- if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
44332
- return node.parentNode;
44333
- }
44334
- // Caret just after an insert span (at an element boundary).
44335
- if (node.nodeType === 1 && range.startOffset > 0) {
44336
- var prev = node.childNodes[range.startOffset - 1];
44337
- if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
44338
- }
44339
- // Caret at the very beginning just before our span.
44340
- if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
44341
- return node.previousSibling;
44342
- }
44343
- return null;
44344
- }
44345
-
44346
- function isMyInsert(el) {
44347
- if (!el || el.nodeType !== 1) return false;
44348
- if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
44349
- return el.getAttribute("data-tc-author") === config.currentUser.id;
44350
- }
44351
-
44352
- // We preventDefault() the browser's own text insertion, which means we also
44353
- // lose the whitespace handling it does for free. A plain second space is
44354
- // collapsed by HTML rendering, so pressing space a second time moved nothing
44355
- // on screen and suggesting mode looked like it had stopped accepting input.
44356
- // Reproduce what contenteditable does: any space followed by another space,
44357
- // and any trailing space, becomes a non-breaking space. Recomputed from
44358
- // scratch each time so an NBSP reverts to a normal space once real text
44359
- // follows it (keeping word wrap and copied text clean). Only typed text
44360
- // reaches this path -- paste arrives as insertFromPaste and is passed
44361
- // through -- so this never rewrites an NBSP the user inserted deliberately.
44362
- function normalizeInsertWhitespace(node) {
44363
- if (!node || node.nodeType !== 3) return;
44364
- var v = node.nodeValue;
44365
- if (v.indexOf(" ") === -1 && v.indexOf("\u00a0") === -1) return;
44366
- v = v.replace(/\u00a0/g, " ");
44367
- v = v.replace(/ (?= )/g, "\u00a0");
44368
- v = v.replace(/ $/, "\u00a0");
44369
- if (v !== node.nodeValue) node.nodeValue = v;
44370
- }
44371
-
44372
- function appendToInsertSpan(span, text) {
44373
- var editdoc = editor.getDocument();
44374
- // Append to the span's last text node (or create one).
44375
- var last = span.lastChild;
44376
- var node;
44377
- if (last && last.nodeType === 3) {
44378
- last.nodeValue = last.nodeValue + text;
44379
- node = last;
44380
- } else {
44381
- node = editdoc.createTextNode(text);
44382
- span.appendChild(node);
44383
- }
44384
- normalizeInsertWhitespace(node);
44385
- placeCaretAtEnd(node);
44386
- updateEntryForSpan(span);
44387
- }
44388
-
44389
- function createInsertSpan(text, range) {
44390
- var editdoc = editor.getDocument();
44391
- var span = editdoc.createElement("span");
44392
- span.className = config.trackChangesInsertClass;
44393
- var user = config.currentUser;
44394
- var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
44395
- span.setAttribute("data-tc-id", id);
44396
- span.setAttribute("data-tc-author", user.id);
44397
- span.setAttribute("data-tc-at", new Date().toISOString());
44398
- span.style.color = user.color;
44399
- span.style.textDecoration = "underline";
44400
- span.appendChild(editdoc.createTextNode(text));
44401
- range.insertNode(span);
44402
- normalizeInsertWhitespace(span.firstChild);
44403
- placeCaretAtEnd(span.firstChild);
44404
-
44405
- editor.reviewLedger.add({
44406
- id: id,
44407
- changeType: "insert",
44408
- author: user,
44409
- text: text,
44410
- status: "pending",
44411
- sourceLabel: "Suggested insert",
44412
- createdAt: Date.now()
44413
- });
44414
- }
44415
-
44416
- function updateEntryForSpan(span) {
44417
- var id = span.getAttribute("data-tc-id");
44418
- if (!id) return;
44419
- editor.reviewLedger.update(id, { text: span.textContent });
44420
- }
44421
-
44422
- // --- delete tracking ---
44423
-
44424
- function handleDelete(mode) {
44425
- var sel = editor.getSelection();
44426
- if (!sel || sel.rangeCount === 0) return;
44427
- var range = sel.getRangeAt(0);
44428
-
44429
- if (!range.collapsed) {
44430
- wrapRangeAsDelete(range);
44431
- return;
44432
- }
44433
-
44434
- // Collapsed: extend by one char in the appropriate direction, then wrap.
44435
- // The extended range covers only that one character — any delete spans between
44436
- // it and the caret should be SKIPPED, not re-wrapped.
44437
- var extended = editor.getDocument().createRange();
44438
- if (mode === "deleteContentBackward") {
44439
- var anchor = prevCharPoint(range.startContainer, range.startOffset);
44440
- if (!anchor) return;
44441
- extended.setStart(anchor.node, anchor.offset);
44442
- extended.setEnd(anchor.node, anchor.offset + 1);
44443
- } else {
44444
- var forward = nextCharPoint(range.startContainer, range.startOffset);
44445
- if (!forward) return;
44446
- extended.setStart(forward.node, forward.offset - 1);
44447
- extended.setEnd(forward.node, forward.offset);
44448
- }
44449
-
44450
- // If that extended range sits entirely inside one of the current user's own
44451
- // insert spans, treat this as undoing their own typing — actually delete.
44452
- if (rangeInsideMyInsert(extended)) {
44453
- extended.deleteContents();
44454
- var sel2 = editor.getSelection();
44455
- sel2.removeAllRanges();
44456
- sel2.addRange(extended);
44457
- // If the insert span is now empty, remove it and its ledger entry.
44458
- cleanupEmptyInsertSpans();
44459
- return;
44460
- }
44461
-
44462
- wrapRangeAsDelete(extended);
44463
- }
44464
-
44465
- function isInsideDeleteSpan(node) {
44466
- var walk = node.nodeType === 1 ? node : node.parentNode;
44467
- while (walk && walk !== editor.getEditable()) {
44468
- if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
44469
- walk = walk.parentNode;
44470
- }
44471
- return false;
44472
- }
44473
-
44474
- // Walk text leaves in the editable, skipping any that live inside an existing delete
44475
- // span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
44476
- function liveTextLeaves() {
44477
- var editable = editor.getEditable();
44478
- var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
44479
- acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
44480
- });
44481
- var out = [], n;
44482
- while ((n = walker.nextNode())) out.push(n);
44483
- return out;
44484
- }
44485
-
44486
- function prevCharPoint(node, offset) {
44487
- var leaves = liveTextLeaves();
44488
- if (!leaves.length) return null;
44489
- var idx = caretIndex(node, offset, leaves);
44490
- if (idx <= 0) return null;
44491
- return leafOffsetAt(idx - 1, leaves);
44492
- }
44493
-
44494
- function nextCharPoint(node, offset) {
44495
- var leaves = liveTextLeaves();
44496
- if (!leaves.length) return null;
44497
- var total = 0;
44498
- for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
44499
- var idx = caretIndex(node, offset, leaves);
44500
- if (idx >= total) return null;
44501
- return leafOffsetAt(idx + 1, leaves);
44502
- }
44503
-
44504
- // How many live characters appear before the given caret position?
44505
- function caretIndex(node, offset, leaves) {
44506
- var r = editor.getDocument().createRange();
44507
- r.setStart(node, offset);
44508
- r.collapse(true);
44509
- var count = 0;
44510
- for (var i = 0; i < leaves.length; i++) {
44511
- var leaf = leaves[i];
44512
- // If the entire leaf is before the caret, add its length.
44513
- var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
44514
- if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
44515
- var cmpStart = r.comparePoint(leaf, 0);
44516
- if (cmpStart >= 0) break; // leaf starts at or after caret
44517
- // Caret falls inside this leaf — binary search the split point.
44518
- var lo = 0, hi = leaf.nodeValue.length;
44519
- while (lo < hi) {
44520
- var mid = (lo + hi) >> 1;
44521
- if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
44522
- else hi = mid;
44523
- }
44524
- count += lo;
44525
- break;
44526
- }
44527
- return count;
44528
- }
44529
-
44530
- function leafOffsetAt(index, leaves) {
44531
- var remaining = index;
44532
- for (var i = 0; i < leaves.length; i++) {
44533
- if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
44534
- remaining -= leaves[i].nodeValue.length;
44535
- }
44536
- return null;
44537
- }
44538
-
44539
- function firstTextLeaf(node) {
44540
- if (!node) return null;
44541
- if (node.nodeType === 3) return node;
44542
- var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
44543
- return walker.nextNode();
44544
- }
44545
-
44546
- function lastTextLeaf(node) {
44547
- if (!node) return null;
44548
- if (node.nodeType === 3) return node;
44549
- var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
44550
- var last = null, n;
44551
- while ((n = walker.nextNode())) last = n;
44552
- return last;
44553
- }
44554
-
44555
- function previousTextLeaf(node) {
44556
- var editable = editor.getEditable();
44557
- var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
44558
- var prev = null, cur;
44559
- while ((cur = walker.nextNode())) {
44560
- if (cur === node) return prev;
44561
- prev = cur;
44562
- }
44563
- return null;
44564
- }
44565
-
44566
- function nextTextLeaf(node) {
44567
- var editable = editor.getEditable();
44568
- var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
44569
- var seen = false, cur;
44570
- while ((cur = walker.nextNode())) {
44571
- if (seen) return cur;
44572
- if (cur === node) seen = true;
44573
- }
44574
- return null;
44575
- }
44576
-
44577
- function rangeInsideMyInsert(range) {
44578
- var span = range.startContainer.parentNode;
44579
- while (span && span !== editor.getEditable()) {
44580
- if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
44581
- span = span.parentNode;
44582
- }
44583
- return false;
44584
- }
44585
-
44586
- function wrapRangeAsDelete(range) {
44587
- if (range.collapsed) return false;
44588
- var editdoc = editor.getDocument();
44589
-
44590
- var fragment = range.cloneContents();
44591
- if (!fragmentHasText(fragment)) return false;
44592
-
44593
- // Find existing same-author delete spans adjacent to the range so we can merge
44594
- // sequential backspaces / forward deletes into one contiguous span.
44595
- var user = config.currentUser;
44596
- var mergeBefore = adjacentDeleteSpan(range, "before", user);
44597
- var mergeAfter = adjacentDeleteSpan(range, "after", user);
44598
-
44599
- range.deleteContents();
44600
-
44601
- if (mergeBefore) {
44602
- // Append the fragment's content to the existing "before" span.
44603
- appendFragmentToSpan(mergeBefore, fragment);
44604
- if (mergeAfter && mergeAfter !== mergeBefore) {
44605
- // Collapse both sides: move "after" span's children into "before" and drop it.
44606
- while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
44607
- var afterId = mergeAfter.getAttribute("data-tc-id");
44608
- mergeAfter.remove();
44609
- if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
44610
- }
44611
- var rangeAfter = editdoc.createRange();
44612
- rangeAfter.setStartAfter(mergeBefore);
44613
- rangeAfter.collapse(true);
44614
- var sel = editor.getSelection();
44615
- sel.removeAllRanges();
44616
- sel.addRange(rangeAfter);
44617
- // 2026-05-28 Defense: if a customer pasted a delete-class span from
44618
- // outside this plugin, it may lack data-tc-id. Skip the ledger
44619
- // update instead of writing under a `null` key (would silently
44620
- // corrupt the ledger via the update("null", ...) coercion).
44621
- var beforeId = mergeBefore.getAttribute("data-tc-id");
44622
- if (beforeId && editor.reviewLedger) {
44623
- editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
44624
- }
44625
- return true;
44626
- }
44627
-
44628
- if (mergeAfter) {
44629
- prependFragmentToSpan(mergeAfter, fragment);
44630
- var rangeBefore = editdoc.createRange();
44631
- rangeBefore.setStartBefore(mergeAfter);
44632
- rangeBefore.collapse(true);
44633
- var sel2 = editor.getSelection();
44634
- sel2.removeAllRanges();
44635
- sel2.addRange(rangeBefore);
44636
- // Same defense as above (mergeAfter may lack data-tc-id).
44637
- var afterMergeId = mergeAfter.getAttribute("data-tc-id");
44638
- if (afterMergeId && editor.reviewLedger) {
44639
- editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
44640
- }
44641
- return true;
44642
- }
44643
-
44644
- var span = editdoc.createElement("span");
44645
- span.className = config.trackChangesDeleteClass;
44646
- var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
44647
- span.setAttribute("data-tc-id", id);
44648
- span.setAttribute("data-tc-author", user.id);
44649
- span.setAttribute("data-tc-at", new Date().toISOString());
44650
- span.style.color = user.color;
44651
- span.style.textDecoration = "line-through";
44652
- span.appendChild(fragment);
44653
- range.insertNode(span);
44654
-
44655
- var after = editdoc.createRange();
44656
- after.setStartAfter(span);
44657
- after.collapse(true);
44658
- var sel3 = editor.getSelection();
44659
- sel3.removeAllRanges();
44660
- sel3.addRange(after);
44661
-
44662
- editor.reviewLedger.add({
44663
- id: id,
44664
- changeType: "delete",
44665
- author: user,
44666
- text: span.textContent,
44667
- originalHtml: span.innerHTML,
44668
- status: "pending",
44669
- sourceLabel: "Suggested delete",
44670
- createdAt: Date.now()
44671
- });
44672
- return true;
44673
- }
44674
-
44675
- function adjacentDeleteSpan(range, side, user) {
44676
- // Find element immediately before or after the range boundaries at the container level.
44677
- var node = side === "before" ? range.startContainer : range.endContainer;
44678
- var offset = side === "before" ? range.startOffset : range.endOffset;
44679
- if (node.nodeType === 3) {
44680
- // Look at the sibling of the text node if we're at the text node's boundary.
44681
- if (side === "before" && offset === 0) {
44682
- var prev = node.previousSibling;
44683
- if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
44684
- }
44685
- if (side === "after" && offset === node.nodeValue.length) {
44686
- var next = node.nextSibling;
44687
- if (next && isDeleteSpanByAuthor(next, user)) return next;
44688
- }
44689
- return null;
44690
- }
44691
- // Element container: look at children on either side of the offset.
44692
- if (side === "before" && offset > 0) {
44693
- var childBefore = node.childNodes[offset - 1];
44694
- if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
44695
- }
44696
- if (side === "after" && offset < node.childNodes.length) {
44697
- var childAfter = node.childNodes[offset];
44698
- if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
44699
- }
44700
- return null;
44701
- }
44702
-
44703
- function isDeleteSpanByAuthor(el, user) {
44704
- if (!el || el.nodeType !== 1) return false;
44705
- if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
44706
- return el.getAttribute("data-tc-author") === user.id;
44707
- }
44708
-
44709
- function appendFragmentToSpan(span, fragment) {
44710
- while (fragment.firstChild) span.appendChild(fragment.firstChild);
44711
- }
44712
-
44713
- function prependFragmentToSpan(span, fragment) {
44714
- var first = span.firstChild;
44715
- while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
44716
- }
44717
-
44718
- function fragmentHasText(fragment) {
44719
- var t = (fragment.textContent || "").replace(/\s+/g, "");
44720
- return !!t;
44721
- }
44722
-
44723
- function cleanupEmptyInsertSpans() {
44724
- var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
44725
- for (var i = 0; i < spans.length; i++) {
44726
- var s = spans[i];
44727
- if (!s.textContent || !s.textContent.length) {
44728
- var id = s.getAttribute("data-tc-id");
44729
- if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
44730
- s.remove();
44731
- } else {
44732
- updateEntryForSpan(s);
44733
- }
44734
- }
44735
- }
44736
-
44737
- function placeCaretAtEnd(node) {
44738
- if (!node) return;
44739
- var editdoc = editor.getDocument();
44740
- var range = editdoc.createRange();
44741
- if (node.nodeType === 3) {
44742
- range.setStart(node, node.nodeValue.length);
44743
- } else {
44744
- range.setStart(node, node.childNodes.length);
44745
- }
44746
- range.collapse(true);
44747
- var sel = editor.getSelection();
44748
- sel.removeAllRanges();
44749
- sel.addRange(range);
44750
- }
44751
-
44752
- // --- accept / reject ---
44753
-
44754
- function decisionUser() {
44755
- return config.currentUser || { id: "user", name: "User", color: "#2563eb" };
44756
- }
44757
-
44758
- function canDecide(entry, action) {
44759
- if (typeof config.reviewCanDecide !== "function") return true;
44760
- var allowed = false;
44761
- var user = decisionUser();
44762
- try { allowed = config.reviewCanDecide(entry, action, user, editor) !== false; }
44763
- catch (err) {
44764
- console.warn("trackedchanges: reviewCanDecide threw; denying decision.", err);
44765
- }
44766
- if (allowed) return true;
44767
- var detail = { entry: entry, action: action, user: user };
44768
- try { typeof config.onReviewDecisionDenied === "function" && config.onReviewDecisionDenied(detail); } catch (ignore) { }
44769
- try { typeof editor.fireEvent === "function" && editor.fireEvent("review_decision_denied", detail); } catch (ignore2) { }
44770
- return false;
44771
- }
44772
-
44773
- function acceptEntry(id) {
44774
- var entry = editor.reviewLedger.get(id);
44775
- if (!entry) return false;
44776
- if (!canDecide(entry, "accept")) return false;
44777
- var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
44778
- if (entry.changeType === "insert") {
44779
- if (span) unwrapKeepChildren(span);
44780
- } else if (entry.changeType === "delete") {
44781
- if (span) span.remove();
44782
- }
44783
- editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now(), decidedBy: decisionUser() });
44784
- return true;
44785
- }
44786
-
44787
- function rejectEntry(id) {
44788
- var entry = editor.reviewLedger.get(id);
44789
- if (!entry) return false;
44790
- if (!canDecide(entry, "reject")) return false;
44791
- var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
44792
- if (entry.changeType === "insert") {
44793
- if (span) span.remove();
44794
- } else if (entry.changeType === "delete") {
44795
- if (span) unwrapKeepChildren(span);
44796
- }
44797
- editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now(), decidedBy: decisionUser() });
44798
- return true;
44799
- }
44800
-
44801
- function acceptAll(filter) {
44802
- var entries = editor.reviewLedger.list(filter).filter(function (e) {
44803
- return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
44804
- });
44805
- var accepted = 0;
44806
- for (var i = 0; i < entries.length; i++) if (acceptEntry(entries[i].id)) accepted++;
44807
- return accepted;
44808
- }
44809
-
44810
- function rejectAll(filter) {
44811
- var entries = editor.reviewLedger.list(filter).filter(function (e) {
44812
- return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
44813
- });
44814
- var rejected = 0;
44815
- for (var i = 0; i < entries.length; i++) if (rejectEntry(entries[i].id)) rejected++;
44816
- return rejected;
44817
- }
44818
-
44819
- function unwrapKeepChildren(el) {
44820
- var parent = el.parentNode;
44821
- if (!parent) return;
44822
- while (el.firstChild) parent.insertBefore(el.firstChild, el);
44823
- parent.removeChild(el);
44824
- }
44825
-
44826
- // 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
44827
- // Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
44828
- // containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
44829
- // colons, base64 IDs with `=` padding).
44830
- function cssEscape(s) {
44831
- var str = String(s);
44832
- if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
44833
- try { return CSS.escape(str); } catch (e) { /* fall through */ }
44834
- }
44835
- return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
44836
- return "\\" + c;
44837
- });
44838
- }
44839
-
44840
- // --- styles ---
44841
-
44842
- function injectStyles() {
44843
- var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
44844
- if (!host.querySelector("style[data-rte-trackchanges]")) {
44845
- var style = host.createElement("style");
44846
- style.setAttribute("data-rte-trackchanges", "1");
44847
- style.textContent = [
44848
- ".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
44849
- ".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active{background:linear-gradient(135deg,#e9f7ef,#eef5ff);color:#17633f;box-shadow:inset 0 0 0 1px rgba(34,132,82,.28),0 6px 14px rgba(34,132,82,.12)}",
44850
- ".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active:after{content:'';position:absolute;right:5px;top:5px;width:6px;height:6px;border-radius:50%;background:#22a66a;box-shadow:0 0 0 2px #fff}",
44851
- ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
44852
- ".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
44853
- ".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
44854
- ].join("\n");
44855
- host.head.appendChild(style);
44856
- }
44857
-
44858
- var editdoc = editor.getDocument();
44859
- if (editdoc && editdoc.head && !editdoc.querySelector("style[data-rte-trackchanges-inline]")) {
44860
- var iStyle = editdoc.createElement("style");
44861
- iStyle.setAttribute("data-rte-trackchanges-inline", "1");
44862
- iStyle.textContent = [
44863
- ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
44864
- ".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
44865
- ".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
44866
- ].join("\n");
44867
- editdoc.head.appendChild(iStyle);
44868
- }
44869
- }
44870
- }
44425
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
44426
+
44427
+ if (!RTE_DefaultConfig.svgCode_trackchanges) {
44428
+ RTE_DefaultConfig.svgCode_trackchanges = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l4-1 10-10-3-3L5 13l-1 4z"/><path d="M14 4l3 3"/><path d="M4 21h10"/></svg>';
44429
+ }
44430
+
44431
+ RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
44432
+
44433
+ function RTE_Plugin_TrackedChanges() {
44434
+ var obj = this;
44435
+ var config;
44436
+ var editor;
44437
+ var enabled = false;
44438
+
44439
+ obj.PluginName = "TrackedChanges";
44440
+
44441
+ obj.InitConfig = function (argconfig) {
44442
+ config = argconfig;
44443
+ if (config.trackChangesEnabled === false) return;
44444
+
44445
+ config.currentUser = config.currentUser || null;
44446
+ config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
44447
+ config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
44448
+ // Optional application policy for accept/reject. Return false to keep a
44449
+ // suggestion pending. The default remains permissive for compatibility.
44450
+ config.reviewCanDecide = config.reviewCanDecide || null;
44451
+ config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
44452
+
44453
+ // Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
44454
+ config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
44455
+ appendToolbarCommand("toolbar_default", "#{trackchanges}");
44456
+ appendToolbarCommand("toolbar_full", "#{trackchanges}");
44457
+ appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
44458
+
44459
+ if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
44460
+ config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
44461
+ }
44462
+ };
44463
+
44464
+ obj.InitEditor = function (argeditor) {
44465
+ editor = argeditor;
44466
+ if (config.trackChangesEnabled === false) return;
44467
+
44468
+ editor.trackedChanges = {
44469
+ enable: function (user) { enable(user); },
44470
+ disable: function () { disable(); },
44471
+ isEnabled: function () { return enabled; },
44472
+ acceptAll: function (filter) { return acceptAll(filter); },
44473
+ rejectAll: function (filter) { return rejectAll(filter); },
44474
+ accept: function (id) { return acceptEntry(id); },
44475
+ reject: function (id) { return rejectEntry(id); },
44476
+ canDecide: function (id, action) {
44477
+ var entry = editor.reviewLedger && editor.reviewLedger.get(id);
44478
+ return !!entry && canDecide(entry, action || "accept");
44479
+ },
44480
+ list: function (opts) {
44481
+ if (!editor.reviewLedger) return [];
44482
+ // Default to the PENDING review queue (matches the internal
44483
+ // accept/reject semantics). Pass {all:true} for full history
44484
+ // including accepted/rejected entries (also via reviewLedger.list()).
44485
+ var includeAll = opts && opts.all === true;
44486
+ return editor.reviewLedger.list().filter(function (e) {
44487
+ if (e.changeType !== "insert" && e.changeType !== "delete") return false;
44488
+ return includeAll || e.status === "pending";
44489
+ });
44490
+ }
44491
+ };
44492
+
44493
+ injectStyles();
44494
+
44495
+ editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
44496
+ editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
44497
+ var btn = editor.createToolbarButton(cmd);
44498
+ btn.__tcSync = function () {
44499
+ if (enabled) btn.classList.add("rte-ui-active");
44500
+ else btn.classList.remove("rte-ui-active");
44501
+ btn.setAttribute("aria-pressed", enabled ? "true" : "false");
44502
+ // WCAG 2.5.3 Label in Name: the visible label (this tooltip) must be
44503
+ // contained in the accessible name. "Turn on suggesting mode" was not
44504
+ // — the accessible name is "Suggesting mode" — so a speech-input user
44505
+ // saying what they see did not match the control. State belongs in
44506
+ // aria-pressed and the active styling, not in the visible label.
44507
+ btn.setAttribute("title", config.text_trackchanges || "Suggesting mode");
44508
+ };
44509
+ btn.__tcSync();
44510
+ return btn;
44511
+ };
44512
+
44513
+ editor.attachEvent("exec_command_trackchanges", function (state) {
44514
+ state.returnValue = true;
44515
+ state.stopBubble = true;
44516
+ if (enabled) disable();
44517
+ else enable();
44518
+ });
44519
+
44520
+ editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
44521
+ };
44522
+
44523
+ function appendToolbarCommand(toolbar, item) {
44524
+ if (!config[toolbar]) return;
44525
+ if (config[toolbar].indexOf(item) !== -1) return;
44526
+ config[toolbar] = config[toolbar] + item;
44527
+ }
44528
+
44529
+ function enable(user) {
44530
+ if (user) config.currentUser = user;
44531
+ if (!config.currentUser) {
44532
+ config.currentUser = { id: "user", name: "User", color: "#2563eb" };
44533
+ console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
44534
+ }
44535
+ enabled = true;
44536
+ syncToolbarButtons();
44537
+ }
44538
+
44539
+ function disable() {
44540
+ enabled = false;
44541
+ syncToolbarButtons();
44542
+ }
44543
+
44544
+ function syncToolbarButtons() {
44545
+ var doc = editor.iframe && editor.iframe.ownerDocument || document;
44546
+ var btns = doc.querySelectorAll(".rte_command_trackchanges");
44547
+ for (var i = 0; i < btns.length; i++) {
44548
+ if (btns[i].__tcSync) btns[i].__tcSync();
44549
+ }
44550
+ }
44551
+
44552
+ // --- beforeinput intercept ---
44553
+
44554
+ function onBeforeInput(e) {
44555
+ if (!enabled) return;
44556
+ if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
44557
+
44558
+ var type = e.inputType;
44559
+ if (!type) return;
44560
+
44561
+ if (type === "insertText" || type === "insertReplacementText") {
44562
+ e.preventDefault();
44563
+ handleInsertText(e.data || "");
44564
+ return;
44565
+ }
44566
+ if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
44567
+ e.preventDefault();
44568
+ handleDelete(type);
44569
+ return;
44570
+ }
44571
+ // insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
44572
+ }
44573
+
44574
+ // --- insert tracking ---
44575
+
44576
+ function handleInsertText(text) {
44577
+ if (!text) return;
44578
+ var sel = editor.getSelection();
44579
+ if (!sel || sel.rangeCount === 0) return;
44580
+ var range = sel.getRangeAt(0);
44581
+ if (!range.collapsed) {
44582
+ // Selection replace: wrap selection as delete, then insert new text.
44583
+ if (!wrapRangeAsDelete(range)) return;
44584
+ // After wrapping, caret should be past the delete span; re-fetch selection.
44585
+ range = sel.getRangeAt(0);
44586
+ createInsertSpan(text, range);
44587
+ return;
44588
+ }
44589
+
44590
+ var mergeTarget = adjacentInsertSpan(range);
44591
+ if (mergeTarget) {
44592
+ appendToInsertSpan(mergeTarget, text);
44593
+ } else {
44594
+ createInsertSpan(text, range);
44595
+ }
44596
+ }
44597
+
44598
+ function adjacentInsertSpan(range) {
44599
+ // If caret sits inside an insert span by the current user, or immediately after one, merge.
44600
+ var node = range.startContainer;
44601
+ if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
44602
+ return node.parentNode;
44603
+ }
44604
+ // Caret just after an insert span (at an element boundary).
44605
+ if (node.nodeType === 1 && range.startOffset > 0) {
44606
+ var prev = node.childNodes[range.startOffset - 1];
44607
+ if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
44608
+ }
44609
+ // Caret at the very beginning just before our span.
44610
+ if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
44611
+ return node.previousSibling;
44612
+ }
44613
+ return null;
44614
+ }
44615
+
44616
+ function isMyInsert(el) {
44617
+ if (!el || el.nodeType !== 1) return false;
44618
+ if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
44619
+ return el.getAttribute("data-tc-author") === config.currentUser.id;
44620
+ }
44621
+
44622
+ // We preventDefault() the browser's own text insertion, which means we also
44623
+ // lose the whitespace handling it does for free. A plain second space is
44624
+ // collapsed by HTML rendering, so pressing space a second time moved nothing
44625
+ // on screen and suggesting mode looked like it had stopped accepting input.
44626
+ // Reproduce what contenteditable does: any space followed by another space,
44627
+ // and any trailing space, becomes a non-breaking space. Recomputed from
44628
+ // scratch each time so an NBSP reverts to a normal space once real text
44629
+ // follows it (keeping word wrap and copied text clean). Only typed text
44630
+ // reaches this path -- paste arrives as insertFromPaste and is passed
44631
+ // through -- so this never rewrites an NBSP the user inserted deliberately.
44632
+ function normalizeInsertWhitespace(node) {
44633
+ if (!node || node.nodeType !== 3) return;
44634
+ var v = node.nodeValue;
44635
+ if (v.indexOf(" ") === -1 && v.indexOf("\u00a0") === -1) return;
44636
+ v = v.replace(/\u00a0/g, " ");
44637
+ v = v.replace(/ (?= )/g, "\u00a0");
44638
+ v = v.replace(/ $/, "\u00a0");
44639
+ if (v !== node.nodeValue) node.nodeValue = v;
44640
+ }
44641
+
44642
+ function appendToInsertSpan(span, text) {
44643
+ var editdoc = editor.getDocument();
44644
+ // Append to the span's last text node (or create one).
44645
+ var last = span.lastChild;
44646
+ var node;
44647
+ if (last && last.nodeType === 3) {
44648
+ last.nodeValue = last.nodeValue + text;
44649
+ node = last;
44650
+ } else {
44651
+ node = editdoc.createTextNode(text);
44652
+ span.appendChild(node);
44653
+ }
44654
+ normalizeInsertWhitespace(node);
44655
+ placeCaretAtEnd(node);
44656
+ updateEntryForSpan(span);
44657
+ }
44658
+
44659
+ function createInsertSpan(text, range) {
44660
+ var editdoc = editor.getDocument();
44661
+ var span = editdoc.createElement("span");
44662
+ span.className = config.trackChangesInsertClass;
44663
+ var user = config.currentUser;
44664
+ var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
44665
+ span.setAttribute("data-tc-id", id);
44666
+ span.setAttribute("data-tc-author", user.id);
44667
+ span.setAttribute("data-tc-at", new Date().toISOString());
44668
+ span.style.color = user.color;
44669
+ span.style.textDecoration = "underline";
44670
+ span.appendChild(editdoc.createTextNode(text));
44671
+ range.insertNode(span);
44672
+ normalizeInsertWhitespace(span.firstChild);
44673
+ placeCaretAtEnd(span.firstChild);
44674
+
44675
+ editor.reviewLedger.add({
44676
+ id: id,
44677
+ changeType: "insert",
44678
+ author: user,
44679
+ text: text,
44680
+ status: "pending",
44681
+ sourceLabel: "Suggested insert",
44682
+ createdAt: Date.now()
44683
+ });
44684
+ }
44685
+
44686
+ function updateEntryForSpan(span) {
44687
+ var id = span.getAttribute("data-tc-id");
44688
+ if (!id) return;
44689
+ editor.reviewLedger.update(id, { text: span.textContent });
44690
+ }
44691
+
44692
+ // --- delete tracking ---
44693
+
44694
+ function handleDelete(mode) {
44695
+ var sel = editor.getSelection();
44696
+ if (!sel || sel.rangeCount === 0) return;
44697
+ var range = sel.getRangeAt(0);
44698
+
44699
+ if (!range.collapsed) {
44700
+ wrapRangeAsDelete(range);
44701
+ return;
44702
+ }
44703
+
44704
+ // Collapsed: extend by one char in the appropriate direction, then wrap.
44705
+ // The extended range covers only that one character — any delete spans between
44706
+ // it and the caret should be SKIPPED, not re-wrapped.
44707
+ var extended = editor.getDocument().createRange();
44708
+ if (mode === "deleteContentBackward") {
44709
+ var anchor = prevCharPoint(range.startContainer, range.startOffset);
44710
+ if (!anchor) return;
44711
+ extended.setStart(anchor.node, anchor.offset);
44712
+ extended.setEnd(anchor.node, anchor.offset + 1);
44713
+ } else {
44714
+ var forward = nextCharPoint(range.startContainer, range.startOffset);
44715
+ if (!forward) return;
44716
+ extended.setStart(forward.node, forward.offset - 1);
44717
+ extended.setEnd(forward.node, forward.offset);
44718
+ }
44719
+
44720
+ // If that extended range sits entirely inside one of the current user's own
44721
+ // insert spans, treat this as undoing their own typing — actually delete.
44722
+ if (rangeInsideMyInsert(extended)) {
44723
+ extended.deleteContents();
44724
+ var sel2 = editor.getSelection();
44725
+ sel2.removeAllRanges();
44726
+ sel2.addRange(extended);
44727
+ // If the insert span is now empty, remove it and its ledger entry.
44728
+ cleanupEmptyInsertSpans();
44729
+ return;
44730
+ }
44731
+
44732
+ wrapRangeAsDelete(extended);
44733
+ }
44734
+
44735
+ function isInsideDeleteSpan(node) {
44736
+ var walk = node.nodeType === 1 ? node : node.parentNode;
44737
+ while (walk && walk !== editor.getEditable()) {
44738
+ if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
44739
+ walk = walk.parentNode;
44740
+ }
44741
+ return false;
44742
+ }
44743
+
44744
+ // Walk text leaves in the editable, skipping any that live inside an existing delete
44745
+ // span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
44746
+ function liveTextLeaves() {
44747
+ var editable = editor.getEditable();
44748
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
44749
+ acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
44750
+ });
44751
+ var out = [], n;
44752
+ while ((n = walker.nextNode())) out.push(n);
44753
+ return out;
44754
+ }
44755
+
44756
+ function prevCharPoint(node, offset) {
44757
+ var leaves = liveTextLeaves();
44758
+ if (!leaves.length) return null;
44759
+ var idx = caretIndex(node, offset, leaves);
44760
+ if (idx <= 0) return null;
44761
+ return leafOffsetAt(idx - 1, leaves);
44762
+ }
44763
+
44764
+ function nextCharPoint(node, offset) {
44765
+ var leaves = liveTextLeaves();
44766
+ if (!leaves.length) return null;
44767
+ var total = 0;
44768
+ for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
44769
+ var idx = caretIndex(node, offset, leaves);
44770
+ if (idx >= total) return null;
44771
+ return leafOffsetAt(idx + 1, leaves);
44772
+ }
44773
+
44774
+ // How many live characters appear before the given caret position?
44775
+ function caretIndex(node, offset, leaves) {
44776
+ var r = editor.getDocument().createRange();
44777
+ r.setStart(node, offset);
44778
+ r.collapse(true);
44779
+ var count = 0;
44780
+ for (var i = 0; i < leaves.length; i++) {
44781
+ var leaf = leaves[i];
44782
+ // If the entire leaf is before the caret, add its length.
44783
+ var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
44784
+ if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
44785
+ var cmpStart = r.comparePoint(leaf, 0);
44786
+ if (cmpStart >= 0) break; // leaf starts at or after caret
44787
+ // Caret falls inside this leaf — binary search the split point.
44788
+ var lo = 0, hi = leaf.nodeValue.length;
44789
+ while (lo < hi) {
44790
+ var mid = (lo + hi) >> 1;
44791
+ if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
44792
+ else hi = mid;
44793
+ }
44794
+ count += lo;
44795
+ break;
44796
+ }
44797
+ return count;
44798
+ }
44799
+
44800
+ function leafOffsetAt(index, leaves) {
44801
+ var remaining = index;
44802
+ for (var i = 0; i < leaves.length; i++) {
44803
+ if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
44804
+ remaining -= leaves[i].nodeValue.length;
44805
+ }
44806
+ return null;
44807
+ }
44808
+
44809
+ function firstTextLeaf(node) {
44810
+ if (!node) return null;
44811
+ if (node.nodeType === 3) return node;
44812
+ var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
44813
+ return walker.nextNode();
44814
+ }
44815
+
44816
+ function lastTextLeaf(node) {
44817
+ if (!node) return null;
44818
+ if (node.nodeType === 3) return node;
44819
+ var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
44820
+ var last = null, n;
44821
+ while ((n = walker.nextNode())) last = n;
44822
+ return last;
44823
+ }
44824
+
44825
+ function previousTextLeaf(node) {
44826
+ var editable = editor.getEditable();
44827
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
44828
+ var prev = null, cur;
44829
+ while ((cur = walker.nextNode())) {
44830
+ if (cur === node) return prev;
44831
+ prev = cur;
44832
+ }
44833
+ return null;
44834
+ }
44835
+
44836
+ function nextTextLeaf(node) {
44837
+ var editable = editor.getEditable();
44838
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
44839
+ var seen = false, cur;
44840
+ while ((cur = walker.nextNode())) {
44841
+ if (seen) return cur;
44842
+ if (cur === node) seen = true;
44843
+ }
44844
+ return null;
44845
+ }
44846
+
44847
+ function rangeInsideMyInsert(range) {
44848
+ var span = range.startContainer.parentNode;
44849
+ while (span && span !== editor.getEditable()) {
44850
+ if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
44851
+ span = span.parentNode;
44852
+ }
44853
+ return false;
44854
+ }
44855
+
44856
+ function wrapRangeAsDelete(range) {
44857
+ if (range.collapsed) return false;
44858
+ var editdoc = editor.getDocument();
44859
+
44860
+ var fragment = range.cloneContents();
44861
+ if (!fragmentHasText(fragment)) return false;
44862
+
44863
+ // Find existing same-author delete spans adjacent to the range so we can merge
44864
+ // sequential backspaces / forward deletes into one contiguous span.
44865
+ var user = config.currentUser;
44866
+ var mergeBefore = adjacentDeleteSpan(range, "before", user);
44867
+ var mergeAfter = adjacentDeleteSpan(range, "after", user);
44868
+
44869
+ range.deleteContents();
44870
+
44871
+ if (mergeBefore) {
44872
+ // Append the fragment's content to the existing "before" span.
44873
+ appendFragmentToSpan(mergeBefore, fragment);
44874
+ if (mergeAfter && mergeAfter !== mergeBefore) {
44875
+ // Collapse both sides: move "after" span's children into "before" and drop it.
44876
+ while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
44877
+ var afterId = mergeAfter.getAttribute("data-tc-id");
44878
+ mergeAfter.remove();
44879
+ if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
44880
+ }
44881
+ var rangeAfter = editdoc.createRange();
44882
+ rangeAfter.setStartAfter(mergeBefore);
44883
+ rangeAfter.collapse(true);
44884
+ var sel = editor.getSelection();
44885
+ sel.removeAllRanges();
44886
+ sel.addRange(rangeAfter);
44887
+ // 2026-05-28 Defense: if a customer pasted a delete-class span from
44888
+ // outside this plugin, it may lack data-tc-id. Skip the ledger
44889
+ // update instead of writing under a `null` key (would silently
44890
+ // corrupt the ledger via the update("null", ...) coercion).
44891
+ var beforeId = mergeBefore.getAttribute("data-tc-id");
44892
+ if (beforeId && editor.reviewLedger) {
44893
+ editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
44894
+ }
44895
+ return true;
44896
+ }
44897
+
44898
+ if (mergeAfter) {
44899
+ prependFragmentToSpan(mergeAfter, fragment);
44900
+ var rangeBefore = editdoc.createRange();
44901
+ rangeBefore.setStartBefore(mergeAfter);
44902
+ rangeBefore.collapse(true);
44903
+ var sel2 = editor.getSelection();
44904
+ sel2.removeAllRanges();
44905
+ sel2.addRange(rangeBefore);
44906
+ // Same defense as above (mergeAfter may lack data-tc-id).
44907
+ var afterMergeId = mergeAfter.getAttribute("data-tc-id");
44908
+ if (afterMergeId && editor.reviewLedger) {
44909
+ editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
44910
+ }
44911
+ return true;
44912
+ }
44913
+
44914
+ var span = editdoc.createElement("span");
44915
+ span.className = config.trackChangesDeleteClass;
44916
+ var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
44917
+ span.setAttribute("data-tc-id", id);
44918
+ span.setAttribute("data-tc-author", user.id);
44919
+ span.setAttribute("data-tc-at", new Date().toISOString());
44920
+ span.style.color = user.color;
44921
+ span.style.textDecoration = "line-through";
44922
+ span.appendChild(fragment);
44923
+ range.insertNode(span);
44924
+
44925
+ var after = editdoc.createRange();
44926
+ after.setStartAfter(span);
44927
+ after.collapse(true);
44928
+ var sel3 = editor.getSelection();
44929
+ sel3.removeAllRanges();
44930
+ sel3.addRange(after);
44931
+
44932
+ editor.reviewLedger.add({
44933
+ id: id,
44934
+ changeType: "delete",
44935
+ author: user,
44936
+ text: span.textContent,
44937
+ originalHtml: span.innerHTML,
44938
+ status: "pending",
44939
+ sourceLabel: "Suggested delete",
44940
+ createdAt: Date.now()
44941
+ });
44942
+ return true;
44943
+ }
44944
+
44945
+ function adjacentDeleteSpan(range, side, user) {
44946
+ // Find element immediately before or after the range boundaries at the container level.
44947
+ var node = side === "before" ? range.startContainer : range.endContainer;
44948
+ var offset = side === "before" ? range.startOffset : range.endOffset;
44949
+ if (node.nodeType === 3) {
44950
+ // Look at the sibling of the text node if we're at the text node's boundary.
44951
+ if (side === "before" && offset === 0) {
44952
+ var prev = node.previousSibling;
44953
+ if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
44954
+ }
44955
+ if (side === "after" && offset === node.nodeValue.length) {
44956
+ var next = node.nextSibling;
44957
+ if (next && isDeleteSpanByAuthor(next, user)) return next;
44958
+ }
44959
+ return null;
44960
+ }
44961
+ // Element container: look at children on either side of the offset.
44962
+ if (side === "before" && offset > 0) {
44963
+ var childBefore = node.childNodes[offset - 1];
44964
+ if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
44965
+ }
44966
+ if (side === "after" && offset < node.childNodes.length) {
44967
+ var childAfter = node.childNodes[offset];
44968
+ if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
44969
+ }
44970
+ return null;
44971
+ }
44972
+
44973
+ function isDeleteSpanByAuthor(el, user) {
44974
+ if (!el || el.nodeType !== 1) return false;
44975
+ if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
44976
+ return el.getAttribute("data-tc-author") === user.id;
44977
+ }
44978
+
44979
+ function appendFragmentToSpan(span, fragment) {
44980
+ while (fragment.firstChild) span.appendChild(fragment.firstChild);
44981
+ }
44982
+
44983
+ function prependFragmentToSpan(span, fragment) {
44984
+ var first = span.firstChild;
44985
+ while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
44986
+ }
44987
+
44988
+ function fragmentHasText(fragment) {
44989
+ var t = (fragment.textContent || "").replace(/\s+/g, "");
44990
+ return !!t;
44991
+ }
44992
+
44993
+ function cleanupEmptyInsertSpans() {
44994
+ var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
44995
+ for (var i = 0; i < spans.length; i++) {
44996
+ var s = spans[i];
44997
+ if (!s.textContent || !s.textContent.length) {
44998
+ var id = s.getAttribute("data-tc-id");
44999
+ if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
45000
+ s.remove();
45001
+ } else {
45002
+ updateEntryForSpan(s);
45003
+ }
45004
+ }
45005
+ }
45006
+
45007
+ function placeCaretAtEnd(node) {
45008
+ if (!node) return;
45009
+ var editdoc = editor.getDocument();
45010
+ var range = editdoc.createRange();
45011
+ if (node.nodeType === 3) {
45012
+ range.setStart(node, node.nodeValue.length);
45013
+ } else {
45014
+ range.setStart(node, node.childNodes.length);
45015
+ }
45016
+ range.collapse(true);
45017
+ var sel = editor.getSelection();
45018
+ sel.removeAllRanges();
45019
+ sel.addRange(range);
45020
+ }
45021
+
45022
+ // --- accept / reject ---
45023
+
45024
+ function decisionUser() {
45025
+ return config.currentUser || { id: "user", name: "User", color: "#2563eb" };
45026
+ }
45027
+
45028
+ function canDecide(entry, action) {
45029
+ if (typeof config.reviewCanDecide !== "function") return true;
45030
+ var allowed = false;
45031
+ var user = decisionUser();
45032
+ try { allowed = config.reviewCanDecide(entry, action, user, editor) !== false; }
45033
+ catch (err) {
45034
+ console.warn("trackedchanges: reviewCanDecide threw; denying decision.", err);
45035
+ }
45036
+ if (allowed) return true;
45037
+ var detail = { entry: entry, action: action, user: user };
45038
+ try { typeof config.onReviewDecisionDenied === "function" && config.onReviewDecisionDenied(detail); } catch (ignore) { }
45039
+ try { typeof editor.fireEvent === "function" && editor.fireEvent("review_decision_denied", detail); } catch (ignore2) { }
45040
+ return false;
45041
+ }
45042
+
45043
+ function acceptEntry(id) {
45044
+ var entry = editor.reviewLedger.get(id);
45045
+ if (!entry) return false;
45046
+ if (!canDecide(entry, "accept")) return false;
45047
+ var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
45048
+ if (entry.changeType === "insert") {
45049
+ if (span) unwrapKeepChildren(span);
45050
+ } else if (entry.changeType === "delete") {
45051
+ if (span) span.remove();
45052
+ }
45053
+ editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now(), decidedBy: decisionUser() });
45054
+ return true;
45055
+ }
45056
+
45057
+ function rejectEntry(id) {
45058
+ var entry = editor.reviewLedger.get(id);
45059
+ if (!entry) return false;
45060
+ if (!canDecide(entry, "reject")) return false;
45061
+ var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
45062
+ if (entry.changeType === "insert") {
45063
+ if (span) span.remove();
45064
+ } else if (entry.changeType === "delete") {
45065
+ if (span) unwrapKeepChildren(span);
45066
+ }
45067
+ editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now(), decidedBy: decisionUser() });
45068
+ return true;
45069
+ }
45070
+
45071
+ function acceptAll(filter) {
45072
+ var entries = editor.reviewLedger.list(filter).filter(function (e) {
45073
+ return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
45074
+ });
45075
+ var accepted = 0;
45076
+ for (var i = 0; i < entries.length; i++) if (acceptEntry(entries[i].id)) accepted++;
45077
+ return accepted;
45078
+ }
45079
+
45080
+ function rejectAll(filter) {
45081
+ var entries = editor.reviewLedger.list(filter).filter(function (e) {
45082
+ return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
45083
+ });
45084
+ var rejected = 0;
45085
+ for (var i = 0; i < entries.length; i++) if (rejectEntry(entries[i].id)) rejected++;
45086
+ return rejected;
45087
+ }
45088
+
45089
+ function unwrapKeepChildren(el) {
45090
+ var parent = el.parentNode;
45091
+ if (!parent) return;
45092
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
45093
+ parent.removeChild(el);
45094
+ }
45095
+
45096
+ // 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
45097
+ // Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
45098
+ // containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
45099
+ // colons, base64 IDs with `=` padding).
45100
+ function cssEscape(s) {
45101
+ var str = String(s);
45102
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
45103
+ try { return CSS.escape(str); } catch (e) { /* fall through */ }
45104
+ }
45105
+ return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
45106
+ return "\\" + c;
45107
+ });
45108
+ }
45109
+
45110
+ // --- styles ---
45111
+
45112
+ function injectStyles() {
45113
+ var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
45114
+ if (!host.querySelector("style[data-rte-trackchanges]")) {
45115
+ var style = host.createElement("style");
45116
+ style.setAttribute("data-rte-trackchanges", "1");
45117
+ style.textContent = [
45118
+ ".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
45119
+ ".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active{background:linear-gradient(135deg,#e9f7ef,#eef5ff);color:#17633f;box-shadow:inset 0 0 0 1px rgba(34,132,82,.28),0 6px 14px rgba(34,132,82,.12)}",
45120
+ ".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active:after{content:'';position:absolute;right:5px;top:5px;width:6px;height:6px;border-radius:50%;background:#22a66a;box-shadow:0 0 0 2px #fff}",
45121
+ ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
45122
+ ".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
45123
+ ".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
45124
+ ].join("\n");
45125
+ host.head.appendChild(style);
45126
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
45127
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
45128
+ // the path everyone else takes; this re-injects through the CSSOM only when
45129
+ // the policy actually emptied it.
45130
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
45131
+ editor.ensureStyleSheetLive(host, style, "rte-trackedchanges-style", style.textContent);
45132
+ }
45133
+ }
45134
+
45135
+ var editdoc = editor.getDocument();
45136
+ if (editdoc && editdoc.head && !editdoc.querySelector("style[data-rte-trackchanges-inline]")) {
45137
+ var iStyle = editdoc.createElement("style");
45138
+ iStyle.setAttribute("data-rte-trackchanges-inline", "1");
45139
+ iStyle.textContent = [
45140
+ ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
45141
+ ".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
45142
+ ".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
45143
+ ].join("\n");
45144
+ editdoc.head.appendChild(iStyle);
45145
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
45146
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
45147
+ // the path everyone else takes; this re-injects through the CSSOM only when
45148
+ // the policy actually emptied it.
45149
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
45150
+ editor.ensureStyleSheetLive(editdoc, iStyle, "rte-trackedchanges-iStyle", iStyle.textContent);
45151
+ }
45152
+ }
45153
+ }
45154
+ }
44871
45155
 
44872
45156
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
44873
45157
 
@@ -45045,6 +45329,13 @@ function RTE_Plugin_Typewriter() {
45045
45329
  st.id = "rte-typewriter-styles";
45046
45330
  st.appendChild(doc.createTextNode(css));
45047
45331
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
45332
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
45333
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
45334
+ // the path everyone else takes; this re-injects through the CSSOM only when
45335
+ // the policy actually emptied it.
45336
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
45337
+ editor.ensureStyleSheetLive(doc, st, "rte-typewriter-styles", st.textContent);
45338
+ }
45048
45339
  }
45049
45340
 
45050
45341
  // Strip the transient focus class around any serialize so saved markup never
@@ -45234,6 +45525,14 @@ function RTE_Plugin_Watermark() {
45234
45525
  ? ""
45235
45526
  : "@media print{[data-rte-watermark]{background-image:none !important;}" +
45236
45527
  "[data-rte-watermark]::before{display:none !important;}}");
45528
+ // Skipped by the scripted conversion on purpose: this element is reused
45529
+ // and its text is rewritten on every watermark change, so the guard has to
45530
+ // sit after the assignment rather than after the append. Same contract as
45531
+ // everywhere else - the element stays primary, the CSSOM path fires only
45532
+ // when a strict style-src emptied it.
45533
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
45534
+ editor.ensureStyleSheetLive(doc, st, "rte-watermark-dynamic", st.textContent);
45535
+ }
45237
45536
  }
45238
45537
 
45239
45538
  // ---- serialization safety -------------------------------------------
@@ -45285,6 +45584,13 @@ function RTE_Plugin_Watermark() {
45285
45584
  st.id = "rte-watermark-styles";
45286
45585
  st.appendChild(doc.createTextNode("[data-rte-watermark]{position:relative;}"));
45287
45586
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
45587
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
45588
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
45589
+ // the path everyone else takes; this re-injects through the CSSOM only when
45590
+ // the policy actually emptied it.
45591
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
45592
+ editor.ensureStyleSheetLive(doc, st, "rte-watermark-styles", st.textContent);
45593
+ }
45288
45594
  }
45289
45595
  }
45290
45596
 
@@ -46335,6 +46641,13 @@ function RTE_Plugin_YjsCollab() {
46335
46641
  ".rte-collab-presence-chip.is-self{outline:2px solid rgba(15,23,42,.25);outline-offset:1px}"
46336
46642
  ].join("\n");
46337
46643
  document.head.appendChild(style);
46644
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
46645
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
46646
+ // the path everyone else takes; this re-injects through the CSSOM only when
46647
+ // the policy actually emptied it.
46648
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
46649
+ editor.ensureStyleSheetLive(document, style, "rte-yjscollab-style", style.textContent);
46650
+ }
46338
46651
  }
46339
46652
  }
46340
46653