@richscripts/richtexteditor 2.14.1 → 2.15.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 (237) hide show
  1. package/README.md +173 -10
  2. package/angular/build-manifest.json +23 -0
  3. package/angular/index.d.ts +71 -76
  4. package/angular/index.mjs +2 -0
  5. package/angular/package.json +4 -0
  6. package/headless/index.d.ts +3 -1
  7. package/headless/index.js +72 -16
  8. package/index.d.ts +33 -54
  9. package/integrations/initialization-scope.js +27 -0
  10. package/integrations/native-mount.js +30 -0
  11. package/integrations/shared.d.ts +23 -1
  12. package/integrations/shared.js +58 -22
  13. package/package.json +25 -3
  14. package/react/index.js +118 -37
  15. package/react/index.mjs +29 -0
  16. package/richtexteditor/all_plugins.js +7001 -1078
  17. package/richtexteditor/all_plugins.min.js +62 -59
  18. package/richtexteditor/lang/emoji/LICENSE-CLDR.txt +41 -0
  19. package/richtexteditor/lang/emoji/rte-emoji-de.js +3 -0
  20. package/richtexteditor/lang/emoji/rte-emoji-es.js +3 -0
  21. package/richtexteditor/lang/emoji/rte-emoji-fr.js +3 -0
  22. package/richtexteditor/lang/emoji/rte-emoji-it.js +3 -0
  23. package/richtexteditor/lang/emoji/rte-emoji-ja.js +3 -0
  24. package/richtexteditor/lang/emoji/rte-emoji-ko.js +3 -0
  25. package/richtexteditor/lang/emoji/rte-emoji-nl.js +3 -0
  26. package/richtexteditor/lang/emoji/rte-emoji-pt-pt.js +3 -0
  27. package/richtexteditor/lang/emoji/rte-emoji-pt.js +3 -0
  28. package/richtexteditor/lang/emoji/rte-emoji-ru.js +3 -0
  29. package/richtexteditor/lang/emoji/rte-emoji-zh-Hans.js +3 -0
  30. package/richtexteditor/lang/emoji/rte-emoji-zh-Hant.js +3 -0
  31. package/richtexteditor/lang/rte-lang-af.js +1 -0
  32. package/richtexteditor/lang/rte-lang-ar.js +1 -0
  33. package/richtexteditor/lang/rte-lang-bg.js +1 -0
  34. package/richtexteditor/lang/rte-lang-bn.js +1 -0
  35. package/richtexteditor/lang/rte-lang-bs.js +1 -0
  36. package/richtexteditor/lang/rte-lang-ca.js +1 -0
  37. package/richtexteditor/lang/rte-lang-cs.js +1 -0
  38. package/richtexteditor/lang/rte-lang-cy.js +1 -0
  39. package/richtexteditor/lang/rte-lang-da.js +1 -0
  40. package/richtexteditor/lang/rte-lang-de.js +1017 -2
  41. package/richtexteditor/lang/rte-lang-el.js +1 -0
  42. package/richtexteditor/lang/rte-lang-en.js +1 -0
  43. package/richtexteditor/lang/rte-lang-es.js +1023 -2
  44. package/richtexteditor/lang/rte-lang-et.js +1 -0
  45. package/richtexteditor/lang/rte-lang-fa.js +1 -0
  46. package/richtexteditor/lang/rte-lang-fi.js +1 -0
  47. package/richtexteditor/lang/rte-lang-fil.js +1 -0
  48. package/richtexteditor/lang/rte-lang-fj.js +1 -0
  49. package/richtexteditor/lang/rte-lang-fr.js +1023 -2
  50. package/richtexteditor/lang/rte-lang-ga.js +1 -0
  51. package/richtexteditor/lang/rte-lang-gu.js +1 -0
  52. package/richtexteditor/lang/rte-lang-he.js +1 -0
  53. package/richtexteditor/lang/rte-lang-hi.js +1 -0
  54. package/richtexteditor/lang/rte-lang-hr.js +1 -0
  55. package/richtexteditor/lang/rte-lang-ht.js +1 -0
  56. package/richtexteditor/lang/rte-lang-hu.js +1 -0
  57. package/richtexteditor/lang/rte-lang-id.js +1 -0
  58. package/richtexteditor/lang/rte-lang-is.js +1 -0
  59. package/richtexteditor/lang/rte-lang-it.js +1023 -2
  60. package/richtexteditor/lang/rte-lang-ja.js +1023 -2
  61. package/richtexteditor/lang/rte-lang-kn.js +1 -0
  62. package/richtexteditor/lang/rte-lang-ko.js +1023 -2
  63. package/richtexteditor/lang/rte-lang-lt.js +1 -0
  64. package/richtexteditor/lang/rte-lang-lv.js +1 -0
  65. package/richtexteditor/lang/rte-lang-mg.js +1 -0
  66. package/richtexteditor/lang/rte-lang-mi.js +1 -0
  67. package/richtexteditor/lang/rte-lang-ml.js +1 -0
  68. package/richtexteditor/lang/rte-lang-mr.js +1 -0
  69. package/richtexteditor/lang/rte-lang-ms.js +1 -0
  70. package/richtexteditor/lang/rte-lang-mt.js +1 -0
  71. package/richtexteditor/lang/rte-lang-mww.js +1 -0
  72. package/richtexteditor/lang/rte-lang-nb.js +1 -0
  73. package/richtexteditor/lang/rte-lang-nl.js +1023 -2
  74. package/richtexteditor/lang/rte-lang-otq.js +1 -0
  75. package/richtexteditor/lang/rte-lang-pa.js +1 -0
  76. package/richtexteditor/lang/rte-lang-pl.js +1 -0
  77. package/richtexteditor/lang/rte-lang-pt-pt.js +1023 -2
  78. package/richtexteditor/lang/rte-lang-pt.js +1023 -2
  79. package/richtexteditor/lang/rte-lang-ro.js +1 -0
  80. package/richtexteditor/lang/rte-lang-ru.js +1023 -2
  81. package/richtexteditor/lang/rte-lang-sk.js +1 -0
  82. package/richtexteditor/lang/rte-lang-sl.js +1 -0
  83. package/richtexteditor/lang/rte-lang-sm.js +1 -0
  84. package/richtexteditor/lang/rte-lang-sr-Cyrl.js +1 -0
  85. package/richtexteditor/lang/rte-lang-sr-Latn.js +1 -0
  86. package/richtexteditor/lang/rte-lang-sv.js +1 -0
  87. package/richtexteditor/lang/rte-lang-sw.js +1 -0
  88. package/richtexteditor/lang/rte-lang-ta.js +1 -0
  89. package/richtexteditor/lang/rte-lang-te.js +1 -0
  90. package/richtexteditor/lang/rte-lang-th.js +1 -0
  91. package/richtexteditor/lang/rte-lang-tlh-Latn.js +1 -0
  92. package/richtexteditor/lang/rte-lang-tlh-Piqd.js +1 -0
  93. package/richtexteditor/lang/rte-lang-to.js +1 -0
  94. package/richtexteditor/lang/rte-lang-tr.js +1 -0
  95. package/richtexteditor/lang/rte-lang-ty.js +1 -0
  96. package/richtexteditor/lang/rte-lang-uk.js +1 -0
  97. package/richtexteditor/lang/rte-lang-ur.js +1 -0
  98. package/richtexteditor/lang/rte-lang-vi.js +1 -0
  99. package/richtexteditor/lang/rte-lang-yua.js +1 -0
  100. package/richtexteditor/lang/rte-lang-zh-Hans.js +1017 -2
  101. package/richtexteditor/lang/rte-lang-zh-Hant.js +1017 -2
  102. package/richtexteditor/plugins/a11yenhance.js +2 -1
  103. package/richtexteditor/plugins/a11yenhance.min.js +1 -1
  104. package/richtexteditor/plugins/accessibilitychecker.js +252 -60
  105. package/richtexteditor/plugins/accessibilitychecker.min.js +1 -1
  106. package/richtexteditor/plugins/aitoolkit.js +966 -303
  107. package/richtexteditor/plugins/aitoolkit.min.js +7 -7
  108. package/richtexteditor/plugins/all_plugins.js +7001 -1078
  109. package/richtexteditor/plugins/all_plugins.min.js +62 -59
  110. package/richtexteditor/plugins/autocorrect.js +66 -31
  111. package/richtexteditor/plugins/autocorrect.min.js +1 -1
  112. package/richtexteditor/plugins/autoembed.js +17 -0
  113. package/richtexteditor/plugins/autoembed.min.js +1 -1
  114. package/richtexteditor/plugins/blocktypes.js +18 -9
  115. package/richtexteditor/plugins/blocktypes.min.js +1 -1
  116. package/richtexteditor/plugins/bookmarkcard.js +12 -5
  117. package/richtexteditor/plugins/bookmarkcard.min.js +2 -2
  118. package/richtexteditor/plugins/changecase.js +72 -19
  119. package/richtexteditor/plugins/changecase.min.js +1 -1
  120. package/richtexteditor/plugins/charlimit.js +62 -2
  121. package/richtexteditor/plugins/charlimit.min.js +2 -2
  122. package/richtexteditor/plugins/comments.js +458 -59
  123. package/richtexteditor/plugins/comments.min.js +4 -3
  124. package/richtexteditor/plugins/contentminimap.js +25 -6
  125. package/richtexteditor/plugins/contentminimap.min.js +1 -1
  126. package/richtexteditor/plugins/crossreference.js +1 -1
  127. package/richtexteditor/plugins/crossreference.min.js +1 -1
  128. package/richtexteditor/plugins/dialoga11y.js +26 -4
  129. package/richtexteditor/plugins/dialoga11y.min.js +1 -1
  130. package/richtexteditor/plugins/dictation.js +1 -1
  131. package/richtexteditor/plugins/documentimport.js +220 -51
  132. package/richtexteditor/plugins/documentimport.min.js +4 -4
  133. package/richtexteditor/plugins/documentoutline.js +47 -8
  134. package/richtexteditor/plugins/documentoutline.min.js +1 -1
  135. package/richtexteditor/plugins/docxexport.js +204 -32
  136. package/richtexteditor/plugins/docxexport.min.js +2 -2
  137. package/richtexteditor/plugins/draghandle.js +267 -3
  138. package/richtexteditor/plugins/draghandle.min.js +1 -1
  139. package/richtexteditor/plugins/emailtoolkit.js +28 -14
  140. package/richtexteditor/plugins/emailtoolkit.min.js +1 -1
  141. package/richtexteditor/plugins/emojiautocomplete.js +110 -56
  142. package/richtexteditor/plugins/emojiautocomplete.min.js +1 -1
  143. package/richtexteditor/plugins/footnotes.js +65 -11
  144. package/richtexteditor/plugins/footnotes.min.js +1 -1
  145. package/richtexteditor/plugins/formatpainter.js +10 -0
  146. package/richtexteditor/plugins/formatpainter.min.js +1 -1
  147. package/richtexteditor/plugins/formattingmarks.js +6 -6
  148. package/richtexteditor/plugins/formattingmarks.min.js +1 -1
  149. package/richtexteditor/plugins/gapcursor.js +60 -0
  150. package/richtexteditor/plugins/gapcursor.min.js +1 -1
  151. package/richtexteditor/plugins/ghostcomplete.js +1 -1
  152. package/richtexteditor/plugins/html2pdf.js +1 -1
  153. package/richtexteditor/plugins/html2pdf.min.js +1 -1
  154. package/richtexteditor/plugins/imageeditor.js +12 -6
  155. package/richtexteditor/plugins/imageeditor.min.js +1 -1
  156. package/richtexteditor/plugins/importstyles.js +1 -1
  157. package/richtexteditor/plugins/inlinecode.js +25 -0
  158. package/richtexteditor/plugins/inlinecode.min.js +1 -1
  159. package/richtexteditor/plugins/insertcode.js +21 -3
  160. package/richtexteditor/plugins/insertcode.min.js +4 -4
  161. package/richtexteditor/plugins/insertemoji.js +112 -15
  162. package/richtexteditor/plugins/insertemoji.min.js +1 -1
  163. package/richtexteditor/plugins/insertgallery.js +42 -36
  164. package/richtexteditor/plugins/insertgallery.min.js +1 -1
  165. package/richtexteditor/plugins/inserttemplate.js +33 -17
  166. package/richtexteditor/plugins/inserttemplate.min.js +1 -1
  167. package/richtexteditor/plugins/keyboarda11y.js +66 -3
  168. package/richtexteditor/plugins/keyboarda11y.min.js +1 -1
  169. package/richtexteditor/plugins/layouttable.js +20 -0
  170. package/richtexteditor/plugins/layouttable.min.js +1 -1
  171. package/richtexteditor/plugins/linkchecker.js +29 -9
  172. package/richtexteditor/plugins/linkchecker.min.js +1 -1
  173. package/richtexteditor/plugins/localdraft.js +51 -7
  174. package/richtexteditor/plugins/localdraft.min.js +1 -1
  175. package/richtexteditor/plugins/markdownexport.js +159 -0
  176. package/richtexteditor/plugins/markdownexport.min.js +2 -2
  177. package/richtexteditor/plugins/mathdialog.js +11 -5
  178. package/richtexteditor/plugins/mathdialog.min.js +1 -1
  179. package/richtexteditor/plugins/mentionplugin.js +88 -25
  180. package/richtexteditor/plugins/mentionplugin.min.js +1 -1
  181. package/richtexteditor/plugins/mermaiddiagram.js +9 -3
  182. package/richtexteditor/plugins/mermaiddiagram.min.js +2 -2
  183. package/richtexteditor/plugins/multilevellist.js +4 -0
  184. package/richtexteditor/plugins/multilevellist.min.js +1 -1
  185. package/richtexteditor/plugins/pdfexport.js +235 -35
  186. package/richtexteditor/plugins/pdfexport.min.js +12 -12
  187. package/richtexteditor/plugins/permanentpen.js +3 -3
  188. package/richtexteditor/plugins/permanentpen.min.js +1 -1
  189. package/richtexteditor/plugins/readabilitystats.js +25 -11
  190. package/richtexteditor/plugins/readabilitystats.min.js +1 -1
  191. package/richtexteditor/plugins/readaloud.js +29 -15
  192. package/richtexteditor/plugins/readaloud.min.js +1 -1
  193. package/richtexteditor/plugins/restrictedediting.js +166 -0
  194. package/richtexteditor/plugins/restrictedediting.min.js +2 -2
  195. package/richtexteditor/plugins/revisionhistory.js +169 -37
  196. package/richtexteditor/plugins/revisionhistory.min.js +3 -3
  197. package/richtexteditor/plugins/rubyannotation.js +19 -9
  198. package/richtexteditor/plugins/rubyannotation.min.js +1 -1
  199. package/richtexteditor/plugins/sanitizer.js +62 -3
  200. package/richtexteditor/plugins/sanitizer.min.js +1 -1
  201. package/richtexteditor/plugins/slashcommand.js +30 -16
  202. package/richtexteditor/plugins/slashcommand.min.js +2 -2
  203. package/richtexteditor/plugins/smartchips.js +36 -22
  204. package/richtexteditor/plugins/smartchips.min.js +2 -2
  205. package/richtexteditor/plugins/spellcheck.js +17 -8
  206. package/richtexteditor/plugins/spellcheck.min.js +2 -2
  207. package/richtexteditor/plugins/stateinspector.js +19 -9
  208. package/richtexteditor/plugins/stateinspector.min.js +2 -2
  209. package/richtexteditor/plugins/structured-content-bridge.js +56 -24
  210. package/richtexteditor/plugins/structured-content-bridge.min.js +7 -7
  211. package/richtexteditor/plugins/tableofcontents.js +5 -2
  212. package/richtexteditor/plugins/tableofcontents.min.js +1 -1
  213. package/richtexteditor/plugins/tabletools.js +388 -3
  214. package/richtexteditor/plugins/tabletools.min.js +1 -1
  215. package/richtexteditor/plugins/textdirection.js +18 -5
  216. package/richtexteditor/plugins/textdirection.min.js +1 -1
  217. package/richtexteditor/plugins/textpartlanguage.js +161 -32
  218. package/richtexteditor/plugins/textpartlanguage.min.js +1 -1
  219. package/richtexteditor/plugins/todolist.js +90 -6
  220. package/richtexteditor/plugins/todolist.min.js +1 -1
  221. package/richtexteditor/plugins/trackedchanges.js +1812 -25
  222. package/richtexteditor/plugins/trackedchanges.min.js +5 -3
  223. package/richtexteditor/plugins/wordexport.js +2 -2
  224. package/richtexteditor/plugins/yjscollab.js +9 -4
  225. package/richtexteditor/plugins/yjscollab.min.js +1 -1
  226. package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +22 -13
  227. package/richtexteditor/plugins/zz_richtextbox_dialog_style.min.js +1 -1
  228. package/richtexteditor/rte-config.js +130 -50
  229. package/richtexteditor/rte-headless.js +19 -4
  230. package/richtexteditor/rte-webcomponent.js +89 -16
  231. package/richtexteditor/rte.js +132 -52
  232. package/scripts/build-angular.mjs +97 -0
  233. package/svelte/index.d.ts +2 -1
  234. package/svelte/index.js +80 -9
  235. package/vue/index.js +130 -34
  236. package/angular/component.ts +0 -297
  237. package/angular/index.ts +0 -24
