@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,693 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ RTE_DefaultConfig.plugin_yjscollab = RTE_Plugin_YjsCollab;
4
+
5
+ // Yjs collaboration — Option B scope:
6
+ // • Awareness (live cursors + presence)
7
+ // • Shared reviewLedger (AI suggestions, tracked changes, comments all sync)
8
+ //
9
+ // NOT in scope:
10
+ // • Concurrent text editing (no content-level CRDT binding — see v1.1 / Option C)
11
+ //
12
+ // Yjs is a PEER DEPENDENCY. Customers load Y.Doc + a provider themselves; this plugin
13
+ // binds to whatever they pass in.
14
+ function RTE_Plugin_YjsCollab() {
15
+ var obj = this;
16
+ var config;
17
+ var editor;
18
+ var session = null; // { doc, provider, awareness, ledgerMap, user, cleanup[] }
19
+ var remoteOrigin = {}; // sentinel used to tag ledger writes coming from Yjs
20
+ var echoGuard = false;
21
+ var overlay = null;
22
+ var presencePanel = null;
23
+
24
+ obj.PluginName = "YjsCollab";
25
+
26
+ obj.InitConfig = function (argconfig) {
27
+ config = argconfig;
28
+ if (config.collabEnabled === false) return;
29
+
30
+ config.collabLedgerMapName = config.collabLedgerMapName || "reviewLedger";
31
+ config.collabShowPresence = config.collabShowPresence !== false;
32
+ config.collabShowRemoteCursors = config.collabShowRemoteCursors !== false;
33
+ };
34
+
35
+ obj.InitEditor = function (argeditor) {
36
+ editor = argeditor;
37
+ if (config.collabEnabled === false) return;
38
+
39
+ editor.collab = {
40
+ attach: function (options) { return attach(options); },
41
+ detach: function () { return detach(); },
42
+ isAttached: function () { return !!session; },
43
+ getSession: function () { return session; },
44
+ peers: function () { return session ? getRemotePeers() : []; },
45
+ setUser: function (user) { if (session) updateLocalUser(user); }
46
+ };
47
+
48
+ injectStyles();
49
+ };
50
+
51
+ function attach(options) {
52
+ if (session) detach();
53
+ options = options || {};
54
+ var doc = options.doc;
55
+ var provider = options.provider;
56
+ if (!doc || typeof doc.getMap !== "function") {
57
+ console.warn("yjscollab: attach() requires a Y.Doc via options.doc");
58
+ return null;
59
+ }
60
+ if (!provider || !provider.awareness) {
61
+ console.warn("yjscollab: attach() requires a provider with an awareness field");
62
+ return null;
63
+ }
64
+
65
+ var user = options.user || config.currentUser || { id: "user", name: "User", color: "#2563eb" };
66
+ var ledgerMap = doc.getMap(config.collabLedgerMapName);
67
+
68
+ // textSync modes:
69
+ // false / unset → awareness/presence/ledger only.
70
+ // true → legacy shared-Y.Text snapshot mode (last-write-
71
+ // wins on same-paragraph conflicts; kept for
72
+ // back-compat).
73
+ // "crdt" → per-node Yjs CRDT via crdt-engine.js. Falls
74
+ // back to legacy mode (with console.warn) if
75
+ // the engine bundle isn't loaded.
76
+ var requestedTextSync = options.textSync;
77
+ if (requestedTextSync === undefined) {
78
+ requestedTextSync = config.collabTextSync === "crdt"
79
+ ? "crdt"
80
+ : (config.collabTextSync === true);
81
+ }
82
+ var textSyncMode; // "off" | "legacy" | "crdt"
83
+ var textMap = null;
84
+ var crdtBinding = null;
85
+ if (requestedTextSync === "crdt") {
86
+ var Crdt = (typeof window !== "undefined") ? window.RichTextEditorCrdt : null;
87
+ if (Crdt && typeof Crdt.attachCrdtBinding === "function") {
88
+ textSyncMode = "crdt";
89
+ } else {
90
+ console.warn("yjscollab: textSync \"crdt\" requested but richtexteditor/plugins/crdt-engine.js is not loaded — falling back to legacy snapshot mode.");
91
+ textSyncMode = "legacy";
92
+ textMap = doc.getText(config.collabTextName || "richtextbox.body");
93
+ }
94
+ } else if (requestedTextSync === true) {
95
+ textSyncMode = "legacy";
96
+ textMap = doc.getText(config.collabTextName || "richtextbox.body");
97
+ } else {
98
+ textSyncMode = "off";
99
+ }
100
+
101
+ session = {
102
+ doc: doc,
103
+ provider: provider,
104
+ awareness: provider.awareness,
105
+ ledgerMap: ledgerMap,
106
+ textMap: textMap,
107
+ textSyncEnabled: textSyncMode !== "off",
108
+ textSyncMode: textSyncMode,
109
+ crdtBinding: crdtBinding,
110
+ user: user,
111
+ cleanup: []
112
+ };
113
+
114
+ // Set our initial awareness state.
115
+ session.awareness.setLocalStateField("user", {
116
+ id: user.id,
117
+ name: user.name,
118
+ color: user.color
119
+ });
120
+
121
+ // Subscribe to selection changes so our caret pos is broadcast.
122
+ wireSelectionBroadcast();
123
+
124
+ // Subscribe to awareness for remote peers.
125
+ wireAwarenessSubscription();
126
+
127
+ // Bridge reviewLedger <-> Y.Map.
128
+ if (editor.reviewLedger) {
129
+ wireLedgerBridge();
130
+ seedLedgerFromRemote();
131
+ }
132
+
133
+ // Wire text sync per the requested mode. For "crdt" the engine
134
+ // owns Y.XmlFragment ↔ DOM mirroring + selection preservation;
135
+ // we retain awareness, presence panel, remote-cursor overlay,
136
+ // and the review ledger bridge.
137
+ if (session.textSyncMode === "crdt") {
138
+ try {
139
+ session.crdtBinding = window.RichTextEditorCrdt.attachCrdtBinding({
140
+ editable: editor.getEditable(),
141
+ ydoc: doc,
142
+ provider: provider,
143
+ awareness: provider.awareness,
144
+ fragmentName: options.fragmentName || "default",
145
+ // Forward the app's Yjs module so the engine builds nodes with
146
+ // the SAME Yjs instance as `doc` (avoids the dual-Yjs
147
+ // "Unexpected content type" crash). Falls back to window.Y.
148
+ Y: options.Y || (typeof window !== "undefined" ? window.Y : null)
149
+ });
150
+ session.cleanup.push(function () {
151
+ try { session.crdtBinding && session.crdtBinding.dispose(); }
152
+ catch (ignore) { }
153
+ });
154
+ } catch (err) {
155
+ console.warn("yjscollab: CRDT engine attach failed; falling back to legacy mode.", err);
156
+ session.textSyncMode = "legacy";
157
+ session.textMap = doc.getText(config.collabTextName || "richtextbox.body");
158
+ wireTextSync();
159
+ }
160
+ } else if (session.textSyncMode === "legacy" && session.textMap) {
161
+ wireTextSync();
162
+ }
163
+
164
+ // Mount overlay + presence panel.
165
+ if (config.collabShowRemoteCursors) mountOverlay();
166
+ if (config.collabShowPresence) mountPresencePanel();
167
+ renderPresence();
168
+ renderRemoteCursors();
169
+
170
+ return session;
171
+ }
172
+
173
+ function detach() {
174
+ if (!session) return;
175
+ for (var i = 0; i < session.cleanup.length; i++) {
176
+ try { session.cleanup[i](); } catch (err) { }
177
+ }
178
+ try { session.awareness.setLocalState(null); } catch (ignore) { }
179
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
180
+ overlay = null;
181
+ if (presencePanel && presencePanel.parentNode) presencePanel.parentNode.removeChild(presencePanel);
182
+ presencePanel = null;
183
+ session = null;
184
+ }
185
+
186
+ function updateLocalUser(user) {
187
+ if (!session) return;
188
+ session.user = user;
189
+ session.awareness.setLocalStateField("user", {
190
+ id: user.id, name: user.name, color: user.color
191
+ });
192
+ }
193
+
194
+ // --- awareness / selection broadcast ---
195
+
196
+ function wireSelectionBroadcast() {
197
+ var editdoc = editor.getDocument();
198
+ var broadcast = function () {
199
+ if (!session) return;
200
+ var sel = editor.getSelection();
201
+ if (!sel || sel.rangeCount === 0) {
202
+ session.awareness.setLocalStateField("caret", null);
203
+ return;
204
+ }
205
+ var range = sel.getRangeAt(0);
206
+ var caret = serializeCaret(range);
207
+ session.awareness.setLocalStateField("caret", caret);
208
+ };
209
+ editdoc.addEventListener("selectionchange", broadcast);
210
+ editdoc.addEventListener("input", broadcast);
211
+ session.cleanup.push(function () {
212
+ editdoc.removeEventListener("selectionchange", broadcast);
213
+ editdoc.removeEventListener("input", broadcast);
214
+ });
215
+ }
216
+
217
+ // Serialize a caret as (blockIndex, offsetInBlock) — simple, robust under single-writer
218
+ // assumptions (Option B scope). Falls back to viewport coordinates if the math fails.
219
+ function serializeCaret(range) {
220
+ var editable = editor.getEditable();
221
+ var block = range.startContainer;
222
+ while (block && block.parentNode !== editable) {
223
+ if (!block.parentNode) return null;
224
+ block = block.parentNode;
225
+ }
226
+ if (!block) return null;
227
+ var blocks = Array.prototype.slice.call(editable.children);
228
+ var blockIndex = blocks.indexOf(block);
229
+ if (blockIndex < 0) return null;
230
+
231
+ // Character offset inside this block.
232
+ var offset = 0;
233
+ var walker = editor.getDocument().createTreeWalker(block, NodeFilter.SHOW_TEXT);
234
+ var n;
235
+ while ((n = walker.nextNode())) {
236
+ if (n === range.startContainer) {
237
+ offset += range.startOffset;
238
+ return { blockIndex: blockIndex, offset: offset, collapsed: range.collapsed };
239
+ }
240
+ offset += n.nodeValue.length;
241
+ }
242
+ // startContainer is not a text node — treat as start of block.
243
+ return { blockIndex: blockIndex, offset: 0, collapsed: range.collapsed };
244
+ }
245
+
246
+ function deserializeCaret(caret) {
247
+ if (!caret) return null;
248
+ var editable = editor.getEditable();
249
+ var block = editable.children[caret.blockIndex];
250
+ if (!block) return null;
251
+ var remaining = caret.offset;
252
+ var walker = editor.getDocument().createTreeWalker(block, NodeFilter.SHOW_TEXT);
253
+ var n;
254
+ while ((n = walker.nextNode())) {
255
+ if (remaining <= n.nodeValue.length) {
256
+ var range = editor.getDocument().createRange();
257
+ range.setStart(n, remaining);
258
+ range.collapse(true);
259
+ return range;
260
+ }
261
+ remaining -= n.nodeValue.length;
262
+ }
263
+ // Past the end — position at end of block.
264
+ var range2 = editor.getDocument().createRange();
265
+ range2.selectNodeContents(block);
266
+ range2.collapse(false);
267
+ return range2;
268
+ }
269
+
270
+ function wireAwarenessSubscription() {
271
+ var handler = function () { renderRemoteCursors(); renderPresence(); };
272
+ session.awareness.on("change", handler);
273
+ session.cleanup.push(function () { session.awareness.off("change", handler); });
274
+ }
275
+
276
+ function getRemotePeers() {
277
+ var map = session.awareness.getStates();
278
+ var me = session.awareness.clientID;
279
+ var peers = [];
280
+ map.forEach(function (state, clientId) {
281
+ if (clientId === me || !state || !state.user) return;
282
+ peers.push({ clientId: clientId, user: state.user, caret: state.caret || null });
283
+ });
284
+ return peers;
285
+ }
286
+
287
+ // --- ledger bridge ---
288
+
289
+ // ---- text sync (opt-in MVP) ----
290
+ //
291
+ // Treats the editor's full HTML as an opaque Y.Text string. CRDT merges
292
+ // concurrent inserts/deletes at character level, so two users typing in
293
+ // different parts of the document converge without losing either edit.
294
+ //
295
+ // Tradeoffs (documented, not fixed by this MVP):
296
+ // • Remote updates re-set the whole body → caret snaps back to start
297
+ // • HTML character offsets don't align with rendered caret offsets
298
+ // • Toolbar actions that mutate DOM sync on the next 250ms push, not instantly
299
+ //
300
+ // The production-grade binding is per-node (y-xml-fragment) and is planned.
301
+ // This MVP closes the biggest procurement-checkbox gap today.
302
+ function wireTextSync() {
303
+ var applying = false; // guard: we're pushing into the editor from Y.Text
304
+ var pushing = false; // guard: we're pushing into Y.Text from the editor
305
+ var pendingPushTimer = null;
306
+ var lastKnownHtml = null;
307
+
308
+ function currentEditorHtml() {
309
+ return editor && editor.getHTMLCode ? editor.getHTMLCode() : "";
310
+ }
311
+
312
+ // Capture the local caret as a serializable offset BEFORE we replace
313
+ // the body, then restore it AFTER. With a whole-HTML-string sync, the
314
+ // caret would otherwise snap to position 0 on every remote update.
315
+ // We use a character offset into the textContent of the editable body —
316
+ // it's not perfect (HTML structure changes can shift it) but it's
317
+ // dramatically better than the unconditional jump-to-start behaviour.
318
+ function captureCaretOffset() {
319
+ try {
320
+ var doc = editor.getDocument && editor.getDocument();
321
+ var body = doc && doc.body;
322
+ if (!body) return null;
323
+ var sel = doc.getSelection ? doc.getSelection() : null;
324
+ if (!sel || sel.rangeCount === 0) return null;
325
+ var range = sel.getRangeAt(0);
326
+ if (!body.contains(range.startContainer)) return null;
327
+ var pre = doc.createRange();
328
+ pre.selectNodeContents(body);
329
+ pre.setEnd(range.startContainer, range.startOffset);
330
+ return pre.toString().length;
331
+ } catch (e) { return null; }
332
+ }
333
+
334
+ function restoreCaretOffset(offset) {
335
+ if (offset == null) return;
336
+ try {
337
+ var doc = editor.getDocument && editor.getDocument();
338
+ var body = doc && doc.body;
339
+ if (!body) return;
340
+ var walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
341
+ var charsLeft = offset;
342
+ var targetNode = null;
343
+ var targetOffset = 0;
344
+ while (walker.nextNode()) {
345
+ var node = walker.currentNode;
346
+ var len = node.nodeValue ? node.nodeValue.length : 0;
347
+ if (charsLeft <= len) {
348
+ targetNode = node;
349
+ targetOffset = charsLeft;
350
+ break;
351
+ }
352
+ charsLeft -= len;
353
+ }
354
+ if (!targetNode) {
355
+ // overflow — drop caret at end of body
356
+ var lastText = null;
357
+ var w2 = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
358
+ while (w2.nextNode()) lastText = w2.currentNode;
359
+ if (!lastText) return;
360
+ targetNode = lastText;
361
+ targetOffset = lastText.nodeValue.length;
362
+ }
363
+ var sel = doc.getSelection ? doc.getSelection() : null;
364
+ if (!sel) return;
365
+ var range = doc.createRange();
366
+ range.setStart(targetNode, targetOffset);
367
+ range.collapse(true);
368
+ sel.removeAllRanges();
369
+ sel.addRange(range);
370
+ } catch (e) { /* swallow — best-effort caret restore */ }
371
+ }
372
+
373
+ function setEditorHtml(html) {
374
+ if (!editor || !editor.setHTMLCode) return;
375
+ var savedCaret = captureCaretOffset();
376
+ applying = true;
377
+ try { editor.setHTMLCode(html); }
378
+ finally { applying = false; }
379
+ // Restore on the next tick so the editor's own setHTMLCode finishes
380
+ // its DOM swap + selection clearing before we reassert the caret.
381
+ setTimeout(function () { restoreCaretOffset(savedCaret); }, 0);
382
+ }
383
+
384
+ // Initial sync: if Y.Text is empty, seed from the editor. Otherwise pull remote.
385
+ var remoteInitial = session.textMap.toString();
386
+ if (remoteInitial && remoteInitial.length > 0) {
387
+ setEditorHtml(remoteInitial);
388
+ lastKnownHtml = remoteInitial;
389
+ } else {
390
+ var localInitial = currentEditorHtml();
391
+ if (localInitial) {
392
+ session.textMap.insert(0, localInitial);
393
+ }
394
+ lastKnownHtml = localInitial;
395
+ }
396
+
397
+ // Y.Text → editor.
398
+ var onTextChange = function (event, transaction) {
399
+ if (pushing) return;
400
+ // Ignore our own transactions — they're already in the editor.
401
+ if (transaction && transaction.local) return;
402
+ var fresh = session.textMap.toString();
403
+ if (fresh === lastKnownHtml) return;
404
+ setEditorHtml(fresh);
405
+ lastKnownHtml = fresh;
406
+ // 2026-05-28 setEditorHtml writes into editable.innerHTML; if that
407
+ // path replaced the iframe body element (e.g. on first-paint after
408
+ // a skin remount), our MutationObserver is now stranded on a
409
+ // detached node. Re-attach to the live body.
410
+ ensureMutationObserverAttached();
411
+ };
412
+ session.textMap.observe(onTextChange);
413
+ session.cleanup.push(function () { session.textMap.unobserve(onTextChange); });
414
+
415
+ // Editor DOM → Y.Text. Debounced so bursts collapse to one CRDT diff.
416
+ var onEditorMutation = function () {
417
+ if (applying) return;
418
+ if (pendingPushTimer) return;
419
+ // 2026-05-28 Re-attach observer if the body was remounted since last
420
+ // mutation. Cheap no-op when body is unchanged.
421
+ ensureMutationObserverAttached();
422
+ pendingPushTimer = setTimeout(function () {
423
+ pendingPushTimer = null;
424
+ if (!session || !session.textMap) return;
425
+ var current = currentEditorHtml();
426
+ if (current === lastKnownHtml) return;
427
+
428
+ // Minimal diff against lastKnownHtml — compute common prefix and
429
+ // suffix so concurrent remote edits outside the local change
430
+ // area are preserved. Not a full CRDT binding but it keeps us
431
+ // from stomping on simultaneous work.
432
+ var oldStr = lastKnownHtml || "";
433
+ var newStr = current;
434
+ var prefix = 0;
435
+ var maxPrefix = Math.min(oldStr.length, newStr.length);
436
+ while (prefix < maxPrefix && oldStr.charCodeAt(prefix) === newStr.charCodeAt(prefix)) prefix++;
437
+ var suffix = 0;
438
+ var maxSuffix = Math.min(oldStr.length - prefix, newStr.length - prefix);
439
+ while (suffix < maxSuffix
440
+ && oldStr.charCodeAt(oldStr.length - 1 - suffix) === newStr.charCodeAt(newStr.length - 1 - suffix)) suffix++;
441
+
442
+ var delLen = oldStr.length - prefix - suffix;
443
+ var insStr = newStr.substring(prefix, newStr.length - suffix);
444
+
445
+ pushing = true;
446
+ try {
447
+ session.doc.transact(function () {
448
+ if (delLen > 0) session.textMap.delete(prefix, delLen);
449
+ if (insStr.length > 0) session.textMap.insert(prefix, insStr);
450
+ }, session.user && session.user.id);
451
+ } finally {
452
+ pushing = false;
453
+ lastKnownHtml = newStr;
454
+ }
455
+ }, 250);
456
+ };
457
+
458
+ // Observe DOM mutations. The editor's iframe body is a moving target
459
+ // (some rte skins remount it), so we re-attach when we notice the body
460
+ // node changed since the last attach.
461
+ var mutationObserver = null;
462
+ var observedBody = null;
463
+ function attachMutationObserver() {
464
+ var editdoc = editor.getDocument && editor.getDocument();
465
+ var body = editdoc && editdoc.body;
466
+ if (!body || typeof MutationObserver !== "function") return;
467
+ // Already attached to this exact body node? Nothing to do.
468
+ if (mutationObserver && observedBody === body) return;
469
+ // Body remounted (or first attach): disconnect old observer and
470
+ // start a new one on the live body.
471
+ if (mutationObserver) {
472
+ try { mutationObserver.disconnect(); } catch (ignore) { }
473
+ mutationObserver = null;
474
+ }
475
+ mutationObserver = new MutationObserver(onEditorMutation);
476
+ mutationObserver.observe(body, { childList: true, characterData: true, subtree: true, attributes: true });
477
+ observedBody = body;
478
+ }
479
+ attachMutationObserver();
480
+ // 2026-05-28 Keep the observer alive across body remounts. Re-attach
481
+ // is a no-op when the body is unchanged (cheap), so we can call it
482
+ // whenever we might be about to write — guarantees the comment above
483
+ // matches actual behavior.
484
+ function ensureMutationObserverAttached() { attachMutationObserver(); }
485
+
486
+ // Also push on blur as a safety net (mutation observer can miss compositionend in some edge cases).
487
+ var blurHandler = function () { onEditorMutation(); };
488
+ if (editor.attachEvent) editor.attachEvent("blur", blurHandler);
489
+
490
+ session.cleanup.push(function () {
491
+ if (pendingPushTimer) { clearTimeout(pendingPushTimer); pendingPushTimer = null; }
492
+ if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; }
493
+ if (editor.detachEvent) { try { editor.detachEvent("blur", blurHandler); } catch (e) { } }
494
+ });
495
+ }
496
+
497
+ function wireLedgerBridge() {
498
+ var ledger = editor.reviewLedger;
499
+ var originalAdd = ledger.add;
500
+ var originalUpdate = ledger.update;
501
+ var originalRemove = ledger.remove;
502
+
503
+ ledger.add = function (entry) {
504
+ var result = originalAdd.call(ledger, entry);
505
+ if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
506
+ return result;
507
+ };
508
+ ledger.update = function (id, patch) {
509
+ var result = originalUpdate.call(ledger, id, patch);
510
+ if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
511
+ return result;
512
+ };
513
+ ledger.remove = function (id) {
514
+ var result = originalRemove.call(ledger, id);
515
+ if (result && !echoGuard) session.ledgerMap.delete(id);
516
+ return result;
517
+ };
518
+
519
+ var observer = function (event) {
520
+ echoGuard = true;
521
+ try {
522
+ event.keysChanged.forEach(function (key) {
523
+ var change = event.changes.keys.get(key);
524
+ if (!change) return;
525
+ if (change.action === "delete") {
526
+ originalRemove.call(ledger, key);
527
+ } else {
528
+ var value = session.ledgerMap.get(key);
529
+ if (!value) return;
530
+ // Upsert: remove any local copy first to avoid duplicate IDs mid-merge.
531
+ originalRemove.call(ledger, key);
532
+ originalAdd.call(ledger, value);
533
+ }
534
+ });
535
+ } finally {
536
+ echoGuard = false;
537
+ }
538
+ if (ledger.refreshPanel) ledger.refreshPanel();
539
+ };
540
+ session.ledgerMap.observe(observer);
541
+ session.cleanup.push(function () {
542
+ session.ledgerMap.unobserve(observer);
543
+ ledger.add = originalAdd;
544
+ ledger.update = originalUpdate;
545
+ ledger.remove = originalRemove;
546
+ });
547
+ }
548
+
549
+ function seedLedgerFromRemote() {
550
+ if (!editor.reviewLedger || !session.ledgerMap) return;
551
+ echoGuard = true;
552
+ try {
553
+ session.ledgerMap.forEach(function (value, key) {
554
+ // If not already local, add it.
555
+ if (!editor.reviewLedger.get(key)) {
556
+ editor.reviewLedger.add(value);
557
+ }
558
+ });
559
+ } finally {
560
+ echoGuard = false;
561
+ }
562
+ }
563
+
564
+ function cloneEntry(entry) {
565
+ // JSON clone strips functions/refs; safe for Yjs sync.
566
+ try { return JSON.parse(JSON.stringify(entry)); }
567
+ catch (err) { return entry; }
568
+ }
569
+
570
+ // --- overlay / cursor rendering ---
571
+
572
+ function mountOverlay() {
573
+ if (overlay && overlay.parentNode) return;
574
+ overlay = document.createElement("div");
575
+ overlay.className = "rte-collab-overlay";
576
+ overlay.setAttribute("aria-hidden", "true");
577
+ document.body.appendChild(overlay);
578
+ var reposition = function () { renderRemoteCursors(); };
579
+ window.addEventListener("scroll", reposition, true);
580
+ window.addEventListener("resize", reposition);
581
+ session.cleanup.push(function () {
582
+ window.removeEventListener("scroll", reposition, true);
583
+ window.removeEventListener("resize", reposition);
584
+ });
585
+ }
586
+
587
+ function renderRemoteCursors() {
588
+ if (!session || !overlay) return;
589
+ overlay.innerHTML = "";
590
+ var peers = getRemotePeers();
591
+ var iframe = editor.iframe;
592
+ var ir = iframe.getBoundingClientRect();
593
+
594
+ for (var i = 0; i < peers.length; i++) {
595
+ var peer = peers[i];
596
+ if (!peer.caret) continue;
597
+ var range = null;
598
+ try { range = deserializeCaret(peer.caret); } catch (ignore) { }
599
+ if (!range) continue;
600
+ var rects = range.getClientRects();
601
+ var rect = rects && rects.length ? rects[0] : range.getBoundingClientRect();
602
+ if (!rect || (!rect.top && !rect.left)) continue;
603
+
604
+ var caretEl = document.createElement("div");
605
+ caretEl.className = "rte-collab-caret";
606
+ // Viewport coordinates (the overlay is position:fixed). Do NOT add
607
+ // pageX/YOffset: a page-absolute caret inside an absolute overlay on
608
+ // <body> extends document height on scrolled/tall pages and, with the
609
+ // scroll re-render below, runs away (host page ballooned to ~83000px).
610
+ caretEl.style.left = (ir.left + rect.left) + "px";
611
+ caretEl.style.top = (ir.top + rect.top) + "px";
612
+ caretEl.style.height = (rect.height || 18) + "px";
613
+ caretEl.style.background = peer.user.color;
614
+
615
+ var label = document.createElement("div");
616
+ label.className = "rte-collab-caret-label";
617
+ label.textContent = peer.user.name;
618
+ label.style.background = peer.user.color;
619
+ caretEl.appendChild(label);
620
+
621
+ overlay.appendChild(caretEl);
622
+ }
623
+ }
624
+
625
+ // --- presence panel ---
626
+
627
+ function mountPresencePanel() {
628
+ if (presencePanel && presencePanel.parentNode) return;
629
+ presencePanel = document.createElement("div");
630
+ presencePanel.className = "rte-collab-presence";
631
+ var shell = getEditorShell();
632
+ if (shell && shell.parentNode) {
633
+ shell.parentNode.insertBefore(presencePanel, shell);
634
+ } else {
635
+ document.body.appendChild(presencePanel);
636
+ }
637
+ }
638
+
639
+ function renderPresence() {
640
+ if (!session || !presencePanel) return;
641
+ presencePanel.innerHTML = "";
642
+ var all = [{ user: session.user, self: true }].concat(
643
+ getRemotePeers().map(function (p) { return { user: p.user, self: false }; })
644
+ );
645
+ var title = document.createElement("span");
646
+ title.className = "rte-collab-presence-title";
647
+ title.textContent = "Online (" + all.length + ")";
648
+ presencePanel.appendChild(title);
649
+ for (var i = 0; i < all.length; i++) {
650
+ var u = all[i].user;
651
+ var chip = document.createElement("span");
652
+ chip.className = "rte-collab-presence-chip" + (all[i].self ? " is-self" : "");
653
+ chip.style.background = u.color;
654
+ chip.title = u.name + (all[i].self ? " (you)" : "");
655
+ chip.textContent = initialsOf(u.name || u.id || "?");
656
+ presencePanel.appendChild(chip);
657
+ }
658
+ }
659
+
660
+ function getEditorShell() {
661
+ var el = editor.iframe;
662
+ while (el && el !== document.body) {
663
+ if (el.classList && el.classList.contains("richtexteditor")) return el;
664
+ el = el.parentNode;
665
+ }
666
+ return null;
667
+ }
668
+
669
+ function initialsOf(label) {
670
+ var parts = String(label).trim().split(/\s+/);
671
+ if (!parts[0]) return "?";
672
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
673
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
674
+ }
675
+
676
+ // --- styles ---
677
+
678
+ function injectStyles() {
679
+ if (document.querySelector("style[data-rte-collab]")) return;
680
+ var style = document.createElement("style");
681
+ style.setAttribute("data-rte-collab", "1");
682
+ style.textContent = [
683
+ ".rte-collab-overlay{position:fixed;top:0;left:0;pointer-events:none;z-index:2147482900}",
684
+ ".rte-collab-caret{position:absolute;width:2px;pointer-events:none}",
685
+ ".rte-collab-caret-label{position:absolute;top:-18px;left:0;padding:1px 6px;border-radius:4px;color:#fff;font:10px/1.3 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;white-space:nowrap;font-weight:600;letter-spacing:.01em;box-shadow:0 1px 2px rgba(15,23,42,.15)}",
686
+ ".rte-collab-presence{display:flex;align-items:center;gap:8px;padding:8px 14px;border-radius:10px 10px 0 0;background:#f4f7ff;border:1px solid rgba(71,85,155,.15);border-bottom:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:12px;color:#33506f;margin:0}",
687
+ ".rte-collab-presence-title{font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#475569}",
688
+ ".rte-collab-presence-chip{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;color:#fff;font-size:10px;font-weight:700;box-shadow:0 0 0 2px #fff;letter-spacing:.02em}",
689
+ ".rte-collab-presence-chip.is-self{outline:2px solid rgba(15,23,42,.25);outline-offset:1px}"
690
+ ].join("\n");
691
+ document.head.appendChild(style);
692
+ }
693
+ }