@richscripts/richtexteditor 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +360 -0
  2. package/admin/index.d.ts +100 -0
  3. package/admin/index.js +94 -0
  4. package/angular/component.ts +284 -0
  5. package/angular/index.d.ts +80 -0
  6. package/angular/index.ts +24 -0
  7. package/headless/index.d.ts +65 -0
  8. package/headless/index.js +272 -0
  9. package/index.d.ts +1412 -0
  10. package/integrations/shared.d.ts +138 -0
  11. package/integrations/shared.js +5182 -0
  12. package/package.json +130 -0
  13. package/react/index.d.ts +104 -0
  14. package/react/index.js +310 -0
  15. package/richtexteditor/CHANGELOG.md +1876 -0
  16. package/richtexteditor/all_plugins.js +31125 -0
  17. package/richtexteditor/all_plugins.min.js +78 -0
  18. package/richtexteditor/lang/rte-lang-af.js +193 -0
  19. package/richtexteditor/lang/rte-lang-ar.js +193 -0
  20. package/richtexteditor/lang/rte-lang-bg.js +193 -0
  21. package/richtexteditor/lang/rte-lang-bn.js +193 -0
  22. package/richtexteditor/lang/rte-lang-bs.js +193 -0
  23. package/richtexteditor/lang/rte-lang-ca.js +193 -0
  24. package/richtexteditor/lang/rte-lang-cs.js +193 -0
  25. package/richtexteditor/lang/rte-lang-cy.js +193 -0
  26. package/richtexteditor/lang/rte-lang-da.js +193 -0
  27. package/richtexteditor/lang/rte-lang-de.js +193 -0
  28. package/richtexteditor/lang/rte-lang-el.js +193 -0
  29. package/richtexteditor/lang/rte-lang-en.js +6 -0
  30. package/richtexteditor/lang/rte-lang-es.js +193 -0
  31. package/richtexteditor/lang/rte-lang-et.js +193 -0
  32. package/richtexteditor/lang/rte-lang-fa.js +193 -0
  33. package/richtexteditor/lang/rte-lang-fi.js +193 -0
  34. package/richtexteditor/lang/rte-lang-fil.js +193 -0
  35. package/richtexteditor/lang/rte-lang-fj.js +193 -0
  36. package/richtexteditor/lang/rte-lang-fr.js +193 -0
  37. package/richtexteditor/lang/rte-lang-ga.js +193 -0
  38. package/richtexteditor/lang/rte-lang-gu.js +193 -0
  39. package/richtexteditor/lang/rte-lang-he.js +193 -0
  40. package/richtexteditor/lang/rte-lang-hi.js +193 -0
  41. package/richtexteditor/lang/rte-lang-hr.js +193 -0
  42. package/richtexteditor/lang/rte-lang-ht.js +193 -0
  43. package/richtexteditor/lang/rte-lang-hu.js +193 -0
  44. package/richtexteditor/lang/rte-lang-id.js +193 -0
  45. package/richtexteditor/lang/rte-lang-is.js +193 -0
  46. package/richtexteditor/lang/rte-lang-it.js +193 -0
  47. package/richtexteditor/lang/rte-lang-ja.js +193 -0
  48. package/richtexteditor/lang/rte-lang-kn.js +193 -0
  49. package/richtexteditor/lang/rte-lang-ko.js +193 -0
  50. package/richtexteditor/lang/rte-lang-lt.js +193 -0
  51. package/richtexteditor/lang/rte-lang-lv.js +193 -0
  52. package/richtexteditor/lang/rte-lang-mg.js +193 -0
  53. package/richtexteditor/lang/rte-lang-mi.js +193 -0
  54. package/richtexteditor/lang/rte-lang-ml.js +193 -0
  55. package/richtexteditor/lang/rte-lang-mr.js +193 -0
  56. package/richtexteditor/lang/rte-lang-ms.js +193 -0
  57. package/richtexteditor/lang/rte-lang-mt.js +193 -0
  58. package/richtexteditor/lang/rte-lang-mww.js +193 -0
  59. package/richtexteditor/lang/rte-lang-nb.js +193 -0
  60. package/richtexteditor/lang/rte-lang-nl.js +193 -0
  61. package/richtexteditor/lang/rte-lang-otq.js +193 -0
  62. package/richtexteditor/lang/rte-lang-pa.js +193 -0
  63. package/richtexteditor/lang/rte-lang-pl.js +193 -0
  64. package/richtexteditor/lang/rte-lang-pt-pt.js +193 -0
  65. package/richtexteditor/lang/rte-lang-pt.js +193 -0
  66. package/richtexteditor/lang/rte-lang-ro.js +193 -0
  67. package/richtexteditor/lang/rte-lang-ru.js +193 -0
  68. package/richtexteditor/lang/rte-lang-sk.js +193 -0
  69. package/richtexteditor/lang/rte-lang-sl.js +193 -0
  70. package/richtexteditor/lang/rte-lang-sm.js +193 -0
  71. package/richtexteditor/lang/rte-lang-sr-Cyrl.js +193 -0
  72. package/richtexteditor/lang/rte-lang-sr-Latn.js +193 -0
  73. package/richtexteditor/lang/rte-lang-sv.js +193 -0
  74. package/richtexteditor/lang/rte-lang-sw.js +193 -0
  75. package/richtexteditor/lang/rte-lang-ta.js +193 -0
  76. package/richtexteditor/lang/rte-lang-te.js +193 -0
  77. package/richtexteditor/lang/rte-lang-th.js +193 -0
  78. package/richtexteditor/lang/rte-lang-tlh-Latn.js +193 -0
  79. package/richtexteditor/lang/rte-lang-tlh-Piqd.js +193 -0
  80. package/richtexteditor/lang/rte-lang-to.js +193 -0
  81. package/richtexteditor/lang/rte-lang-tr.js +193 -0
  82. package/richtexteditor/lang/rte-lang-ty.js +193 -0
  83. package/richtexteditor/lang/rte-lang-uk.js +193 -0
  84. package/richtexteditor/lang/rte-lang-ur.js +193 -0
  85. package/richtexteditor/lang/rte-lang-vi.js +193 -0
  86. package/richtexteditor/lang/rte-lang-yua.js +193 -0
  87. package/richtexteditor/lang/rte-lang-zh-Hans.js +193 -0
  88. package/richtexteditor/lang/rte-lang-zh-Hant.js +193 -0
  89. package/richtexteditor/lang/rte-lang.js +1 -0
  90. package/richtexteditor/plugins/-combine-plugin.txt +2 -0
  91. package/richtexteditor/plugins/-combine-plugins.bat +20 -0
  92. package/richtexteditor/plugins/a11yenhance.js +90 -0
  93. package/richtexteditor/plugins/accessibilitychecker.js +579 -0
  94. package/richtexteditor/plugins/aitoolkit.css +15830 -0
  95. package/richtexteditor/plugins/aitoolkit.js +19171 -0
  96. package/richtexteditor/plugins/all_plugins.js +31125 -0
  97. package/richtexteditor/plugins/all_plugins.min.js +78 -0
  98. package/richtexteditor/plugins/autoembed.js +174 -0
  99. package/richtexteditor/plugins/blocktypes.js +175 -0
  100. package/richtexteditor/plugins/bookmarkcard.js +226 -0
  101. package/richtexteditor/plugins/charlimit.js +195 -0
  102. package/richtexteditor/plugins/comments.js +672 -0
  103. package/richtexteditor/plugins/contentminimap.js +369 -0
  104. package/richtexteditor/plugins/crdt-engine.js +8905 -0
  105. package/richtexteditor/plugins/crdt-engine.min.js +5 -0
  106. package/richtexteditor/plugins/dialoga11y.js +174 -0
  107. package/richtexteditor/plugins/dictation.js +268 -0
  108. package/richtexteditor/plugins/documentimport.js +369 -0
  109. package/richtexteditor/plugins/documentoutline.js +341 -0
  110. package/richtexteditor/plugins/emailtoolkit.js +165 -0
  111. package/richtexteditor/plugins/emojiautocomplete.js +241 -0
  112. package/richtexteditor/plugins/foldheadings.js +181 -0
  113. package/richtexteditor/plugins/ghostcomplete.js +297 -0
  114. package/richtexteditor/plugins/html2pdf.js +158 -0
  115. package/richtexteditor/plugins/imageeditor.js +159 -0
  116. package/richtexteditor/plugins/insertcode.js +319 -0
  117. package/richtexteditor/plugins/insertemoji.js +229 -0
  118. package/richtexteditor/plugins/insertgallery.js +362 -0
  119. package/richtexteditor/plugins/inserttemplate.js +261 -0
  120. package/richtexteditor/plugins/localdraft.js +213 -0
  121. package/richtexteditor/plugins/markdownexport.js +110 -0
  122. package/richtexteditor/plugins/mathdialog.js +153 -0
  123. package/richtexteditor/plugins/mentionplugin.js +534 -0
  124. package/richtexteditor/plugins/mermaiddiagram.js +294 -0
  125. package/richtexteditor/plugins/readabilitystats.js +148 -0
  126. package/richtexteditor/plugins/readaloud.js +243 -0
  127. package/richtexteditor/plugins/restrictedediting.js +208 -0
  128. package/richtexteditor/plugins/revisionhistory.js +670 -0
  129. package/richtexteditor/plugins/samples/insertcode_v1.js +76 -0
  130. package/richtexteditor/plugins/slashcommand.js +705 -0
  131. package/richtexteditor/plugins/smartchips.js +250 -0
  132. package/richtexteditor/plugins/spellcheck.js +181 -0
  133. package/richtexteditor/plugins/todolist.js +137 -0
  134. package/richtexteditor/plugins/trackedchanges.js +658 -0
  135. package/richtexteditor/plugins/tui.image-editor/fabric.min.js +8 -0
  136. package/richtexteditor/plugins/tui.image-editor/note.txt +5 -0
  137. package/richtexteditor/plugins/tui.image-editor/svg/icon-a.svg +235 -0
  138. package/richtexteditor/plugins/tui.image-editor/svg/icon-b.svg +224 -0
  139. package/richtexteditor/plugins/tui.image-editor/svg/icon-c.svg +224 -0
  140. package/richtexteditor/plugins/tui.image-editor/svg/icon-d.svg +224 -0
  141. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +7 -0
  142. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.css +6 -0
  143. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +7 -0
  144. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.css +7 -0
  145. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +15 -0
  146. package/richtexteditor/plugins/typewriter.js +203 -0
  147. package/richtexteditor/plugins/wordexport.js +0 -0
  148. package/richtexteditor/plugins/yjscollab.js +693 -0
  149. package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +335 -0
  150. package/richtexteditor/rte-config.js +933 -0
  151. package/richtexteditor/rte.js +943 -0
  152. package/richtexteditor/rte_mobile.css +234 -0
  153. package/richtexteditor/rte_theme_default.css +4819 -0
  154. package/richtexteditor/runtime/colorpicker_more_ns.htm +2613 -0
  155. package/richtexteditor/runtime/help.htm +162 -0
  156. package/richtexteditor/runtime/images/1x1.gif +0 -0
  157. package/richtexteditor/runtime/images/cpie_Color.cur +0 -0
  158. package/richtexteditor/runtime/images/cpie_GradientPositionDark.gif +0 -0
  159. package/richtexteditor/runtime/images/cpie_GradientPositionLight.gif +0 -0
  160. package/richtexteditor/runtime/images/cpie_VerticalPosition.gif +0 -0
  161. package/richtexteditor/runtime/images/cpie_WebSafe.gif +0 -0
  162. package/richtexteditor/runtime/images/cpie_gradients.png +0 -0
  163. package/richtexteditor/runtime/images/cpns_Color.cur +0 -0
  164. package/richtexteditor/runtime/images/cpns_ColorSpace1.png +0 -0
  165. package/richtexteditor/runtime/images/cpns_ColorSpace2.png +0 -0
  166. package/richtexteditor/runtime/images/cpns_GradientPositionDark.gif +0 -0
  167. package/richtexteditor/runtime/images/cpns_GradientPositionLight.gif +0 -0
  168. package/richtexteditor/runtime/images/cpns_Vertical1.png +0 -0
  169. package/richtexteditor/runtime/images/cpns_Vertical2.png +0 -0
  170. package/richtexteditor/runtime/images/cpns_VerticalPosition.gif +0 -0
  171. package/richtexteditor/runtime/images/cpns_WebSafe.gif +0 -0
  172. package/richtexteditor/runtime/images/cpns_gradients.png +0 -0
  173. package/richtexteditor/runtime/images/cpns_hue2.png +0 -0
  174. package/richtexteditor/runtime/images/formbn.gif +0 -0
  175. package/richtexteditor/runtime/images/multiclavier.gif +0 -0
  176. package/richtexteditor/runtime/richtexteditor_content.css +362 -0
  177. package/richtexteditor/runtime/richtexteditor_preview.css +302 -0
  178. package/richtexteditor/runtime/richtexteditor_preview.js +1 -0
  179. package/server/index.d.ts +21 -0
  180. package/server/index.js +23 -0
  181. package/svelte/index.d.ts +19 -0
  182. package/svelte/index.js +59 -0
  183. package/vue/index.d.ts +94 -0
  184. package/vue/index.js +291 -0