@@ -44,7 +44,10 @@ function RTE_Plugin_Comments() {
44
44
  openSidebar: function () { openSidebar(); },
45
45
  closeSidebar: function () { closeSidebar(); },
46
46
  toggleSidebar: function () { if (sidebar && sidebar.isConnected) closeSidebar(); else openSidebar(); },
47
- focusComment: function (id) { focusComment(id); }
47
+ focusComment: function (id) { focusComment(id); },
48
+ react: function (commentId, emoji) { return reactToComment(commentId, emoji); },
49
+ next: function () { return stepComment(1); },
50
+ previous: function () { return stepComment(-1); }
48
51
  };
49
52
 
50
53
  injectStyles();
@@ -54,13 +57,46 @@ function RTE_Plugin_Comments() {
54
57
  return editor.createToolbarButton(cmd);
55
58
  };
56
59
 
60
+ // 2026-10-17 Ctrl/Cmd+Alt+M adds a comment on the selection (Google Docs, CKEditor and TinyMCE bind it). A key that
61
+ // reports a character other than "m" is AltGr typing (German AltGr+M is a micro sign) and is left alone.
62
+ // tools/keyboard/comment-shortcut.mjs
63
+ (editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("keydown", fn, true); } : function (fn) { editor.getDocument().addEventListener("keydown", fn, true); })(function (e) {
64
+ if (!(e.ctrlKey || e.metaKey) || !e.altKey || e.shiftKey) return;
65
+ if (e.key !== "m" && e.key !== "M") return;
66
+ e.preventDefault();
67
+ openComposerForSelection();
68
+ });
69
+
70
+ // 2026-10-17 Google Docs' chords: Ctrl/Cmd+Alt+N then C = next comment, Ctrl/Cmd+Alt+P then C = previous. A key
71
+ // that reports a character other than n / p is AltGr typing and is left alone. tools/comments/comment-navigation.mjs
72
+ var chordDir = 0, chordAt = 0;
73
+ (editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("keydown", fn, true); } : function (fn) { editor.getDocument().addEventListener("keydown", fn, true); })(function (e) {
74
+ if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === "n" || e.key === "N" || e.key === "p" || e.key === "P")) {
75
+ chordDir = (e.key === "n" || e.key === "N") ? 1 : -1;
76
+ chordAt = Date.now();
77
+ e.preventDefault();
78
+ return;
79
+ }
80
+ if (chordDir && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === "c" || e.key === "C") && Date.now() - chordAt < 2000) {
81
+ var dir = chordDir;
82
+ chordDir = 0;
83
+ e.preventDefault();
84
+ stepComment(dir);
85
+ return;
86
+ }
87
+ if (e.key !== "Control" && e.key !== "Alt" && e.key !== "Meta" && e.key !== "Shift") chordDir = 0;
88
+ });
89
+
57
90
  editor.attachEvent("exec_command_commentadd", function (state) {
58
91
  state.returnValue = true;
59
92
  state.stopBubble = true;
60
93
  openComposerForSelection();
61
94
  });
