@richscripts/richtexteditor 2.8.0 → 2.9.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 +1020 -719
- package/richtexteditor/all_plugins.min.js +41 -41
- package/richtexteditor/plugins/accessibilitychecker.js +7 -0
- package/richtexteditor/plugins/all_plugins.js +1020 -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/slashcommand.js +7 -0
- package/richtexteditor/plugins/smartchips.js +7 -0
- package/richtexteditor/plugins/spellcheck.js +7 -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
|
|
|
@@ -41137,6 +41346,13 @@ function RTE_Plugin_SlashCommand() {
|
|
|
41137
41346
|
".rte-slash-popup-dark .rte-slash-empty{color:#94a3b8}"
|
|
41138
41347
|
].join("\n");
|
|
41139
41348
|
host.head.appendChild(style);
|
|
41349
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
41350
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
41351
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
41352
|
+
// the policy actually emptied it.
|
|
41353
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
41354
|
+
editor.ensureStyleSheetLive(host, style, "rte-slashcommand-style", style.textContent);
|
|
41355
|
+
}
|
|
41140
41356
|
}
|
|
41141
41357
|
|
|
41142
41358
|
// --- Icons (minimal inline SVGs) ---
|
|
@@ -41441,6 +41657,13 @@ function RTE_Plugin_SmartChips() {
|
|
|
41441
41657
|
st.setAttribute("data-rte-smartchips", "1");
|
|
41442
41658
|
st.textContent = css;
|
|
41443
41659
|
head.appendChild(st);
|
|
41660
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
41661
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
41662
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
41663
|
+
// the policy actually emptied it.
|
|
41664
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
41665
|
+
editor.ensureStyleSheetLive(editdoc, st, "rte-smartchips-st", st.textContent);
|
|
41666
|
+
}
|
|
41444
41667
|
}
|
|
41445
41668
|
} catch (e) { /* ignore */ }
|
|
41446
41669
|
}
|
|
@@ -41626,6 +41849,13 @@ function RTE_Plugin_SpellCheck() {
|
|
|
41626
41849
|
st.setAttribute("data-rte-spellcheck", "1");
|
|
41627
41850
|
st.textContent = css;
|
|
41628
41851
|
(host.head || host.documentElement).appendChild(st);
|
|
41852
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
41853
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
41854
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
41855
|
+
// the policy actually emptied it.
|
|
41856
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
41857
|
+
editor.ensureStyleSheetLive(host, st, "rte-spellcheck-st", st.textContent);
|
|
41858
|
+
}
|
|
41629
41859
|
}
|
|
41630
41860
|
} catch (e) { /* ignore */ }
|
|
41631
41861
|
}
|
|
@@ -42849,6 +43079,13 @@ function RTE_Plugin_TableOfContents() {
|
|
|
42849
43079
|
st.setAttribute("data-css", text);
|
|
42850
43080
|
st.appendChild(doc.createTextNode(text));
|
|
42851
43081
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
43082
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
43083
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
43084
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
43085
|
+
// the policy actually emptied it.
|
|
43086
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
43087
|
+
editor.ensureStyleSheetLive(doc, st, "rte-toc-styles", st.textContent);
|
|
43088
|
+
}
|
|
42852
43089
|
}
|
|
42853
43090
|
}
|
|
42854
43091
|
|
|
@@ -43506,6 +43743,13 @@ function RTE_Plugin_TextDirection() {
|
|
|
43506
43743
|
st.setAttribute("data-css", text);
|
|
43507
43744
|
st.appendChild(doc.createTextNode(text));
|
|
43508
43745
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
43746
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
43747
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
43748
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
43749
|
+
// the policy actually emptied it.
|
|
43750
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
43751
|
+
editor.ensureStyleSheetLive(doc, st, "rte-textdirection-styles", st.textContent);
|
|
43752
|
+
}
|
|
43509
43753
|
}
|
|
43510
43754
|
}
|
|
43511
43755
|
|
|
@@ -44011,6 +44255,13 @@ function RTE_Plugin_TextPartLanguage() {
|
|
|
44011
44255
|
st.setAttribute("data-css", text);
|
|
44012
44256
|
st.appendChild(doc.createTextNode(text));
|
|
44013
44257
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
44258
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
44259
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
44260
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
44261
|
+
// the policy actually emptied it.
|
|
44262
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
44263
|
+
editor.ensureStyleSheetLive(doc, st, "rte-textpartlanguage-styles", st.textContent);
|
|
44264
|
+
}
|
|
44014
44265
|
}
|
|
44015
44266
|
}
|
|
44016
44267
|
|
|
@@ -44099,6 +44350,13 @@ function RTE_Plugin_TodoList() {
|
|
|
44099
44350
|
st.id = "rte-todolist-styles";
|
|
44100
44351
|
st.appendChild(doc.createTextNode(css));
|
|
44101
44352
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
44353
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
44354
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
44355
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
44356
|
+
// the policy actually emptied it.
|
|
44357
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
44358
|
+
editor.ensureStyleSheetLive(doc, st, "rte-todolist-styles", st.textContent);
|
|
44359
|
+
}
|
|
44102
44360
|
}
|
|
44103
44361
|
|
|
44104
44362
|
function makeCheckbox() {
|
|
@@ -44152,722 +44410,736 @@ function RTE_Plugin_TodoList() {
|
|
|
44152
44410
|
};
|
|
44153
44411
|
}
|
|
44154
44412
|
|
|
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
|
-
}
|
|
44413
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
44414
|
+
|
|
44415
|
+
if (!RTE_DefaultConfig.svgCode_trackchanges) {
|
|
44416
|
+
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>';
|
|
44417
|
+
}
|
|
44418
|
+
|
|
44419
|
+
RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
|
|
44420
|
+
|
|
44421
|
+
function RTE_Plugin_TrackedChanges() {
|
|
44422
|
+
var obj = this;
|
|
44423
|
+
var config;
|
|
44424
|
+
var editor;
|
|
44425
|
+
var enabled = false;
|
|
44426
|
+
|
|
44427
|
+
obj.PluginName = "TrackedChanges";
|
|
44428
|
+
|
|
44429
|
+
obj.InitConfig = function (argconfig) {
|
|
44430
|
+
config = argconfig;
|
|
44431
|
+
if (config.trackChangesEnabled === false) return;
|
|
44432
|
+
|
|
44433
|
+
config.currentUser = config.currentUser || null;
|
|
44434
|
+
config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
|
|
44435
|
+
config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
|
|
44436
|
+
// Optional application policy for accept/reject. Return false to keep a
|
|
44437
|
+
// suggestion pending. The default remains permissive for compatibility.
|
|
44438
|
+
config.reviewCanDecide = config.reviewCanDecide || null;
|
|
44439
|
+
config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
|
|
44440
|
+
|
|
44441
|
+
// Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
|
|
44442
|
+
config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
|
|
44443
|
+
appendToolbarCommand("toolbar_default", "#{trackchanges}");
|
|
44444
|
+
appendToolbarCommand("toolbar_full", "#{trackchanges}");
|
|
44445
|
+
appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
|
|
44446
|
+
|
|
44447
|
+
if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
|
|
44448
|
+
config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
|
|
44449
|
+
}
|
|
44450
|
+
};
|
|
44451
|
+
|
|
44452
|
+
obj.InitEditor = function (argeditor) {
|
|
44453
|
+
editor = argeditor;
|
|
44454
|
+
if (config.trackChangesEnabled === false) return;
|
|
44455
|
+
|
|
44456
|
+
editor.trackedChanges = {
|
|
44457
|
+
enable: function (user) { enable(user); },
|
|
44458
|
+
disable: function () { disable(); },
|
|
44459
|
+
isEnabled: function () { return enabled; },
|
|
44460
|
+
acceptAll: function (filter) { return acceptAll(filter); },
|
|
44461
|
+
rejectAll: function (filter) { return rejectAll(filter); },
|
|
44462
|
+
accept: function (id) { return acceptEntry(id); },
|
|
44463
|
+
reject: function (id) { return rejectEntry(id); },
|
|
44464
|
+
canDecide: function (id, action) {
|
|
44465
|
+
var entry = editor.reviewLedger && editor.reviewLedger.get(id);
|
|
44466
|
+
return !!entry && canDecide(entry, action || "accept");
|
|
44467
|
+
},
|
|
44468
|
+
list: function (opts) {
|
|
44469
|
+
if (!editor.reviewLedger) return [];
|
|
44470
|
+
// Default to the PENDING review queue (matches the internal
|
|
44471
|
+
// accept/reject semantics). Pass {all:true} for full history
|
|
44472
|
+
// including accepted/rejected entries (also via reviewLedger.list()).
|
|
44473
|
+
var includeAll = opts && opts.all === true;
|
|
44474
|
+
return editor.reviewLedger.list().filter(function (e) {
|
|
44475
|
+
if (e.changeType !== "insert" && e.changeType !== "delete") return false;
|
|
44476
|
+
return includeAll || e.status === "pending";
|
|
44477
|
+
});
|
|
44478
|
+
}
|
|
44479
|
+
};
|
|
44480
|
+
|
|
44481
|
+
injectStyles();
|
|
44482
|
+
|
|
44483
|
+
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
44484
|
+
editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
|
|
44485
|
+
var btn = editor.createToolbarButton(cmd);
|
|
44486
|
+
btn.__tcSync = function () {
|
|
44487
|
+
if (enabled) btn.classList.add("rte-ui-active");
|
|
44488
|
+
else btn.classList.remove("rte-ui-active");
|
|
44489
|
+
btn.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
44490
|
+
// WCAG 2.5.3 Label in Name: the visible label (this tooltip) must be
|
|
44491
|
+
// contained in the accessible name. "Turn on suggesting mode" was not
|
|
44492
|
+
// — the accessible name is "Suggesting mode" — so a speech-input user
|
|
44493
|
+
// saying what they see did not match the control. State belongs in
|
|
44494
|
+
// aria-pressed and the active styling, not in the visible label.
|
|
44495
|
+
btn.setAttribute("title", config.text_trackchanges || "Suggesting mode");
|
|
44496
|
+
};
|
|
44497
|
+
btn.__tcSync();
|
|
44498
|
+
return btn;
|
|
44499
|
+
};
|
|
44500
|
+
|
|
44501
|
+
editor.attachEvent("exec_command_trackchanges", function (state) {
|
|
44502
|
+
state.returnValue = true;
|
|
44503
|
+
state.stopBubble = true;
|
|
44504
|
+
if (enabled) disable();
|
|
44505
|
+
else enable();
|
|
44506
|
+
});
|
|
44507
|
+
|
|
44508
|
+
editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
|
|
44509
|
+
};
|
|
44510
|
+
|
|
44511
|
+
function appendToolbarCommand(toolbar, item) {
|
|
44512
|
+
if (!config[toolbar]) return;
|
|
44513
|
+
if (config[toolbar].indexOf(item) !== -1) return;
|
|
44514
|
+
config[toolbar] = config[toolbar] + item;
|
|
44515
|
+
}
|
|
44516
|
+
|
|
44517
|
+
function enable(user) {
|
|
44518
|
+
if (user) config.currentUser = user;
|
|
44519
|
+
if (!config.currentUser) {
|
|
44520
|
+
config.currentUser = { id: "user", name: "User", color: "#2563eb" };
|
|
44521
|
+
console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
|
|
44522
|
+
}
|
|
44523
|
+
enabled = true;
|
|
44524
|
+
syncToolbarButtons();
|
|
44525
|
+
}
|
|
44526
|
+
|
|
44527
|
+
function disable() {
|
|
44528
|
+
enabled = false;
|
|
44529
|
+
syncToolbarButtons();
|
|
44530
|
+
}
|
|
44531
|
+
|
|
44532
|
+
function syncToolbarButtons() {
|
|
44533
|
+
var doc = editor.iframe && editor.iframe.ownerDocument || document;
|
|
44534
|
+
var btns = doc.querySelectorAll(".rte_command_trackchanges");
|
|
44535
|
+
for (var i = 0; i < btns.length; i++) {
|
|
44536
|
+
if (btns[i].__tcSync) btns[i].__tcSync();
|
|
44537
|
+
}
|
|
44538
|
+
}
|
|
44539
|
+
|
|
44540
|
+
// --- beforeinput intercept ---
|
|
44541
|
+
|
|
44542
|
+
function onBeforeInput(e) {
|
|
44543
|
+
if (!enabled) return;
|
|
44544
|
+
if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
|
|
44545
|
+
|
|
44546
|
+
var type = e.inputType;
|
|
44547
|
+
if (!type) return;
|
|
44548
|
+
|
|
44549
|
+
if (type === "insertText" || type === "insertReplacementText") {
|
|
44550
|
+
e.preventDefault();
|
|
44551
|
+
handleInsertText(e.data || "");
|
|
44552
|
+
return;
|
|
44553
|
+
}
|
|
44554
|
+
if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
|
|
44555
|
+
e.preventDefault();
|
|
44556
|
+
handleDelete(type);
|
|
44557
|
+
return;
|
|
44558
|
+
}
|
|
44559
|
+
// insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
|
|
44560
|
+
}
|
|
44561
|
+
|
|
44562
|
+
// --- insert tracking ---
|
|
44563
|
+
|
|
44564
|
+
function handleInsertText(text) {
|
|
44565
|
+
if (!text) return;
|
|
44566
|
+
var sel = editor.getSelection();
|
|
44567
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
44568
|
+
var range = sel.getRangeAt(0);
|
|
44569
|
+
if (!range.collapsed) {
|
|
44570
|
+
// Selection replace: wrap selection as delete, then insert new text.
|
|
44571
|
+
if (!wrapRangeAsDelete(range)) return;
|
|
44572
|
+
// After wrapping, caret should be past the delete span; re-fetch selection.
|
|
44573
|
+
range = sel.getRangeAt(0);
|
|
44574
|
+
createInsertSpan(text, range);
|
|
44575
|
+
return;
|
|
44576
|
+
}
|
|
44577
|
+
|
|
44578
|
+
var mergeTarget = adjacentInsertSpan(range);
|
|
44579
|
+
if (mergeTarget) {
|
|
44580
|
+
appendToInsertSpan(mergeTarget, text);
|
|
44581
|
+
} else {
|
|
44582
|
+
createInsertSpan(text, range);
|
|
44583
|
+
}
|
|
44584
|
+
}
|
|
44585
|
+
|
|
44586
|
+
function adjacentInsertSpan(range) {
|
|
44587
|
+
// If caret sits inside an insert span by the current user, or immediately after one, merge.
|
|
44588
|
+
var node = range.startContainer;
|
|
44589
|
+
if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
|
|
44590
|
+
return node.parentNode;
|
|
44591
|
+
}
|
|
44592
|
+
// Caret just after an insert span (at an element boundary).
|
|
44593
|
+
if (node.nodeType === 1 && range.startOffset > 0) {
|
|
44594
|
+
var prev = node.childNodes[range.startOffset - 1];
|
|
44595
|
+
if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
|
|
44596
|
+
}
|
|
44597
|
+
// Caret at the very beginning just before our span.
|
|
44598
|
+
if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
|
|
44599
|
+
return node.previousSibling;
|
|
44600
|
+
}
|
|
44601
|
+
return null;
|
|
44602
|
+
}
|
|
44603
|
+
|
|
44604
|
+
function isMyInsert(el) {
|
|
44605
|
+
if (!el || el.nodeType !== 1) return false;
|
|
44606
|
+
if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
|
|
44607
|
+
return el.getAttribute("data-tc-author") === config.currentUser.id;
|
|
44608
|
+
}
|
|
44609
|
+
|
|
44610
|
+
// We preventDefault() the browser's own text insertion, which means we also
|
|
44611
|
+
// lose the whitespace handling it does for free. A plain second space is
|
|
44612
|
+
// collapsed by HTML rendering, so pressing space a second time moved nothing
|
|
44613
|
+
// on screen and suggesting mode looked like it had stopped accepting input.
|
|
44614
|
+
// Reproduce what contenteditable does: any space followed by another space,
|
|
44615
|
+
// and any trailing space, becomes a non-breaking space. Recomputed from
|
|
44616
|
+
// scratch each time so an NBSP reverts to a normal space once real text
|
|
44617
|
+
// follows it (keeping word wrap and copied text clean). Only typed text
|
|
44618
|
+
// reaches this path -- paste arrives as insertFromPaste and is passed
|
|
44619
|
+
// through -- so this never rewrites an NBSP the user inserted deliberately.
|
|
44620
|
+
function normalizeInsertWhitespace(node) {
|
|
44621
|
+
if (!node || node.nodeType !== 3) return;
|
|
44622
|
+
var v = node.nodeValue;
|
|
44623
|
+
if (v.indexOf(" ") === -1 && v.indexOf("\u00a0") === -1) return;
|
|
44624
|
+
v = v.replace(/\u00a0/g, " ");
|
|
44625
|
+
v = v.replace(/ (?= )/g, "\u00a0");
|
|
44626
|
+
v = v.replace(/ $/, "\u00a0");
|
|
44627
|
+
if (v !== node.nodeValue) node.nodeValue = v;
|
|
44628
|
+
}
|
|
44629
|
+
|
|
44630
|
+
function appendToInsertSpan(span, text) {
|
|
44631
|
+
var editdoc = editor.getDocument();
|
|
44632
|
+
// Append to the span's last text node (or create one).
|
|
44633
|
+
var last = span.lastChild;
|
|
44634
|
+
var node;
|
|
44635
|
+
if (last && last.nodeType === 3) {
|
|
44636
|
+
last.nodeValue = last.nodeValue + text;
|
|
44637
|
+
node = last;
|
|
44638
|
+
} else {
|
|
44639
|
+
node = editdoc.createTextNode(text);
|
|
44640
|
+
span.appendChild(node);
|
|
44641
|
+
}
|
|
44642
|
+
normalizeInsertWhitespace(node);
|
|
44643
|
+
placeCaretAtEnd(node);
|
|
44644
|
+
updateEntryForSpan(span);
|
|
44645
|
+
}
|
|
44646
|
+
|
|
44647
|
+
function createInsertSpan(text, range) {
|
|
44648
|
+
var editdoc = editor.getDocument();
|
|
44649
|
+
var span = editdoc.createElement("span");
|
|
44650
|
+
span.className = config.trackChangesInsertClass;
|
|
44651
|
+
var user = config.currentUser;
|
|
44652
|
+
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
44653
|
+
span.setAttribute("data-tc-id", id);
|
|
44654
|
+
span.setAttribute("data-tc-author", user.id);
|
|
44655
|
+
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
44656
|
+
span.style.color = user.color;
|
|
44657
|
+
span.style.textDecoration = "underline";
|
|
44658
|
+
span.appendChild(editdoc.createTextNode(text));
|
|
44659
|
+
range.insertNode(span);
|
|
44660
|
+
normalizeInsertWhitespace(span.firstChild);
|
|
44661
|
+
placeCaretAtEnd(span.firstChild);
|
|
44662
|
+
|
|
44663
|
+
editor.reviewLedger.add({
|
|
44664
|
+
id: id,
|
|
44665
|
+
changeType: "insert",
|
|
44666
|
+
author: user,
|
|
44667
|
+
text: text,
|
|
44668
|
+
status: "pending",
|
|
44669
|
+
sourceLabel: "Suggested insert",
|
|
44670
|
+
createdAt: Date.now()
|
|
44671
|
+
});
|
|
44672
|
+
}
|
|
44673
|
+
|
|
44674
|
+
function updateEntryForSpan(span) {
|
|
44675
|
+
var id = span.getAttribute("data-tc-id");
|
|
44676
|
+
if (!id) return;
|
|
44677
|
+
editor.reviewLedger.update(id, { text: span.textContent });
|
|
44678
|
+
}
|
|
44679
|
+
|
|
44680
|
+
// --- delete tracking ---
|
|
44681
|
+
|
|
44682
|
+
function handleDelete(mode) {
|
|
44683
|
+
var sel = editor.getSelection();
|
|
44684
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
44685
|
+
var range = sel.getRangeAt(0);
|
|
44686
|
+
|
|
44687
|
+
if (!range.collapsed) {
|
|
44688
|
+
wrapRangeAsDelete(range);
|
|
44689
|
+
return;
|
|
44690
|
+
}
|
|
44691
|
+
|
|
44692
|
+
// Collapsed: extend by one char in the appropriate direction, then wrap.
|
|
44693
|
+
// The extended range covers only that one character — any delete spans between
|
|
44694
|
+
// it and the caret should be SKIPPED, not re-wrapped.
|
|
44695
|
+
var extended = editor.getDocument().createRange();
|
|
44696
|
+
if (mode === "deleteContentBackward") {
|
|
44697
|
+
var anchor = prevCharPoint(range.startContainer, range.startOffset);
|
|
44698
|
+
if (!anchor) return;
|
|
44699
|
+
extended.setStart(anchor.node, anchor.offset);
|
|
44700
|
+
extended.setEnd(anchor.node, anchor.offset + 1);
|
|
44701
|
+
} else {
|
|
44702
|
+
var forward = nextCharPoint(range.startContainer, range.startOffset);
|
|
44703
|
+
if (!forward) return;
|
|
44704
|
+
extended.setStart(forward.node, forward.offset - 1);
|
|
44705
|
+
extended.setEnd(forward.node, forward.offset);
|
|
44706
|
+
}
|
|
44707
|
+
|
|
44708
|
+
// If that extended range sits entirely inside one of the current user's own
|
|
44709
|
+
// insert spans, treat this as undoing their own typing — actually delete.
|
|
44710
|
+
if (rangeInsideMyInsert(extended)) {
|
|
44711
|
+
extended.deleteContents();
|
|
44712
|
+
var sel2 = editor.getSelection();
|
|
44713
|
+
sel2.removeAllRanges();
|
|
44714
|
+
sel2.addRange(extended);
|
|
44715
|
+
// If the insert span is now empty, remove it and its ledger entry.
|
|
44716
|
+
cleanupEmptyInsertSpans();
|
|
44717
|
+
return;
|
|
44718
|
+
}
|
|
44719
|
+
|
|
44720
|
+
wrapRangeAsDelete(extended);
|
|
44721
|
+
}
|
|
44722
|
+
|
|
44723
|
+
function isInsideDeleteSpan(node) {
|
|
44724
|
+
var walk = node.nodeType === 1 ? node : node.parentNode;
|
|
44725
|
+
while (walk && walk !== editor.getEditable()) {
|
|
44726
|
+
if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
|
|
44727
|
+
walk = walk.parentNode;
|
|
44728
|
+
}
|
|
44729
|
+
return false;
|
|
44730
|
+
}
|
|
44731
|
+
|
|
44732
|
+
// Walk text leaves in the editable, skipping any that live inside an existing delete
|
|
44733
|
+
// span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
|
|
44734
|
+
function liveTextLeaves() {
|
|
44735
|
+
var editable = editor.getEditable();
|
|
44736
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
|
|
44737
|
+
acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
|
|
44738
|
+
});
|
|
44739
|
+
var out = [], n;
|
|
44740
|
+
while ((n = walker.nextNode())) out.push(n);
|
|
44741
|
+
return out;
|
|
44742
|
+
}
|
|
44743
|
+
|
|
44744
|
+
function prevCharPoint(node, offset) {
|
|
44745
|
+
var leaves = liveTextLeaves();
|
|
44746
|
+
if (!leaves.length) return null;
|
|
44747
|
+
var idx = caretIndex(node, offset, leaves);
|
|
44748
|
+
if (idx <= 0) return null;
|
|
44749
|
+
return leafOffsetAt(idx - 1, leaves);
|
|
44750
|
+
}
|
|
44751
|
+
|
|
44752
|
+
function nextCharPoint(node, offset) {
|
|
44753
|
+
var leaves = liveTextLeaves();
|
|
44754
|
+
if (!leaves.length) return null;
|
|
44755
|
+
var total = 0;
|
|
44756
|
+
for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
|
|
44757
|
+
var idx = caretIndex(node, offset, leaves);
|
|
44758
|
+
if (idx >= total) return null;
|
|
44759
|
+
return leafOffsetAt(idx + 1, leaves);
|
|
44760
|
+
}
|
|
44761
|
+
|
|
44762
|
+
// How many live characters appear before the given caret position?
|
|
44763
|
+
function caretIndex(node, offset, leaves) {
|
|
44764
|
+
var r = editor.getDocument().createRange();
|
|
44765
|
+
r.setStart(node, offset);
|
|
44766
|
+
r.collapse(true);
|
|
44767
|
+
var count = 0;
|
|
44768
|
+
for (var i = 0; i < leaves.length; i++) {
|
|
44769
|
+
var leaf = leaves[i];
|
|
44770
|
+
// If the entire leaf is before the caret, add its length.
|
|
44771
|
+
var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
|
|
44772
|
+
if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
|
|
44773
|
+
var cmpStart = r.comparePoint(leaf, 0);
|
|
44774
|
+
if (cmpStart >= 0) break; // leaf starts at or after caret
|
|
44775
|
+
// Caret falls inside this leaf — binary search the split point.
|
|
44776
|
+
var lo = 0, hi = leaf.nodeValue.length;
|
|
44777
|
+
while (lo < hi) {
|
|
44778
|
+
var mid = (lo + hi) >> 1;
|
|
44779
|
+
if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
|
|
44780
|
+
else hi = mid;
|
|
44781
|
+
}
|
|
44782
|
+
count += lo;
|
|
44783
|
+
break;
|
|
44784
|
+
}
|
|
44785
|
+
return count;
|
|
44786
|
+
}
|
|
44787
|
+
|
|
44788
|
+
function leafOffsetAt(index, leaves) {
|
|
44789
|
+
var remaining = index;
|
|
44790
|
+
for (var i = 0; i < leaves.length; i++) {
|
|
44791
|
+
if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
|
|
44792
|
+
remaining -= leaves[i].nodeValue.length;
|
|
44793
|
+
}
|
|
44794
|
+
return null;
|
|
44795
|
+
}
|
|
44796
|
+
|
|
44797
|
+
function firstTextLeaf(node) {
|
|
44798
|
+
if (!node) return null;
|
|
44799
|
+
if (node.nodeType === 3) return node;
|
|
44800
|
+
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
44801
|
+
return walker.nextNode();
|
|
44802
|
+
}
|
|
44803
|
+
|
|
44804
|
+
function lastTextLeaf(node) {
|
|
44805
|
+
if (!node) return null;
|
|
44806
|
+
if (node.nodeType === 3) return node;
|
|
44807
|
+
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
44808
|
+
var last = null, n;
|
|
44809
|
+
while ((n = walker.nextNode())) last = n;
|
|
44810
|
+
return last;
|
|
44811
|
+
}
|
|
44812
|
+
|
|
44813
|
+
function previousTextLeaf(node) {
|
|
44814
|
+
var editable = editor.getEditable();
|
|
44815
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
44816
|
+
var prev = null, cur;
|
|
44817
|
+
while ((cur = walker.nextNode())) {
|
|
44818
|
+
if (cur === node) return prev;
|
|
44819
|
+
prev = cur;
|
|
44820
|
+
}
|
|
44821
|
+
return null;
|
|
44822
|
+
}
|
|
44823
|
+
|
|
44824
|
+
function nextTextLeaf(node) {
|
|
44825
|
+
var editable = editor.getEditable();
|
|
44826
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
44827
|
+
var seen = false, cur;
|
|
44828
|
+
while ((cur = walker.nextNode())) {
|
|
44829
|
+
if (seen) return cur;
|
|
44830
|
+
if (cur === node) seen = true;
|
|
44831
|
+
}
|
|
44832
|
+
return null;
|
|
44833
|
+
}
|
|
44834
|
+
|
|
44835
|
+
function rangeInsideMyInsert(range) {
|
|
44836
|
+
var span = range.startContainer.parentNode;
|
|
44837
|
+
while (span && span !== editor.getEditable()) {
|
|
44838
|
+
if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
|
|
44839
|
+
span = span.parentNode;
|
|
44840
|
+
}
|
|
44841
|
+
return false;
|
|
44842
|
+
}
|
|
44843
|
+
|
|
44844
|
+
function wrapRangeAsDelete(range) {
|
|
44845
|
+
if (range.collapsed) return false;
|
|
44846
|
+
var editdoc = editor.getDocument();
|
|
44847
|
+
|
|
44848
|
+
var fragment = range.cloneContents();
|
|
44849
|
+
if (!fragmentHasText(fragment)) return false;
|
|
44850
|
+
|
|
44851
|
+
// Find existing same-author delete spans adjacent to the range so we can merge
|
|
44852
|
+
// sequential backspaces / forward deletes into one contiguous span.
|
|
44853
|
+
var user = config.currentUser;
|
|
44854
|
+
var mergeBefore = adjacentDeleteSpan(range, "before", user);
|
|
44855
|
+
var mergeAfter = adjacentDeleteSpan(range, "after", user);
|
|
44856
|
+
|
|
44857
|
+
range.deleteContents();
|
|
44858
|
+
|
|
44859
|
+
if (mergeBefore) {
|
|
44860
|
+
// Append the fragment's content to the existing "before" span.
|
|
44861
|
+
appendFragmentToSpan(mergeBefore, fragment);
|
|
44862
|
+
if (mergeAfter && mergeAfter !== mergeBefore) {
|
|
44863
|
+
// Collapse both sides: move "after" span's children into "before" and drop it.
|
|
44864
|
+
while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
|
|
44865
|
+
var afterId = mergeAfter.getAttribute("data-tc-id");
|
|
44866
|
+
mergeAfter.remove();
|
|
44867
|
+
if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
|
|
44868
|
+
}
|
|
44869
|
+
var rangeAfter = editdoc.createRange();
|
|
44870
|
+
rangeAfter.setStartAfter(mergeBefore);
|
|
44871
|
+
rangeAfter.collapse(true);
|
|
44872
|
+
var sel = editor.getSelection();
|
|
44873
|
+
sel.removeAllRanges();
|
|
44874
|
+
sel.addRange(rangeAfter);
|
|
44875
|
+
// 2026-05-28 Defense: if a customer pasted a delete-class span from
|
|
44876
|
+
// outside this plugin, it may lack data-tc-id. Skip the ledger
|
|
44877
|
+
// update instead of writing under a `null` key (would silently
|
|
44878
|
+
// corrupt the ledger via the update("null", ...) coercion).
|
|
44879
|
+
var beforeId = mergeBefore.getAttribute("data-tc-id");
|
|
44880
|
+
if (beforeId && editor.reviewLedger) {
|
|
44881
|
+
editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
|
|
44882
|
+
}
|
|
44883
|
+
return true;
|
|
44884
|
+
}
|
|
44885
|
+
|
|
44886
|
+
if (mergeAfter) {
|
|
44887
|
+
prependFragmentToSpan(mergeAfter, fragment);
|
|
44888
|
+
var rangeBefore = editdoc.createRange();
|
|
44889
|
+
rangeBefore.setStartBefore(mergeAfter);
|
|
44890
|
+
rangeBefore.collapse(true);
|
|
44891
|
+
var sel2 = editor.getSelection();
|
|
44892
|
+
sel2.removeAllRanges();
|
|
44893
|
+
sel2.addRange(rangeBefore);
|
|
44894
|
+
// Same defense as above (mergeAfter may lack data-tc-id).
|
|
44895
|
+
var afterMergeId = mergeAfter.getAttribute("data-tc-id");
|
|
44896
|
+
if (afterMergeId && editor.reviewLedger) {
|
|
44897
|
+
editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
|
|
44898
|
+
}
|
|
44899
|
+
return true;
|
|
44900
|
+
}
|
|
44901
|
+
|
|
44902
|
+
var span = editdoc.createElement("span");
|
|
44903
|
+
span.className = config.trackChangesDeleteClass;
|
|
44904
|
+
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
44905
|
+
span.setAttribute("data-tc-id", id);
|
|
44906
|
+
span.setAttribute("data-tc-author", user.id);
|
|
44907
|
+
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
44908
|
+
span.style.color = user.color;
|
|
44909
|
+
span.style.textDecoration = "line-through";
|
|
44910
|
+
span.appendChild(fragment);
|
|
44911
|
+
range.insertNode(span);
|
|
44912
|
+
|
|
44913
|
+
var after = editdoc.createRange();
|
|
44914
|
+
after.setStartAfter(span);
|
|
44915
|
+
after.collapse(true);
|
|
44916
|
+
var sel3 = editor.getSelection();
|
|
44917
|
+
sel3.removeAllRanges();
|
|
44918
|
+
sel3.addRange(after);
|
|
44919
|
+
|
|
44920
|
+
editor.reviewLedger.add({
|
|
44921
|
+
id: id,
|
|
44922
|
+
changeType: "delete",
|
|
44923
|
+
author: user,
|
|
44924
|
+
text: span.textContent,
|
|
44925
|
+
originalHtml: span.innerHTML,
|
|
44926
|
+
status: "pending",
|
|
44927
|
+
sourceLabel: "Suggested delete",
|
|
44928
|
+
createdAt: Date.now()
|
|
44929
|
+
});
|
|
44930
|
+
return true;
|
|
44931
|
+
}
|
|
44932
|
+
|
|
44933
|
+
function adjacentDeleteSpan(range, side, user) {
|
|
44934
|
+
// Find element immediately before or after the range boundaries at the container level.
|
|
44935
|
+
var node = side === "before" ? range.startContainer : range.endContainer;
|
|
44936
|
+
var offset = side === "before" ? range.startOffset : range.endOffset;
|
|
44937
|
+
if (node.nodeType === 3) {
|
|
44938
|
+
// Look at the sibling of the text node if we're at the text node's boundary.
|
|
44939
|
+
if (side === "before" && offset === 0) {
|
|
44940
|
+
var prev = node.previousSibling;
|
|
44941
|
+
if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
|
|
44942
|
+
}
|
|
44943
|
+
if (side === "after" && offset === node.nodeValue.length) {
|
|
44944
|
+
var next = node.nextSibling;
|
|
44945
|
+
if (next && isDeleteSpanByAuthor(next, user)) return next;
|
|
44946
|
+
}
|
|
44947
|
+
return null;
|
|
44948
|
+
}
|
|
44949
|
+
// Element container: look at children on either side of the offset.
|
|
44950
|
+
if (side === "before" && offset > 0) {
|
|
44951
|
+
var childBefore = node.childNodes[offset - 1];
|
|
44952
|
+
if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
|
|
44953
|
+
}
|
|
44954
|
+
if (side === "after" && offset < node.childNodes.length) {
|
|
44955
|
+
var childAfter = node.childNodes[offset];
|
|
44956
|
+
if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
|
|
44957
|
+
}
|
|
44958
|
+
return null;
|
|
44959
|
+
}
|
|
44960
|
+
|
|
44961
|
+
function isDeleteSpanByAuthor(el, user) {
|
|
44962
|
+
if (!el || el.nodeType !== 1) return false;
|
|
44963
|
+
if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
|
|
44964
|
+
return el.getAttribute("data-tc-author") === user.id;
|
|
44965
|
+
}
|
|
44966
|
+
|
|
44967
|
+
function appendFragmentToSpan(span, fragment) {
|
|
44968
|
+
while (fragment.firstChild) span.appendChild(fragment.firstChild);
|
|
44969
|
+
}
|
|
44970
|
+
|
|
44971
|
+
function prependFragmentToSpan(span, fragment) {
|
|
44972
|
+
var first = span.firstChild;
|
|
44973
|
+
while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
|
|
44974
|
+
}
|
|
44975
|
+
|
|
44976
|
+
function fragmentHasText(fragment) {
|
|
44977
|
+
var t = (fragment.textContent || "").replace(/\s+/g, "");
|
|
44978
|
+
return !!t;
|
|
44979
|
+
}
|
|
44980
|
+
|
|
44981
|
+
function cleanupEmptyInsertSpans() {
|
|
44982
|
+
var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
|
|
44983
|
+
for (var i = 0; i < spans.length; i++) {
|
|
44984
|
+
var s = spans[i];
|
|
44985
|
+
if (!s.textContent || !s.textContent.length) {
|
|
44986
|
+
var id = s.getAttribute("data-tc-id");
|
|
44987
|
+
if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
|
|
44988
|
+
s.remove();
|
|
44989
|
+
} else {
|
|
44990
|
+
updateEntryForSpan(s);
|
|
44991
|
+
}
|
|
44992
|
+
}
|
|
44993
|
+
}
|
|
44994
|
+
|
|
44995
|
+
function placeCaretAtEnd(node) {
|
|
44996
|
+
if (!node) return;
|
|
44997
|
+
var editdoc = editor.getDocument();
|
|
44998
|
+
var range = editdoc.createRange();
|
|
44999
|
+
if (node.nodeType === 3) {
|
|
45000
|
+
range.setStart(node, node.nodeValue.length);
|
|
45001
|
+
} else {
|
|
45002
|
+
range.setStart(node, node.childNodes.length);
|
|
45003
|
+
}
|
|
45004
|
+
range.collapse(true);
|
|
45005
|
+
var sel = editor.getSelection();
|
|
45006
|
+
sel.removeAllRanges();
|
|
45007
|
+
sel.addRange(range);
|
|
45008
|
+
}
|
|
45009
|
+
|
|
45010
|
+
// --- accept / reject ---
|
|
45011
|
+
|
|
45012
|
+
function decisionUser() {
|
|
45013
|
+
return config.currentUser || { id: "user", name: "User", color: "#2563eb" };
|
|
45014
|
+
}
|
|
45015
|
+
|
|
45016
|
+
function canDecide(entry, action) {
|
|
45017
|
+
if (typeof config.reviewCanDecide !== "function") return true;
|
|
45018
|
+
var allowed = false;
|
|
45019
|
+
var user = decisionUser();
|
|
45020
|
+
try { allowed = config.reviewCanDecide(entry, action, user, editor) !== false; }
|
|
45021
|
+
catch (err) {
|
|
45022
|
+
console.warn("trackedchanges: reviewCanDecide threw; denying decision.", err);
|
|
45023
|
+
}
|
|
45024
|
+
if (allowed) return true;
|
|
45025
|
+
var detail = { entry: entry, action: action, user: user };
|
|
45026
|
+
try { typeof config.onReviewDecisionDenied === "function" && config.onReviewDecisionDenied(detail); } catch (ignore) { }
|
|
45027
|
+
try { typeof editor.fireEvent === "function" && editor.fireEvent("review_decision_denied", detail); } catch (ignore2) { }
|
|
45028
|
+
return false;
|
|
45029
|
+
}
|
|
45030
|
+
|
|
45031
|
+
function acceptEntry(id) {
|
|
45032
|
+
var entry = editor.reviewLedger.get(id);
|
|
45033
|
+
if (!entry) return false;
|
|
45034
|
+
if (!canDecide(entry, "accept")) return false;
|
|
45035
|
+
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
45036
|
+
if (entry.changeType === "insert") {
|
|
45037
|
+
if (span) unwrapKeepChildren(span);
|
|
45038
|
+
} else if (entry.changeType === "delete") {
|
|
45039
|
+
if (span) span.remove();
|
|
45040
|
+
}
|
|
45041
|
+
editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
45042
|
+
return true;
|
|
45043
|
+
}
|
|
45044
|
+
|
|
45045
|
+
function rejectEntry(id) {
|
|
45046
|
+
var entry = editor.reviewLedger.get(id);
|
|
45047
|
+
if (!entry) return false;
|
|
45048
|
+
if (!canDecide(entry, "reject")) return false;
|
|
45049
|
+
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
45050
|
+
if (entry.changeType === "insert") {
|
|
45051
|
+
if (span) span.remove();
|
|
45052
|
+
} else if (entry.changeType === "delete") {
|
|
45053
|
+
if (span) unwrapKeepChildren(span);
|
|
45054
|
+
}
|
|
45055
|
+
editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
45056
|
+
return true;
|
|
45057
|
+
}
|
|
45058
|
+
|
|
45059
|
+
function acceptAll(filter) {
|
|
45060
|
+
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
45061
|
+
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
45062
|
+
});
|
|
45063
|
+
var accepted = 0;
|
|
45064
|
+
for (var i = 0; i < entries.length; i++) if (acceptEntry(entries[i].id)) accepted++;
|
|
45065
|
+
return accepted;
|
|
45066
|
+
}
|
|
45067
|
+
|
|
45068
|
+
function rejectAll(filter) {
|
|
45069
|
+
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
45070
|
+
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
45071
|
+
});
|
|
45072
|
+
var rejected = 0;
|
|
45073
|
+
for (var i = 0; i < entries.length; i++) if (rejectEntry(entries[i].id)) rejected++;
|
|
45074
|
+
return rejected;
|
|
45075
|
+
}
|
|
45076
|
+
|
|
45077
|
+
function unwrapKeepChildren(el) {
|
|
45078
|
+
var parent = el.parentNode;
|
|
45079
|
+
if (!parent) return;
|
|
45080
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
45081
|
+
parent.removeChild(el);
|
|
45082
|
+
}
|
|
45083
|
+
|
|
45084
|
+
// 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
|
|
45085
|
+
// Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
|
|
45086
|
+
// containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
|
|
45087
|
+
// colons, base64 IDs with `=` padding).
|
|
45088
|
+
function cssEscape(s) {
|
|
45089
|
+
var str = String(s);
|
|
45090
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
45091
|
+
try { return CSS.escape(str); } catch (e) { /* fall through */ }
|
|
45092
|
+
}
|
|
45093
|
+
return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
|
|
45094
|
+
return "\\" + c;
|
|
45095
|
+
});
|
|
45096
|
+
}
|
|
45097
|
+
|
|
45098
|
+
// --- styles ---
|
|
45099
|
+
|
|
45100
|
+
function injectStyles() {
|
|
45101
|
+
var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
|
|
45102
|
+
if (!host.querySelector("style[data-rte-trackchanges]")) {
|
|
45103
|
+
var style = host.createElement("style");
|
|
45104
|
+
style.setAttribute("data-rte-trackchanges", "1");
|
|
45105
|
+
style.textContent = [
|
|
45106
|
+
".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
|
|
45107
|
+
".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)}",
|
|
45108
|
+
".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}",
|
|
45109
|
+
".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
|
|
45110
|
+
".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}",
|
|
45111
|
+
".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}"
|
|
45112
|
+
].join("\n");
|
|
45113
|
+
host.head.appendChild(style);
|
|
45114
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
45115
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
45116
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
45117
|
+
// the policy actually emptied it.
|
|
45118
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
45119
|
+
editor.ensureStyleSheetLive(host, style, "rte-trackedchanges-style", style.textContent);
|
|
45120
|
+
}
|
|
45121
|
+
}
|
|
45122
|
+
|
|
45123
|
+
var editdoc = editor.getDocument();
|
|
45124
|
+
if (editdoc && editdoc.head && !editdoc.querySelector("style[data-rte-trackchanges-inline]")) {
|
|
45125
|
+
var iStyle = editdoc.createElement("style");
|
|
45126
|
+
iStyle.setAttribute("data-rte-trackchanges-inline", "1");
|
|
45127
|
+
iStyle.textContent = [
|
|
45128
|
+
".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
|
|
45129
|
+
".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}",
|
|
45130
|
+
".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}"
|
|
45131
|
+
].join("\n");
|
|
45132
|
+
editdoc.head.appendChild(iStyle);
|
|
45133
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
45134
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
45135
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
45136
|
+
// the policy actually emptied it.
|
|
45137
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
45138
|
+
editor.ensureStyleSheetLive(editdoc, iStyle, "rte-trackedchanges-iStyle", iStyle.textContent);
|
|
45139
|
+
}
|
|
45140
|
+
}
|
|
45141
|
+
}
|
|
45142
|
+
}
|
|
44871
45143
|
|
|
44872
45144
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
44873
45145
|
|
|
@@ -45045,6 +45317,13 @@ function RTE_Plugin_Typewriter() {
|
|
|
45045
45317
|
st.id = "rte-typewriter-styles";
|
|
45046
45318
|
st.appendChild(doc.createTextNode(css));
|
|
45047
45319
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
45320
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
45321
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
45322
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
45323
|
+
// the policy actually emptied it.
|
|
45324
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
45325
|
+
editor.ensureStyleSheetLive(doc, st, "rte-typewriter-styles", st.textContent);
|
|
45326
|
+
}
|
|
45048
45327
|
}
|
|
45049
45328
|
|
|
45050
45329
|
// Strip the transient focus class around any serialize so saved markup never
|
|
@@ -45234,6 +45513,14 @@ function RTE_Plugin_Watermark() {
|
|
|
45234
45513
|
? ""
|
|
45235
45514
|
: "@media print{[data-rte-watermark]{background-image:none !important;}" +
|
|
45236
45515
|
"[data-rte-watermark]::before{display:none !important;}}");
|
|
45516
|
+
// Skipped by the scripted conversion on purpose: this element is reused
|
|
45517
|
+
// and its text is rewritten on every watermark change, so the guard has to
|
|
45518
|
+
// sit after the assignment rather than after the append. Same contract as
|
|
45519
|
+
// everywhere else - the element stays primary, the CSSOM path fires only
|
|
45520
|
+
// when a strict style-src emptied it.
|
|
45521
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
45522
|
+
editor.ensureStyleSheetLive(doc, st, "rte-watermark-dynamic", st.textContent);
|
|
45523
|
+
}
|
|
45237
45524
|
}
|
|
45238
45525
|
|
|
45239
45526
|
// ---- serialization safety -------------------------------------------
|
|
@@ -45285,6 +45572,13 @@ function RTE_Plugin_Watermark() {
|
|
|
45285
45572
|
st.id = "rte-watermark-styles";
|
|
45286
45573
|
st.appendChild(doc.createTextNode("[data-rte-watermark]{position:relative;}"));
|
|
45287
45574
|
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
45575
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
45576
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
45577
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
45578
|
+
// the policy actually emptied it.
|
|
45579
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
45580
|
+
editor.ensureStyleSheetLive(doc, st, "rte-watermark-styles", st.textContent);
|
|
45581
|
+
}
|
|
45288
45582
|
}
|
|
45289
45583
|
}
|
|
45290
45584
|
|
|
@@ -46335,6 +46629,13 @@ function RTE_Plugin_YjsCollab() {
|
|
|
46335
46629
|
".rte-collab-presence-chip.is-self{outline:2px solid rgba(15,23,42,.25);outline-offset:1px}"
|
|
46336
46630
|
].join("\n");
|
|
46337
46631
|
document.head.appendChild(style);
|
|
46632
|
+
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|
|
46633
|
+
// drops its rules, leaving this plugin's UI unstyled. The element above stays
|
|
46634
|
+
// the path everyone else takes; this re-injects through the CSSOM only when
|
|
46635
|
+
// the policy actually emptied it.
|
|
46636
|
+
if (editor && typeof editor.ensureStyleSheetLive === "function") {
|
|
46637
|
+
editor.ensureStyleSheetLive(document, style, "rte-yjscollab-style", style.textContent);
|
|
46638
|
+
}
|
|
46338
46639
|
}
|
|
46339
46640
|
}
|
|
46340
46641
|
|