@@ -0,0 +1,658 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ if (!RTE_DefaultConfig.svgCode_trackchanges) {
4
+ 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>';
5
+ }
6
+
7
+ RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
8
+
9
+ function RTE_Plugin_TrackedChanges() {
10
+ var obj = this;
11
+ var config;
12
+ var editor;
13
+ var enabled = false;
14
+
15
+ obj.PluginName = "TrackedChanges";
16
+
17
+ obj.InitConfig = function (argconfig) {
18
+ config = argconfig;
19
+ if (config.trackChangesEnabled === false) return;
20
+
21
+ config.currentUser = config.currentUser || null;
22
+ config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
23
+ config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
24
+
25
+ // Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
26
+ config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
27
+ appendToolbarCommand("toolbar_default", "#{trackchanges}");
28
+ appendToolbarCommand("toolbar_full", "#{trackchanges}");
29
+ appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
30
+
31
+ if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
32
+ config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
33
+ }
34
+ };
35
+
36
+ obj.InitEditor = function (argeditor) {
37
+ editor = argeditor;
38
+ if (config.trackChangesEnabled === false) return;
39
+
40
+ editor.trackedChanges = {
41
+ enable: function (user) { enable(user); },
42
+ disable: function () { disable(); },
43
+ isEnabled: function () { return enabled; },
44
+ acceptAll: function (filter) { return acceptAll(filter); },
45
+ rejectAll: function (filter) { return rejectAll(filter); },
46
+ accept: function (id) { return acceptEntry(id); },
47
+ reject: function (id) { return rejectEntry(id); },
48
+ list: function (opts) {
49
+ if (!editor.reviewLedger) return [];
50
+ // Default to the PENDING review queue (matches the internal
51
+ // accept/reject semantics). Pass {all:true} for full history
52
+ // including accepted/rejected entries (also via reviewLedger.list()).
53
+ var includeAll = opts && opts.all === true;
54
+ return editor.reviewLedger.list().filter(function (e) {
55
+ if (e.changeType !== "insert" && e.changeType !== "delete") return false;
56
+ return includeAll || e.status === "pending";
57
+ });
58
+ }
59
+ };
60
+
61
+ injectStyles();
62
+
63
+ editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
64
+ editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
65
+ var btn = editor.createToolbarButton(cmd);
66
+ btn.__tcSync = function () {
67
+ if (enabled) btn.classList.add("rte-ui-active");
68
+ else btn.classList.remove("rte-ui-active");
69
+ btn.setAttribute("aria-pressed", enabled ? "true" : "false");
70
+ btn.setAttribute("title", enabled ? "Suggesting mode is on" : "Turn on suggesting mode");
71
+ };
72
+ btn.__tcSync();
73
+ return btn;
74
+ };
75
+
76
+ editor.attachEvent("exec_command_trackchanges", function (state) {
77
+ state.returnValue = true;
78
+ state.stopBubble = true;
79
+ if (enabled) disable();
80
+ else enable();
81
+ });
82
+
83
+ editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
84
+ };
85
+
86
+ function appendToolbarCommand(toolbar, item) {
87
+ if (!config[toolbar]) return;
88
+ if (config[toolbar].indexOf(item) !== -1) return;
89
+ config[toolbar] = config[toolbar] + item;
90
+ }
91
+
92
+ function enable(user) {
93
+ if (user) config.currentUser = user;
94
+ if (!config.currentUser) {
95
+ config.currentUser = { id: "user", name: "User", color: "#2563eb" };
96
+ console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
97
+ }
98
+ enabled = true;
99
+ syncToolbarButtons();
100
+ }
101
+
102
+ function disable() {
103
+ enabled = false;
104
+ syncToolbarButtons();
105
+ }
106
+
107
+ function syncToolbarButtons() {
108
+ var doc = editor.iframe && editor.iframe.ownerDocument || document;
109
+ var btns = doc.querySelectorAll(".rte_command_trackchanges");
110
+ for (var i = 0; i < btns.length; i++) {
111
+ if (btns[i].__tcSync) btns[i].__tcSync();
112
+ }
113
+ }
114
+
115
+ // --- beforeinput intercept ---
116
+
117
+ function onBeforeInput(e) {
118
+ if (!enabled) return;
119
+ if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
120
+
121
+ var type = e.inputType;
122
+ if (!type) return;
123
+
124
+ if (type === "insertText" || type === "insertReplacementText") {
125
+ e.preventDefault();
126
+ handleInsertText(e.data || "");
127
+ return;
128
+ }
129
+ if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
130
+ e.preventDefault();
131
+ handleDelete(type);
132
+ return;
133
+ }
134
+ // insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
135
+ }
136
+
137
+ // --- insert tracking ---
138
+
139
+ function handleInsertText(text) {
140
+ if (!text) return;
141
+ var sel = editor.getSelection();
142
+ if (!sel || sel.rangeCount === 0) return;
143
+ var range = sel.getRangeAt(0);
144
+ if (!range.collapsed) {
145
+ // Selection replace: wrap selection as delete, then insert new text.
146
+ if (!wrapRangeAsDelete(range)) return;
147
+ // After wrapping, caret should be past the delete span; re-fetch selection.
148
+ range = sel.getRangeAt(0);
149
+ createInsertSpan(text, range);
150
+ return;
151
+ }
152
+
153
+ var mergeTarget = adjacentInsertSpan(range);
154
+ if (mergeTarget) {
155
+ appendToInsertSpan(mergeTarget, text);
156
+ } else {
157
+ createInsertSpan(text, range);
158
+ }
159
+ }
160
+
161
+ function adjacentInsertSpan(range) {
162
+ // If caret sits inside an insert span by the current user, or immediately after one, merge.
163
+ var node = range.startContainer;
164
+ if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
165
+ return node.parentNode;
166
+ }
167
+ // Caret just after an insert span (at an element boundary).
168
+ if (node.nodeType === 1 && range.startOffset > 0) {
169
+ var prev = node.childNodes[range.startOffset - 1];
170
+ if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
171
+ }
172
+ // Caret at the very beginning just before our span.
173
+ if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
174
+ return node.previousSibling;
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function isMyInsert(el) {
180
+ if (!el || el.nodeType !== 1) return false;
181
+ if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
182
+ return el.getAttribute("data-tc-author") === config.currentUser.id;
183
+ }
184
+
185
+ function appendToInsertSpan(span, text) {
186
+ var editdoc = editor.getDocument();
187
+ // Append to the span's last text node (or create one).
188
+ var last = span.lastChild;
189
+ var node;
190
+ if (last && last.nodeType === 3) {
191
+ last.nodeValue = last.nodeValue + text;
192
+ node = last;
193
+ } else {
194
+ node = editdoc.createTextNode(text);
195
+ span.appendChild(node);
196
+ }
197
+ placeCaretAtEnd(node);
198
+ updateEntryForSpan(span);
199
+ }
200
+
201
+ function createInsertSpan(text, range) {
202
+ var editdoc = editor.getDocument();
203
+ var span = editdoc.createElement("span");
204
+ span.className = config.trackChangesInsertClass;
205
+ var user = config.currentUser;
206
+ var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
207
+ span.setAttribute("data-tc-id", id);
208
+ span.setAttribute("data-tc-author", user.id);
209
+ span.setAttribute("data-tc-at", new Date().toISOString());
210
+ span.style.color = user.color;
211
+ span.style.textDecoration = "underline";
212
+ span.appendChild(editdoc.createTextNode(text));
213
+ range.insertNode(span);
214
+ placeCaretAtEnd(span.firstChild);
215
+
216
+ editor.reviewLedger.add({
217
+ id: id,
218
+ changeType: "insert",
219
+ author: user,
220
+ text: text,
221
+ status: "pending",
222
+ sourceLabel: "Suggested insert",
223
+ createdAt: Date.now()
224
+ });
225
+ }
226
+
227
+ function updateEntryForSpan(span) {
228
+ var id = span.getAttribute("data-tc-id");
229
+ if (!id) return;
230
+ editor.reviewLedger.update(id, { text: span.textContent });
231
+ }
232
+
233
+ // --- delete tracking ---
234
+
235
+ function handleDelete(mode) {
236
+ var sel = editor.getSelection();
237
+ if (!sel || sel.rangeCount === 0) return;
238
+ var range = sel.getRangeAt(0);
239
+
240
+ if (!range.collapsed) {
241
+ wrapRangeAsDelete(range);
242
+ return;
243
+ }
244
+
245
+ // Collapsed: extend by one char in the appropriate direction, then wrap.
246
+ // The extended range covers only that one character — any delete spans between
247
+ // it and the caret should be SKIPPED, not re-wrapped.
248
+ var extended = editor.getDocument().createRange();
249
+ if (mode === "deleteContentBackward") {
250
+ var anchor = prevCharPoint(range.startContainer, range.startOffset);
251
+ if (!anchor) return;
252
+ extended.setStart(anchor.node, anchor.offset);
253
+ extended.setEnd(anchor.node, anchor.offset + 1);
254
+ } else {
255
+ var forward = nextCharPoint(range.startContainer, range.startOffset);
256
+ if (!forward) return;
257
+ extended.setStart(forward.node, forward.offset - 1);
258
+ extended.setEnd(forward.node, forward.offset);
259
+ }
260
+
261
+ // If that extended range sits entirely inside one of the current user's own
262
+ // insert spans, treat this as undoing their own typing — actually delete.
263
+ if (rangeInsideMyInsert(extended)) {
264
+ extended.deleteContents();
265
+ var sel2 = editor.getSelection();
266
+ sel2.removeAllRanges();
267
+ sel2.addRange(extended);
268
+ // If the insert span is now empty, remove it and its ledger entry.
269
+ cleanupEmptyInsertSpans();
270
+ return;
271
+ }
272
+
273
+ wrapRangeAsDelete(extended);
274
+ }
275
+
276
+ function isInsideDeleteSpan(node) {
277
+ var walk = node.nodeType === 1 ? node : node.parentNode;
278
+ while (walk && walk !== editor.getEditable()) {
279
+ if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
280
+ walk = walk.parentNode;
281
+ }
282
+ return false;
283
+ }
284
+
285
+ // Walk text leaves in the editable, skipping any that live inside an existing delete
286
+ // span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
287
+ function liveTextLeaves() {
288
+ var editable = editor.getEditable();
289
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
290
+ acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
291
+ });
292
+ var out = [], n;
293
+ while ((n = walker.nextNode())) out.push(n);
294
+ return out;
295
+ }
296
+
297
+ function prevCharPoint(node, offset) {
298
+ var leaves = liveTextLeaves();
299
+ if (!leaves.length) return null;
300
+ var idx = caretIndex(node, offset, leaves);
301
+ if (idx <= 0) return null;
302
+ return leafOffsetAt(idx - 1, leaves);
303
+ }
304
+
305
+ function nextCharPoint(node, offset) {
306
+ var leaves = liveTextLeaves();
307
+ if (!leaves.length) return null;
308
+ var total = 0;
309
+ for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
310
+ var idx = caretIndex(node, offset, leaves);
311
+ if (idx >= total) return null;
312
+ return leafOffsetAt(idx + 1, leaves);
313
+ }
314
+
315
+ // How many live characters appear before the given caret position?
316
+ function caretIndex(node, offset, leaves) {
317
+ var r = editor.getDocument().createRange();
318
+ r.setStart(node, offset);
319
+ r.collapse(true);
320
+ var count = 0;
321
+ for (var i = 0; i < leaves.length; i++) {
322
+ var leaf = leaves[i];
323
+ // If the entire leaf is before the caret, add its length.
324
+ var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
325
+ if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
326
+ var cmpStart = r.comparePoint(leaf, 0);
327
+ if (cmpStart >= 0) break; // leaf starts at or after caret
328
+ // Caret falls inside this leaf — binary search the split point.
329
+ var lo = 0, hi = leaf.nodeValue.length;
330
+ while (lo < hi) {
331
+ var mid = (lo + hi) >> 1;
332
+ if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
333
+ else hi = mid;
334
+ }
335
+ count += lo;
336
+ break;
337
+ }
338
+ return count;
339
+ }
340
+
341
+ function leafOffsetAt(index, leaves) {
342
+ var remaining = index;
343
+ for (var i = 0; i < leaves.length; i++) {
344
+ if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
345
+ remaining -= leaves[i].nodeValue.length;
346
+ }
347
+ return null;
348
+ }
349
+
350
+ function firstTextLeaf(node) {
351
+ if (!node) return null;
352
+ if (node.nodeType === 3) return node;
353
+ var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
354
+ return walker.nextNode();
355
+ }
356
+
357
+ function lastTextLeaf(node) {
358
+ if (!node) return null;
359
+ if (node.nodeType === 3) return node;
360
+ var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
361
+ var last = null, n;
362
+ while ((n = walker.nextNode())) last = n;
363
+ return last;
364
+ }
365
+
366
+ function previousTextLeaf(node) {
367
+ var editable = editor.getEditable();
368
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
369
+ var prev = null, cur;
370
+ while ((cur = walker.nextNode())) {
371
+ if (cur === node) return prev;
372
+ prev = cur;
373
+ }
374
+ return null;
375
+ }
376
+
377
+ function nextTextLeaf(node) {
378
+ var editable = editor.getEditable();
379
+ var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
380
+ var seen = false, cur;
381
+ while ((cur = walker.nextNode())) {
382
+ if (seen) return cur;
383
+ if (cur === node) seen = true;
384
+ }
385
+ return null;
386
+ }
387
+
388
+ function rangeInsideMyInsert(range) {
389
+ var span = range.startContainer.parentNode;
390
+ while (span && span !== editor.getEditable()) {
391
+ if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
392
+ span = span.parentNode;
393
+ }
394
+ return false;
395
+ }
396
+
397
+ function wrapRangeAsDelete(range) {
398
+ if (range.collapsed) return false;
399
+ var editdoc = editor.getDocument();
400
+
401
+ var fragment = range.cloneContents();
402
+ if (!fragmentHasText(fragment)) return false;
403
+
404
+ // Find existing same-author delete spans adjacent to the range so we can merge
405
+ // sequential backspaces / forward deletes into one contiguous span.
406
+ var user = config.currentUser;
407
+ var mergeBefore = adjacentDeleteSpan(range, "before", user);
408
+ var mergeAfter = adjacentDeleteSpan(range, "after", user);
409
+
410
+ range.deleteContents();
411
+
412
+ if (mergeBefore) {
413
+ // Append the fragment's content to the existing "before" span.
414
+ appendFragmentToSpan(mergeBefore, fragment);
415
+ if (mergeAfter && mergeAfter !== mergeBefore) {
416
+ // Collapse both sides: move "after" span's children into "before" and drop it.
417
+ while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
418
+ var afterId = mergeAfter.getAttribute("data-tc-id");
419
+ mergeAfter.remove();
420
+ if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
421
+ }
422
+ var rangeAfter = editdoc.createRange();
423
+ rangeAfter.setStartAfter(mergeBefore);
424
+ rangeAfter.collapse(true);
425
+ var sel = editor.getSelection();
426
+ sel.removeAllRanges();
427
+ sel.addRange(rangeAfter);
428
+ // 2026-05-28 Defense: if a customer pasted a delete-class span from
429
+ // outside this plugin, it may lack data-tc-id. Skip the ledger
430
+ // update instead of writing under a `null` key (would silently
431
+ // corrupt the ledger via the update("null", ...) coercion).
432
+ var beforeId = mergeBefore.getAttribute("data-tc-id");
433
+ if (beforeId && editor.reviewLedger) {
434
+ editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
435
+ }
436
+ return true;
437
+ }
438
+
439
+ if (mergeAfter) {
440
+ prependFragmentToSpan(mergeAfter, fragment);
441
+ var rangeBefore = editdoc.createRange();
442
+ rangeBefore.setStartBefore(mergeAfter);
443
+ rangeBefore.collapse(true);
444
+ var sel2 = editor.getSelection();
445
+ sel2.removeAllRanges();
446
+ sel2.addRange(rangeBefore);
447
+ // Same defense as above (mergeAfter may lack data-tc-id).
448
+ var afterMergeId = mergeAfter.getAttribute("data-tc-id");
449
+ if (afterMergeId && editor.reviewLedger) {
450
+ editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
451
+ }
452
+ return true;
453
+ }
454
+
455
+ var span = editdoc.createElement("span");
456
+ span.className = config.trackChangesDeleteClass;
457
+ var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
458
+ span.setAttribute("data-tc-id", id);
459
+ span.setAttribute("data-tc-author", user.id);
460
+ span.setAttribute("data-tc-at", new Date().toISOString());
461
+ span.style.color = user.color;
462
+ span.style.textDecoration = "line-through";
463
+ span.appendChild(fragment);
464
+ range.insertNode(span);
465
+
466
+ var after = editdoc.createRange();
467
+ after.setStartAfter(span);
468
+ after.collapse(true);
469
+ var sel3 = editor.getSelection();
470
+ sel3.removeAllRanges();
471
+ sel3.addRange(after);
472
+
473
+ editor.reviewLedger.add({
474
+ id: id,
475
+ changeType: "delete",
476
+ author: user,
477
+ text: span.textContent,
478
+ originalHtml: span.innerHTML,
479
+ status: "pending",
480
+ sourceLabel: "Suggested delete",
481
+ createdAt: Date.now()
482
+ });
483
+ return true;
484
+ }
485
+
486
+ function adjacentDeleteSpan(range, side, user) {
487
+ // Find element immediately before or after the range boundaries at the container level.
488
+ var node = side === "before" ? range.startContainer : range.endContainer;
489
+ var offset = side === "before" ? range.startOffset : range.endOffset;
490
+ if (node.nodeType === 3) {
491
+ // Look at the sibling of the text node if we're at the text node's boundary.
492
+ if (side === "before" && offset === 0) {
493
+ var prev = node.previousSibling;
494
+ if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
495
+ }
496
+ if (side === "after" && offset === node.nodeValue.length) {
497
+ var next = node.nextSibling;
498
+ if (next && isDeleteSpanByAuthor(next, user)) return next;
499
+ }
500
+ return null;
501
+ }
502
+ // Element container: look at children on either side of the offset.
503
+ if (side === "before" && offset > 0) {
504
+ var childBefore = node.childNodes[offset - 1];
505
+ if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
506
+ }
507
+ if (side === "after" && offset < node.childNodes.length) {
508
+ var childAfter = node.childNodes[offset];
509
+ if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
510
+ }
511
+ return null;
512
+ }
513
+
514
+ function isDeleteSpanByAuthor(el, user) {
515
+ if (!el || el.nodeType !== 1) return false;
516
+ if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
517
+ return el.getAttribute("data-tc-author") === user.id;
518
+ }
519
+
520
+ function appendFragmentToSpan(span, fragment) {
521
+ while (fragment.firstChild) span.appendChild(fragment.firstChild);
522
+ }
523
+
524
+ function prependFragmentToSpan(span, fragment) {
525
+ var first = span.firstChild;
526
+ while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
527
+ }
528
+
529
+ function fragmentHasText(fragment) {
530
+ var t = (fragment.textContent || "").replace(/\s+/g, "");
531
+ return !!t;
532
+ }
533
+
534
+ function cleanupEmptyInsertSpans() {
535
+ var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
536
+ for (var i = 0; i < spans.length; i++) {
537
+ var s = spans[i];
538
+ if (!s.textContent || !s.textContent.length) {
539
+ var id = s.getAttribute("data-tc-id");
540
+ if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
541
+ s.remove();
542
+ } else {
543
+ updateEntryForSpan(s);
544
+ }
545
+ }
546
+ }
547
+
548
+ function placeCaretAtEnd(node) {
549
+ if (!node) return;
550
+ var editdoc = editor.getDocument();
551
+ var range = editdoc.createRange();
552
+ if (node.nodeType === 3) {
553
+ range.setStart(node, node.nodeValue.length);
554
+ } else {
555
+ range.setStart(node, node.childNodes.length);
556
+ }
557
+ range.collapse(true);
558
+ var sel = editor.getSelection();
559
+ sel.removeAllRanges();
560
+ sel.addRange(range);
561
+ }
562
+
563
+ // --- accept / reject ---
564
+
565
+ function acceptEntry(id) {
566
+ var entry = editor.reviewLedger.get(id);
567
+ if (!entry) return false;
568
+ var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
569
+ if (entry.changeType === "insert") {
570
+ if (span) unwrapKeepChildren(span);
571
+ } else if (entry.changeType === "delete") {
572
+ if (span) span.remove();
573
+ }
574
+ editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now() });
575
+ return true;
576
+ }
577
+
578
+ function rejectEntry(id) {
579
+ var entry = editor.reviewLedger.get(id);
580
+ if (!entry) return false;
581
+ var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
582
+ if (entry.changeType === "insert") {
583
+ if (span) span.remove();
584
+ } else if (entry.changeType === "delete") {
585
+ if (span) unwrapKeepChildren(span);
586
+ }
587
+ editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now() });
588
+ return true;
589
+ }
590
+
591
+ function acceptAll(filter) {
592
+ var entries = editor.reviewLedger.list(filter).filter(function (e) {
593
+ return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
594
+ });
595
+ for (var i = 0; i < entries.length; i++) acceptEntry(entries[i].id);
596
+ return entries.length;
597
+ }
598
+
599
+ function rejectAll(filter) {
600
+ var entries = editor.reviewLedger.list(filter).filter(function (e) {
601
+ return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
602
+ });
603
+ for (var i = 0; i < entries.length; i++) rejectEntry(entries[i].id);
604
+ return entries.length;
605
+ }
606
+
607
+ function unwrapKeepChildren(el) {
608
+ var parent = el.parentNode;
609
+ if (!parent) return;
610
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
611
+ parent.removeChild(el);
612
+ }
613
+
614
+ // 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
615
+ // Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
616
+ // containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
617
+ // colons, base64 IDs with `=` padding).
618
+ function cssEscape(s) {
619
+ var str = String(s);
620
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
621
+ try { return CSS.escape(str); } catch (e) { /* fall through */ }
622
+ }
623
+ return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
624
+ return "\\" + c;
625
+ });
626
+ }
627
+
628
+ // --- styles ---
629
+
630
+ function injectStyles() {
631
+ var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
632
+ if (!host.querySelector("style[data-rte-trackchanges]")) {
633
+ var style = host.createElement("style");
634
+ style.setAttribute("data-rte-trackchanges", "1");
635
+ style.textContent = [
636
+ ".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
637
+ ".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)}",
638
+ ".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}",
639
+ ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
640
+ ".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}",
641
+ ".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}"
642
+ ].join("\n");
643
+ host.head.appendChild(style);
644
+ }
645
+
646
+ var editdoc = editor.getDocument();
647
+ if (editdoc && editdoc.head && !editdoc.querySelector("style[data-rte-trackchanges-inline]")) {
648
+ var iStyle = editdoc.createElement("style");
649
+ iStyle.setAttribute("data-rte-trackchanges-inline", "1");
650
+ iStyle.textContent = [
651
+ ".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
652
+ ".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}",
653
+ ".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}"
654
+ ].join("\n");
655
+ editdoc.head.appendChild(iStyle);
656
+ }
657
+ }
658
+ }