@richscripts/richtexteditor 2.7.2 → 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 +2583 -719
- package/richtexteditor/all_plugins.min.js +59 -58
- package/richtexteditor/plugins/accessibilitychecker.js +7 -0
- package/richtexteditor/plugins/all_plugins.js +2583 -719
- package/richtexteditor/plugins/all_plugins.min.js +59 -58
- 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/nodetypes.js +439 -0
- package/richtexteditor/plugins/pagination.js +7 -0
- package/richtexteditor/plugins/restrictedediting.js +7 -0
- package/richtexteditor/plugins/revisionhistory.js +7 -0
- package/richtexteditor/plugins/rtlui.js +7 -0
- package/richtexteditor/plugins/rubyannotation.js +255 -0
- package/richtexteditor/plugins/slashcommand.js +7 -0
- package/richtexteditor/plugins/smartchips.js +7 -0
- package/richtexteditor/plugins/spellcheck.js +7 -0
- package/richtexteditor/plugins/stateinspector.js +866 -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
|
|
|
@@ -35368,6 +35549,446 @@ function RTE_Plugin_MultiRoot() {
|
|
|
35368
35549
|
|
|
35369
35550
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
35370
35551
|
|
|
35552
|
+
RTE_DefaultConfig.plugin_nodetypes = RTE_Plugin_NodeTypes;
|
|
35553
|
+
|
|
35554
|
+
// 2026-09-04 Custom node type registry.
|
|
35555
|
+
//
|
|
35556
|
+
// WHAT THE GAP ACTUALLY IS. Measured before this was designed, rather than
|
|
35557
|
+
// assumed. A custom element with data-* attributes already survives more of this
|
|
35558
|
+
// editor than expected:
|
|
35559
|
+
//
|
|
35560
|
+
// setHTML -> getHTML round trip ......... survives, attributes intact
|
|
35561
|
+
// contenteditable="false" atomicity ..... preserved
|
|
35562
|
+
// undo of an editor-driven delete ....... restored, attributes intact
|
|
35563
|
+
// sanitizer ............................. already extensible via
|
|
35564
|
+
// config.sanitizerAllowTags / -Attributes
|
|
35565
|
+
// markdown export ....................... DEGRADES to the element's text
|
|
35566
|
+
// .docx export .......................... DEGRADES to the element's text
|
|
35567
|
+
// an API to declare any of this ......... DID NOT EXIST
|
|
35568
|
+
//
|
|
35569
|
+
// So the gap is not "you cannot put your own element in the document" - you can.
|
|
35570
|
+
// The gap is that nothing lets you DECLARE that the element is a node type, and
|
|
35571
|
+
// therefore nothing downstream can treat it deliberately. Degrading to text in
|
|
35572
|
+
// Markdown and .docx is a defensible default; having no way to say what should
|
|
35573
|
+
// happen instead is not.
|
|
35574
|
+
//
|
|
35575
|
+
// This registry is that declaration. Register a type once and it gets:
|
|
35576
|
+
// - its tags and attributes allowed through the sanitizer automatically
|
|
35577
|
+
// - atomicity enforced and repaired (chips stop being atomic the moment
|
|
35578
|
+
// contenteditable is lost, and then the caret edits their internals)
|
|
35579
|
+
// - JSON serialization of every instance, for saving state outside the HTML
|
|
35580
|
+
// - a Markdown representation you control instead of raw text content
|
|
35581
|
+
//
|
|
35582
|
+
// ORDERING NOTE: plugins initialise in alphabetical bundle order. "nodetypes"
|
|
35583
|
+
// sorts BEFORE "sanitizer" and BEFORE "slashcommand", so this file must NOT call
|
|
35584
|
+
// editor.slashCommands.register() from init - it would silently no-op. Feeding
|
|
35585
|
+
// the sanitizer works precisely because we run first: we write config before
|
|
35586
|
+
// sanitizer.js reads it.
|
|
35587
|
+
function RTE_Plugin_NodeTypes() {
|
|
35588
|
+
var obj = this;
|
|
35589
|
+
var config;
|
|
35590
|
+
var editor;
|
|
35591
|
+
var types = []; // registration order
|
|
35592
|
+
var byName = {};
|
|
35593
|
+
var enforceTimer = 0;
|
|
35594
|
+
var originalToMarkdown = null;
|
|
35595
|
+
|
|
35596
|
+
obj.PluginName = "NodeTypes";
|
|
35597
|
+
|
|
35598
|
+
obj.InitConfig = function (argconfig) {
|
|
35599
|
+
config = argconfig;
|
|
35600
|
+
if (config.nodeTypesEnabled === false) return;
|
|
35601
|
+
|
|
35602
|
+
// Types can be declared in config so a host does not have to wait for an
|
|
35603
|
+
// editor instance to exist.
|
|
35604
|
+
if (Array.isArray(config.nodeTypes)) {
|
|
35605
|
+
for (var i = 0; i < config.nodeTypes.length; i++) registerType(config.nodeTypes[i]);
|
|
35606
|
+
}
|
|
35607
|
+
};
|
|
35608
|
+
|
|
35609
|
+
obj.InitEditor = function (argeditor) {
|
|
35610
|
+
editor = argeditor;
|
|
35611
|
+
if (config.nodeTypesEnabled === false) return;
|
|
35612
|
+
|
|
35613
|
+
editor.nodeTypes = {
|
|
35614
|
+
find: function (name) { return findInstances(name); },
|
|
35615
|
+
get: function (name) { return byName[name] || null; },
|
|
35616
|
+
insert: function (name, data) { return insertNode(name, data); },
|
|
35617
|
+
list: function () { return types.map(describe); },
|
|
35618
|
+
register: function (def) { return registerType(def); },
|
|
35619
|
+
serialize: function () { return serializeAll(); },
|
|
35620
|
+
typeOf: function (el) { var t = typeFor(el); return t ? t.name : null; },
|
|
35621
|
+
unregister: function (name) { return unregisterType(name); }
|
|
35622
|
+
};
|
|
35623
|
+
|
|
35624
|
+
// Atomicity is a live invariant, not a one-off: a paste, an undo or a
|
|
35625
|
+
// competing plugin can strip contenteditable and the node silently stops
|
|
35626
|
+
// behaving like one unit.
|
|
35627
|
+
//
|
|
35628
|
+
// Observed rather than event-driven, deliberately. setHTML() does not
|
|
35629
|
+
// fire "change", so an invariant hung off editor events is not enforced
|
|
35630
|
+
// on loaded content at all - which is the most common way a node enters
|
|
35631
|
+
// the document. Measured: with only the "change" handler, a node loaded
|
|
35632
|
+
// via setHTML never gained contenteditable="false".
|
|
35633
|
+
editor.attachEvent("change", function () { scheduleEnforce(); });
|
|
35634
|
+
observeEditable();
|
|
35635
|
+
|
|
35636
|
+
wrapMarkdown();
|
|
35637
|
+
|
|
35638
|
+
// Deferred deliberately. Plugins initialise in alphabetical bundle order:
|
|
35639
|
+
// docxexport (19) runs BEFORE nodetypes (49), but pdfexport (51) and
|
|
35640
|
+
// wordexport (72) run AFTER, so editor.getPdfBytes does not exist yet at
|
|
35641
|
+
// this point. Wrapping now would silently miss PDF entirely - the same
|
|
35642
|
+
// ordering trap that makes slashCommands.register() a no-op from here.
|
|
35643
|
+
// A zero-delay timeout lands after every plugin has initialised.
|
|
35644
|
+
setTimeout(installExportHooks, 0);
|
|
35645
|
+
|
|
35646
|
+
enforceAll();
|
|
35647
|
+
};
|
|
35648
|
+
|
|
35649
|
+
// ------------------------------------------------------------ registration
|
|
35650
|
+
|
|
35651
|
+
function registerType(def) {
|
|
35652
|
+
if (!def || !def.name) return false;
|
|
35653
|
+
if (typeof def.match !== "string" && typeof def.match !== "function") return false;
|
|
35654
|
+
if (byName[def.name]) unregisterType(def.name);
|
|
35655
|
+
|
|
35656
|
+
var type = {
|
|
35657
|
+
name: def.name,
|
|
35658
|
+
match: def.match,
|
|
35659
|
+
atomic: def.atomic !== false, // atomic by default
|
|
35660
|
+
inline: !!def.inline,
|
|
35661
|
+
toJSON: typeof def.toJSON === "function" ? def.toJSON : null,
|
|
35662
|
+
fromJSON: typeof def.fromJSON === "function" ? def.fromJSON : null,
|
|
35663
|
+
toMarkdown: typeof def.toMarkdown === "function" ? def.toMarkdown : null,
|
|
35664
|
+
// Export representations. toExportHTML covers .docx and PDF at once;
|
|
35665
|
+
// toDocx / toPdf override it per format when they differ.
|
|
35666
|
+
toExportHTML: typeof def.toExportHTML === "function" ? def.toExportHTML : null,
|
|
35667
|
+
toDocx: typeof def.toDocx === "function" ? def.toDocx : null,
|
|
35668
|
+
toPdf: typeof def.toPdf === "function" ? def.toPdf : null,
|
|
35669
|
+
toWord: typeof def.toWord === "function" ? def.toWord : null,
|
|
35670
|
+
allowTags: def.allowTags || [],
|
|
35671
|
+
allowAttributes: def.allowAttributes || []
|
|
35672
|
+
};
|
|
35673
|
+
types.push(type);
|
|
35674
|
+
byName[type.name] = type;
|
|
35675
|
+
feedSanitizer(type);
|
|
35676
|
+
if (editor) { enforceAll(); }
|
|
35677
|
+
return true;
|
|
35678
|
+
}
|
|
35679
|
+
|
|
35680
|
+
function unregisterType(name) {
|
|
35681
|
+
if (!byName[name]) return false;
|
|
35682
|
+
types = types.filter(function (t) { return t.name !== name; });
|
|
35683
|
+
delete byName[name];
|
|
35684
|
+
return true;
|
|
35685
|
+
}
|
|
35686
|
+
|
|
35687
|
+
function describe(t) {
|
|
35688
|
+
return {
|
|
35689
|
+
name: t.name, atomic: t.atomic, inline: t.inline,
|
|
35690
|
+
hasToJSON: !!t.toJSON, hasFromJSON: !!t.fromJSON, hasToMarkdown: !!t.toMarkdown,
|
|
35691
|
+
hasExportHTML: !!(t.toExportHTML || t.toDocx || t.toPdf),
|
|
35692
|
+
instances: findInstances(t.name).length
|
|
35693
|
+
};
|
|
35694
|
+
}
|
|
35695
|
+
|
|
35696
|
+
// The sanitizer is an allowlist: an unknown tag is dropped on input, on
|
|
35697
|
+
// output and from the live DOM. A registered type that did not feed the
|
|
35698
|
+
// allowlist would be deleted by the editor's own security layer, so this is
|
|
35699
|
+
// wired automatically rather than left to the host to remember.
|
|
35700
|
+
function feedSanitizer(type) {
|
|
35701
|
+
if (!config) return;
|
|
35702
|
+
mergeInto("sanitizerAllowTags", type.allowTags);
|
|
35703
|
+
mergeInto("sanitizerAllowAttributes", type.allowAttributes);
|
|
35704
|
+
}
|
|
35705
|
+
|
|
35706
|
+
function mergeInto(key, values) {
|
|
35707
|
+
if (!values || !values.length) return;
|
|
35708
|
+
var list = config[key];
|
|
35709
|
+
if (!Array.isArray(list)) list = list ? [list] : [];
|
|
35710
|
+
for (var i = 0; i < values.length; i++) {
|
|
35711
|
+
if (list.indexOf(values[i]) === -1) list.push(values[i]);
|
|
35712
|
+
}
|
|
35713
|
+
config[key] = list;
|
|
35714
|
+
}
|
|
35715
|
+
|
|
35716
|
+
// ------------------------------------------------------------- instances
|
|
35717
|
+
|
|
35718
|
+
function matches(type, el) {
|
|
35719
|
+
if (!el || el.nodeType !== 1) return false;
|
|
35720
|
+
if (typeof type.match === "string") {
|
|
35721
|
+
try { return el.matches(type.match); } catch (e) { return false; }
|
|
35722
|
+
}
|
|
35723
|
+
try { return !!type.match(el); } catch (e) { return false; }
|
|
35724
|
+
}
|
|
35725
|
+
|
|
35726
|
+
function typeFor(el) {
|
|
35727
|
+
for (var i = 0; i < types.length; i++) {
|
|
35728
|
+
if (matches(types[i], el)) return types[i];
|
|
35729
|
+
}
|
|
35730
|
+
return null;
|
|
35731
|
+
}
|
|
35732
|
+
|
|
35733
|
+
function editable() {
|
|
35734
|
+
return editor && editor.getEditable ? editor.getEditable() : null;
|
|
35735
|
+
}
|
|
35736
|
+
|
|
35737
|
+
function findInstances(name) {
|
|
35738
|
+
var root = editable();
|
|
35739
|
+
var type = byName[name];
|
|
35740
|
+
var out = [];
|
|
35741
|
+
if (!root || !type) return out;
|
|
35742
|
+
// Include the root's own children: querySelectorAll finds descendants
|
|
35743
|
+
// only, and a node pasted at the top level is a child of the root. That
|
|
35744
|
+
// exact blind spot is why the accessibility checker used to miss
|
|
35745
|
+
// top-level images.
|
|
35746
|
+
var all = root.querySelectorAll("*");
|
|
35747
|
+
for (var i = 0; i < all.length; i++) {
|
|
35748
|
+
if (matches(type, all[i])) out.push(all[i]);
|
|
35749
|
+
}
|
|
35750
|
+
return out;
|
|
35751
|
+
}
|
|
35752
|
+
|
|
35753
|
+
function allInstances() {
|
|
35754
|
+
var root = editable();
|
|
35755
|
+
var out = [];
|
|
35756
|
+
if (!root) return out;
|
|
35757
|
+
var all = root.querySelectorAll("*");
|
|
35758
|
+
for (var i = 0; i < all.length; i++) {
|
|
35759
|
+
var t = typeFor(all[i]);
|
|
35760
|
+
if (t) out.push({ type: t, el: all[i] });
|
|
35761
|
+
}
|
|
35762
|
+
return out;
|
|
35763
|
+
}
|
|
35764
|
+
|
|
35765
|
+
function insertNode(name, data) {
|
|
35766
|
+
var type = byName[name];
|
|
35767
|
+
if (!type || !type.fromJSON || !editor) return null;
|
|
35768
|
+
var produced = type.fromJSON(data || {});
|
|
35769
|
+
var html = typeof produced === "string" ? produced : (produced && produced.outerHTML);
|
|
35770
|
+
if (!html) return null;
|
|
35771
|
+
editor.insertHTML(html);
|
|
35772
|
+
scheduleEnforce();
|
|
35773
|
+
return html;
|
|
35774
|
+
}
|
|
35775
|
+
|
|
35776
|
+
function serializeAll() {
|
|
35777
|
+
var out = [];
|
|
35778
|
+
var found = allInstances();
|
|
35779
|
+
for (var i = 0; i < found.length; i++) {
|
|
35780
|
+
var t = found[i].type, el = found[i].el;
|
|
35781
|
+
out.push({
|
|
35782
|
+
name: t.name,
|
|
35783
|
+
data: t.toJSON ? safe(t.toJSON, el, {}) : readDataAttributes(el)
|
|
35784
|
+
});
|
|
35785
|
+
}
|
|
35786
|
+
return out;
|
|
35787
|
+
}
|
|
35788
|
+
|
|
35789
|
+
function readDataAttributes(el) {
|
|
35790
|
+
var data = {};
|
|
35791
|
+
var attrs = el.attributes;
|
|
35792
|
+
for (var i = 0; attrs && i < attrs.length; i++) {
|
|
35793
|
+
if (attrs[i].name.indexOf("data-") === 0) data[attrs[i].name] = attrs[i].value;
|
|
35794
|
+
}
|
|
35795
|
+
return data;
|
|
35796
|
+
}
|
|
35797
|
+
|
|
35798
|
+
function safe(fn, arg, fallback) {
|
|
35799
|
+
try { return fn(arg); } catch (e) { return fallback; }
|
|
35800
|
+
}
|
|
35801
|
+
|
|
35802
|
+
// -------------------------------------------------------------- atomicity
|
|
35803
|
+
|
|
35804
|
+
function observeEditable() {
|
|
35805
|
+
var root = editable();
|
|
35806
|
+
if (!root || typeof MutationObserver === "undefined") return;
|
|
35807
|
+
var observer = new MutationObserver(function () { scheduleEnforce(); });
|
|
35808
|
+
observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["contenteditable"] });
|
|
35809
|
+
}
|
|
35810
|
+
|
|
35811
|
+
function scheduleEnforce() {
|
|
35812
|
+
clearTimeout(enforceTimer);
|
|
35813
|
+
enforceTimer = setTimeout(enforceAll, 60);
|
|
35814
|
+
}
|
|
35815
|
+
|
|
35816
|
+
function enforceAll() {
|
|
35817
|
+
var found = allInstances();
|
|
35818
|
+
for (var i = 0; i < found.length; i++) {
|
|
35819
|
+
if (found[i].type.atomic && found[i].el.getAttribute("contenteditable") !== "false") {
|
|
35820
|
+
found[i].el.setAttribute("contenteditable", "false");
|
|
35821
|
+
}
|
|
35822
|
+
}
|
|
35823
|
+
}
|
|
35824
|
+
|
|
35825
|
+
// ----------------------------------------------------------------- export
|
|
35826
|
+
|
|
35827
|
+
// .docx and PDF both build from the live editable, so the same swap the
|
|
35828
|
+
// Markdown path uses works here: replace each registered node with the HTML
|
|
35829
|
+
// the type says represents it, export, then put the originals back.
|
|
35830
|
+
//
|
|
35831
|
+
// Unlike Markdown these are partly ASYNCHRONOUS - getDocxBlob returns a
|
|
35832
|
+
// Promise - so the restore has to wait for the result to settle. Restoring
|
|
35833
|
+
// synchronously would put the originals back while the exporter was still
|
|
35834
|
+
// reading the DOM.
|
|
35835
|
+
function installExportHooks() {
|
|
35836
|
+
wrapExport("getDocxBlob", "toDocx");
|
|
35837
|
+
wrapExport("exportToDocx", "toDocx");
|
|
35838
|
+
wrapExport("getPdfBytes", "toPdf");
|
|
35839
|
+
wrapExport("exportToPdf", "toPdf");
|
|
35840
|
+
wrapExport("downloadPdf", "toPdf");
|
|
35841
|
+
// wordexport.js sorts at 72 - after this file at 49 - which is exactly
|
|
35842
|
+
// why the install is deferred. It builds from editor.getHTMLCode(), and
|
|
35843
|
+
// getHTMLCode() serializes the live DOM, so the same swap reaches it.
|
|
35844
|
+
wrapExport("getWordDocument", "toWord");
|
|
35845
|
+
wrapExport("exportToWord", "toWord");
|
|
35846
|
+
wrapExport("downloadWord", "toWord");
|
|
35847
|
+
}
|
|
35848
|
+
|
|
35849
|
+
function wrapExport(apiName, hookName) {
|
|
35850
|
+
if (!editor || typeof editor[apiName] !== "function") return;
|
|
35851
|
+
var original = editor[apiName];
|
|
35852
|
+
editor[apiName] = function () {
|
|
35853
|
+
var args = arguments;
|
|
35854
|
+
var found = allInstances().filter(function (f) {
|
|
35855
|
+
return f.type[hookName] || f.type.toExportHTML;
|
|
35856
|
+
});
|
|
35857
|
+
if (!found.length) return original.apply(editor, args);
|
|
35858
|
+
|
|
35859
|
+
var restore = swapNodes(found, hookName);
|
|
35860
|
+
var result;
|
|
35861
|
+
try {
|
|
35862
|
+
result = original.apply(editor, args);
|
|
35863
|
+
} catch (e) {
|
|
35864
|
+
restore();
|
|
35865
|
+
throw e;
|
|
35866
|
+
}
|
|
35867
|
+
if (result && typeof result.then === "function") {
|
|
35868
|
+
return result.then(function (v) { restore(); return v; },
|
|
35869
|
+
function (e) { restore(); throw e; });
|
|
35870
|
+
}
|
|
35871
|
+
restore();
|
|
35872
|
+
return result;
|
|
35873
|
+
};
|
|
35874
|
+
}
|
|
35875
|
+
|
|
35876
|
+
// Replaces each instance with its declared export HTML and returns an
|
|
35877
|
+
// idempotent restore function.
|
|
35878
|
+
function swapNodes(found, hookName) {
|
|
35879
|
+
var doc = editor.getDocument();
|
|
35880
|
+
var swaps = [];
|
|
35881
|
+
var savedRange = null;
|
|
35882
|
+
try {
|
|
35883
|
+
var sel = doc.getSelection();
|
|
35884
|
+
if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
|
|
35885
|
+
} catch (e) { savedRange = null; }
|
|
35886
|
+
|
|
35887
|
+
for (var i = 0; i < found.length; i++) {
|
|
35888
|
+
var el = found[i].el;
|
|
35889
|
+
var fn = found[i].type[hookName] || found[i].type.toExportHTML;
|
|
35890
|
+
var html = fn ? safe(fn, el, null) : null;
|
|
35891
|
+
if (html === null || html === undefined) continue;
|
|
35892
|
+
var holder = doc.createElement(found[i].type.inline ? "span" : "div");
|
|
35893
|
+
holder.innerHTML = String(html);
|
|
35894
|
+
el.parentNode.insertBefore(holder, el);
|
|
35895
|
+
el.parentNode.removeChild(el);
|
|
35896
|
+
swaps.push({ holder: holder, original: el });
|
|
35897
|
+
}
|
|
35898
|
+
|
|
35899
|
+
var done = false;
|
|
35900
|
+
return function () {
|
|
35901
|
+
if (done) return;
|
|
35902
|
+
done = true;
|
|
35903
|
+
for (var j = swaps.length - 1; j >= 0; j--) {
|
|
35904
|
+
var s = swaps[j];
|
|
35905
|
+
if (s.holder.parentNode) {
|
|
35906
|
+
s.holder.parentNode.insertBefore(s.original, s.holder);
|
|
35907
|
+
s.holder.parentNode.removeChild(s.holder);
|
|
35908
|
+
}
|
|
35909
|
+
}
|
|
35910
|
+
if (savedRange) {
|
|
35911
|
+
try {
|
|
35912
|
+
var sel2 = doc.getSelection();
|
|
35913
|
+
sel2.removeAllRanges();
|
|
35914
|
+
sel2.addRange(savedRange);
|
|
35915
|
+
} catch (e) { /* selection may no longer be placeable */ }
|
|
35916
|
+
}
|
|
35917
|
+
};
|
|
35918
|
+
}
|
|
35919
|
+
|
|
35920
|
+
// --------------------------------------------------------------- markdown
|
|
35921
|
+
|
|
35922
|
+
// The core's markdown engine takes a root element, but the public
|
|
35923
|
+
// editor.toMarkdown() closes over the live editable and accepts no argument,
|
|
35924
|
+
// so there is no way to hand it a modified clone. The only seam is to swap
|
|
35925
|
+
// registered nodes for their markdown text in the live DOM, convert, and put
|
|
35926
|
+
// the originals back.
|
|
35927
|
+
//
|
|
35928
|
+
// That mutates the document the user is editing, so it is wrapped in
|
|
35929
|
+
// try/finally: if a host's toMarkdown throws, the originals are restored
|
|
35930
|
+
// anyway. Selection is saved and restored for the same reason.
|
|
35931
|
+
function wrapMarkdown() {
|
|
35932
|
+
if (!editor || typeof editor.toMarkdown !== "function") return;
|
|
35933
|
+
originalToMarkdown = editor.toMarkdown;
|
|
35934
|
+
editor.toMarkdown = function () {
|
|
35935
|
+
var found = allInstances().filter(function (f) { return f.type.toMarkdown; });
|
|
35936
|
+
if (!found.length) return originalToMarkdown.call(editor);
|
|
35937
|
+
|
|
35938
|
+
var doc = editor.getDocument();
|
|
35939
|
+
var swaps = [];
|
|
35940
|
+
var tokens = [];
|
|
35941
|
+
var savedRange = null;
|
|
35942
|
+
try {
|
|
35943
|
+
var sel = doc.getSelection();
|
|
35944
|
+
if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
|
|
35945
|
+
} catch (e) { savedRange = null; }
|
|
35946
|
+
|
|
35947
|
+
try {
|
|
35948
|
+
for (var i = 0; i < found.length; i++) {
|
|
35949
|
+
var el = found[i].el;
|
|
35950
|
+
var md = safe(found[i].type.toMarkdown, el, null);
|
|
35951
|
+
if (md === null || md === undefined) continue;
|
|
35952
|
+
// An ALPHANUMERIC sentinel, not the markdown itself. The core
|
|
35953
|
+
// engine escapes markdown punctuation in text content, so a
|
|
35954
|
+
// hook returning "**Approval**" would emit "\*\*Approval\*\*".
|
|
35955
|
+
// The sentinel has nothing to escape; the real markdown is
|
|
35956
|
+
// substituted into the finished string instead.
|
|
35957
|
+
var token = "RTENODETYPETOKEN" + i + "ENDRTENODETYPETOKEN";
|
|
35958
|
+
var holder = doc.createElement(found[i].type.inline ? "span" : "p");
|
|
35959
|
+
holder.textContent = token;
|
|
35960
|
+
el.parentNode.insertBefore(holder, el);
|
|
35961
|
+
el.parentNode.removeChild(el);
|
|
35962
|
+
swaps.push({ holder: holder, original: el });
|
|
35963
|
+
tokens.push({ token: token, markdown: String(md) });
|
|
35964
|
+
}
|
|
35965
|
+
var out = originalToMarkdown.call(editor);
|
|
35966
|
+
for (var k = 0; k < tokens.length; k++) {
|
|
35967
|
+
out = out.split(tokens[k].token).join(tokens[k].markdown);
|
|
35968
|
+
}
|
|
35969
|
+
return out;
|
|
35970
|
+
} finally {
|
|
35971
|
+
for (var j = swaps.length - 1; j >= 0; j--) {
|
|
35972
|
+
var s = swaps[j];
|
|
35973
|
+
if (s.holder.parentNode) {
|
|
35974
|
+
s.holder.parentNode.insertBefore(s.original, s.holder);
|
|
35975
|
+
s.holder.parentNode.removeChild(s.holder);
|
|
35976
|
+
}
|
|
35977
|
+
}
|
|
35978
|
+
if (savedRange) {
|
|
35979
|
+
try {
|
|
35980
|
+
var sel2 = doc.getSelection();
|
|
35981
|
+
sel2.removeAllRanges();
|
|
35982
|
+
sel2.addRange(savedRange);
|
|
35983
|
+
} catch (e) { /* selection may no longer be placeable */ }
|
|
35984
|
+
}
|
|
35985
|
+
}
|
|
35986
|
+
};
|
|
35987
|
+
}
|
|
35988
|
+
}
|
|
35989
|
+
|
|
35990
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
35991
|
+
|
|
35371
35992
|
// 2026-07-17 Pagination / page view. Renders the editable as a stack of paper
|
|
35372
35993
|
// pages — the "print layout" view Word and Google Docs have, and the feature
|
|
35373
35994
|
// CKEditor and Tiptap both sell as a paid add-on.
|
|
@@ -35826,6 +36447,13 @@ function RTE_Plugin_Pagination() {
|
|
|
35826
36447
|
st.id = "rte-pagination-styles";
|
|
35827
36448
|
st.appendChild(doc.createTextNode(css));
|
|
35828
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
|
+
}
|
|
35829
36457
|
}
|
|
35830
36458
|
|
|
35831
36459
|
// ---- serialization guard --------------------------------------------
|
|
@@ -37725,6 +38353,13 @@ function RTE_Plugin_RestrictedEditing() {
|
|
|
37725
38353
|
"body.rte-restricted [" + editableAttr + "='true']{background:rgba(34,197,94,.16)}"
|
|
37726
38354
|
].join("\n");
|
|
37727
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
|
+
}
|
|
37728
38363
|
}
|
|
37729
38364
|
|
|
37730
38365
|
function enable() {
|
|
@@ -38535,6 +39170,13 @@ function RTE_Plugin_RevisionHistory() {
|
|
|
38535
39170
|
".rte-rev-dark .rte-rev-diff-del{background:rgba(244,63,94,.16);color:#fda4af}"
|
|
38536
39171
|
].join("\n");
|
|
38537
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
|
+
}
|
|
38538
39180
|
}
|
|
38539
39181
|
}
|
|
38540
39182
|
|
|
@@ -38686,7 +39328,270 @@ function RTE_Plugin_RtlUi() {
|
|
|
38686
39328
|
st.id = "rte-rtl-ui-styles";
|
|
38687
39329
|
st.appendChild(doc.createTextNode(css()));
|
|
38688
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
|
+
}
|
|
39338
|
+
}
|
|
39339
|
+
}
|
|
39340
|
+
|
|
39341
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
39342
|
+
|
|
39343
|
+
RTE_DefaultConfig.plugin_rubyannotation = RTE_Plugin_RubyAnnotation;
|
|
39344
|
+
|
|
39345
|
+
// 2026-09-04 Ruby annotation (phonetic guides above base text).
|
|
39346
|
+
//
|
|
39347
|
+
// Furigana in Japanese, pinyin/bopomofo in Chinese, ruby glosses in academic
|
|
39348
|
+
// typesetting. Word has it, Lexical ships a RubyExtension, and it was the one
|
|
39349
|
+
// item in the Lexical feature gap register worth scheduling: small, self
|
|
39350
|
+
// contained, and an i18n/accessibility feature rather than a novelty.
|
|
39351
|
+
//
|
|
39352
|
+
// The markup is standard HTML, not an invention:
|
|
39353
|
+
//
|
|
39354
|
+
// <ruby>漢字<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>
|
|
39355
|
+
//
|
|
39356
|
+
// The <rp> parentheses are the accessibility half and are why they are emitted
|
|
39357
|
+
// by default: a browser or assistive technology with no ruby support falls back
|
|
39358
|
+
// to rendering "漢字(かんじ)" inline instead of running the reading straight
|
|
39359
|
+
// into the base text as "漢字かんじ". Copy-paste to a plain-text target gets the
|
|
39360
|
+
// same readable fallback.
|
|
39361
|
+
//
|
|
39362
|
+
// ruby/rt/rp are ALREADY in the sanitizer allowlist (checked, not assumed), so
|
|
39363
|
+
// the markup survives sanitisation with no allowlist change.
|
|
39364
|
+
function RTE_Plugin_RubyAnnotation() {
|
|
39365
|
+
var obj = this;
|
|
39366
|
+
var config;
|
|
39367
|
+
var editor;
|
|
39368
|
+
|
|
39369
|
+
obj.PluginName = "RubyAnnotation";
|
|
39370
|
+
|
|
39371
|
+
obj.InitConfig = function (argconfig) {
|
|
39372
|
+
config = argconfig;
|
|
39373
|
+
if (config.rubyAnnotationEnabled === false) return;
|
|
39374
|
+
|
|
39375
|
+
if (typeof config.rubyAnnotationParentheses !== "boolean") config.rubyAnnotationParentheses = true;
|
|
39376
|
+
|
|
39377
|
+
// BOTH toolbars. config.toolbar defaults to "default", which resolves to
|
|
39378
|
+
// toolbar_default, so appending only to toolbar_full would leave the
|
|
39379
|
+
// button unreachable for every host that never configured a toolbar -
|
|
39380
|
+
// the mistake that shipped inlinecode invisible for a whole release.
|
|
39381
|
+
// The default toolbar gets it in the overflow set, where a niche
|
|
39382
|
+
// typographic feature belongs; toolbar_full gets it inline.
|
|
39383
|
+
appendToolbarCommand("toolbar_full", "#{rubyannotation}");
|
|
39384
|
+
appendSubToolbarCommand("subtoolbar_more", "rubyannotation");
|
|
39385
|
+
};
|
|
39386
|
+
|
|
39387
|
+
obj.InitEditor = function (argeditor) {
|
|
39388
|
+
editor = argeditor;
|
|
39389
|
+
if (config.rubyAnnotationEnabled === false) return;
|
|
39390
|
+
|
|
39391
|
+
editor.ruby = {
|
|
39392
|
+
add: function (reading) { return applyRuby(reading); },
|
|
39393
|
+
get: function () { var r = findRubyAtSelection(); return r ? readingOf(r) : null; },
|
|
39394
|
+
isRuby: function () { return !!findRubyAtSelection(); },
|
|
39395
|
+
open: function () { obj.OpenRubyDialog(); },
|
|
39396
|
+
remove: function () { return removeRuby(); }
|
|
39397
|
+
};
|
|
39398
|
+
|
|
39399
|
+
injectStyles();
|
|
39400
|
+
|
|
39401
|
+
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
39402
|
+
editor.toolbarFactoryMap["rubyannotation"] = function (cmd) {
|
|
39403
|
+
return editor.createToolbarButton(cmd);
|
|
39404
|
+
};
|
|
39405
|
+
|
|
39406
|
+
editor.attachEvent("exec_command_rubyannotation", function (state) {
|
|
39407
|
+
state.returnValue = true;
|
|
39408
|
+
state.stopBubble = true;
|
|
39409
|
+
obj.OpenRubyDialog();
|
|
39410
|
+
});
|
|
39411
|
+
};
|
|
39412
|
+
|
|
39413
|
+
function appendToolbarCommand(toolbar, item) {
|
|
39414
|
+
if (!config[toolbar]) return;
|
|
39415
|
+
if (config[toolbar].indexOf(item) !== -1) return;
|
|
39416
|
+
config[toolbar] = config[toolbar] + item;
|
|
39417
|
+
}
|
|
39418
|
+
|
|
39419
|
+
// subtoolbar_* entries are a braced, comma-separated list rather than the
|
|
39420
|
+
// "#{name}" form the main toolbars use.
|
|
39421
|
+
function appendSubToolbarCommand(toolbar, name) {
|
|
39422
|
+
var val = config[toolbar];
|
|
39423
|
+
if (!val || val.indexOf(name) !== -1) return;
|
|
39424
|
+
var i = val.indexOf("}");
|
|
39425
|
+
if (i === -1) return;
|
|
39426
|
+
config[toolbar] = val.substring(0, i) + "," + name + val.substring(i);
|
|
39427
|
+
}
|
|
39428
|
+
|
|
39429
|
+
// Ruby has no default styling worth relying on: browsers vary in whether the
|
|
39430
|
+
// reading is sized down at all, and <rp> must be hidden wherever ruby DOES
|
|
39431
|
+
// render or the parentheses show up twice.
|
|
39432
|
+
function injectStyles() {
|
|
39433
|
+
var hostDoc = config.container.ownerDocument;
|
|
39434
|
+
if (hostDoc.getElementById("rte-ruby-style")) return;
|
|
39435
|
+
var style = hostDoc.createElement("style");
|
|
39436
|
+
style.id = "rte-ruby-style";
|
|
39437
|
+
style.innerHTML = [
|
|
39438
|
+
".rte-ruby-annotated{ruby-position:over;}",
|
|
39439
|
+
".rte-ruby-annotated>rt{font-size:.55em;line-height:1.15;opacity:.85;user-select:none;}",
|
|
39440
|
+
"@supports (display:ruby){.rte-ruby-annotated>rp{display:none;}}"
|
|
39441
|
+
].join("");
|
|
39442
|
+
hostDoc.head.appendChild(style);
|
|
39443
|
+
}
|
|
39444
|
+
|
|
39445
|
+
function editableRoot() {
|
|
39446
|
+
return editor && editor.getEditable ? editor.getEditable() : null;
|
|
39447
|
+
}
|
|
39448
|
+
|
|
39449
|
+
function currentRange() {
|
|
39450
|
+
var doc = editor.getDocument();
|
|
39451
|
+
try {
|
|
39452
|
+
var sel = doc.getSelection();
|
|
39453
|
+
if (!sel || !sel.rangeCount) return null;
|
|
39454
|
+
return sel.getRangeAt(0);
|
|
39455
|
+
} catch (e) { return null; }
|
|
39456
|
+
}
|
|
39457
|
+
|
|
39458
|
+
function findRubyAtSelection() {
|
|
39459
|
+
var range = currentRange();
|
|
39460
|
+
var root = editableRoot();
|
|
39461
|
+
if (!range || !root) return null;
|
|
39462
|
+
var node = range.startContainer;
|
|
39463
|
+
while (node && node !== root) {
|
|
39464
|
+
if (node.nodeType === 1 && node.nodeName === "RUBY") return node;
|
|
39465
|
+
node = node.parentNode;
|
|
39466
|
+
}
|
|
39467
|
+
return null;
|
|
39468
|
+
}
|
|
39469
|
+
|
|
39470
|
+
function readingOf(rubyEl) {
|
|
39471
|
+
var rt = rubyEl.querySelector("rt");
|
|
39472
|
+
return rt ? (rt.textContent || "") : "";
|
|
39473
|
+
}
|
|
39474
|
+
|
|
39475
|
+
function baseOf(rubyEl) {
|
|
39476
|
+
var out = "";
|
|
39477
|
+
for (var i = 0; i < rubyEl.childNodes.length; i++) {
|
|
39478
|
+
var n = rubyEl.childNodes[i];
|
|
39479
|
+
if (n.nodeType === 3) out += n.nodeValue;
|
|
39480
|
+
else if (n.nodeType === 1 && n.nodeName !== "RT" && n.nodeName !== "RP") out += n.textContent || "";
|
|
39481
|
+
}
|
|
39482
|
+
return out;
|
|
39483
|
+
}
|
|
39484
|
+
|
|
39485
|
+
function esc(s) {
|
|
39486
|
+
return String(s == null ? "" : s)
|
|
39487
|
+
.replace(/&/g, "&").replace(/</g, "<")
|
|
39488
|
+
.replace(/>/g, ">").replace(/"/g, """);
|
|
39489
|
+
}
|
|
39490
|
+
|
|
39491
|
+
function buildRuby(base, reading) {
|
|
39492
|
+
var parens = config.rubyAnnotationParentheses;
|
|
39493
|
+
return '<ruby class="rte-ruby-annotated">' + esc(base) +
|
|
39494
|
+
(parens ? "<rp>(</rp>" : "") +
|
|
39495
|
+
"<rt>" + esc(reading) + "</rt>" +
|
|
39496
|
+
(parens ? "<rp>)</rp>" : "") +
|
|
39497
|
+
"</ruby>";
|
|
39498
|
+
}
|
|
39499
|
+
|
|
39500
|
+
function applyRuby(reading) {
|
|
39501
|
+
var existing = findRubyAtSelection();
|
|
39502
|
+
if (existing) {
|
|
39503
|
+
var rt = existing.querySelector("rt");
|
|
39504
|
+
if (!rt) {
|
|
39505
|
+
rt = existing.ownerDocument.createElement("rt");
|
|
39506
|
+
existing.appendChild(rt);
|
|
39507
|
+
}
|
|
39508
|
+
rt.textContent = String(reading == null ? "" : reading);
|
|
39509
|
+
editor.focus();
|
|
39510
|
+
return existing;
|
|
39511
|
+
}
|
|
39512
|
+
var range = currentRange();
|
|
39513
|
+
if (!range || range.collapsed) return null;
|
|
39514
|
+
var base = String(range.toString() || "");
|
|
39515
|
+
if (!base) return null;
|
|
39516
|
+
editor.insertHTML(buildRuby(base, reading));
|
|
39517
|
+
editor.focus();
|
|
39518
|
+
return true;
|
|
39519
|
+
}
|
|
39520
|
+
|
|
39521
|
+
// Unwrap back to the base text, discarding the reading and the <rp> fallback
|
|
39522
|
+
// parentheses. Without dropping <rp> the text would read "漢字()".
|
|
39523
|
+
function removeRuby() {
|
|
39524
|
+
var rubyEl = findRubyAtSelection();
|
|
39525
|
+
if (!rubyEl) return false;
|
|
39526
|
+
var doc = rubyEl.ownerDocument;
|
|
39527
|
+
var text = doc.createTextNode(baseOf(rubyEl));
|
|
39528
|
+
rubyEl.parentNode.insertBefore(text, rubyEl);
|
|
39529
|
+
rubyEl.parentNode.removeChild(rubyEl);
|
|
39530
|
+
editor.focus();
|
|
39531
|
+
return true;
|
|
39532
|
+
}
|
|
39533
|
+
|
|
39534
|
+
function append(parent, tag, cssText, className) {
|
|
39535
|
+
var el = parent.ownerDocument.createElement(tag);
|
|
39536
|
+
if (cssText) el.style.cssText = cssText;
|
|
39537
|
+
if (className) el.className = className;
|
|
39538
|
+
parent.appendChild(el);
|
|
39539
|
+
return el;
|
|
38689
39540
|
}
|
|
39541
|
+
|
|
39542
|
+
obj.OpenRubyDialog = function () {
|
|
39543
|
+
var existing = findRubyAtSelection();
|
|
39544
|
+
var range = currentRange();
|
|
39545
|
+
var baseText = existing ? baseOf(existing) : (range ? String(range.toString() || "") : "");
|
|
39546
|
+
|
|
39547
|
+
var dlg = editor.createDialog(
|
|
39548
|
+
(editor.getLangText && editor.getLangText("rubyannotationtitle")) || "Ruby annotation",
|
|
39549
|
+
"rte-dialog-ruby"
|
|
39550
|
+
);
|
|
39551
|
+
var close = typeof dlg.close === "function" ? function () { dlg.close(); } : function () { editor.closeCurrentPopup(); };
|
|
39552
|
+
|
|
39553
|
+
var wrap = append(dlg, "div", "padding:14px;min-width:340px;font:13px -apple-system,Segoe UI,sans-serif");
|
|
39554
|
+
|
|
39555
|
+
append(wrap, "div", "font-weight:600;margin-bottom:4px").innerText = "Base text";
|
|
39556
|
+
var baseView = append(wrap, "div",
|
|
39557
|
+
"padding:8px 10px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;min-height:20px;word-break:break-word");
|
|
39558
|
+
baseView.textContent = baseText;
|
|
39559
|
+
|
|
39560
|
+
append(wrap, "div", "font-weight:600;margin:10px 0 4px").innerText = "Reading";
|
|
39561
|
+
var input = append(wrap, "input",
|
|
39562
|
+
"width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #cbd5e1;border-radius:8px;font:13px inherit");
|
|
39563
|
+
input.type = "text";
|
|
39564
|
+
input.value = existing ? readingOf(existing) : "";
|
|
39565
|
+
input.placeholder = "e.g. かんじ";
|
|
39566
|
+
|
|
39567
|
+
var note = append(wrap, "div", "font-size:11px;color:#94a3b8;margin-top:6px");
|
|
39568
|
+
note.innerText = baseText
|
|
39569
|
+
? "Shown above the base text. Parentheses are emitted for readers without ruby support."
|
|
39570
|
+
: "Select the text to annotate first, then reopen this dialog.";
|
|
39571
|
+
|
|
39572
|
+
var footer = append(wrap, "div", "display:flex;justify-content:space-between;gap:8px;margin-top:14px");
|
|
39573
|
+
var left = append(footer, "div", "display:flex;gap:8px");
|
|
39574
|
+
if (existing) {
|
|
39575
|
+
var rm = append(left, "button", "padding:6px 14px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#b91c1c;cursor:pointer");
|
|
39576
|
+
rm.type = "button"; rm.textContent = "Remove";
|
|
39577
|
+
rm.onclick = function () { removeRuby(); close(); };
|
|
39578
|
+
}
|
|
39579
|
+
var right = append(footer, "div", "display:flex;gap:8px");
|
|
39580
|
+
var cancel = append(right, "button", "padding:6px 14px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;cursor:pointer");
|
|
39581
|
+
cancel.type = "button"; cancel.textContent = "Cancel"; cancel.onclick = close;
|
|
39582
|
+
var ok = append(right, "button", "padding:6px 14px;border:1px solid #1d67ba;border-radius:8px;background:#1d67ba;color:#fff;cursor:pointer");
|
|
39583
|
+
ok.type = "button"; ok.textContent = existing ? "Update" : "Add";
|
|
39584
|
+
ok.disabled = !baseText;
|
|
39585
|
+
|
|
39586
|
+
ok.onclick = function () {
|
|
39587
|
+
var reading = input.value.trim();
|
|
39588
|
+
if (!reading || !baseText) { close(); return; }
|
|
39589
|
+
applyRuby(reading);
|
|
39590
|
+
close();
|
|
39591
|
+
};
|
|
39592
|
+
|
|
39593
|
+
setTimeout(function () { input.focus(); }, 0);
|
|
39594
|
+
};
|
|
38690
39595
|
}
|
|
38691
39596
|
|
|
38692
39597
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
@@ -40441,6 +41346,13 @@ function RTE_Plugin_SlashCommand() {
|
|
|
40441
41346
|
".rte-slash-popup-dark .rte-slash-empty{color:#94a3b8}"
|
|
40442
41347
|
].join("\n");
|
|
40443
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
|
+
}
|
|
40444
41356
|
}
|
|
40445
41357
|
|
|
40446
41358
|
// --- Icons (minimal inline SVGs) ---
|
|
@@ -40745,6 +41657,13 @@ function RTE_Plugin_SmartChips() {
|
|
|
40745
41657
|
st.setAttribute("data-rte-smartchips", "1");
|
|
40746
41658
|
st.textContent = css;
|
|
40747
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
|
+
}
|
|
40748
41667
|
}
|
|
40749
41668
|
} catch (e) { /* ignore */ }
|
|
40750
41669
|
}
|
|
@@ -40930,6 +41849,13 @@ function RTE_Plugin_SpellCheck() {
|
|
|
40930
41849
|
st.setAttribute("data-rte-spellcheck", "1");
|
|
40931
41850
|
st.textContent = css;
|
|
40932
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
|
+
}
|
|
40933
41859
|
}
|
|
40934
41860
|
} catch (e) { /* ignore */ }
|
|
40935
41861
|
}
|
|
@@ -40937,6 +41863,873 @@ function RTE_Plugin_SpellCheck() {
|
|
|
40937
41863
|
|
|
40938
41864
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
40939
41865
|
|
|
41866
|
+
RTE_DefaultConfig.plugin_stateinspector = RTE_Plugin_StateInspector;
|
|
41867
|
+
|
|
41868
|
+
// 2026-09-04 Document state inspector (developer tool).
|
|
41869
|
+
//
|
|
41870
|
+
// The editor's source of truth is the contenteditable DOM, not a separate
|
|
41871
|
+
// document model, so there is no state object to print. This plugin derives
|
|
41872
|
+
// one: it walks the editable subtree and produces a normalized, serializable
|
|
41873
|
+
// snapshot (tree + selection + plugin init order), then runs a rule set over
|
|
41874
|
+
// that snapshot looking for the structural faults that have actually caused
|
|
41875
|
+
// defects in this product - zero-length text nodes, empty inline wrappers,
|
|
41876
|
+
// duplicate nested formatting, control characters, sanitizer bypasses, broken
|
|
41877
|
+
// atomic chips, invalid list and heading structure.
|
|
41878
|
+
//
|
|
41879
|
+
// The snapshot is the point. `editor.stateInspector.snapshot()` returns plain
|
|
41880
|
+
// JSON, so a support case can carry the document's real structure instead of a
|
|
41881
|
+
// screenshot, and a test can assert on structure instead of on an HTML string.
|
|
41882
|
+
//
|
|
41883
|
+
// It is deliberately NOT in any default toolbar: set stateInspectorToolbarButton
|
|
41884
|
+
// to true to add it, or drive it from the API.
|
|
41885
|
+
function RTE_Plugin_StateInspector() {
|
|
41886
|
+
var obj = this;
|
|
41887
|
+
var config;
|
|
41888
|
+
var editor;
|
|
41889
|
+
var shell = null;
|
|
41890
|
+
var panel = null;
|
|
41891
|
+
var body = null;
|
|
41892
|
+
var tabsBar = null;
|
|
41893
|
+
var summaryEl = null;
|
|
41894
|
+
var refreshTimer = 0;
|
|
41895
|
+
var activeTab = "issues";
|
|
41896
|
+
var pluginOrder = [];
|
|
41897
|
+
var eventLog = [];
|
|
41898
|
+
var logLimit = 60;
|
|
41899
|
+
|
|
41900
|
+
var TABS = [
|
|
41901
|
+
{ id: "issues", label: "Issues" },
|
|
41902
|
+
{ id: "tree", label: "Tree" },
|
|
41903
|
+
{ id: "selection", label: "Selection" },
|
|
41904
|
+
{ id: "plugins", label: "Plugins" },
|
|
41905
|
+
{ id: "log", label: "Log" }
|
|
41906
|
+
];
|
|
41907
|
+
|
|
41908
|
+
var INLINE_TAGS = {
|
|
41909
|
+
A: 1, ABBR: 1, B: 1, BDI: 1, BDO: 1, CITE: 1, CODE: 1, DEL: 1, DFN: 1,
|
|
41910
|
+
EM: 1, I: 1, INS: 1, KBD: 1, MARK: 1, Q: 1, S: 1, SAMP: 1, SMALL: 1,
|
|
41911
|
+
SPAN: 1, STRONG: 1, SUB: 1, SUP: 1, U: 1, VAR: 1, FONT: 1
|
|
41912
|
+
};
|
|
41913
|
+
var VOID_TAGS = {
|
|
41914
|
+
AREA: 1, BASE: 1, BR: 1, COL: 1, EMBED: 1, HR: 1, IMG: 1, INPUT: 1,
|
|
41915
|
+
LINK: 1, META: 1, PARAM: 1, SOURCE: 1, TRACK: 1, WBR: 1
|
|
41916
|
+
};
|
|
41917
|
+
var BLOCK_TAGS = {
|
|
41918
|
+
ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DIV: 1, DL: 1, DT: 1,
|
|
41919
|
+
DD: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1, FOOTER: 1, FORM: 1,
|
|
41920
|
+
H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, HEADER: 1, HR: 1, LI: 1,
|
|
41921
|
+
MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1, SECTION: 1, TABLE: 1, UL: 1
|
|
41922
|
+
};
|
|
41923
|
+
// Control characters that make Word reject an entire .docx package and that
|
|
41924
|
+
// silently truncate grep over a bundle. NUL through backspace, vertical tab,
|
|
41925
|
+
// form feed, and shift-out through unit separator.
|
|
41926
|
+
// Written as escapes, never as literal bytes: a literal control character
|
|
41927
|
+
// in source makes grep treat the whole bundle as binary and silently
|
|
41928
|
+
// truncate every search over it.
|
|
41929
|
+
var CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/;
|
|
41930
|
+
|
|
41931
|
+
obj.PluginName = "StateInspector";
|
|
41932
|
+
|
|
41933
|
+
obj.InitConfig = function (argconfig) {
|
|
41934
|
+
config = argconfig;
|
|
41935
|
+
if (config.stateInspectorEnabled === false) return;
|
|
41936
|
+
|
|
41937
|
+
config.stateInspectorTitle = config.stateInspectorTitle || "State inspector";
|
|
41938
|
+
config.stateInspectorHint = config.stateInspectorHint || "Derived document model, structural checks, and a copyable snapshot for bug reports.";
|
|
41939
|
+
config.stateInspectorEmptyText = config.stateInspectorEmptyText || "No content to inspect yet.";
|
|
41940
|
+
if (typeof config.stateInspectorAutoOpen !== "boolean") config.stateInspectorAutoOpen = false;
|
|
41941
|
+
if (typeof config.stateInspectorToolbarButton !== "boolean") config.stateInspectorToolbarButton = false;
|
|
41942
|
+
if (typeof config.stateInspectorMaxDepth !== "number") config.stateInspectorMaxDepth = 24;
|
|
41943
|
+
if (typeof config.stateInspectorLogLimit === "number") logLimit = Math.max(10, Math.min(500, config.stateInspectorLogLimit));
|
|
41944
|
+
|
|
41945
|
+
// Captured here rather than in InitEditor because `for (var p in config)`
|
|
41946
|
+
// reproduces exactly the order the core uses to instantiate plugins -
|
|
41947
|
+
// which is bundle concatenation order, i.e. alphabetical by filename.
|
|
41948
|
+
// A plugin that registers into another plugin's API only works if it
|
|
41949
|
+
// sorts after it; that has silently broken registration before, so the
|
|
41950
|
+
// order is worth being able to see.
|
|
41951
|
+
pluginOrder = collectPluginOrder(config);
|
|
41952
|
+
|
|
41953
|
+
if (config.stateInspectorToolbarButton) {
|
|
41954
|
+
// BOTH presets, deliberately. config.toolbar defaults to "default",
|
|
41955
|
+
// which resolves to toolbar_default - so appending only to
|
|
41956
|
+
// toolbar_full means a host that sets stateInspectorToolbarButton on
|
|
41957
|
+
// a default toolbar gets no button at all. "It is in the config" and
|
|
41958
|
+
// "a user can see it" are different claims. inlinecode shipped a
|
|
41959
|
+
// whole release cycle invisible for exactly this reason.
|
|
41960
|
+
//
|
|
41961
|
+
// If you write a test for this, scope it to [role=toolbar]. Searching
|
|
41962
|
+
// the whole document for the button's label matches the inspector
|
|
41963
|
+
// PANEL's aria-label - the panel is built at InitEditor and merely
|
|
41964
|
+
// hidden - so a page-wide text search reports the button present in
|
|
41965
|
+
// every case, including the two where no button exists. A check whose
|
|
41966
|
+
// population is wrong reports confidently and does not look broken.
|
|
41967
|
+
appendToolbarCommand("toolbar_default", "#{stateinspector}");
|
|
41968
|
+
appendToolbarCommand("toolbar_full", "#{stateinspector}");
|
|
41969
|
+
}
|
|
41970
|
+
};
|
|
41971
|
+
|
|
41972
|
+
obj.InitEditor = function (argeditor) {
|
|
41973
|
+
editor = argeditor;
|
|
41974
|
+
if (config.stateInspectorEnabled === false) return;
|
|
41975
|
+
|
|
41976
|
+
editor.stateInspector = {
|
|
41977
|
+
close: function () { closePanel(); },
|
|
41978
|
+
isOpen: function () { return !!(shell && shell.classList.contains("is-open")); },
|
|
41979
|
+
issues: function () { return analyze(snapshotTree()).issues; },
|
|
41980
|
+
log: function () { return eventLog.slice(0); },
|
|
41981
|
+
open: function () { openPanel(); },
|
|
41982
|
+
plugins: function () { return pluginOrder.slice(0); },
|
|
41983
|
+
refresh: function () { render(); },
|
|
41984
|
+
snapshot: function () { return buildSnapshot(); },
|
|
41985
|
+
toggle: function () { togglePanel(); }
|
|
41986
|
+
};
|
|
41987
|
+
|
|
41988
|
+
injectStyles();
|
|
41989
|
+
|
|
41990
|
+
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
41991
|
+
editor.toolbarFactoryMap["stateinspector"] = function (cmd) {
|
|
41992
|
+
return editor.createToolbarButton(cmd);
|
|
41993
|
+
};
|
|
41994
|
+
|
|
41995
|
+
editor.attachEvent("exec_command_stateinspector", function (state) {
|
|
41996
|
+
state.returnValue = true;
|
|
41997
|
+
state.stopBubble = true;
|
|
41998
|
+
togglePanel();
|
|
41999
|
+
});
|
|
42000
|
+
editor.attachEvent("change", function () {
|
|
42001
|
+
recordEvent("change");
|
|
42002
|
+
scheduleRefresh();
|
|
42003
|
+
});
|
|
42004
|
+
editor.attachEvent("selectionchange", function () {
|
|
42005
|
+
recordEvent("selectionchange");
|
|
42006
|
+
scheduleRefresh();
|
|
42007
|
+
});
|
|
42008
|
+
|
|
42009
|
+
// Safe to register here, unlike documentimport/pdfexport/readabilitystats:
|
|
42010
|
+
// plugins initialise in alphabetical bundle order and "stateinspector"
|
|
42011
|
+
// sorts AFTER "slashcommand", so editor.slashCommands already exists.
|
|
42012
|
+
// Guarded anyway, because that ordering is a filename away from changing.
|
|
42013
|
+
if (editor.slashCommands && editor.slashCommands.register) {
|
|
42014
|
+
editor.slashCommands.register({
|
|
42015
|
+
id: "stateinspector",
|
|
42016
|
+
section: "Tools",
|
|
42017
|
+
title: config.stateInspectorTitle,
|
|
42018
|
+
description: "Inspect document structure, selection, and structural issues",
|
|
42019
|
+
keywords: ["inspect", "debug", "devtools", "state", "structure"],
|
|
42020
|
+
run: function () { openPanel(); }
|
|
42021
|
+
});
|
|
42022
|
+
}
|
|
42023
|
+
|
|
42024
|
+
// Build the wrapper NOW, not lazily on first open. ensureShell() reparents
|
|
42025
|
+
// the editor container, and reparenting a populated editing surface makes
|
|
42026
|
+
// Chrome reseat it - which blanks the visible content while the document
|
|
42027
|
+
// is still there (the status bar keeps counting words nobody can see).
|
|
42028
|
+
// documentoutline.js and contentminimap.js do the same thing for the same
|
|
42029
|
+
// reason: at InitEditor the surface is still empty, so the reseat is free.
|
|
42030
|
+
ensureShell();
|
|
42031
|
+
|
|
42032
|
+
if (config.stateInspectorAutoOpen) openPanel();
|
|
42033
|
+
};
|
|
42034
|
+
|
|
42035
|
+
function appendToolbarCommand(toolbar, item) {
|
|
42036
|
+
if (!config[toolbar]) return;
|
|
42037
|
+
if (config[toolbar].indexOf(item) !== -1) return;
|
|
42038
|
+
config[toolbar] = config[toolbar] + item;
|
|
42039
|
+
}
|
|
42040
|
+
|
|
42041
|
+
function collectPluginOrder(cfg) {
|
|
42042
|
+
var order = [];
|
|
42043
|
+
for (var p in cfg) {
|
|
42044
|
+
if (p[0] !== "p" || p.substr(0, 7) !== "plugin_") continue;
|
|
42045
|
+
if (!cfg[p] || !(cfg[p] instanceof Function)) continue;
|
|
42046
|
+
order.push({ index: order.length, key: p, name: p.substring(7) });
|
|
42047
|
+
}
|
|
42048
|
+
return order;
|
|
42049
|
+
}
|
|
42050
|
+
|
|
42051
|
+
function recordEvent(name) {
|
|
42052
|
+
eventLog.push({ at: Date.now(), event: name });
|
|
42053
|
+
if (eventLog.length > logLimit) eventLog.splice(0, eventLog.length - logLimit);
|
|
42054
|
+
}
|
|
42055
|
+
|
|
42056
|
+
// ---------------------------------------------------------------- snapshot
|
|
42057
|
+
|
|
42058
|
+
function describeAttributes(el) {
|
|
42059
|
+
var attrs = {};
|
|
42060
|
+
var list = el.attributes;
|
|
42061
|
+
if (!list) return attrs;
|
|
42062
|
+
for (var i = 0; i < list.length; i++) {
|
|
42063
|
+
var value = list[i].value;
|
|
42064
|
+
attrs[list[i].name] = value.length > 220 ? value.substring(0, 220) + "…" : value;
|
|
42065
|
+
}
|
|
42066
|
+
return attrs;
|
|
42067
|
+
}
|
|
42068
|
+
|
|
42069
|
+
function walk(node, depth, maxDepth, counters) {
|
|
42070
|
+
if (node.nodeType === 3) {
|
|
42071
|
+
var text = node.nodeValue || "";
|
|
42072
|
+
counters.textNodes++;
|
|
42073
|
+
counters.characters += text.length;
|
|
42074
|
+
return {
|
|
42075
|
+
type: "text",
|
|
42076
|
+
length: text.length,
|
|
42077
|
+
text: text.length > 80 ? text.substring(0, 80) + "…" : text
|
|
42078
|
+
};
|
|
42079
|
+
}
|
|
42080
|
+
if (node.nodeType === 8) {
|
|
42081
|
+
counters.comments++;
|
|
42082
|
+
return { type: "comment", length: (node.nodeValue || "").length };
|
|
42083
|
+
}
|
|
42084
|
+
if (node.nodeType !== 1) return null;
|
|
42085
|
+
|
|
42086
|
+
counters.elements++;
|
|
42087
|
+
var name = node.nodeName;
|
|
42088
|
+
counters.tags[name] = (counters.tags[name] || 0) + 1;
|
|
42089
|
+
|
|
42090
|
+
var entry = {
|
|
42091
|
+
type: "element",
|
|
42092
|
+
name: name.toLowerCase(),
|
|
42093
|
+
attrs: describeAttributes(node),
|
|
42094
|
+
children: []
|
|
42095
|
+
};
|
|
42096
|
+
if (depth >= maxDepth) {
|
|
42097
|
+
entry.truncated = true;
|
|
42098
|
+
entry.childCount = node.childNodes.length;
|
|
42099
|
+
return entry;
|
|
42100
|
+
}
|
|
42101
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
42102
|
+
var child = walk(node.childNodes[i], depth + 1, maxDepth, counters);
|
|
42103
|
+
if (child) entry.children.push(child);
|
|
42104
|
+
}
|
|
42105
|
+
return entry;
|
|
42106
|
+
}
|
|
42107
|
+
|
|
42108
|
+
function snapshotTree() {
|
|
42109
|
+
var editable = editor && editor.getEditable ? editor.getEditable() : null;
|
|
42110
|
+
var counters = { elements: 0, textNodes: 0, comments: 0, characters: 0, tags: {} };
|
|
42111
|
+
if (!editable) {
|
|
42112
|
+
return { root: null, counters: counters, editable: null };
|
|
42113
|
+
}
|
|
42114
|
+
var maxDepth = Math.max(2, config.stateInspectorMaxDepth || 24);
|
|
42115
|
+
var root = {
|
|
42116
|
+
type: "element",
|
|
42117
|
+
name: "root",
|
|
42118
|
+
attrs: {},
|
|
42119
|
+
children: []
|
|
42120
|
+
};
|
|
42121
|
+
for (var i = 0; i < editable.childNodes.length; i++) {
|
|
42122
|
+
var child = walk(editable.childNodes[i], 1, maxDepth, counters);
|
|
42123
|
+
if (child) root.children.push(child);
|
|
42124
|
+
}
|
|
42125
|
+
return { root: root, counters: counters, editable: editable };
|
|
42126
|
+
}
|
|
42127
|
+
|
|
42128
|
+
function describeSelection() {
|
|
42129
|
+
var doc = editor && editor.getDocument ? editor.getDocument() : null;
|
|
42130
|
+
var editable = editor && editor.getEditable ? editor.getEditable() : null;
|
|
42131
|
+
var info = { available: false };
|
|
42132
|
+
if (!doc || !doc.getSelection) return info;
|
|
42133
|
+
|
|
42134
|
+
var sel;
|
|
42135
|
+
try { sel = doc.getSelection(); } catch (e) { return info; }
|
|
42136
|
+
if (!sel || sel.rangeCount === 0) return info;
|
|
42137
|
+
|
|
42138
|
+
var range;
|
|
42139
|
+
try { range = sel.getRangeAt(0); } catch (e) { return info; }
|
|
42140
|
+
if (!range) return info;
|
|
42141
|
+
|
|
42142
|
+
info.available = true;
|
|
42143
|
+
info.collapsed = !!range.collapsed;
|
|
42144
|
+
info.anchor = describeBoundary(range.startContainer, range.startOffset);
|
|
42145
|
+
info.focus = describeBoundary(range.endContainer, range.endOffset);
|
|
42146
|
+
info.insideEditable = !!(editable && editable.contains(range.startContainer) && editable.contains(range.endContainer));
|
|
42147
|
+
try {
|
|
42148
|
+
var selected = String(range.toString() || "");
|
|
42149
|
+
info.textLength = selected.length;
|
|
42150
|
+
info.text = selected.length > 120 ? selected.substring(0, 120) + "…" : selected;
|
|
42151
|
+
} catch (e) {
|
|
42152
|
+
info.textLength = 0;
|
|
42153
|
+
}
|
|
42154
|
+
info.block = describeBlockPath(range.startContainer, editable);
|
|
42155
|
+
return info;
|
|
42156
|
+
}
|
|
42157
|
+
|
|
42158
|
+
function describeBoundary(container, offset) {
|
|
42159
|
+
if (!container) return { node: "(none)", offset: offset };
|
|
42160
|
+
if (container.nodeType === 3) {
|
|
42161
|
+
var text = container.nodeValue || "";
|
|
42162
|
+
return {
|
|
42163
|
+
node: "#text",
|
|
42164
|
+
offset: offset,
|
|
42165
|
+
length: text.length,
|
|
42166
|
+
// A caret sitting in a zero-length text node is the shape of a
|
|
42167
|
+
// caret trap; surfacing it here is the whole point.
|
|
42168
|
+
zeroLength: text.length === 0
|
|
42169
|
+
};
|
|
42170
|
+
}
|
|
42171
|
+
return {
|
|
42172
|
+
node: container.nodeName ? container.nodeName.toLowerCase() : "(unknown)",
|
|
42173
|
+
offset: offset,
|
|
42174
|
+
childCount: container.childNodes ? container.childNodes.length : 0
|
|
42175
|
+
};
|
|
42176
|
+
}
|
|
42177
|
+
|
|
42178
|
+
function describeBlockPath(node, editable) {
|
|
42179
|
+
var path = [];
|
|
42180
|
+
var current = node;
|
|
42181
|
+
while (current && current !== editable) {
|
|
42182
|
+
if (current.nodeType === 1) path.unshift(current.nodeName.toLowerCase());
|
|
42183
|
+
current = current.parentNode;
|
|
42184
|
+
}
|
|
42185
|
+
return path.join(" > ");
|
|
42186
|
+
}
|
|
42187
|
+
|
|
42188
|
+
function buildSnapshot() {
|
|
42189
|
+
var tree = snapshotTree();
|
|
42190
|
+
var analysis = analyze(tree);
|
|
42191
|
+
return {
|
|
42192
|
+
snapshotVersion: 1,
|
|
42193
|
+
generatedAt: new Date().toISOString(),
|
|
42194
|
+
counters: tree.counters,
|
|
42195
|
+
issues: analysis.issues,
|
|
42196
|
+
plugins: pluginOrder.slice(0),
|
|
42197
|
+
recentEvents: eventLog.slice(-20),
|
|
42198
|
+
selection: describeSelection(),
|
|
42199
|
+
tree: tree.root
|
|
42200
|
+
};
|
|
42201
|
+
}
|
|
42202
|
+
|
|
42203
|
+
// ----------------------------------------------------------------- analysis
|
|
42204
|
+
|
|
42205
|
+
function issue(list, id, severity, message, detail) {
|
|
42206
|
+
list.push({ id: id, severity: severity, message: message, detail: detail || "" });
|
|
42207
|
+
}
|
|
42208
|
+
|
|
42209
|
+
function elementPath(el, editable) {
|
|
42210
|
+
var path = [];
|
|
42211
|
+
var current = el;
|
|
42212
|
+
while (current && current !== editable && current.nodeType === 1) {
|
|
42213
|
+
var step = current.nodeName.toLowerCase();
|
|
42214
|
+
if (current.id) step += "#" + current.id;
|
|
42215
|
+
path.unshift(step);
|
|
42216
|
+
current = current.parentNode;
|
|
42217
|
+
}
|
|
42218
|
+
return path.join(" > ");
|
|
42219
|
+
}
|
|
42220
|
+
|
|
42221
|
+
function hasMeaningfulContent(el) {
|
|
42222
|
+
if ((el.textContent || "").replace(/[\s\u200B\uFEFF]/g, "") !== "") return true;
|
|
42223
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
42224
|
+
var child = el.childNodes[i];
|
|
42225
|
+
if (child.nodeType === 1 && VOID_TAGS[child.nodeName]) return true;
|
|
42226
|
+
}
|
|
42227
|
+
return false;
|
|
42228
|
+
}
|
|
42229
|
+
|
|
42230
|
+
function analyze(tree) {
|
|
42231
|
+
var issues = [];
|
|
42232
|
+
var editable = tree.editable;
|
|
42233
|
+
if (!editable) return { issues: issues };
|
|
42234
|
+
|
|
42235
|
+
var walker = editable.ownerDocument.createTreeWalker(
|
|
42236
|
+
editable,
|
|
42237
|
+
// NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
|
|
42238
|
+
1 | 4,
|
|
42239
|
+
null,
|
|
42240
|
+
false
|
|
42241
|
+
);
|
|
42242
|
+
var lastHeadingLevel = 0;
|
|
42243
|
+
var node;
|
|
42244
|
+
|
|
42245
|
+
while ((node = walker.nextNode())) {
|
|
42246
|
+
if (node.nodeType === 3) {
|
|
42247
|
+
analyzeText(node, editable, issues);
|
|
42248
|
+
continue;
|
|
42249
|
+
}
|
|
42250
|
+
analyzeElement(node, editable, issues);
|
|
42251
|
+
|
|
42252
|
+
var level = headingLevel(node);
|
|
42253
|
+
if (level) {
|
|
42254
|
+
if (lastHeadingLevel && level > lastHeadingLevel + 1) {
|
|
42255
|
+
issue(issues, "heading-skip", "warn",
|
|
42256
|
+
"Heading level jumps from h" + lastHeadingLevel + " to h" + level,
|
|
42257
|
+
elementPath(node, editable));
|
|
42258
|
+
}
|
|
42259
|
+
lastHeadingLevel = level;
|
|
42260
|
+
}
|
|
42261
|
+
}
|
|
42262
|
+
|
|
42263
|
+
// Top-level images are children of the editable root, so a walker that
|
|
42264
|
+
// starts below the root would miss them - which is exactly how the
|
|
42265
|
+
// accessibility checker used to skip pasted images.
|
|
42266
|
+
for (var i = 0; i < editable.childNodes.length; i++) {
|
|
42267
|
+
var child = editable.childNodes[i];
|
|
42268
|
+
if (child.nodeType === 1 && child.nodeName === "IMG") {
|
|
42269
|
+
checkImageAlt(child, editable, issues);
|
|
42270
|
+
}
|
|
42271
|
+
}
|
|
42272
|
+
|
|
42273
|
+
return { issues: issues };
|
|
42274
|
+
}
|
|
42275
|
+
|
|
42276
|
+
function headingLevel(el) {
|
|
42277
|
+
var name = el.nodeName;
|
|
42278
|
+
if (name.length === 2 && name[0] === "H" && name[1] >= "1" && name[1] <= "6") {
|
|
42279
|
+
return parseInt(name[1], 10);
|
|
42280
|
+
}
|
|
42281
|
+
return 0;
|
|
42282
|
+
}
|
|
42283
|
+
|
|
42284
|
+
function analyzeText(node, editable, issues) {
|
|
42285
|
+
var text = node.nodeValue || "";
|
|
42286
|
+
if (text.length === 0) {
|
|
42287
|
+
issue(issues, "zero-length-text", "warn",
|
|
42288
|
+
"Zero-length text node",
|
|
42289
|
+
elementPath(node.parentNode, editable));
|
|
42290
|
+
return;
|
|
42291
|
+
}
|
|
42292
|
+
if (CONTROL_CHARS.test(text)) {
|
|
42293
|
+
issue(issues, "control-characters", "error",
|
|
42294
|
+
"Text contains control characters - Word will reject an exported .docx",
|
|
42295
|
+
elementPath(node.parentNode, editable));
|
|
42296
|
+
}
|
|
42297
|
+
}
|
|
42298
|
+
|
|
42299
|
+
function analyzeElement(el, editable, issues) {
|
|
42300
|
+
var name = el.nodeName;
|
|
42301
|
+
var path = elementPath(el, editable);
|
|
42302
|
+
|
|
42303
|
+
// Sanitizer invariants. These are the shapes that got through before.
|
|
42304
|
+
var attrs = el.attributes;
|
|
42305
|
+
for (var i = 0; attrs && i < attrs.length; i++) {
|
|
42306
|
+
var attrName = attrs[i].name.toLowerCase();
|
|
42307
|
+
var attrValue = attrs[i].value || "";
|
|
42308
|
+
if (attrName.indexOf("on") === 0 && attrName.length > 2) {
|
|
42309
|
+
issue(issues, "event-handler-attribute", "error",
|
|
42310
|
+
"Inline event handler " + attrName + " present in document content", path);
|
|
42311
|
+
}
|
|
42312
|
+
if (attrName === "href" || attrName === "src" || attrName === "xlink:href") {
|
|
42313
|
+
// Strip control characters and whitespace BEFORE testing the scheme;
|
|
42314
|
+
// "java script:" is a live bypass otherwise.
|
|
42315
|
+
var scheme = attrValue.replace(/[\u0000-\u0020]/g, "").toLowerCase();
|
|
42316
|
+
if (scheme.indexOf("javascript:") === 0) {
|
|
42317
|
+
issue(issues, "unsafe-url-scheme", "error",
|
|
42318
|
+
"javascript: URL in " + attrName, path);
|
|
42319
|
+
} else if (scheme.indexOf("data:text/html") === 0 || scheme.indexOf("data:image/svg+xml") === 0) {
|
|
42320
|
+
issue(issues, "unsafe-url-scheme", "error",
|
|
42321
|
+
"Scriptable data: URL in " + attrName, path);
|
|
42322
|
+
}
|
|
42323
|
+
}
|
|
42324
|
+
}
|
|
42325
|
+
if (name === "IFRAME" && el.hasAttribute("srcdoc")) {
|
|
42326
|
+
issue(issues, "iframe-srcdoc", "error",
|
|
42327
|
+
"iframe[srcdoc] executes when the saved content is rendered downstream", path);
|
|
42328
|
+
}
|
|
42329
|
+
|
|
42330
|
+
// Structural invariants.
|
|
42331
|
+
if (INLINE_TAGS[name] && !hasMeaningfulContent(el)) {
|
|
42332
|
+
issue(issues, "empty-inline", "warn",
|
|
42333
|
+
"Empty inline <" + name.toLowerCase() + "> - a caret entering it cannot leave by typing", path);
|
|
42334
|
+
}
|
|
42335
|
+
if (INLINE_TAGS[name] && el.parentNode && el.parentNode.nodeName === name) {
|
|
42336
|
+
issue(issues, "nested-duplicate-inline", "warn",
|
|
42337
|
+
"<" + name.toLowerCase() + "> nested directly inside another <" + name.toLowerCase() + ">", path);
|
|
42338
|
+
}
|
|
42339
|
+
if (INLINE_TAGS[name]) {
|
|
42340
|
+
for (var c = 0; c < el.childNodes.length; c++) {
|
|
42341
|
+
var child = el.childNodes[c];
|
|
42342
|
+
if (child.nodeType === 1 && BLOCK_TAGS[child.nodeName]) {
|
|
42343
|
+
issue(issues, "block-inside-inline", "warn",
|
|
42344
|
+
"Block <" + child.nodeName.toLowerCase() + "> inside inline <" + name.toLowerCase() + ">", path);
|
|
42345
|
+
break;
|
|
42346
|
+
}
|
|
42347
|
+
}
|
|
42348
|
+
}
|
|
42349
|
+
if (name === "LI") {
|
|
42350
|
+
var parentName = el.parentNode ? el.parentNode.nodeName : "";
|
|
42351
|
+
if (parentName !== "UL" && parentName !== "OL") {
|
|
42352
|
+
issue(issues, "orphan-list-item", "warn",
|
|
42353
|
+
"<li> outside a <ul> or <ol>", path);
|
|
42354
|
+
}
|
|
42355
|
+
}
|
|
42356
|
+
if (name === "IMG") {
|
|
42357
|
+
checkImageAlt(el, editable, issues);
|
|
42358
|
+
}
|
|
42359
|
+
// Atomic content - chips, footnote markers, merge fields - stops being
|
|
42360
|
+
// atomic the moment it loses contenteditable=false, and the caret then
|
|
42361
|
+
// edits its internals.
|
|
42362
|
+
if (el.hasAttribute && el.hasAttribute("data-rte-atomic") && el.getAttribute("contenteditable") !== "false") {
|
|
42363
|
+
issue(issues, "atomic-not-protected", "error",
|
|
42364
|
+
"Atomic element without contenteditable=\"false\"", path);
|
|
42365
|
+
}
|
|
42366
|
+
}
|
|
42367
|
+
|
|
42368
|
+
function checkImageAlt(el, editable, issues) {
|
|
42369
|
+
if (el.getAttribute("role") === "presentation" || el.getAttribute("aria-hidden") === "true") return;
|
|
42370
|
+
var alt = el.getAttribute("alt");
|
|
42371
|
+
if (alt === null) {
|
|
42372
|
+
issue(issues, "image-missing-alt", "warn",
|
|
42373
|
+
"Image has no alt attribute", elementPath(el, editable));
|
|
42374
|
+
}
|
|
42375
|
+
}
|
|
42376
|
+
|
|
42377
|
+
// -------------------------------------------------------------------- view
|
|
42378
|
+
|
|
42379
|
+
function injectStyles() {
|
|
42380
|
+
var hostDoc = config.container.ownerDocument;
|
|
42381
|
+
if (hostDoc.getElementById("rte-state-inspector-style")) return;
|
|
42382
|
+
var style = hostDoc.createElement("style");
|
|
42383
|
+
style.id = "rte-state-inspector-style";
|
|
42384
|
+
style.innerHTML = [
|
|
42385
|
+
".rte-state-inspector-shell{display:flex;align-items:stretch;gap:10px;}",
|
|
42386
|
+
".rte-state-inspector-shell>.rte-state-inspector-host{flex:1 1 auto;min-width:0;}",
|
|
42387
|
+
".rte-state-inspector-panel{display:none;flex:0 0 340px;min-width:290px;max-width:min(400px,36vw);border:1px solid rgba(148,163,184,.22);border-radius:8px;background:#fff;box-shadow:0 14px 32px rgba(29,78,216,.1),0 2px 8px rgba(15,23,42,.06);overflow:hidden;color:#172033;font-family:Aptos,'Segoe UI',sans-serif;}",
|
|
42388
|
+
".rte-state-inspector-shell.is-open>.rte-state-inspector-panel{display:flex;flex-direction:column;}",
|
|
42389
|
+
".rte-state-inspector-header{padding:12px 12px 9px 14px;border-bottom:1px solid rgba(148,163,184,.16);background:rgba(248,251,255,.72);}",
|
|
42390
|
+
".rte-state-inspector-kicker{font-size:10px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase;color:#52657e;font-weight:850;}",
|
|
42391
|
+
".rte-state-inspector-title{margin-top:3px;font-size:15px;line-height:1.2;font-weight:850;color:#172033;}",
|
|
42392
|
+
".rte-state-inspector-copy{margin-top:5px;font-size:12px;line-height:1.42;color:#52657e;}",
|
|
42393
|
+
".rte-state-inspector-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px 8px 14px;border-bottom:1px solid rgba(148,163,184,.16);}",
|
|
42394
|
+
".rte-state-inspector-summary{min-width:0;font-size:12px;font-weight:750;color:#52657e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
42395
|
+
".rte-state-inspector-actions{display:flex;gap:6px;flex:0 0 auto;}",
|
|
42396
|
+
".rte-state-inspector-btn{appearance:none;border:1px solid rgba(100,116,139,.18);background:#fff;color:#315277;cursor:pointer;font-size:12px;font-weight:750;padding:6px 9px;border-radius:7px;line-height:1;}",
|
|
42397
|
+
".rte-state-inspector-btn:hover,.rte-state-inspector-btn:focus-visible{background:#eef4ff;color:#0f3f9f;border-color:rgba(37,99,235,.28);}",
|
|
42398
|
+
".rte-state-inspector-tabs{display:flex;gap:2px;padding:8px 10px 0 10px;border-bottom:1px solid rgba(148,163,184,.16);}",
|
|
42399
|
+
".rte-state-inspector-tab{appearance:none;border:1px solid transparent;border-bottom:none;background:transparent;color:#52657e;cursor:pointer;font-size:12px;font-weight:800;padding:7px 10px;border-radius:7px 7px 0 0;line-height:1;}",
|
|
42400
|
+
".rte-state-inspector-tab:hover,.rte-state-inspector-tab:focus-visible{background:#f8fbff;color:#0f3f9f;}",
|
|
42401
|
+
".rte-state-inspector-tab[aria-selected='true']{background:#eef4ff;border-color:rgba(37,99,235,.26);color:#0f3f9f;}",
|
|
42402
|
+
".rte-state-inspector-body{padding:10px;overflow:auto;min-height:160px;max-height:540px;scrollbar-width:thin;font-size:12px;line-height:1.5;}",
|
|
42403
|
+
".rte-state-inspector-issue{display:block;padding:8px 10px;margin-bottom:6px;border:1px solid rgba(148,163,184,.24);border-left-width:3px;border-radius:7px;background:#fff;}",
|
|
42404
|
+
".rte-state-inspector-issue.is-error{border-left-color:#dc2626;background:#fef4f4;}",
|
|
42405
|
+
".rte-state-inspector-issue.is-warn{border-left-color:#d97706;background:#fffaf1;}",
|
|
42406
|
+
".rte-state-inspector-issue-msg{font-weight:750;color:#172033;}",
|
|
42407
|
+
".rte-state-inspector-issue-meta{margin-top:3px;font-size:11px;color:#52657e;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
|
|
42408
|
+
".rte-state-inspector-ok{padding:14px;color:#166534;font-weight:750;background:#f0fdf4;border:1px dashed rgba(22,101,52,.3);border-radius:8px;}",
|
|
42409
|
+
".rte-state-inspector-empty{padding:14px;color:#52657e;font-weight:700;background:#fff;border:1px dashed rgba(148,163,184,.34);border-radius:8px;}",
|
|
42410
|
+
".rte-state-inspector-pre{margin:0;font-family:Consolas,'Cascadia Mono',monospace;font-size:11px;line-height:1.55;color:#172033;white-space:pre;}",
|
|
42411
|
+
".rte-state-inspector-row{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid rgba(148,163,184,.14);}",
|
|
42412
|
+
".rte-state-inspector-row-key{flex:0 0 116px;color:#52657e;font-weight:750;}",
|
|
42413
|
+
".rte-state-inspector-row-val{flex:1 1 auto;min-width:0;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
|
|
42414
|
+
".rte-state-inspector-row.is-flagged .rte-state-inspector-row-val{color:#b91c1c;font-weight:750;}",
|
|
42415
|
+
"@media (max-width: 1100px){.rte-state-inspector-shell{display:block;}.rte-state-inspector-panel{margin-top:12px;max-width:none;width:100%;}.rte-state-inspector-body{max-height:340px;}}"
|
|
42416
|
+
].join("");
|
|
42417
|
+
hostDoc.head.appendChild(style);
|
|
42418
|
+
}
|
|
42419
|
+
|
|
42420
|
+
function ensureShell() {
|
|
42421
|
+
if (shell) return shell;
|
|
42422
|
+
var container = config.container;
|
|
42423
|
+
var hostDoc = container.ownerDocument;
|
|
42424
|
+
shell = hostDoc.createElement("div");
|
|
42425
|
+
shell.className = "rte-state-inspector-shell";
|
|
42426
|
+
|
|
42427
|
+
var host = hostDoc.createElement("div");
|
|
42428
|
+
host.className = "rte-state-inspector-host";
|
|
42429
|
+
|
|
42430
|
+
container.parentNode.insertBefore(shell, container);
|
|
42431
|
+
shell.appendChild(host);
|
|
42432
|
+
host.appendChild(container);
|
|
42433
|
+
|
|
42434
|
+
panel = hostDoc.createElement("aside");
|
|
42435
|
+
panel.className = "rte-state-inspector-panel";
|
|
42436
|
+
panel.setAttribute("role", "complementary");
|
|
42437
|
+
panel.setAttribute("aria-label", config.stateInspectorTitle);
|
|
42438
|
+
|
|
42439
|
+
var header = hostDoc.createElement("div");
|
|
42440
|
+
header.className = "rte-state-inspector-header";
|
|
42441
|
+
var kicker = hostDoc.createElement("div");
|
|
42442
|
+
kicker.className = "rte-state-inspector-kicker";
|
|
42443
|
+
kicker.innerText = "Developer";
|
|
42444
|
+
var title = hostDoc.createElement("div");
|
|
42445
|
+
title.className = "rte-state-inspector-title";
|
|
42446
|
+
title.innerText = config.stateInspectorTitle;
|
|
42447
|
+
var copy = hostDoc.createElement("div");
|
|
42448
|
+
copy.className = "rte-state-inspector-copy";
|
|
42449
|
+
copy.innerText = config.stateInspectorHint;
|
|
42450
|
+
header.appendChild(kicker);
|
|
42451
|
+
header.appendChild(title);
|
|
42452
|
+
header.appendChild(copy);
|
|
42453
|
+
|
|
42454
|
+
var toolbar = hostDoc.createElement("div");
|
|
42455
|
+
toolbar.className = "rte-state-inspector-toolbar";
|
|
42456
|
+
summaryEl = hostDoc.createElement("div");
|
|
42457
|
+
summaryEl.className = "rte-state-inspector-summary";
|
|
42458
|
+
summaryEl.setAttribute("data-rte-inspector-summary", "1");
|
|
42459
|
+
var actions = hostDoc.createElement("div");
|
|
42460
|
+
actions.className = "rte-state-inspector-actions";
|
|
42461
|
+
actions.appendChild(makeButton(hostDoc, "Copy", "Copy the state snapshot as JSON", copySnapshot));
|
|
42462
|
+
actions.appendChild(makeButton(hostDoc, "Hide", "Hide the state inspector", closePanel));
|
|
42463
|
+
toolbar.appendChild(summaryEl);
|
|
42464
|
+
toolbar.appendChild(actions);
|
|
42465
|
+
|
|
42466
|
+
tabsBar = hostDoc.createElement("div");
|
|
42467
|
+
tabsBar.className = "rte-state-inspector-tabs";
|
|
42468
|
+
tabsBar.setAttribute("role", "tablist");
|
|
42469
|
+
for (var i = 0; i < TABS.length; i++) {
|
|
42470
|
+
tabsBar.appendChild(makeTab(hostDoc, TABS[i]));
|
|
42471
|
+
}
|
|
42472
|
+
|
|
42473
|
+
body = hostDoc.createElement("div");
|
|
42474
|
+
body.className = "rte-state-inspector-body";
|
|
42475
|
+
|
|
42476
|
+
panel.appendChild(header);
|
|
42477
|
+
panel.appendChild(toolbar);
|
|
42478
|
+
panel.appendChild(tabsBar);
|
|
42479
|
+
panel.appendChild(body);
|
|
42480
|
+
shell.appendChild(panel);
|
|
42481
|
+
return shell;
|
|
42482
|
+
}
|
|
42483
|
+
|
|
42484
|
+
function makeButton(hostDoc, label, aria, handler) {
|
|
42485
|
+
var btn = hostDoc.createElement("button");
|
|
42486
|
+
btn.type = "button";
|
|
42487
|
+
btn.className = "rte-state-inspector-btn";
|
|
42488
|
+
btn.innerText = label;
|
|
42489
|
+
btn.setAttribute("aria-label", aria);
|
|
42490
|
+
btn.onclick = handler;
|
|
42491
|
+
return btn;
|
|
42492
|
+
}
|
|
42493
|
+
|
|
42494
|
+
function makeTab(hostDoc, tab) {
|
|
42495
|
+
var btn = hostDoc.createElement("button");
|
|
42496
|
+
btn.type = "button";
|
|
42497
|
+
btn.className = "rte-state-inspector-tab";
|
|
42498
|
+
btn.innerText = tab.label;
|
|
42499
|
+
btn.setAttribute("role", "tab");
|
|
42500
|
+
btn.setAttribute("data-rte-inspector-tab", tab.id);
|
|
42501
|
+
btn.setAttribute("aria-selected", tab.id === activeTab ? "true" : "false");
|
|
42502
|
+
btn.onclick = function () {
|
|
42503
|
+
activeTab = tab.id;
|
|
42504
|
+
syncTabs();
|
|
42505
|
+
render();
|
|
42506
|
+
};
|
|
42507
|
+
return btn;
|
|
42508
|
+
}
|
|
42509
|
+
|
|
42510
|
+
function syncTabs() {
|
|
42511
|
+
if (!tabsBar) return;
|
|
42512
|
+
var buttons = tabsBar.querySelectorAll("[data-rte-inspector-tab]");
|
|
42513
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
42514
|
+
var id = buttons[i].getAttribute("data-rte-inspector-tab");
|
|
42515
|
+
buttons[i].setAttribute("aria-selected", id === activeTab ? "true" : "false");
|
|
42516
|
+
}
|
|
42517
|
+
}
|
|
42518
|
+
|
|
42519
|
+
function togglePanel() {
|
|
42520
|
+
if (shell && shell.classList.contains("is-open")) closePanel();
|
|
42521
|
+
else openPanel();
|
|
42522
|
+
}
|
|
42523
|
+
|
|
42524
|
+
function openPanel() {
|
|
42525
|
+
ensureShell();
|
|
42526
|
+
shell.classList.add("is-open");
|
|
42527
|
+
render();
|
|
42528
|
+
}
|
|
42529
|
+
|
|
42530
|
+
function closePanel() {
|
|
42531
|
+
if (!shell) return;
|
|
42532
|
+
shell.classList.remove("is-open");
|
|
42533
|
+
}
|
|
42534
|
+
|
|
42535
|
+
function scheduleRefresh() {
|
|
42536
|
+
if (!shell || !shell.classList.contains("is-open")) return;
|
|
42537
|
+
clearTimeout(refreshTimer);
|
|
42538
|
+
refreshTimer = setTimeout(function () { render(); }, 120);
|
|
42539
|
+
}
|
|
42540
|
+
|
|
42541
|
+
function copySnapshot() {
|
|
42542
|
+
var json = JSON.stringify(buildSnapshot(), null, 2);
|
|
42543
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
42544
|
+
navigator.clipboard.writeText(json);
|
|
42545
|
+
return;
|
|
42546
|
+
}
|
|
42547
|
+
var hostDoc = config.container.ownerDocument;
|
|
42548
|
+
var area = hostDoc.createElement("textarea");
|
|
42549
|
+
area.value = json;
|
|
42550
|
+
area.style.position = "fixed";
|
|
42551
|
+
area.style.opacity = "0";
|
|
42552
|
+
hostDoc.body.appendChild(area);
|
|
42553
|
+
area.select();
|
|
42554
|
+
try { hostDoc.execCommand("copy"); } catch (e) { /* clipboard unavailable */ }
|
|
42555
|
+
hostDoc.body.removeChild(area);
|
|
42556
|
+
}
|
|
42557
|
+
|
|
42558
|
+
function clear(el) {
|
|
42559
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
42560
|
+
}
|
|
42561
|
+
|
|
42562
|
+
function render() {
|
|
42563
|
+
if (!shell || !body) return;
|
|
42564
|
+
var tree = snapshotTree();
|
|
42565
|
+
var analysis = analyze(tree);
|
|
42566
|
+
var hostDoc = config.container.ownerDocument;
|
|
42567
|
+
|
|
42568
|
+
var errors = 0;
|
|
42569
|
+
for (var i = 0; i < analysis.issues.length; i++) {
|
|
42570
|
+
if (analysis.issues[i].severity === "error") errors++;
|
|
42571
|
+
}
|
|
42572
|
+
if (summaryEl) {
|
|
42573
|
+
// Kept short on purpose: the toolbar row ellipsises, and "3 errors" is
|
|
42574
|
+
// the part that must survive the truncation. Full counts are in the
|
|
42575
|
+
// snapshot and the tree tab.
|
|
42576
|
+
summaryEl.innerText = tree.counters.elements + " el · " +
|
|
42577
|
+
analysis.issues.length + " issue" + (analysis.issues.length === 1 ? "" : "s") +
|
|
42578
|
+
(errors ? " · " + errors + " error" + (errors === 1 ? "" : "s") : "");
|
|
42579
|
+
}
|
|
42580
|
+
|
|
42581
|
+
clear(body);
|
|
42582
|
+
if (activeTab === "issues") renderIssues(hostDoc, analysis.issues);
|
|
42583
|
+
else if (activeTab === "tree") renderTree(hostDoc, tree);
|
|
42584
|
+
else if (activeTab === "selection") renderSelection(hostDoc);
|
|
42585
|
+
else if (activeTab === "plugins") renderPlugins(hostDoc);
|
|
42586
|
+
else if (activeTab === "log") renderLog(hostDoc);
|
|
42587
|
+
}
|
|
42588
|
+
|
|
42589
|
+
function renderIssues(hostDoc, issues) {
|
|
42590
|
+
if (!issues.length) {
|
|
42591
|
+
var ok = hostDoc.createElement("div");
|
|
42592
|
+
ok.className = "rte-state-inspector-ok";
|
|
42593
|
+
ok.innerText = "No structural issues found.";
|
|
42594
|
+
body.appendChild(ok);
|
|
42595
|
+
return;
|
|
42596
|
+
}
|
|
42597
|
+
for (var i = 0; i < issues.length; i++) {
|
|
42598
|
+
var item = issues[i];
|
|
42599
|
+
var wrap = hostDoc.createElement("div");
|
|
42600
|
+
wrap.className = "rte-state-inspector-issue is-" + (item.severity === "error" ? "error" : "warn");
|
|
42601
|
+
wrap.setAttribute("data-rte-issue-id", item.id);
|
|
42602
|
+
var msg = hostDoc.createElement("div");
|
|
42603
|
+
msg.className = "rte-state-inspector-issue-msg";
|
|
42604
|
+
msg.innerText = item.message;
|
|
42605
|
+
var meta = hostDoc.createElement("div");
|
|
42606
|
+
meta.className = "rte-state-inspector-issue-meta";
|
|
42607
|
+
meta.innerText = (item.detail || "(document root)") + " · " + item.id;
|
|
42608
|
+
wrap.appendChild(msg);
|
|
42609
|
+
wrap.appendChild(meta);
|
|
42610
|
+
body.appendChild(wrap);
|
|
42611
|
+
}
|
|
42612
|
+
}
|
|
42613
|
+
|
|
42614
|
+
function renderTree(hostDoc, tree) {
|
|
42615
|
+
if (!tree.root || !tree.root.children.length) {
|
|
42616
|
+
var empty = hostDoc.createElement("div");
|
|
42617
|
+
empty.className = "rte-state-inspector-empty";
|
|
42618
|
+
empty.innerText = config.stateInspectorEmptyText;
|
|
42619
|
+
body.appendChild(empty);
|
|
42620
|
+
return;
|
|
42621
|
+
}
|
|
42622
|
+
var pre = hostDoc.createElement("pre");
|
|
42623
|
+
pre.className = "rte-state-inspector-pre";
|
|
42624
|
+
pre.innerText = formatTree(tree.root, 0).join("\n");
|
|
42625
|
+
body.appendChild(pre);
|
|
42626
|
+
}
|
|
42627
|
+
|
|
42628
|
+
function formatTree(node, depth) {
|
|
42629
|
+
var lines = [];
|
|
42630
|
+
var pad = new Array(depth + 1).join(" ");
|
|
42631
|
+
if (node.type === "text") {
|
|
42632
|
+
lines.push(pad + "#text(" + node.length + ") " + JSON.stringify(node.text));
|
|
42633
|
+
return lines;
|
|
42634
|
+
}
|
|
42635
|
+
if (node.type === "comment") {
|
|
42636
|
+
lines.push(pad + "#comment(" + node.length + ")");
|
|
42637
|
+
return lines;
|
|
42638
|
+
}
|
|
42639
|
+
var label = pad + "<" + node.name;
|
|
42640
|
+
var keys = [];
|
|
42641
|
+
for (var k in node.attrs) keys.push(k);
|
|
42642
|
+
if (keys.length) {
|
|
42643
|
+
keys.sort();
|
|
42644
|
+
for (var i = 0; i < keys.length && i < 4; i++) {
|
|
42645
|
+
label += " " + keys[i] + "=" + JSON.stringify(node.attrs[keys[i]]);
|
|
42646
|
+
}
|
|
42647
|
+
if (keys.length > 4) label += " …+" + (keys.length - 4);
|
|
42648
|
+
}
|
|
42649
|
+
label += ">";
|
|
42650
|
+
if (node.truncated) label += " … " + node.childCount + " children (depth limit)";
|
|
42651
|
+
lines.push(label);
|
|
42652
|
+
for (var c = 0; c < (node.children || []).length; c++) {
|
|
42653
|
+
lines = lines.concat(formatTree(node.children[c], depth + 1));
|
|
42654
|
+
}
|
|
42655
|
+
return lines;
|
|
42656
|
+
}
|
|
42657
|
+
|
|
42658
|
+
function renderSelection(hostDoc) {
|
|
42659
|
+
var info = describeSelection();
|
|
42660
|
+
if (!info.available) {
|
|
42661
|
+
var empty = hostDoc.createElement("div");
|
|
42662
|
+
empty.className = "rte-state-inspector-empty";
|
|
42663
|
+
empty.innerText = "No selection in the document.";
|
|
42664
|
+
body.appendChild(empty);
|
|
42665
|
+
return;
|
|
42666
|
+
}
|
|
42667
|
+
addRow(hostDoc, "Collapsed", String(info.collapsed));
|
|
42668
|
+
addRow(hostDoc, "In editable", String(info.insideEditable), !info.insideEditable);
|
|
42669
|
+
addRow(hostDoc, "Block path", info.block || "(root)");
|
|
42670
|
+
addRow(hostDoc, "Anchor", formatBoundary(info.anchor), !!(info.anchor && info.anchor.zeroLength));
|
|
42671
|
+
addRow(hostDoc, "Focus", formatBoundary(info.focus), !!(info.focus && info.focus.zeroLength));
|
|
42672
|
+
addRow(hostDoc, "Selected", info.textLength + " chars");
|
|
42673
|
+
if (info.text) addRow(hostDoc, "Text", info.text);
|
|
42674
|
+
}
|
|
42675
|
+
|
|
42676
|
+
function formatBoundary(boundary) {
|
|
42677
|
+
if (!boundary) return "(none)";
|
|
42678
|
+
var out = boundary.node + " @ " + boundary.offset;
|
|
42679
|
+
if (typeof boundary.length === "number") out += " (len " + boundary.length + ")";
|
|
42680
|
+
if (typeof boundary.childCount === "number") out += " (" + boundary.childCount + " children)";
|
|
42681
|
+
if (boundary.zeroLength) out += " ← zero-length text node";
|
|
42682
|
+
return out;
|
|
42683
|
+
}
|
|
42684
|
+
|
|
42685
|
+
function addRow(hostDoc, key, value, flagged) {
|
|
42686
|
+
var row = hostDoc.createElement("div");
|
|
42687
|
+
row.className = "rte-state-inspector-row" + (flagged ? " is-flagged" : "");
|
|
42688
|
+
var k = hostDoc.createElement("div");
|
|
42689
|
+
k.className = "rte-state-inspector-row-key";
|
|
42690
|
+
k.innerText = key;
|
|
42691
|
+
var v = hostDoc.createElement("div");
|
|
42692
|
+
v.className = "rte-state-inspector-row-val";
|
|
42693
|
+
v.innerText = value;
|
|
42694
|
+
row.appendChild(k);
|
|
42695
|
+
row.appendChild(v);
|
|
42696
|
+
body.appendChild(row);
|
|
42697
|
+
}
|
|
42698
|
+
|
|
42699
|
+
function renderPlugins(hostDoc) {
|
|
42700
|
+
if (!pluginOrder.length) {
|
|
42701
|
+
var empty = hostDoc.createElement("div");
|
|
42702
|
+
empty.className = "rte-state-inspector-empty";
|
|
42703
|
+
empty.innerText = "No plugins registered.";
|
|
42704
|
+
body.appendChild(empty);
|
|
42705
|
+
return;
|
|
42706
|
+
}
|
|
42707
|
+
var note = hostDoc.createElement("div");
|
|
42708
|
+
note.className = "rte-state-inspector-copy";
|
|
42709
|
+
note.innerText = "Initialization order. A plugin can only register into another plugin's API if it appears below it.";
|
|
42710
|
+
body.appendChild(note);
|
|
42711
|
+
for (var i = 0; i < pluginOrder.length; i++) {
|
|
42712
|
+
addRow(hostDoc, String(i + 1).padStart ? String(i + 1).padStart(3, "0") : String(i + 1), pluginOrder[i].name);
|
|
42713
|
+
}
|
|
42714
|
+
}
|
|
42715
|
+
|
|
42716
|
+
function renderLog(hostDoc) {
|
|
42717
|
+
if (!eventLog.length) {
|
|
42718
|
+
var empty = hostDoc.createElement("div");
|
|
42719
|
+
empty.className = "rte-state-inspector-empty";
|
|
42720
|
+
empty.innerText = "No editor events recorded yet.";
|
|
42721
|
+
body.appendChild(empty);
|
|
42722
|
+
return;
|
|
42723
|
+
}
|
|
42724
|
+
var start = eventLog[0].at;
|
|
42725
|
+
for (var i = eventLog.length - 1; i >= 0; i--) {
|
|
42726
|
+
addRow(hostDoc, "+" + (eventLog[i].at - start) + "ms", eventLog[i].event);
|
|
42727
|
+
}
|
|
42728
|
+
}
|
|
42729
|
+
}
|
|
42730
|
+
|
|
42731
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
42732
|
+
|
|
40940
42733
|
// 2026-07-27 Table of contents. An insertable, self-updating contents block
|
|
40941
42734
|
// built from the document's headings, with links that jump to them.
|
|
40942
42735
|
//
|
|
@@ -41286,6 +43079,13 @@ function RTE_Plugin_TableOfContents() {
|
|
|
41286
43079
|
st.setAttribute("data-css", text);
|
|
41287
43080
|
st.appendChild(doc.createTextNode(text));
|
|
41288
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
|
+
}
|
|
41289
43089
|
}
|
|
41290
43090
|
}
|
|
41291
43091
|
|
|
@@ -41943,6 +43743,13 @@ function RTE_Plugin_TextDirection() {
|
|
|
41943
43743
|
st.setAttribute("data-css", text);
|
|
41944
43744
|
st.appendChild(doc.createTextNode(text));
|
|
41945
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
|
+
}
|
|
41946
43753
|
}
|
|
41947
43754
|
}
|
|
41948
43755
|
|
|
@@ -42448,6 +44255,13 @@ function RTE_Plugin_TextPartLanguage() {
|
|
|
42448
44255
|
st.setAttribute("data-css", text);
|
|
42449
44256
|
st.appendChild(doc.createTextNode(text));
|
|
42450
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
|
+
}
|
|
42451
44265
|
}
|
|
42452
44266
|
}
|
|
42453
44267
|
|
|
@@ -42536,6 +44350,13 @@ function RTE_Plugin_TodoList() {
|
|
|
42536
44350
|
st.id = "rte-todolist-styles";
|
|
42537
44351
|
st.appendChild(doc.createTextNode(css));
|
|
42538
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
|
+
}
|
|
42539
44360
|
}
|
|
42540
44361
|
|
|
42541
44362
|
function makeCheckbox() {
|
|
@@ -42589,722 +44410,736 @@ function RTE_Plugin_TodoList() {
|
|
|
42589
44410
|
};
|
|
42590
44411
|
}
|
|
42591
44412
|
|
|
42592
|
-
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
42593
|
-
|
|
42594
|
-
if (!RTE_DefaultConfig.svgCode_trackchanges) {
|
|
42595
|
-
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>';
|
|
42596
|
-
}
|
|
42597
|
-
|
|
42598
|
-
RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
|
|
42599
|
-
|
|
42600
|
-
function RTE_Plugin_TrackedChanges() {
|
|
42601
|
-
var obj = this;
|
|
42602
|
-
var config;
|
|
42603
|
-
var editor;
|
|
42604
|
-
var enabled = false;
|
|
42605
|
-
|
|
42606
|
-
obj.PluginName = "TrackedChanges";
|
|
42607
|
-
|
|
42608
|
-
obj.InitConfig = function (argconfig) {
|
|
42609
|
-
config = argconfig;
|
|
42610
|
-
if (config.trackChangesEnabled === false) return;
|
|
42611
|
-
|
|
42612
|
-
config.currentUser = config.currentUser || null;
|
|
42613
|
-
config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
|
|
42614
|
-
config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
|
|
42615
|
-
// Optional application policy for accept/reject. Return false to keep a
|
|
42616
|
-
// suggestion pending. The default remains permissive for compatibility.
|
|
42617
|
-
config.reviewCanDecide = config.reviewCanDecide || null;
|
|
42618
|
-
config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
|
|
42619
|
-
|
|
42620
|
-
// Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
|
|
42621
|
-
config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
|
|
42622
|
-
appendToolbarCommand("toolbar_default", "#{trackchanges}");
|
|
42623
|
-
appendToolbarCommand("toolbar_full", "#{trackchanges}");
|
|
42624
|
-
appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
|
|
42625
|
-
|
|
42626
|
-
if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
|
|
42627
|
-
config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
|
|
42628
|
-
}
|
|
42629
|
-
};
|
|
42630
|
-
|
|
42631
|
-
obj.InitEditor = function (argeditor) {
|
|
42632
|
-
editor = argeditor;
|
|
42633
|
-
if (config.trackChangesEnabled === false) return;
|
|
42634
|
-
|
|
42635
|
-
editor.trackedChanges = {
|
|
42636
|
-
enable: function (user) { enable(user); },
|
|
42637
|
-
disable: function () { disable(); },
|
|
42638
|
-
isEnabled: function () { return enabled; },
|
|
42639
|
-
acceptAll: function (filter) { return acceptAll(filter); },
|
|
42640
|
-
rejectAll: function (filter) { return rejectAll(filter); },
|
|
42641
|
-
accept: function (id) { return acceptEntry(id); },
|
|
42642
|
-
reject: function (id) { return rejectEntry(id); },
|
|
42643
|
-
canDecide: function (id, action) {
|
|
42644
|
-
var entry = editor.reviewLedger && editor.reviewLedger.get(id);
|
|
42645
|
-
return !!entry && canDecide(entry, action || "accept");
|
|
42646
|
-
},
|
|
42647
|
-
list: function (opts) {
|
|
42648
|
-
if (!editor.reviewLedger) return [];
|
|
42649
|
-
// Default to the PENDING review queue (matches the internal
|
|
42650
|
-
// accept/reject semantics). Pass {all:true} for full history
|
|
42651
|
-
// including accepted/rejected entries (also via reviewLedger.list()).
|
|
42652
|
-
var includeAll = opts && opts.all === true;
|
|
42653
|
-
return editor.reviewLedger.list().filter(function (e) {
|
|
42654
|
-
if (e.changeType !== "insert" && e.changeType !== "delete") return false;
|
|
42655
|
-
return includeAll || e.status === "pending";
|
|
42656
|
-
});
|
|
42657
|
-
}
|
|
42658
|
-
};
|
|
42659
|
-
|
|
42660
|
-
injectStyles();
|
|
42661
|
-
|
|
42662
|
-
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
42663
|
-
editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
|
|
42664
|
-
var btn = editor.createToolbarButton(cmd);
|
|
42665
|
-
btn.__tcSync = function () {
|
|
42666
|
-
if (enabled) btn.classList.add("rte-ui-active");
|
|
42667
|
-
else btn.classList.remove("rte-ui-active");
|
|
42668
|
-
btn.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
42669
|
-
// WCAG 2.5.3 Label in Name: the visible label (this tooltip) must be
|
|
42670
|
-
// contained in the accessible name. "Turn on suggesting mode" was not
|
|
42671
|
-
// — the accessible name is "Suggesting mode" — so a speech-input user
|
|
42672
|
-
// saying what they see did not match the control. State belongs in
|
|
42673
|
-
// aria-pressed and the active styling, not in the visible label.
|
|
42674
|
-
btn.setAttribute("title", config.text_trackchanges || "Suggesting mode");
|
|
42675
|
-
};
|
|
42676
|
-
btn.__tcSync();
|
|
42677
|
-
return btn;
|
|
42678
|
-
};
|
|
42679
|
-
|
|
42680
|
-
editor.attachEvent("exec_command_trackchanges", function (state) {
|
|
42681
|
-
state.returnValue = true;
|
|
42682
|
-
state.stopBubble = true;
|
|
42683
|
-
if (enabled) disable();
|
|
42684
|
-
else enable();
|
|
42685
|
-
});
|
|
42686
|
-
|
|
42687
|
-
editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
|
|
42688
|
-
};
|
|
42689
|
-
|
|
42690
|
-
function appendToolbarCommand(toolbar, item) {
|
|
42691
|
-
if (!config[toolbar]) return;
|
|
42692
|
-
if (config[toolbar].indexOf(item) !== -1) return;
|
|
42693
|
-
config[toolbar] = config[toolbar] + item;
|
|
42694
|
-
}
|
|
42695
|
-
|
|
42696
|
-
function enable(user) {
|
|
42697
|
-
if (user) config.currentUser = user;
|
|
42698
|
-
if (!config.currentUser) {
|
|
42699
|
-
config.currentUser = { id: "user", name: "User", color: "#2563eb" };
|
|
42700
|
-
console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
|
|
42701
|
-
}
|
|
42702
|
-
enabled = true;
|
|
42703
|
-
syncToolbarButtons();
|
|
42704
|
-
}
|
|
42705
|
-
|
|
42706
|
-
function disable() {
|
|
42707
|
-
enabled = false;
|
|
42708
|
-
syncToolbarButtons();
|
|
42709
|
-
}
|
|
42710
|
-
|
|
42711
|
-
function syncToolbarButtons() {
|
|
42712
|
-
var doc = editor.iframe && editor.iframe.ownerDocument || document;
|
|
42713
|
-
var btns = doc.querySelectorAll(".rte_command_trackchanges");
|
|
42714
|
-
for (var i = 0; i < btns.length; i++) {
|
|
42715
|
-
if (btns[i].__tcSync) btns[i].__tcSync();
|
|
42716
|
-
}
|
|
42717
|
-
}
|
|
42718
|
-
|
|
42719
|
-
// --- beforeinput intercept ---
|
|
42720
|
-
|
|
42721
|
-
function onBeforeInput(e) {
|
|
42722
|
-
if (!enabled) return;
|
|
42723
|
-
if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
|
|
42724
|
-
|
|
42725
|
-
var type = e.inputType;
|
|
42726
|
-
if (!type) return;
|
|
42727
|
-
|
|
42728
|
-
if (type === "insertText" || type === "insertReplacementText") {
|
|
42729
|
-
e.preventDefault();
|
|
42730
|
-
handleInsertText(e.data || "");
|
|
42731
|
-
return;
|
|
42732
|
-
}
|
|
42733
|
-
if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
|
|
42734
|
-
e.preventDefault();
|
|
42735
|
-
handleDelete(type);
|
|
42736
|
-
return;
|
|
42737
|
-
}
|
|
42738
|
-
// insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
|
|
42739
|
-
}
|
|
42740
|
-
|
|
42741
|
-
// --- insert tracking ---
|
|
42742
|
-
|
|
42743
|
-
function handleInsertText(text) {
|
|
42744
|
-
if (!text) return;
|
|
42745
|
-
var sel = editor.getSelection();
|
|
42746
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
42747
|
-
var range = sel.getRangeAt(0);
|
|
42748
|
-
if (!range.collapsed) {
|
|
42749
|
-
// Selection replace: wrap selection as delete, then insert new text.
|
|
42750
|
-
if (!wrapRangeAsDelete(range)) return;
|
|
42751
|
-
// After wrapping, caret should be past the delete span; re-fetch selection.
|
|
42752
|
-
range = sel.getRangeAt(0);
|
|
42753
|
-
createInsertSpan(text, range);
|
|
42754
|
-
return;
|
|
42755
|
-
}
|
|
42756
|
-
|
|
42757
|
-
var mergeTarget = adjacentInsertSpan(range);
|
|
42758
|
-
if (mergeTarget) {
|
|
42759
|
-
appendToInsertSpan(mergeTarget, text);
|
|
42760
|
-
} else {
|
|
42761
|
-
createInsertSpan(text, range);
|
|
42762
|
-
}
|
|
42763
|
-
}
|
|
42764
|
-
|
|
42765
|
-
function adjacentInsertSpan(range) {
|
|
42766
|
-
// If caret sits inside an insert span by the current user, or immediately after one, merge.
|
|
42767
|
-
var node = range.startContainer;
|
|
42768
|
-
if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
|
|
42769
|
-
return node.parentNode;
|
|
42770
|
-
}
|
|
42771
|
-
// Caret just after an insert span (at an element boundary).
|
|
42772
|
-
if (node.nodeType === 1 && range.startOffset > 0) {
|
|
42773
|
-
var prev = node.childNodes[range.startOffset - 1];
|
|
42774
|
-
if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
|
|
42775
|
-
}
|
|
42776
|
-
// Caret at the very beginning just before our span.
|
|
42777
|
-
if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
|
|
42778
|
-
return node.previousSibling;
|
|
42779
|
-
}
|
|
42780
|
-
return null;
|
|
42781
|
-
}
|
|
42782
|
-
|
|
42783
|
-
function isMyInsert(el) {
|
|
42784
|
-
if (!el || el.nodeType !== 1) return false;
|
|
42785
|
-
if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
|
|
42786
|
-
return el.getAttribute("data-tc-author") === config.currentUser.id;
|
|
42787
|
-
}
|
|
42788
|
-
|
|
42789
|
-
// We preventDefault() the browser's own text insertion, which means we also
|
|
42790
|
-
// lose the whitespace handling it does for free. A plain second space is
|
|
42791
|
-
// collapsed by HTML rendering, so pressing space a second time moved nothing
|
|
42792
|
-
// on screen and suggesting mode looked like it had stopped accepting input.
|
|
42793
|
-
// Reproduce what contenteditable does: any space followed by another space,
|
|
42794
|
-
// and any trailing space, becomes a non-breaking space. Recomputed from
|
|
42795
|
-
// scratch each time so an NBSP reverts to a normal space once real text
|
|
42796
|
-
// follows it (keeping word wrap and copied text clean). Only typed text
|
|
42797
|
-
// reaches this path -- paste arrives as insertFromPaste and is passed
|
|
42798
|
-
// through -- so this never rewrites an NBSP the user inserted deliberately.
|
|
42799
|
-
function normalizeInsertWhitespace(node) {
|
|
42800
|
-
if (!node || node.nodeType !== 3) return;
|
|
42801
|
-
var v = node.nodeValue;
|
|
42802
|
-
if (v.indexOf(" ") === -1 && v.indexOf("\u00a0") === -1) return;
|
|
42803
|
-
v = v.replace(/\u00a0/g, " ");
|
|
42804
|
-
v = v.replace(/ (?= )/g, "\u00a0");
|
|
42805
|
-
v = v.replace(/ $/, "\u00a0");
|
|
42806
|
-
if (v !== node.nodeValue) node.nodeValue = v;
|
|
42807
|
-
}
|
|
42808
|
-
|
|
42809
|
-
function appendToInsertSpan(span, text) {
|
|
42810
|
-
var editdoc = editor.getDocument();
|
|
42811
|
-
// Append to the span's last text node (or create one).
|
|
42812
|
-
var last = span.lastChild;
|
|
42813
|
-
var node;
|
|
42814
|
-
if (last && last.nodeType === 3) {
|
|
42815
|
-
last.nodeValue = last.nodeValue + text;
|
|
42816
|
-
node = last;
|
|
42817
|
-
} else {
|
|
42818
|
-
node = editdoc.createTextNode(text);
|
|
42819
|
-
span.appendChild(node);
|
|
42820
|
-
}
|
|
42821
|
-
normalizeInsertWhitespace(node);
|
|
42822
|
-
placeCaretAtEnd(node);
|
|
42823
|
-
updateEntryForSpan(span);
|
|
42824
|
-
}
|
|
42825
|
-
|
|
42826
|
-
function createInsertSpan(text, range) {
|
|
42827
|
-
var editdoc = editor.getDocument();
|
|
42828
|
-
var span = editdoc.createElement("span");
|
|
42829
|
-
span.className = config.trackChangesInsertClass;
|
|
42830
|
-
var user = config.currentUser;
|
|
42831
|
-
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
42832
|
-
span.setAttribute("data-tc-id", id);
|
|
42833
|
-
span.setAttribute("data-tc-author", user.id);
|
|
42834
|
-
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
42835
|
-
span.style.color = user.color;
|
|
42836
|
-
span.style.textDecoration = "underline";
|
|
42837
|
-
span.appendChild(editdoc.createTextNode(text));
|
|
42838
|
-
range.insertNode(span);
|
|
42839
|
-
normalizeInsertWhitespace(span.firstChild);
|
|
42840
|
-
placeCaretAtEnd(span.firstChild);
|
|
42841
|
-
|
|
42842
|
-
editor.reviewLedger.add({
|
|
42843
|
-
id: id,
|
|
42844
|
-
changeType: "insert",
|
|
42845
|
-
author: user,
|
|
42846
|
-
text: text,
|
|
42847
|
-
status: "pending",
|
|
42848
|
-
sourceLabel: "Suggested insert",
|
|
42849
|
-
createdAt: Date.now()
|
|
42850
|
-
});
|
|
42851
|
-
}
|
|
42852
|
-
|
|
42853
|
-
function updateEntryForSpan(span) {
|
|
42854
|
-
var id = span.getAttribute("data-tc-id");
|
|
42855
|
-
if (!id) return;
|
|
42856
|
-
editor.reviewLedger.update(id, { text: span.textContent });
|
|
42857
|
-
}
|
|
42858
|
-
|
|
42859
|
-
// --- delete tracking ---
|
|
42860
|
-
|
|
42861
|
-
function handleDelete(mode) {
|
|
42862
|
-
var sel = editor.getSelection();
|
|
42863
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
42864
|
-
var range = sel.getRangeAt(0);
|
|
42865
|
-
|
|
42866
|
-
if (!range.collapsed) {
|
|
42867
|
-
wrapRangeAsDelete(range);
|
|
42868
|
-
return;
|
|
42869
|
-
}
|
|
42870
|
-
|
|
42871
|
-
// Collapsed: extend by one char in the appropriate direction, then wrap.
|
|
42872
|
-
// The extended range covers only that one character — any delete spans between
|
|
42873
|
-
// it and the caret should be SKIPPED, not re-wrapped.
|
|
42874
|
-
var extended = editor.getDocument().createRange();
|
|
42875
|
-
if (mode === "deleteContentBackward") {
|
|
42876
|
-
var anchor = prevCharPoint(range.startContainer, range.startOffset);
|
|
42877
|
-
if (!anchor) return;
|
|
42878
|
-
extended.setStart(anchor.node, anchor.offset);
|
|
42879
|
-
extended.setEnd(anchor.node, anchor.offset + 1);
|
|
42880
|
-
} else {
|
|
42881
|
-
var forward = nextCharPoint(range.startContainer, range.startOffset);
|
|
42882
|
-
if (!forward) return;
|
|
42883
|
-
extended.setStart(forward.node, forward.offset - 1);
|
|
42884
|
-
extended.setEnd(forward.node, forward.offset);
|
|
42885
|
-
}
|
|
42886
|
-
|
|
42887
|
-
// If that extended range sits entirely inside one of the current user's own
|
|
42888
|
-
// insert spans, treat this as undoing their own typing — actually delete.
|
|
42889
|
-
if (rangeInsideMyInsert(extended)) {
|
|
42890
|
-
extended.deleteContents();
|
|
42891
|
-
var sel2 = editor.getSelection();
|
|
42892
|
-
sel2.removeAllRanges();
|
|
42893
|
-
sel2.addRange(extended);
|
|
42894
|
-
// If the insert span is now empty, remove it and its ledger entry.
|
|
42895
|
-
cleanupEmptyInsertSpans();
|
|
42896
|
-
return;
|
|
42897
|
-
}
|
|
42898
|
-
|
|
42899
|
-
wrapRangeAsDelete(extended);
|
|
42900
|
-
}
|
|
42901
|
-
|
|
42902
|
-
function isInsideDeleteSpan(node) {
|
|
42903
|
-
var walk = node.nodeType === 1 ? node : node.parentNode;
|
|
42904
|
-
while (walk && walk !== editor.getEditable()) {
|
|
42905
|
-
if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
|
|
42906
|
-
walk = walk.parentNode;
|
|
42907
|
-
}
|
|
42908
|
-
return false;
|
|
42909
|
-
}
|
|
42910
|
-
|
|
42911
|
-
// Walk text leaves in the editable, skipping any that live inside an existing delete
|
|
42912
|
-
// span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
|
|
42913
|
-
function liveTextLeaves() {
|
|
42914
|
-
var editable = editor.getEditable();
|
|
42915
|
-
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
|
|
42916
|
-
acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
|
|
42917
|
-
});
|
|
42918
|
-
var out = [], n;
|
|
42919
|
-
while ((n = walker.nextNode())) out.push(n);
|
|
42920
|
-
return out;
|
|
42921
|
-
}
|
|
42922
|
-
|
|
42923
|
-
function prevCharPoint(node, offset) {
|
|
42924
|
-
var leaves = liveTextLeaves();
|
|
42925
|
-
if (!leaves.length) return null;
|
|
42926
|
-
var idx = caretIndex(node, offset, leaves);
|
|
42927
|
-
if (idx <= 0) return null;
|
|
42928
|
-
return leafOffsetAt(idx - 1, leaves);
|
|
42929
|
-
}
|
|
42930
|
-
|
|
42931
|
-
function nextCharPoint(node, offset) {
|
|
42932
|
-
var leaves = liveTextLeaves();
|
|
42933
|
-
if (!leaves.length) return null;
|
|
42934
|
-
var total = 0;
|
|
42935
|
-
for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
|
|
42936
|
-
var idx = caretIndex(node, offset, leaves);
|
|
42937
|
-
if (idx >= total) return null;
|
|
42938
|
-
return leafOffsetAt(idx + 1, leaves);
|
|
42939
|
-
}
|
|
42940
|
-
|
|
42941
|
-
// How many live characters appear before the given caret position?
|
|
42942
|
-
function caretIndex(node, offset, leaves) {
|
|
42943
|
-
var r = editor.getDocument().createRange();
|
|
42944
|
-
r.setStart(node, offset);
|
|
42945
|
-
r.collapse(true);
|
|
42946
|
-
var count = 0;
|
|
42947
|
-
for (var i = 0; i < leaves.length; i++) {
|
|
42948
|
-
var leaf = leaves[i];
|
|
42949
|
-
// If the entire leaf is before the caret, add its length.
|
|
42950
|
-
var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
|
|
42951
|
-
if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
|
|
42952
|
-
var cmpStart = r.comparePoint(leaf, 0);
|
|
42953
|
-
if (cmpStart >= 0) break; // leaf starts at or after caret
|
|
42954
|
-
// Caret falls inside this leaf — binary search the split point.
|
|
42955
|
-
var lo = 0, hi = leaf.nodeValue.length;
|
|
42956
|
-
while (lo < hi) {
|
|
42957
|
-
var mid = (lo + hi) >> 1;
|
|
42958
|
-
if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
|
|
42959
|
-
else hi = mid;
|
|
42960
|
-
}
|
|
42961
|
-
count += lo;
|
|
42962
|
-
break;
|
|
42963
|
-
}
|
|
42964
|
-
return count;
|
|
42965
|
-
}
|
|
42966
|
-
|
|
42967
|
-
function leafOffsetAt(index, leaves) {
|
|
42968
|
-
var remaining = index;
|
|
42969
|
-
for (var i = 0; i < leaves.length; i++) {
|
|
42970
|
-
if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
|
|
42971
|
-
remaining -= leaves[i].nodeValue.length;
|
|
42972
|
-
}
|
|
42973
|
-
return null;
|
|
42974
|
-
}
|
|
42975
|
-
|
|
42976
|
-
function firstTextLeaf(node) {
|
|
42977
|
-
if (!node) return null;
|
|
42978
|
-
if (node.nodeType === 3) return node;
|
|
42979
|
-
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
42980
|
-
return walker.nextNode();
|
|
42981
|
-
}
|
|
42982
|
-
|
|
42983
|
-
function lastTextLeaf(node) {
|
|
42984
|
-
if (!node) return null;
|
|
42985
|
-
if (node.nodeType === 3) return node;
|
|
42986
|
-
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
42987
|
-
var last = null, n;
|
|
42988
|
-
while ((n = walker.nextNode())) last = n;
|
|
42989
|
-
return last;
|
|
42990
|
-
}
|
|
42991
|
-
|
|
42992
|
-
function previousTextLeaf(node) {
|
|
42993
|
-
var editable = editor.getEditable();
|
|
42994
|
-
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
42995
|
-
var prev = null, cur;
|
|
42996
|
-
while ((cur = walker.nextNode())) {
|
|
42997
|
-
if (cur === node) return prev;
|
|
42998
|
-
prev = cur;
|
|
42999
|
-
}
|
|
43000
|
-
return null;
|
|
43001
|
-
}
|
|
43002
|
-
|
|
43003
|
-
function nextTextLeaf(node) {
|
|
43004
|
-
var editable = editor.getEditable();
|
|
43005
|
-
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
43006
|
-
var seen = false, cur;
|
|
43007
|
-
while ((cur = walker.nextNode())) {
|
|
43008
|
-
if (seen) return cur;
|
|
43009
|
-
if (cur === node) seen = true;
|
|
43010
|
-
}
|
|
43011
|
-
return null;
|
|
43012
|
-
}
|
|
43013
|
-
|
|
43014
|
-
function rangeInsideMyInsert(range) {
|
|
43015
|
-
var span = range.startContainer.parentNode;
|
|
43016
|
-
while (span && span !== editor.getEditable()) {
|
|
43017
|
-
if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
|
|
43018
|
-
span = span.parentNode;
|
|
43019
|
-
}
|
|
43020
|
-
return false;
|
|
43021
|
-
}
|
|
43022
|
-
|
|
43023
|
-
function wrapRangeAsDelete(range) {
|
|
43024
|
-
if (range.collapsed) return false;
|
|
43025
|
-
var editdoc = editor.getDocument();
|
|
43026
|
-
|
|
43027
|
-
var fragment = range.cloneContents();
|
|
43028
|
-
if (!fragmentHasText(fragment)) return false;
|
|
43029
|
-
|
|
43030
|
-
// Find existing same-author delete spans adjacent to the range so we can merge
|
|
43031
|
-
// sequential backspaces / forward deletes into one contiguous span.
|
|
43032
|
-
var user = config.currentUser;
|
|
43033
|
-
var mergeBefore = adjacentDeleteSpan(range, "before", user);
|
|
43034
|
-
var mergeAfter = adjacentDeleteSpan(range, "after", user);
|
|
43035
|
-
|
|
43036
|
-
range.deleteContents();
|
|
43037
|
-
|
|
43038
|
-
if (mergeBefore) {
|
|
43039
|
-
// Append the fragment's content to the existing "before" span.
|
|
43040
|
-
appendFragmentToSpan(mergeBefore, fragment);
|
|
43041
|
-
if (mergeAfter && mergeAfter !== mergeBefore) {
|
|
43042
|
-
// Collapse both sides: move "after" span's children into "before" and drop it.
|
|
43043
|
-
while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
|
|
43044
|
-
var afterId = mergeAfter.getAttribute("data-tc-id");
|
|
43045
|
-
mergeAfter.remove();
|
|
43046
|
-
if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
|
|
43047
|
-
}
|
|
43048
|
-
var rangeAfter = editdoc.createRange();
|
|
43049
|
-
rangeAfter.setStartAfter(mergeBefore);
|
|
43050
|
-
rangeAfter.collapse(true);
|
|
43051
|
-
var sel = editor.getSelection();
|
|
43052
|
-
sel.removeAllRanges();
|
|
43053
|
-
sel.addRange(rangeAfter);
|
|
43054
|
-
// 2026-05-28 Defense: if a customer pasted a delete-class span from
|
|
43055
|
-
// outside this plugin, it may lack data-tc-id. Skip the ledger
|
|
43056
|
-
// update instead of writing under a `null` key (would silently
|
|
43057
|
-
// corrupt the ledger via the update("null", ...) coercion).
|
|
43058
|
-
var beforeId = mergeBefore.getAttribute("data-tc-id");
|
|
43059
|
-
if (beforeId && editor.reviewLedger) {
|
|
43060
|
-
editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
|
|
43061
|
-
}
|
|
43062
|
-
return true;
|
|
43063
|
-
}
|
|
43064
|
-
|
|
43065
|
-
if (mergeAfter) {
|
|
43066
|
-
prependFragmentToSpan(mergeAfter, fragment);
|
|
43067
|
-
var rangeBefore = editdoc.createRange();
|
|
43068
|
-
rangeBefore.setStartBefore(mergeAfter);
|
|
43069
|
-
rangeBefore.collapse(true);
|
|
43070
|
-
var sel2 = editor.getSelection();
|
|
43071
|
-
sel2.removeAllRanges();
|
|
43072
|
-
sel2.addRange(rangeBefore);
|
|
43073
|
-
// Same defense as above (mergeAfter may lack data-tc-id).
|
|
43074
|
-
var afterMergeId = mergeAfter.getAttribute("data-tc-id");
|
|
43075
|
-
if (afterMergeId && editor.reviewLedger) {
|
|
43076
|
-
editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
|
|
43077
|
-
}
|
|
43078
|
-
return true;
|
|
43079
|
-
}
|
|
43080
|
-
|
|
43081
|
-
var span = editdoc.createElement("span");
|
|
43082
|
-
span.className = config.trackChangesDeleteClass;
|
|
43083
|
-
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
43084
|
-
span.setAttribute("data-tc-id", id);
|
|
43085
|
-
span.setAttribute("data-tc-author", user.id);
|
|
43086
|
-
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
43087
|
-
span.style.color = user.color;
|
|
43088
|
-
span.style.textDecoration = "line-through";
|
|
43089
|
-
span.appendChild(fragment);
|
|
43090
|
-
range.insertNode(span);
|
|
43091
|
-
|
|
43092
|
-
var after = editdoc.createRange();
|
|
43093
|
-
after.setStartAfter(span);
|
|
43094
|
-
after.collapse(true);
|
|
43095
|
-
var sel3 = editor.getSelection();
|
|
43096
|
-
sel3.removeAllRanges();
|
|
43097
|
-
sel3.addRange(after);
|
|
43098
|
-
|
|
43099
|
-
editor.reviewLedger.add({
|
|
43100
|
-
id: id,
|
|
43101
|
-
changeType: "delete",
|
|
43102
|
-
author: user,
|
|
43103
|
-
text: span.textContent,
|
|
43104
|
-
originalHtml: span.innerHTML,
|
|
43105
|
-
status: "pending",
|
|
43106
|
-
sourceLabel: "Suggested delete",
|
|
43107
|
-
createdAt: Date.now()
|
|
43108
|
-
});
|
|
43109
|
-
return true;
|
|
43110
|
-
}
|
|
43111
|
-
|
|
43112
|
-
function adjacentDeleteSpan(range, side, user) {
|
|
43113
|
-
// Find element immediately before or after the range boundaries at the container level.
|
|
43114
|
-
var node = side === "before" ? range.startContainer : range.endContainer;
|
|
43115
|
-
var offset = side === "before" ? range.startOffset : range.endOffset;
|
|
43116
|
-
if (node.nodeType === 3) {
|
|
43117
|
-
// Look at the sibling of the text node if we're at the text node's boundary.
|
|
43118
|
-
if (side === "before" && offset === 0) {
|
|
43119
|
-
var prev = node.previousSibling;
|
|
43120
|
-
if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
|
|
43121
|
-
}
|
|
43122
|
-
if (side === "after" && offset === node.nodeValue.length) {
|
|
43123
|
-
var next = node.nextSibling;
|
|
43124
|
-
if (next && isDeleteSpanByAuthor(next, user)) return next;
|
|
43125
|
-
}
|
|
43126
|
-
return null;
|
|
43127
|
-
}
|
|
43128
|
-
// Element container: look at children on either side of the offset.
|
|
43129
|
-
if (side === "before" && offset > 0) {
|
|
43130
|
-
var childBefore = node.childNodes[offset - 1];
|
|
43131
|
-
if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
|
|
43132
|
-
}
|
|
43133
|
-
if (side === "after" && offset < node.childNodes.length) {
|
|
43134
|
-
var childAfter = node.childNodes[offset];
|
|
43135
|
-
if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
|
|
43136
|
-
}
|
|
43137
|
-
return null;
|
|
43138
|
-
}
|
|
43139
|
-
|
|
43140
|
-
function isDeleteSpanByAuthor(el, user) {
|
|
43141
|
-
if (!el || el.nodeType !== 1) return false;
|
|
43142
|
-
if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
|
|
43143
|
-
return el.getAttribute("data-tc-author") === user.id;
|
|
43144
|
-
}
|
|
43145
|
-
|
|
43146
|
-
function appendFragmentToSpan(span, fragment) {
|
|
43147
|
-
while (fragment.firstChild) span.appendChild(fragment.firstChild);
|
|
43148
|
-
}
|
|
43149
|
-
|
|
43150
|
-
function prependFragmentToSpan(span, fragment) {
|
|
43151
|
-
var first = span.firstChild;
|
|
43152
|
-
while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
|
|
43153
|
-
}
|
|
43154
|
-
|
|
43155
|
-
function fragmentHasText(fragment) {
|
|
43156
|
-
var t = (fragment.textContent || "").replace(/\s+/g, "");
|
|
43157
|
-
return !!t;
|
|
43158
|
-
}
|
|
43159
|
-
|
|
43160
|
-
function cleanupEmptyInsertSpans() {
|
|
43161
|
-
var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
|
|
43162
|
-
for (var i = 0; i < spans.length; i++) {
|
|
43163
|
-
var s = spans[i];
|
|
43164
|
-
if (!s.textContent || !s.textContent.length) {
|
|
43165
|
-
var id = s.getAttribute("data-tc-id");
|
|
43166
|
-
if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
|
|
43167
|
-
s.remove();
|
|
43168
|
-
} else {
|
|
43169
|
-
updateEntryForSpan(s);
|
|
43170
|
-
}
|
|
43171
|
-
}
|
|
43172
|
-
}
|
|
43173
|
-
|
|
43174
|
-
function placeCaretAtEnd(node) {
|
|
43175
|
-
if (!node) return;
|
|
43176
|
-
var editdoc = editor.getDocument();
|
|
43177
|
-
var range = editdoc.createRange();
|
|
43178
|
-
if (node.nodeType === 3) {
|
|
43179
|
-
range.setStart(node, node.nodeValue.length);
|
|
43180
|
-
} else {
|
|
43181
|
-
range.setStart(node, node.childNodes.length);
|
|
43182
|
-
}
|
|
43183
|
-
range.collapse(true);
|
|
43184
|
-
var sel = editor.getSelection();
|
|
43185
|
-
sel.removeAllRanges();
|
|
43186
|
-
sel.addRange(range);
|
|
43187
|
-
}
|
|
43188
|
-
|
|
43189
|
-
// --- accept / reject ---
|
|
43190
|
-
|
|
43191
|
-
function decisionUser() {
|
|
43192
|
-
return config.currentUser || { id: "user", name: "User", color: "#2563eb" };
|
|
43193
|
-
}
|
|
43194
|
-
|
|
43195
|
-
function canDecide(entry, action) {
|
|
43196
|
-
if (typeof config.reviewCanDecide !== "function") return true;
|
|
43197
|
-
var allowed = false;
|
|
43198
|
-
var user = decisionUser();
|
|
43199
|
-
try { allowed = config.reviewCanDecide(entry, action, user, editor) !== false; }
|
|
43200
|
-
catch (err) {
|
|
43201
|
-
console.warn("trackedchanges: reviewCanDecide threw; denying decision.", err);
|
|
43202
|
-
}
|
|
43203
|
-
if (allowed) return true;
|
|
43204
|
-
var detail = { entry: entry, action: action, user: user };
|
|
43205
|
-
try { typeof config.onReviewDecisionDenied === "function" && config.onReviewDecisionDenied(detail); } catch (ignore) { }
|
|
43206
|
-
try { typeof editor.fireEvent === "function" && editor.fireEvent("review_decision_denied", detail); } catch (ignore2) { }
|
|
43207
|
-
return false;
|
|
43208
|
-
}
|
|
43209
|
-
|
|
43210
|
-
function acceptEntry(id) {
|
|
43211
|
-
var entry = editor.reviewLedger.get(id);
|
|
43212
|
-
if (!entry) return false;
|
|
43213
|
-
if (!canDecide(entry, "accept")) return false;
|
|
43214
|
-
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
43215
|
-
if (entry.changeType === "insert") {
|
|
43216
|
-
if (span) unwrapKeepChildren(span);
|
|
43217
|
-
} else if (entry.changeType === "delete") {
|
|
43218
|
-
if (span) span.remove();
|
|
43219
|
-
}
|
|
43220
|
-
editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
43221
|
-
return true;
|
|
43222
|
-
}
|
|
43223
|
-
|
|
43224
|
-
function rejectEntry(id) {
|
|
43225
|
-
var entry = editor.reviewLedger.get(id);
|
|
43226
|
-
if (!entry) return false;
|
|
43227
|
-
if (!canDecide(entry, "reject")) return false;
|
|
43228
|
-
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
43229
|
-
if (entry.changeType === "insert") {
|
|
43230
|
-
if (span) span.remove();
|
|
43231
|
-
} else if (entry.changeType === "delete") {
|
|
43232
|
-
if (span) unwrapKeepChildren(span);
|
|
43233
|
-
}
|
|
43234
|
-
editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
43235
|
-
return true;
|
|
43236
|
-
}
|
|
43237
|
-
|
|
43238
|
-
function acceptAll(filter) {
|
|
43239
|
-
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
43240
|
-
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
43241
|
-
});
|
|
43242
|
-
var accepted = 0;
|
|
43243
|
-
for (var i = 0; i < entries.length; i++) if (acceptEntry(entries[i].id)) accepted++;
|
|
43244
|
-
return accepted;
|
|
43245
|
-
}
|
|
43246
|
-
|
|
43247
|
-
function rejectAll(filter) {
|
|
43248
|
-
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
43249
|
-
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
43250
|
-
});
|
|
43251
|
-
var rejected = 0;
|
|
43252
|
-
for (var i = 0; i < entries.length; i++) if (rejectEntry(entries[i].id)) rejected++;
|
|
43253
|
-
return rejected;
|
|
43254
|
-
}
|
|
43255
|
-
|
|
43256
|
-
function unwrapKeepChildren(el) {
|
|
43257
|
-
var parent = el.parentNode;
|
|
43258
|
-
if (!parent) return;
|
|
43259
|
-
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
43260
|
-
parent.removeChild(el);
|
|
43261
|
-
}
|
|
43262
|
-
|
|
43263
|
-
// 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
|
|
43264
|
-
// Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
|
|
43265
|
-
// containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
|
|
43266
|
-
// colons, base64 IDs with `=` padding).
|
|
43267
|
-
function cssEscape(s) {
|
|
43268
|
-
var str = String(s);
|
|
43269
|
-
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
43270
|
-
try { return CSS.escape(str); } catch (e) { /* fall through */ }
|
|
43271
|
-
}
|
|
43272
|
-
return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
|
|
43273
|
-
return "\\" + c;
|
|
43274
|
-
});
|
|
43275
|
-
}
|
|
43276
|
-
|
|
43277
|
-
// --- styles ---
|
|
43278
|
-
|
|
43279
|
-
function injectStyles() {
|
|
43280
|
-
var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
|
|
43281
|
-
if (!host.querySelector("style[data-rte-trackchanges]")) {
|
|
43282
|
-
var style = host.createElement("style");
|
|
43283
|
-
style.setAttribute("data-rte-trackchanges", "1");
|
|
43284
|
-
style.textContent = [
|
|
43285
|
-
".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
|
|
43286
|
-
".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)}",
|
|
43287
|
-
".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}",
|
|
43288
|
-
".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
|
|
43289
|
-
".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}",
|
|
43290
|
-
".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}"
|
|
43291
|
-
].join("\n");
|
|
43292
|
-
host.head.appendChild(style);
|
|
43293
|
-
|
|
43294
|
-
|
|
43295
|
-
|
|
43296
|
-
|
|
43297
|
-
|
|
43298
|
-
|
|
43299
|
-
|
|
43300
|
-
|
|
43301
|
-
|
|
43302
|
-
|
|
43303
|
-
|
|
43304
|
-
editdoc.
|
|
43305
|
-
|
|
43306
|
-
|
|
43307
|
-
}
|
|
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
|
+
}
|
|
43308
45143
|
|
|
43309
45144
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
43310
45145
|
|
|
@@ -43482,6 +45317,13 @@ function RTE_Plugin_Typewriter() {
|
|
|
43482
45317
|
st.id = "rte-typewriter-styles";
|
|
43483
45318
|
st.appendChild(doc.createTextNode(css));
|
|
43484
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
|
+
}
|
|
43485
45327
|
}
|
|
43486
45328
|
|
|
43487
45329
|
// Strip the transient focus class around any serialize so saved markup never
|
|
@@ -43671,6 +45513,14 @@ function RTE_Plugin_Watermark() {
|
|
|
43671
45513
|
? ""
|
|
43672
45514
|
: "@media print{[data-rte-watermark]{background-image:none !important;}" +
|
|
43673
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
|
+
}
|
|
43674
45524
|
}
|
|
43675
45525
|
|
|
43676
45526
|
// ---- serialization safety -------------------------------------------
|
|
@@ -43722,6 +45572,13 @@ function RTE_Plugin_Watermark() {
|
|
|
43722
45572
|
st.id = "rte-watermark-styles";
|
|
43723
45573
|
st.appendChild(doc.createTextNode("[data-rte-watermark]{position:relative;}"));
|
|
43724
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
|
+
}
|
|
43725
45582
|
}
|
|
43726
45583
|
}
|
|
43727
45584
|
|
|
@@ -44772,6 +46629,13 @@ function RTE_Plugin_YjsCollab() {
|
|
|
44772
46629
|
".rte-collab-presence-chip.is-self{outline:2px solid rgba(15,23,42,.25);outline-offset:1px}"
|
|
44773
46630
|
].join("\n");
|
|
44774
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
|
+
}
|
|
44775
46639
|
}
|
|
44776
46640
|
}
|
|
44777
46641
|
|