@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.
- package/package.json +1 -1
- package/richtexteditor/all_plugins.js +1032 -719
- package/richtexteditor/all_plugins.min.js +41 -41
- package/richtexteditor/plugins/accessibilitychecker.js +7 -0
- package/richtexteditor/plugins/all_plugins.js +1032 -719
- package/richtexteditor/plugins/all_plugins.min.js +41 -41
- package/richtexteditor/plugins/autoembed.js +7 -0
- package/richtexteditor/plugins/blocktypes.js +7 -0
- package/richtexteditor/plugins/bookmarkcard.js +7 -0
- package/richtexteditor/plugins/charlimit.js +7 -0
- package/richtexteditor/plugins/comments.js +14 -0
- package/richtexteditor/plugins/contentminimap.js +7 -0
- package/richtexteditor/plugins/crossreference.js +7 -0
- package/richtexteditor/plugins/dictation.js +7 -0
- package/richtexteditor/plugins/documentoutline.js +9 -3
- package/richtexteditor/plugins/draghandle.js +7 -0
- package/richtexteditor/plugins/foldheadings.js +7 -0
- package/richtexteditor/plugins/footnotes.js +7 -0
- package/richtexteditor/plugins/formatpainter.js +7 -0
- package/richtexteditor/plugins/formattingmarks.js +7 -0
- package/richtexteditor/plugins/gapcursor.js +7 -0
- package/richtexteditor/plugins/ghostcomplete.js +7 -0
- package/richtexteditor/plugins/layouttable.js +7 -0
- package/richtexteditor/plugins/linenumbers.js +7 -0
- package/richtexteditor/plugins/linkchecker.js +7 -0
- package/richtexteditor/plugins/mentionplugin.js +14 -0
- package/richtexteditor/plugins/mergefields.js +7 -0
- package/richtexteditor/plugins/mermaiddiagram.js +7 -0
- package/richtexteditor/plugins/multilevellist.js +7 -0
- package/richtexteditor/plugins/pagination.js +7 -0
- package/richtexteditor/plugins/restrictedediting.js +7 -0
- package/richtexteditor/plugins/revisionhistory.js +7 -0
- package/richtexteditor/plugins/rtlui.js +7 -0
- package/richtexteditor/plugins/rubyannotation.js +6 -0
- package/richtexteditor/plugins/slashcommand.js +7 -0
- package/richtexteditor/plugins/smartchips.js +7 -0
- package/richtexteditor/plugins/spellcheck.js +7 -0
- package/richtexteditor/plugins/stateinspector.js +6 -0
- package/richtexteditor/plugins/tableofcontents.js +7 -0
- package/richtexteditor/plugins/textdirection.js +7 -0
- package/richtexteditor/plugins/textpartlanguage.js +7 -0
- package/richtexteditor/plugins/todolist.js +7 -0
- package/richtexteditor/plugins/trackedchanges.js +730 -716
- package/richtexteditor/plugins/typewriter.js +7 -0
- package/richtexteditor/plugins/watermark.js +15 -0
- package/richtexteditor/plugins/yjscollab.js +7 -0
- 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
|
|
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
|
-
|
|
44859
|
-
|
|
44860
|
-
|
|
44861
|
-
|
|
44862
|
-
|
|
44863
|
-
|
|
44864
|
-
|
|
44865
|
-
|
|
44866
|
-
|
|
44867
|
-
editdoc.
|
|
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
|
|