62
95
 
63
- editor.getEditable().addEventListener("click", function (e) {
96
+ // 2026-10-17 bound through the core so it follows the live editing document: plugins that wrap the content area move
97
+ // the iframe during init, and a listener on the old document died (a click on a comment's highlight did nothing).
98
+ // tools/keyboard/plugin-listeners-live.mjs
99
+ (editor.bindEditableEvent ? function (fn) { editor.bindEditableEvent("click", fn); } : function (fn) { editor.getEditable().addEventListener("click", fn); })(function (e) {
64
100
  var span = e.target && e.target.closest ? e.target.closest(".rte-comment") : null;
65
101
  if (!span) return;
66
102
  var id = span.getAttribute("data-comment-id");
@@ -120,6 +156,24 @@ function RTE_Plugin_Comments() {
120
156
 
121
157
  // --- creating comments ---
122
158
 
159
+ // 2026-10-18 a comments button runs its action on click, so Enter and Space work (they fire click, not mousedown;
160
+ // these ran on mousedown only and did nothing from the keyboard). mousedown still prevents default so a mouse press
161
+ // keeps the editor's selection. If a keyboard press re-rendered the sidebar and took focus with it, focus goes to
162
+ // the sidebar's first control. tools/a11y/comment-buttons-keyboard.mjs
163
+ function onActivate(el, fn) {
164
+ el.addEventListener("mousedown", function (e) { e.preventDefault(); });
165
+ el.addEventListener("click", function (e) {
166
+ e.preventDefault();
167
+ var fromKeyboard = e.detail === 0;
168
+ fn();
169
+ if (!fromKeyboard || !sidebar || !sidebar.isConnected) return;
170
+ var a = sidebar.ownerDocument.activeElement;
171
+ if (a && a !== sidebar.ownerDocument.body && a.isConnected) return;
172
+ var first = sidebar.querySelector("button, [tabindex]:not([tabindex='-1'])");
173
+ if (first) { try { first.focus(); } catch (x) { /* ignore */ } }
174
+ });
175
+ }
176
+
123
177
  function openComposerForSelection() {
124
178
  var sel = editor.getSelection();
125
179
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
@@ -150,35 +204,117 @@ function RTE_Plugin_Comments() {
150
204
  composer = host.createElement("div");
151
205
  composer.className = "rte-comment-composer" + (isEditorDark() ? " rte-comment-dark" : "");
152
206
  composer.setAttribute("role", "dialog");
153
- composer.setAttribute("aria-label", "Add comment");
207
+ composer.setAttribute("aria-label", cmText("cm_addcomment", "Add comment"));
154
208
 
155
209
  var header = host.createElement("div");
156
210
  header.className = "rte-comment-composer-header";
157
- header.textContent = "Add comment";
211
+ header.textContent = cmText("cm_addcomment", "Add comment");
158
212
  composer.appendChild(header);
159
213
 
160
214
  var textarea = host.createElement("textarea");
161
215
  textarea.className = "rte-comment-composer-textarea";
162
- textarea.placeholder = "Type your comment...";
216
+ textarea.placeholder = cmText("cm_typecomment", "Type your comment...");
163
217
  textarea.rows = 3;
164
218
  composer.appendChild(textarea);
165
219
 
220
+ // Mentions: "@" starts a list of the users in config.commentMentions. Choosing one inserts "@Name" and keeps
221
+ // the user id with the comment. tools/comments/comment-mentions.mjs
222
+ var mentionBox = host.createElement("div");
223
+ mentionBox.className = "rte-comment-mentions";
224
+ mentionBox.setAttribute("role", "listbox");
225
+ mentionBox.style.display = "none";
226
+ composer.appendChild(mentionBox);
227
+ var picked = [];
228
+ var mentionItems = [];
229
+ var mentionActive = 0;
230
+ var mentionStart = -1;
231
+ function mentionQuery() {
232
+ var before = textarea.value.slice(0, textarea.selectionStart);
233
+ var m = /(^|\s)@([^\s@]*)$/.exec(before);
234
+ if (!m) return null;
235
+ mentionStart = before.length - m[2].length - 1;
236
+ return m[2];
237
+ }
238
+ function hideMentions() {
239
+ while (mentionBox.firstChild) mentionBox.removeChild(mentionBox.firstChild);
240
+ mentionBox.style.display = "none";
241
+ mentionItems = [];
242
+ mentionStart = -1;
243
+ }
244
+ function paintMentions() {
245
+ while (mentionBox.firstChild) mentionBox.removeChild(mentionBox.firstChild);
246
+ for (var i = 0; i < mentionItems.length; i++) {
247
+ var opt = host.createElement("div");
248
+ opt.className = "rte-comment-mention-option" + (i === mentionActive ? " rte-comment-mention-active" : "");
249
+ opt.setAttribute("role", "option");
250
+ opt.setAttribute("aria-selected", i === mentionActive ? "true" : "false");
251
+ opt.textContent = mentionItems[i].name;
252
+ opt.addEventListener("mousedown", (function (u) { return function (e) { e.preventDefault(); pickMention(u); }; })(mentionItems[i]));
253
+ mentionBox.appendChild(opt);
254
+ }
255
+ }
256
+ function refreshMentions() {
257
+ var q = mentionQuery();
258
+ var users = Array.isArray(config.commentMentions) ? config.commentMentions : [];
259
+ mentionItems = [];
260
+ if (q !== null) {
261
+ var ql = q.toLowerCase();
262
+ for (var i = 0; i < users.length; i++) {
263
+ var u = users[i];
264
+ if (!u || !u.name || u.id == null) continue;
265
+ var words = String(u.name).toLowerCase().split(/\s+/);
266
+ for (var w = 0; w < words.length; w++) {
267
+ if (words[w].indexOf(ql) === 0) { mentionItems.push(u); break; }
268
+ }
269
+ }
270
+ }
271
+ if (!mentionItems.length) { hideMentions(); return; }
272
+ mentionActive = 0;
273
+ mentionBox.style.display = "";
274
+ paintMentions();
275
+ }
276
+ function pickMention(user) {
277
+ if (!user || mentionStart < 0) return;
278
+ var insert = "@" + user.name + " ";
279
+ var caret = textarea.selectionStart;
280
+ textarea.value = textarea.value.slice(0, mentionStart) + insert + textarea.value.slice(caret);
281
+ var pos = mentionStart + insert.length;
282
+ textarea.setSelectionRange(pos, pos);
283
+ if (!picked.some(function (m) { return m.id === user.id; })) picked.push({ id: user.id, name: user.name });
284
+ hideMentions();
285
+ }
286
+ textarea.addEventListener("input", refreshMentions);
287
+ textarea.addEventListener("keydown", function (e) {
288
+ if (mentionBox.style.display === "none") return;
289
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
290
+ var step = e.key === "ArrowDown" ? 1 : -1;
291
+ mentionActive = (mentionActive + step + mentionItems.length) % mentionItems.length;
292
+ paintMentions();
293
+ e.preventDefault();
294
+ } else if (e.key === "Enter" || e.key === "Tab") {
295
+ pickMention(mentionItems[mentionActive]);
296
+ e.preventDefault();
297
+ } else if (e.key === "Escape" || e.key === "Esc") {
298
+ hideMentions();
299
+ e.stopPropagation();
300
+ }
301
+ });
302
+
166
303
  var actions = host.createElement("div");
167
304
  actions.className = "rte-comment-composer-actions";
168
305
  var cancel = host.createElement("button");
169
306
  cancel.type = "button";
170
307
  cancel.className = "rte-comment-btn rte-comment-btn-ghost";
171
- cancel.textContent = "Cancel";
172
- cancel.addEventListener("mousedown", function (e) { e.preventDefault(); closeComposer(); });
308
+ cancel.textContent = cmText("cancel", "Cancel");
309
+ onActivate(cancel, closeComposer);
173
310
  var submit = host.createElement("button");
174
311
  submit.type = "button";
175
312
  submit.className = "rte-comment-btn rte-comment-btn-primary";
176
- submit.textContent = "Comment";
177
- submit.addEventListener("mousedown", function (e) {
178
- e.preventDefault();
313
+ submit.textContent = cmText("cm_submit", "Comment");
314
+ onActivate(submit, function () {
179
315
  var text = textarea.value.replace(/^\s+|\s+$/g, "");
180
316
  if (!text) return;
181
- addComment({ anchor: options.anchor, text: text });
317
+ addComment({ anchor: options.anchor, text: text, mentions: picked.filter(function (m) { return text.indexOf("@" + m.name) >= 0; }) });
182
318
  closeComposer();
183
319
  });
184
320
  actions.appendChild(cancel);
@@ -195,6 +331,35 @@ function RTE_Plugin_Comments() {
195
331
  composer.addEventListener("keydown", escListener);
196
332
  }
197
333
 
334
+ // Users a comment mentions: { id, name } pairs, with anything else dropped.
335
+ function sanitizeMentions(list) {
336
+ var out = [];
337
+ if (!Array.isArray(list)) return out;
338
+ for (var i = 0; i < list.length; i++) {
339
+ var m = list[i];
340
+ if (m && m.id != null && m.name) out.push({ id: String(m.id), name: String(m.name) });
341
+ }
342
+ return out;
343
+ }
344
+
345
+ // Comment text with each mention ("@Name") in its own highlighted span. Built from text nodes, never innerHTML.
346
+ function appendTextWithMentions(host, parent, text, mentions) {
347
+ var names = sanitizeMentions(mentions).sort(function (a, b) { return b.name.length - a.name.length; });
348
+ if (!names.length || !text) { parent.textContent = text || ""; return; }
349
+ var pattern = names.map(function (m) { return m.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }).join("|");
350
+ var re = new RegExp("@(" + pattern + ")", "g");
351
+ var last = 0, hit;
352
+ while ((hit = re.exec(text)) !== null) {
353
+ if (hit.index > last) parent.appendChild(host.createTextNode(text.slice(last, hit.index)));
354
+ var span = host.createElement("span");
355
+ span.className = "rte-comment-mention";
356
+ span.textContent = hit[0];
357
+ parent.appendChild(span);
358
+ last = hit.index + hit[0].length;
359
+ }
360
+ if (last < text.length) parent.appendChild(host.createTextNode(text.slice(last)));
361
+ }
362
+
198
363
  function positionComposer(anchor) {
199
364
  if (!composer) return;
200
365
  composer.style.position = "absolute";
@@ -209,8 +374,10 @@ function RTE_Plugin_Comments() {
209
374
  r.setEnd(anchor.endContainer, anchor.endOffset);
210
375
  var rects = r.getClientRects();
211
376
  var rect = rects && rects.length ? rects[rects.length - 1] : r.getBoundingClientRect();
212
- left = ir.left + (rect.left || 0) + window.pageXOffset;
213
- top = ir.top + (rect.bottom || rect.top || 0) + window.pageYOffset + 6;
377
+ // the rect is in iframe pixels, scaled on screen inside a transform:scale() ancestor
378
+ var sk = iframe.offsetWidth && ir.width > 0 ? ir.width / iframe.offsetWidth : 1;
379
+ left = ir.left + (rect.left || 0) * sk + window.pageXOffset;
380
+ top = ir.top + (rect.bottom || rect.top || 0) * sk + window.pageYOffset + 6;
214
381
  } catch (ignore) { }
215
382
  }
216
383
  composer.style.left = left + "px";
@@ -249,21 +416,26 @@ function RTE_Plugin_Comments() {
249
416
  var wrapRange = editdoc.createRange();
250
417
  wrapRange.setStart(anchor.startContainer, anchor.startOffset);
251
418
  wrapRange.setEnd(anchor.endContainer, anchor.endOffset);
252
- var span = editdoc.createElement("span");
253
- span.className = "rte-comment";
254
- span.setAttribute("data-comment-id", id);
255
- span.setAttribute("data-comment-author", user.id);
256
- var fragment = wrapRange.extractContents();
257
- span.appendChild(fragment);
258
- wrapRange.insertNode(span);
259
- wrappedText = span.textContent;
260
- // Collapse caret after the span.
261
- var after = editdoc.createRange();
262
- after.setStartAfter(span);
263
- after.collapse(true);
264
- var sel = editor.getSelection();
265
- sel.removeAllRanges();
266
- sel.addRange(after);
419
+ // 2026-10-02 one highlight per block the selection crosses (wrapEachBlock)
420
+ var spans = wrapEachBlock(editdoc, wrapRange, function () {
421
+ var span = editdoc.createElement("span");
422
+ span.className = "rte-comment";
423
+ span.setAttribute("data-comment-id", id);
424
+ span.setAttribute("data-comment-author", user.id);
425
+ return span;
426
+ });
427
+ var parts = [];
428
+ for (var si = 0; si < spans.length; si++) parts.push(spans[si].textContent);
429
+ wrappedText = parts.join("\n");
430
+ if (spans.length) {
431
+ // Collapse caret after the last highlight.
432
+ var after = editdoc.createRange();
433
+ after.setStartAfter(spans[spans.length - 1]);
434
+ after.collapse(true);
435
+ var sel = editor.getSelection();
436
+ sel.removeAllRanges();
437
+ sel.addRange(after);
438
+ }
267
439
  } catch (err) {
268
440
  console.warn("comments: range wrap failed:", err);
269
441
  }
@@ -274,6 +446,7 @@ function RTE_Plugin_Comments() {
274
446
  changeType: "comment",
275
447
  author: user,
276
448
  text: text,
449
+ mentions: sanitizeMentions(options.mentions),
277
450
  originalText: wrappedText || (anchor && anchor.text) || "",
278
451
  status: "pending",
279
452
  sourceLabel: "Comment",
@@ -304,10 +477,91 @@ function RTE_Plugin_Comments() {
304
477
  return true;
305
478
  }
306
479
 
480
+ // 2026-10-15 reactions (BlockNote has them): a person adds an emoji to a comment and clicks it again to take it
481
+ // back. Kept with the comment in the shared review store as [{ emoji, users: [id] }]; only this set is accepted.
482
+ // tools/comments/comment-reactions.mjs
483
+ var REACTIONS = [
484
+ { emoji: "\uD83D\uDC4D", label: "Thumbs up", key: "cm_thumbsup" },
485
+ { emoji: "\u2705", label: "Done", key: "cm_done" },
486
+ { emoji: "\uD83C\uDF89", label: "Celebrate", key: "cm_celebrate" },
487
+ { emoji: "\uD83D\uDC40", label: "Looking", key: "cm_looking" },
488
+ { emoji: "\u2753", label: "Question", key: "cm_question" }
489
+ ];
490
+ // 2026-10-18 the comments UI reads config.text_ keys (language packs), English as the fallback; {0} / {1} fill a
491
+ // template. tools/i18n/comments-language.mjs
492
+ function cmText(key, fallback) {
493
+ var s = (config && config["text_" + key]) || fallback;
494
+ for (var i = 2; i < arguments.length; i++) s = s.split("{" + (i - 2) + "}").join(String(arguments[i]));
495
+ return s;
496
+ }
497
+
498
+ function reactionIndex(emoji) {
499
+ for (var i = 0; i < REACTIONS.length; i++) if (REACTIONS[i].emoji === emoji) return i;
500
+ return -1;
501
+ }
502
+
503
+ function reactToComment(commentId, emoji) {
504
+ if (!editor.reviewLedger || reactionIndex(emoji) < 0) return false;
505
+ var entry = editor.reviewLedger.get(commentId);
506
+ if (!entry || entry.changeType !== "comment") return false;
507
+ var me = String(getCurrentUser().id);
508
+ var list = Array.isArray(entry.reactions) ? entry.reactions : [];
509
+ var out = [], found = false;
510
+ for (var i = 0; i < list.length; i++) {
511
+ if (!list[i] || !Array.isArray(list[i].users)) continue;
512
+ var users = list[i].users.slice();
513
+ if (list[i].emoji === emoji) {
514
+ found = true;
515
+ var at = users.indexOf(me);
516
+ if (at >= 0) users.splice(at, 1); else users.push(me);
517
+ }
518
+ if (users.length) out.push({ emoji: list[i].emoji, users: users });
519
+ }
520
+ if (!found) out.push({ emoji: emoji, users: [me] });
521
+ editor.reviewLedger.update(commentId, { reactions: out });
522
+ if (sidebar && sidebar.isConnected) renderSidebar();
523
+ try { if (typeof editor.fireEvent === "function") editor.fireEvent("comment_reacted", { id: commentId, emoji: emoji, user: me }); } catch (e) { }
524
+ return true;
525
+ }
526
+
527
+ function renderReactions(entry, host) {
528
+ var row = host.createElement("div");
529
+ row.className = "rte-comment-reactions";
530
+ row.setAttribute("role", "group");
531
+ row.setAttribute("aria-label", cmText("cm_reactions", "Reactions"));
532
+ var me = String(getCurrentUser().id);
533
+ var list = Array.isArray(entry.reactions) ? entry.reactions : [];
534
+ REACTIONS.forEach(function (r, index) {
535
+ var users = [];
536
+ for (var i = 0; i < list.length; i++) if (list[i] && list[i].emoji === r.emoji && Array.isArray(list[i].users)) users = list[i].users;
537
+ var mine = users.indexOf(me) >= 0;
538
+ var b = host.createElement("button");
539
+ b.type = "button";
540
+ b.className = "rte-comment-reaction";
541
+ b.setAttribute("data-reaction", String(index));
542
+ b.setAttribute("aria-pressed", mine ? "true" : "false");
543
+ var rLabel = cmText(r.key, r.label);
544
+ b.setAttribute("aria-label", users.length === 1 ? cmText("cm_reactioncount", "{0}, {1} reaction", rLabel, 1) : cmText("cm_reactionscount", "{0}, {1} reactions", rLabel, users.length));
545
+ b.title = rLabel;
546
+ b.textContent = r.emoji + (users.length ? " " + users.length : "");
547
+ b.addEventListener("mousedown", function (e) { e.preventDefault(); });
548
+ b.addEventListener("click", function (e) {
549
+ e.preventDefault();
550
+ reactToComment(entry.id, r.emoji);
551
+ // the sidebar was rebuilt; keep keyboard focus on the same button
552
+ try {
553
+ var again = sidebar && sidebar.querySelector('[data-comment-ref="' + cssEscape(entry.id) + '"] .rte-comment-reaction[data-reaction="' + index + '"]');
554
+ if (again) again.focus();
555
+ } catch (err) { }
556
+ });
557
+ row.appendChild(b);
558
+ });
559
+ return row;
560
+ }
561
+
307
562
  function resolveComment(commentId) {
308
563
  if (!editor.reviewLedger) return false;
309
- var span = editor.getEditable().querySelector('[data-comment-id="' + cssEscape(commentId) + '"]');
310
- if (span) unwrapKeepChildren(span);
564
+ unwrapCommentSpans(commentId);
311
565
  editor.reviewLedger.update(commentId, { status: "resolved", decidedAt: Date.now() });
312
566
  if (sidebar && sidebar.isConnected) renderSidebar();
313
567
  try { if (typeof editor.fireEvent === "function") editor.fireEvent("comment_resolved", { id: commentId }); } catch (e) { }
@@ -316,14 +570,62 @@ function RTE_Plugin_Comments() {
316
570
 
317
571
  function deleteComment(commentId) {
318
572
  if (!editor.reviewLedger) return false;
319
- var span = editor.getEditable().querySelector('[data-comment-id="' + cssEscape(commentId) + '"]');
320
- if (span) unwrapKeepChildren(span);
573
+ unwrapCommentSpans(commentId);
321
574
  editor.reviewLedger.remove(commentId);
322
575
  if (sidebar && sidebar.isConnected) renderSidebar();
323
576
  return true;
324
577
  }
325
578
 
579
+ // 2026-10-17 select the next / previous commented text after / before the caret (wrapping) and show it in the
580
+ // sidebar. tools/comments/comment-navigation.mjs
581
+ function stepComment(dir) {
582
+ var doc = editor.getDocument(), ed = editor.getEditable();
583
+ if (!doc || !ed) return null;
584
+ var spans = ed.querySelectorAll("[data-comment-id]"), firsts = [], seen = {};
585
+ for (var i = 0; i < spans.length; i++) {
586
+ var cid = spans[i].getAttribute("data-comment-id");
587
+ if (!cid || seen[cid]) continue;
588
+ seen[cid] = true;
589
+ firsts.push(spans[i]);
590
+ }
591
+ if (!firsts.length) return null;
592
+ var sel = doc.getSelection(), cur = sel && sel.rangeCount ? sel.getRangeAt(0) : null;
593
+ var pick = null;
594
+ if (cur) {
595
+ if (dir > 0) {
596
+ for (var a = 0; a < firsts.length && !pick; a++) {
597
+ var ra = doc.createRange(); ra.selectNodeContents(firsts[a]);
598
+ if (ra.compareBoundaryPoints(Range.START_TO_END, cur) >= 0 && ra.compareBoundaryPoints(Range.START_TO_START, cur) > 0) pick = firsts[a];
599
+ else if (ra.compareBoundaryPoints(Range.START_TO_START, cur) >= 0 && !cur.collapsed && ra.compareBoundaryPoints(Range.START_TO_START, cur) !== 0) pick = firsts[a];
600
+ }
601
+ }
602
+ else {
603
+ for (var b = firsts.length - 1; b >= 0 && !pick; b--) {
604
+ var rb = doc.createRange(); rb.selectNodeContents(firsts[b]);
605
+ if (rb.compareBoundaryPoints(Range.START_TO_START, cur) < 0) pick = firsts[b];
606
+ }
607
+ }
608
+ }
609
+ if (!pick) pick = dir > 0 ? firsts[0] : firsts[firsts.length - 1];
610
+ var id = pick.getAttribute("data-comment-id");
611
+ var all = ed.querySelectorAll('[data-comment-id="' + cssEscape(id) + '"]');
612
+ var r = doc.createRange();
613
+ r.setStart(all[0], 0);
614
+ r.setEnd(all[all.length - 1], all[all.length - 1].childNodes.length);
615
+ try { editor.focus(); } catch (x) { /* best effort */ }
616
+ sel.removeAllRanges();
617
+ sel.addRange(r);
618
+ try { pick.scrollIntoView({ block: "nearest" }); } catch (x) { /* best effort */ }
619
+ focusComment(id);
620
+ return id;
621
+ }
622
+
623
+ // 2026-10-17 the highlighted comment survives a re-render of the sidebar (selecting its text re-renders it, which
624
+ // dropped the highlight at once). tools/comments/comment-navigation.mjs
625
+ var activeCommentId = null, activeCommentUntil = 0;
326
626
  function focusComment(commentId) {
627
+ activeCommentId = commentId;
628
+ activeCommentUntil = Date.now() + 1400;
327
629
  if (!sidebar || !sidebar.isConnected) openSidebar();
328
630
  setTimeout(function () {
329
631
  if (!sidebar) return;
@@ -331,11 +633,84 @@ function RTE_Plugin_Comments() {
331
633
  if (item) {
332
634
  item.scrollIntoView({ block: "nearest" });
333
635
  item.classList.add("rte-comment-item-active");
334
- setTimeout(function () { item.classList.remove("rte-comment-item-active"); }, 1400);
636
+ // a timer from an earlier highlight of the same comment must not clear a newer one
637
+ setTimeout(function () { if (activeCommentId !== commentId || Date.now() >= activeCommentUntil - 20) item.classList.remove("rte-comment-item-active"); }, 1400);
335
638
  }
336
639
  }, 30);
337
640
  }
338
641
 
642
+ // 2026-10-02 A comment on a selection that crosses blocks highlights the selected text of EACH
643
+ // block. One span went AROUND the blocks: two paragraphs became four
644
+ // (<p>al</p><span><p>pha</p><p>be</p></span><p>ta</p>), list items and table cells got a span
645
+ // inside <ul> / <tr>, and resolving the comment left the four paragraphs. The selection is cut
646
+ // into runs of text that share a block, and each run is wrapped in its own make() element; all
647
+ // carry the comment's id, so resolve and delete remove every one. A run starting or ending at
648
+ // the edge of an inline element (<b>) takes the whole element, so no empty <b></b> is left
649
+ // behind. Text inside a non-editable element (a chip) moves with its run.
650
+ // tools/comments/comments-across-blocks.mjs
651
+ var BLOCK_RE = /^(P|DIV|LI|UL|OL|DL|DT|DD|TABLE|TBODY|THEAD|TFOOT|TR|TD|TH|CAPTION|H[1-6]|PRE|BLOCKQUOTE|ADDRESS|FIGURE|FIGCAPTION|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|NAV|MAIN|DETAILS|SUMMARY|FORM|FIELDSET|LEGEND|HR|BODY)$/;
652
+ function blockOf(node) {
653
+ for (var n = node; n; n = n.parentNode)
654
+ if (n.nodeType === 1 && BLOCK_RE.test(n.nodeName)) return n;
655
+ return null;
656
+ }
657
+ function wrapEachBlock(doc, range, make) {
658
+ var root = range.commonAncestorContainer;
659
+ if (root.nodeType !== 1) root = root.parentNode;
660
+ var runs = [], cur = null;
661
+ var w = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
662
+ acceptNode: function (n) { return n.nodeType === 1 && n.getAttribute("contenteditable") === "false" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
663
+ });
664
+ for (var n = w.nextNode(); n; n = w.nextNode()) {
665
+ if (n.nodeType === 1) {
666
+ if (BLOCK_RE.test(n.nodeName)) cur = null;
667
+ continue;
668
+ }
669
+ if (!range.intersectsNode(n)) continue;
670
+ var a = n === range.startContainer ? range.startOffset : 0, b = n === range.endContainer ? range.endOffset : n.data.length;
671
+ if (b <= a) continue;
672
+ var blk = blockOf(n);
673
+ if (!cur || cur.block !== blk) {
674
+ cur = { block: blk, sn: n, so: a, text: "" };
675
+ runs.push(cur);
676
+ }
677
+ cur.en = n;
678
+ cur.eo = b;
679
+ cur.text += n.data.substring(a, b);
680
+ }
681
+ var out = [];
682
+ // from the last run back: wrapping one never moves the text of a run still to come
683
+ for (var i = runs.length - 1; i >= 0; i--) {
684
+ var run = runs[i];
685
+ if (!/\S/.test(run.text)) continue; // the line break between two blocks
686
+ var sub = doc.createRange();
687
+ sub.setStart(run.sn, run.so);
688
+ sub.setEnd(run.en, run.eo);
689
+ widenToInlineEdges(sub, run.block);
690
+ var el = make();
691
+ el.appendChild(sub.extractContents());
692
+ sub.insertNode(el);
693
+ out.unshift(el);
694
+ }
695
+ return out;
696
+ }
697
+ function widenToInlineEdges(range, block) {
698
+ var n = range.startContainer;
699
+ if (n.nodeType === 3 && range.startOffset === 0) {
700
+ while (n.parentNode && n.parentNode !== block && !n.previousSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
701
+ range.setStartBefore(n);
702
+ }
703
+ n = range.endContainer;
704
+ if (n.nodeType === 3 && range.endOffset === n.data.length) {
705
+ while (n.parentNode && n.parentNode !== block && !n.nextSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
706
+ range.setEndAfter(n);
707
+ }
708
+ }
709
+ function unwrapCommentSpans(commentId) {
710
+ var spans = editor.getEditable().querySelectorAll('[data-comment-id="' + cssEscape(commentId) + '"]');
711
+ for (var i = 0; i < spans.length; i++) unwrapKeepChildren(spans[i]);
712
+ }
713
+
339
714
  function unwrapKeepChildren(el) {
340
715
  var parent = el.parentNode;
341
716
  if (!parent) return;
@@ -367,7 +742,9 @@ function RTE_Plugin_Comments() {
367
742
  sidebar = host.createElement("div");
368
743
  sidebar.className = "rte-comment-sidebar" + (isEditorDark() ? " rte-comment-dark" : "");
369
744
  sidebar.setAttribute("role", "complementary");
370
- sidebar.setAttribute("aria-label", "Comments");
745
+ sidebar.setAttribute("aria-label", cmText("cm_comments", "Comments"));
746
+ // 2026-10-17 drag / arrow-key resizable (tools/ui/panel-resize.mjs)
747
+ setTimeout(function () { if (editor && sidebar && typeof editor.makePanelResizable === "function") editor.makePanelResizable(sidebar, { label: cmText("cm_resizepanel", "Resize comments panel") }); }, 0);
371
748
  var shell = getEditorShell();
372
749
  if (shell) {
373
750
  shell.parentNode.insertBefore(sidebar, shell.nextSibling);
@@ -403,14 +780,14 @@ function RTE_Plugin_Comments() {
403
780
  header.className = "rte-comment-sidebar-header";
404
781
  var title = host.createElement("div");
405
782
  title.className = "rte-comment-sidebar-title";
406
- title.textContent = "Comments";
783
+ title.textContent = cmText("cm_comments", "Comments");
407
784
  header.appendChild(title);
408
785
  var close = host.createElement("button");
409
786
  close.type = "button";
410
787
  close.className = "rte-comment-sidebar-close";
411
- close.setAttribute("aria-label", "Close");
788
+ close.setAttribute("aria-label", cmText("close", "Close"));
412
789
  close.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg>';
413
- close.addEventListener("mousedown", function (e) { e.preventDefault(); closeSidebar(); });
790
+ onActivate(close, closeSidebar);
414
791
  header.appendChild(close);
415
792
  sidebar.appendChild(header);
416
793
 
@@ -422,7 +799,7 @@ function RTE_Plugin_Comments() {
422
799
  if (!entries.length) {
423
800
  var empty = host.createElement("div");
424
801
  empty.className = "rte-comment-empty";
425
- empty.textContent = "No open comments. Select text and click the Comment button to start.";
802
+ empty.textContent = cmText("cm_empty", "No open comments. Select text and click the Comment button to start.");
426
803
  list.appendChild(empty);
427
804
  return;
428
805
  }
@@ -437,7 +814,11 @@ function RTE_Plugin_Comments() {
437
814
  function renderEntry(entry, host) {
438
815
  var wrap = host.createElement("div");
439
816
  wrap.className = "rte-comment-item";
440
- wrap.setAttribute("data-comment-ref", entry.id);
817
+ wrap.setAttribute("data-comment-ref", entry.id);
818
+ if (entry.id === activeCommentId && Date.now() < activeCommentUntil) {
819
+ wrap.classList.add("rte-comment-item-active");
820
+ setTimeout(function () { if (activeCommentId !== entry.id || Date.now() >= activeCommentUntil - 20) wrap.classList.remove("rte-comment-item-active"); }, Math.max(0, activeCommentUntil - Date.now()));
821
+ }
441
822
  wrap.setAttribute("role", "article");
442
823
 
443
824
  var topRow = host.createElement("div");
@@ -464,15 +845,15 @@ function RTE_Plugin_Comments() {
464
845
  var resolveBtn = host.createElement("button");
465
846
  resolveBtn.type = "button";
466
847
  resolveBtn.className = "rte-comment-action rte-comment-resolve";
467
- resolveBtn.textContent = "Resolve";
468
- resolveBtn.setAttribute("aria-label", "Resolve comment");
469
- resolveBtn.addEventListener("mousedown", function (e) { e.preventDefault(); resolveComment(entry.id); });
848
+ resolveBtn.textContent = cmText("cm_resolve", "Resolve");
849
+ resolveBtn.setAttribute("aria-label", cmText("cm_resolvecomment", "Resolve comment"));
850
+ onActivate(resolveBtn, function () { resolveComment(entry.id); });
470
851
  var delBtn = host.createElement("button");
471
852
  delBtn.type = "button";
472
853
  delBtn.className = "rte-comment-action rte-comment-delete";
473
- delBtn.textContent = "Delete";
474
- delBtn.setAttribute("aria-label", "Delete comment");
475
- delBtn.addEventListener("mousedown", function (e) { e.preventDefault(); deleteComment(entry.id); });
854
+ delBtn.textContent = cmText("delete", "Delete");
855
+ delBtn.setAttribute("aria-label", cmText("cm_deletecomment", "Delete comment"));
856
+ onActivate(delBtn, function () { deleteComment(entry.id); });
476
857
  actions.appendChild(resolveBtn);
477
858
  actions.appendChild(delBtn);
478
859
  topRow.appendChild(actions);
@@ -489,8 +870,9 @@ function RTE_Plugin_Comments() {
489
870
 
490
871
  var body = host.createElement("div");
491
872
  body.className = "rte-comment-body";
492
- body.textContent = entry.text;
873
+ appendTextWithMentions(host, body, entry.text, entry.mentions);
493
874
  wrap.appendChild(body);
875
+ wrap.appendChild(renderReactions(entry, host));
494
876
 
495
877
  var replies = Array.isArray(entry.replies) ? entry.replies : [];
496
878
  for (var i = 0; i < replies.length; i++) {
@@ -505,7 +887,7 @@ function RTE_Plugin_Comments() {
505
887
  repBody.className = "rte-comment-reply-body";
506
888
  var repName = host.createElement("div");
507
889
  repName.className = "rte-comment-reply-name";
508
- repName.textContent = ((rep.author && rep.author.name) || "User") + " \u00B7 " + relativeTime(rep.createdAt);
890
+ repName.textContent = ((rep.author && rep.author.name) || cmText("cm_user", "User")) + " \u00B7 " + relativeTime(rep.createdAt);
509
891
  var repText = host.createElement("div");
510
892
  repText.className = "rte-comment-reply-text";
511
893
  repText.textContent = rep.text;
@@ -522,8 +904,8 @@ function RTE_Plugin_Comments() {
522
904
  var replyInput = host.createElement("input");
523
905
  replyInput.type = "text";
524
906
  replyInput.className = "rte-comment-reply-input";
525
- replyInput.placeholder = "Reply...";
526
- replyInput.setAttribute("aria-label", "Reply to comment");
907
+ replyInput.placeholder = cmText("cm_reply", "Reply...");
908
+ replyInput.setAttribute("aria-label", cmText("cm_replytocomment", "Reply to comment"));
527
909
  replyInput.addEventListener("keydown", function (e) {
528
910
  if (e.key === "Enter" && !e.shiftKey) {
529
911
  e.preventDefault();
@@ -539,11 +921,13 @@ function RTE_Plugin_Comments() {
539
921
  }
540
922
 
541
923
  function scrollToSpan(commentId) {
542
- var span = editor.getEditable().querySelector('[data-comment-id="' + cssEscape(commentId) + '"]');
543
- if (!span) return;
544
- span.scrollIntoView({ block: "center", behavior: "smooth" });
545
- span.classList.add("rte-comment-flash");
546
- setTimeout(function () { span.classList.remove("rte-comment-flash"); }, 1200);
924
+ var spans = editor.getEditable().querySelectorAll('[data-comment-id="' + cssEscape(commentId) + '"]');
925
+ if (!spans.length) return;
926
+ spans[0].scrollIntoView({ block: "center", behavior: "smooth" });
927
+ for (var i = 0; i < spans.length; i++) spans[i].classList.add("rte-comment-flash");
928
+ setTimeout(function () {
929
+ for (var j = 0; j < spans.length; j++) spans[j].classList.remove("rte-comment-flash");
930
+ }, 1200);
547
931
  }
548
932
 
549
933
  function initialsOf(label) {
@@ -556,10 +940,10 @@ function RTE_Plugin_Comments() {
556
940
  function relativeTime(ts) {
557
941
  if (!ts) return "";
558
942
  var diff = Math.floor((Date.now() - ts) / 1000);
559
- if (diff < 60) return "just now";
560
- if (diff < 3600) return Math.floor(diff / 60) + "m ago";
561
- if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
562
- return Math.floor(diff / 86400) + "d ago";
943
+ if (diff < 60) return cmText("cm_justnow", "just now");
944
+ if (diff < 3600) return cmText("cm_minutesago", "{0}m ago", Math.floor(diff / 60));
945
+ if (diff < 86400) return cmText("cm_hoursago", "{0}h ago", Math.floor(diff / 3600));
946
+ return cmText("cm_daysago", "{0}d ago", Math.floor(diff / 86400));
563
947
  }
564
948
 
565
949
  function truncate(s, n) {
@@ -604,6 +988,11 @@ function RTE_Plugin_Comments() {
604
988
  ".rte-comment-reply-name{font-size:11px;color:#64748b}",
605
989
  ".rte-comment-reply-text{font-size:12px;color:#172033;line-height:1.45;white-space:pre-wrap;margin-top:2px}",
606
990
  ".rte-comment-reply-composer{margin-top:8px;display:flex}",
991
+ ".rte-comment-reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}",
992
+ ".rte-comment-reaction{display:inline-flex;align-items:center;gap:3px;min-height:24px;padding:1px 7px;border-radius:12px;border:1px solid rgba(148,163,184,.28);background:#fff;font:inherit;font-size:12px;line-height:1;color:#334155;cursor:pointer;opacity:.6}",
993
+ ".rte-comment-reaction:hover,.rte-comment-reaction:focus-visible{opacity:1;border-color:rgba(37,99,235,.38)}",
994
+ ".rte-comment-reaction:focus-visible{outline:2px solid rgba(37,99,235,.45);outline-offset:1px}",
995
+ ".rte-comment-reaction[aria-pressed=true]{opacity:1;background:#eef4ff;border-color:rgba(37,99,235,.45);color:#0f3f9f}",
607
996
  ".rte-comment-reply-input{flex:1;min-width:0;padding:7px 10px;border-radius:7px;border:1px solid rgba(148,163,184,.24);font:inherit;font-size:12px;background:#fff}",
608
997
  ".rte-comment-reply-input:focus,.rte-comment-composer-textarea:focus{outline:2px solid rgba(37,99,235,.18);border-color:rgba(37,99,235,.38)}",
609
998
  ".rte-comment-composer{position:absolute;z-index:2147483000;width:min(320px,calc(100vw - 24px));background:linear-gradient(180deg,#fff,#f8fbff);border:1px solid rgba(148,163,184,.22);box-shadow:0 14px 32px rgba(29,78,216,.12),0 2px 8px rgba(15,23,42,.08);border-radius:8px;padding:12px;font-family:Aptos,'Segoe UI',sans-serif;color:#172033}",
@@ -624,7 +1013,17 @@ function RTE_Plugin_Comments() {
624
1013
  ".rte-comment-dark .rte-comment-body,.rte-comment-dark .rte-comment-reply-text,.rte-comment-dark .rte-comment-composer-header{color:#e2e8f0}",
625
1014
  ".rte-comment-dark .rte-comment-reply-input,.rte-comment-dark .rte-comment-composer-textarea{background:#0f172a;color:#e2e8f0;border-color:#334155}",
626
1015
  ".rte-comment-dark .rte-comment-action,.rte-comment-dark .rte-comment-btn-ghost,.rte-comment-dark .rte-comment-sidebar-close{background:#0f172a;color:#cbd5e1;border-color:#334155}",
627
- ".rte-comment-dark .rte-comment-quote{background:rgba(245,158,11,.12);color:#fcd34d}"
1016
+ ".rte-comment-dark .rte-comment-quote{background:rgba(245,158,11,.12);color:#fcd34d}",
1017
+ ".rte-comment-dark .rte-comment-reaction{background:#0f172a;color:#cbd5e1;border-color:#334155}",
1018
+ ".rte-comment-dark .rte-comment-reaction[aria-pressed=true]{background:#1e3a8a;color:#e2e8f0;border-color:#3b82f6}",
1019
+ ".rte-comment-mentions{margin-top:4px;max-height:160px;overflow:auto;border:1px solid rgba(148,163,184,.3);border-radius:7px;background:#fff;box-shadow:0 6px 18px rgba(15,23,42,.12)}",
1020
+ ".rte-comment-mention-option{padding:6px 10px;font-size:13px;color:#172033;cursor:pointer}",
1021
+ ".rte-comment-mention-option.rte-comment-mention-active{background:#dbeafe}",
1022
+ ".rte-comment-mention{font-weight:600;color:#1d4ed8;background:rgba(37,99,235,.1);border-radius:4px;padding:0 2px}",
1023
+ ".rte-comment-dark .rte-comment-mentions{background:#0f172a;border-color:#334155}",
1024
+ ".rte-comment-dark .rte-comment-mention-option{color:#e2e8f0}",
1025
+ ".rte-comment-dark .rte-comment-mention-option.rte-comment-mention-active{background:#1e3a8a}",
1026
+ ".rte-comment-dark .rte-comment-mention{color:#93c5fd;background:rgba(147,197,253,.14)}"
628
1027
  ].join("\n");
629
1028
  host.head.appendChild(style);
630
1029
  // A <style> ELEMENT is governed by style-src: under a strict policy the browser