@richscripts/richtexteditor 2.14.0 → 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 (238) 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 +7113 -1111
  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 +7113 -1111
  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 +88 -38
  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 +53 -8
  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 +86 -4
  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 +131 -50
  229. package/richtexteditor/rte-headless.js +19 -4
  230. package/richtexteditor/rte-webcomponent.js +89 -16
  231. package/richtexteditor/rte.js +133 -52
  232. package/richtexteditor/runtime/colorpicker_more_ns.htm +93 -16
  233. package/scripts/build-angular.mjs +97 -0
  234. package/svelte/index.d.ts +2 -1
  235. package/svelte/index.js +80 -9
  236. package/vue/index.js +130 -34
  237. package/angular/component.ts +0 -297
  238. package/angular/index.ts +0 -24
@@ -66,6 +66,86 @@ function RTE_Plugin_RestrictedEditing() {
66
66
  refreshStyles();
67
67
  };
68
68
 
69
+ // 2026-10-14 The lockdown is enforced, not only drawn. A paragraph that holds a field stays contenteditable so the
70
+ // caret can reach the field, which left the protected text around the field editable; Delete over a locked
71
+ // selection, Alt+Arrow (move block) and Ctrl+Shift+D (copy block) changed protected text too, because the core
72
+ // handles those keys itself. While the lockdown is on, a key or an input whose selection is not entirely inside
73
+ // one marked region is cancelled at the editing window, in the capture phase, before anything else sees it.
74
+ // Backspace at a field's start and Delete at its end would reach outside it, so they are cancelled as well.
75
+ // tools/restricted/restricted-audit.mjs
76
+ var guardedWindows = [];
77
+ function regionOf(node) {
78
+ var el = node && node.nodeType === 1 ? node : (node && node.parentNode);
79
+ while (el && el.nodeType === 1) {
80
+ if (el.getAttribute && el.getAttribute(editableAttr) === "true") return el;
81
+ el = el.parentNode;
82
+ }
83
+ return null;
84
+ }
85
+ // the marked region that holds the whole selection, or null
86
+ function selectionRegion(doc) {
87
+ var sel = doc.getSelection();
88
+ if (!sel || sel.rangeCount === 0) return null;
89
+ var r = sel.getRangeAt(0);
90
+ var a = regionOf(r.startContainer), b = regionOf(r.endContainer);
91
+ return a && a === b ? { region: a, range: r } : null;
92
+ }
93
+ function atEdge(doc, region, range, atStart) {
94
+ var probe = doc.createRange();
95
+ probe.selectNodeContents(region);
96
+ if (atStart) probe.setEnd(range.startContainer, range.startOffset);
97
+ else probe.setStart(range.endContainer, range.endOffset);
98
+ return probe.toString() === "";
99
+ }
100
+ function editsDocument(e) {
101
+ var k = e.key || "";
102
+ if (e.isComposing) return true;
103
+ if (k === "Backspace" || k === "Delete" || k === "Enter" || k === "Tab") return true;
104
+ if (e.altKey && (k === "ArrowUp" || k === "ArrowDown")) return true;
105
+ if (e.ctrlKey || e.metaKey) {
106
+ var c = k.toLowerCase();
107
+ return c === "x" || c === "v" || c === "b" || c === "i" || c === "u" || c === "d";
108
+ }
109
+ return k.length === 1 && !e.altKey;
110
+ }
111
+ function guardKey(e) {
112
+ if (!enabled || !editsDocument(e)) return;
113
+ var doc = e.target && (e.target.ownerDocument || e.target);
114
+ if (!doc || !doc.getSelection) return;
115
+ var hit = selectionRegion(doc);
116
+ var refuse = !hit;
117
+ if (hit && hit.range.collapsed) {
118
+ if (e.key === "Backspace" && atEdge(doc, hit.region, hit.range, true)) refuse = true;
119
+ if (e.key === "Delete" && atEdge(doc, hit.region, hit.range, false)) refuse = true;
120
+ }
121
+ if (hit && (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown"))) refuse = true;
122
+ if (hit && (e.ctrlKey || e.metaKey) && e.shiftKey && String(e.key).toLowerCase() === "d") refuse = true;
123
+ if (!refuse) return;
124
+ e.preventDefault();
125
+ e.stopImmediatePropagation();
126
+ }
127
+ function guardInput(e) {
128
+ if (!enabled) return;
129
+ var type = e.inputType || "";
130
+ if (type.indexOf("history") === 0) return;
131
+ var doc = e.target && (e.target.ownerDocument || e.target);
132
+ if (!doc || !doc.getSelection) return;
133
+ if (selectionRegion(doc)) return;
134
+ e.preventDefault();
135
+ e.stopImmediatePropagation();
136
+ }
137
+ function ensureGuards() {
138
+ var body = getEditable();
139
+ var win = body && body.ownerDocument && body.ownerDocument.defaultView;
140
+ if (!win || guardedWindows.indexOf(win) >= 0) return;
141
+ guardedWindows.push(win);
142
+ win.addEventListener("keydown", guardKey, true);
143
+ win.addEventListener("beforeinput", guardInput, true);
144
+ win.addEventListener("cut", function (e) { if (enabled && !selectionRegion(e.target.ownerDocument || e.target)) { e.preventDefault(); e.stopImmediatePropagation(); } }, true);
145
+ win.addEventListener("paste", function (e) { if (enabled && !selectionRegion(e.target.ownerDocument || e.target)) { e.preventDefault(); e.stopImmediatePropagation(); } }, true);
146
+ win.addEventListener("drop", function (e) { if (enabled) { e.preventDefault(); e.stopImmediatePropagation(); } }, true);
147
+ }
148
+
69
149
  function getEditable() {
70
150
  return editor.getEditable ? editor.getEditable() : null;
71
151
  }
@@ -111,6 +191,7 @@ function RTE_Plugin_RestrictedEditing() {
111
191
  }
112
192
  // Make the body itself NOT editable; rely on per-element contenteditable.
113
193
  body.setAttribute("contenteditable", "false");
194
+ ensureGuards();
114
195
  refreshStyles();
115
196
  return true;
116
197
  }
@@ -139,6 +220,38 @@ function RTE_Plugin_RestrictedEditing() {
139
220
  if (!sel || sel.rangeCount === 0) return null;
140
221
  var range = sel.getRangeAt(0);
141
222
  if (range.collapsed) return null;
223
+ // 2026-10-02 A selection that crosses blocks marks the selected text of EACH block. One span
224
+ // went AROUND the blocks: two paragraphs became four (<p>al</p><span><p>pha</p><p>be</p>
225
+ // </span><p>ta</p>), list items and table cells got a span inside <ul> / <tr> - and that
226
+ // span, a direct child of the body, was LOCKED by enable() (it looks for a region among a
227
+ // child's descendants): the region could not be typed into. Returns the first region.
228
+ // tools/restricted/mark-across-blocks.mjs
229
+ var runs = crossBlockRuns(doc, range);
230
+ if (runs) {
231
+ var regions = [];
232
+ // from the last run back: wrapping one never moves the text of a run still to come
233
+ for (var ri = runs.length - 1; ri >= 0; ri--) {
234
+ var sub = doc.createRange();
235
+ sub.setStart(runs[ri].sn, runs[ri].so);
236
+ sub.setEnd(runs[ri].en, runs[ri].eo);
237
+ widenToInlineEdges(sub, runs[ri].block);
238
+ var part = doc.createElement("span");
239
+ part.setAttribute(editableAttr, "true");
240
+ if (options.label) part.setAttribute("aria-label", options.label);
241
+ part.appendChild(sub.extractContents());
242
+ sub.insertNode(part);
243
+ regions.unshift(part);
244
+ }
245
+ if (!regions.length) return null;
246
+ var all = doc.createRange();
247
+ all.setStart(regions[0], 0);
248
+ all.setEnd(regions[regions.length - 1], regions[regions.length - 1].childNodes.length);
249
+ sel.removeAllRanges();
250
+ sel.addRange(all);
251
+ refreshStyles();
252
+ if (enabled) enable();
253
+ return regions[0];
254
+ }
142
255
  var span = doc.createElement("span");
143
256
  span.setAttribute(editableAttr, "true");
144
257
  if (options.label) span.setAttribute("aria-label", options.label);
@@ -161,6 +274,59 @@ function RTE_Plugin_RestrictedEditing() {
161
274
  return span;
162
275
  }
163
276
 
277
+ // The runs of selected text that share a block, when the selection crosses blocks; null when it
278
+ // starts and ends in one block (that keeps the single-span path). Whitespace-only runs (the line
279
+ // break between two blocks) are dropped; text inside a non-editable element moves with its run.
280
+ 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)$/;
281
+ function blockOf(node) {
282
+ for (var n = node; n; n = n.parentNode)
283
+ if (n.nodeType === 1 && BLOCK_RE.test(n.nodeName)) return n;
284
+ return null;
285
+ }
286
+ function crossBlockRuns(doc, range) {
287
+ if (blockOf(range.startContainer) === blockOf(range.endContainer)) return null;
288
+ var root = range.commonAncestorContainer;
289
+ if (root.nodeType !== 1) root = root.parentNode;
290
+ var runs = [], cur = null;
291
+ var w = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
292
+ acceptNode: function (n) { return n.nodeType === 1 && n.getAttribute("contenteditable") === "false" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
293
+ });
294
+ for (var n = w.nextNode(); n; n = w.nextNode()) {
295
+ if (n.nodeType === 1) {
296
+ if (BLOCK_RE.test(n.nodeName)) cur = null;
297
+ continue;
298
+ }
299
+ if (!range.intersectsNode(n)) continue;
300
+ var a = n === range.startContainer ? range.startOffset : 0, b = n === range.endContainer ? range.endOffset : n.data.length;
301
+ if (b <= a) continue;
302
+ var blk = blockOf(n);
303
+ if (!cur || cur.block !== blk) {
304
+ cur = { block: blk, sn: n, so: a, text: "" };
305
+ runs.push(cur);
306
+ }
307
+ cur.en = n;
308
+ cur.eo = b;
309
+ cur.text += n.data.substring(a, b);
310
+ }
311
+ var keep = [];
312
+ for (var i = 0; i < runs.length; i++) if (/\S/.test(runs[i].text)) keep.push(runs[i]);
313
+ return keep;
314
+ }
315
+ // A run starting or ending at the edge of an inline element (<b>) takes the whole element, so
316
+ // no empty <b></b> is left behind by extractContents.
317
+ function widenToInlineEdges(range, block) {
318
+ var n = range.startContainer;
319
+ if (n.nodeType === 3 && range.startOffset === 0) {
320
+ while (n.parentNode && n.parentNode !== block && !n.previousSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
321
+ range.setStartBefore(n);
322
+ }
323
+ n = range.endContainer;
324
+ if (n.nodeType === 3 && range.endOffset === n.data.length) {
325
+ while (n.parentNode && n.parentNode !== block && !n.nextSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
326
+ range.setEndAfter(n);
327
+ }
328
+ }
329
+
164
330
  function unmark(node) {
165
331
  node = node || (function () {
166
332
  var body = getEditable();
@@ -1,2 +1,2 @@
1
- window.RTE_DefaultConfig||(window.RTE_DefaultConfig={}),RTE_DefaultConfig.svgCode_restrictedediting||(RTE_DefaultConfig.svgCode_restrictedediting='<svg viewBox="0 0 24 24" fill="none" stroke="#5F6368" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>'),RTE_DefaultConfig.plugin_restrictedediting=RTE_Plugin_RestrictedEditing;function RTE_Plugin_RestrictedEditing(){var v=this,l,o,u=!1,d="data-rte-editable";v.PluginName="RestrictedEditing",v.InitConfig=function(e){l=e,l.text_restrictedediting=l.text_restrictedediting||"Restricted editing",l.text_restrictedediting_mark=l.text_restrictedediting_mark||"Mark as editable",l.text_restrictedediting_next=l.text_restrictedediting_next||"Next editable region",l.text_restrictedediting_hint=l.text_restrictedediting_hint||"Lock the document; only marked regions accept input."},v.InitEditor=function(e){o=e,u=!!l.restrictedEditingMode,o.restrictedEditing={isEnabled:function(){return u},enable:g,disable:_,toggle:function(){return u?_():g()},markSelection:m,unmark:x,list:p,goToNext:function(){return y(1)},goToPrev:function(){return y(-1)}},o.attachEvent("exec_command_restrictedediting",function(t){t.returnValue=!0,t.stopBubble=!0,o.restrictedEditing.toggle()}),u&&g(),o.attachEvent("change",f),f()};function c(){return o.getEditable?o.getEditable():null}function f(){var e=c();if(e){var t=e.ownerDocument;if(!(!t||t.getElementById("__rte_restrictedediting_styles"))){var r=t.createElement("style");r.id="__rte_restrictedediting_styles",r.textContent=["["+d+"='true']{background:rgba(34,197,94,.10);outline:1px dashed rgba(34,197,94,.55);outline-offset:1px;border-radius:2px;padding:1px 2px}","body.rte-restricted [contenteditable='false']{cursor:not-allowed}","body.rte-restricted ["+d+"='true']{background:rgba(34,197,94,.16)}"].join(`
2
- `),(t.head||t.getElementsByTagName("head")[0]).appendChild(r),o&&typeof o.ensureStyleSheetLive=="function"&&o.ensureStyleSheetLive(t,r,"__rte_restrictedediting_styles",r.textContent)}}}function g(){var e=c();if(!e)return!1;u=!0,e.classList.add("rte-restricted");for(var t=0;t<e.children.length;t++){var r=e.children[t];r.querySelector("["+d+"='true']")?(r.setAttribute("contenteditable","true"),r.setAttribute(d+"-wrapper","true")):r.setAttribute("contenteditable","false")}return e.setAttribute("contenteditable","false"),f(),!0}function _(){var e=c();if(!e)return!1;u=!1,e.classList.remove("rte-restricted");for(var t=e.querySelectorAll("[contenteditable]"),r=0;r<t.length;r++)t[r].removeAttribute("contenteditable"),t[r].removeAttribute(d+"-wrapper");return e.setAttribute("contenteditable","true"),!0}function m(e){e=e||{};var t=c();if(!t)return null;var r=t.ownerDocument,a=r.getSelection();if(!a||a.rangeCount===0)return null;var n=a.getRangeAt(0);if(n.collapsed)return null;var i=r.createElement("span");i.setAttribute(d,"true"),e.label&&i.setAttribute("aria-label",e.label);try{n.surroundContents(i)}catch{var h=n.extractContents();i.appendChild(h),n.insertNode(i)}var s=r.createRange();return s.selectNodeContents(i),a.removeAllRanges(),a.addRange(s),f(),u&&g(),i}function x(e){if(e=e||(function(){var r=c(),a=r&&r.ownerDocument.getSelection();if(!a||a.rangeCount===0)return null;for(var n=a.anchorNode;n&&n!==r;){if(n.nodeType===1&&n.getAttribute&&n.getAttribute(d)==="true")return n;n=n.parentNode}return null})(),!e)return!1;for(var t=e.parentNode;e.firstChild;)t.insertBefore(e.firstChild,e);return t.removeChild(e),u&&g(),!0}function p(){var e=c();return e?Array.prototype.slice.call(e.querySelectorAll("["+d+"='true']")):[]}function y(e){var t=p();if(t.length===0)return null;var r=c(),a=r&&r.ownerDocument.getSelection(),n=null;if(a&&a.anchorNode)for(var i=a.anchorNode;i&&i!==r;){if(i.nodeType===1&&i.getAttribute&&i.getAttribute(d)==="true"){n=i;break}i=i.parentNode}var h=n?t.indexOf(n):-1,s=t[(h+e+t.length)%t.length];if(!s)return null;var b=r.ownerDocument.createRange();return b.selectNodeContents(s),b.collapse(!1),a.removeAllRanges(),a.addRange(b),s.scrollIntoView({block:"center",behavior:"smooth"}),s}}
1
+ window.RTE_DefaultConfig||(window.RTE_DefaultConfig={}),RTE_DefaultConfig.svgCode_restrictedediting||(RTE_DefaultConfig.svgCode_restrictedediting='<svg viewBox="0 0 24 24" fill="none" stroke="#5F6368" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>'),RTE_DefaultConfig.plugin_restrictedediting=RTE_Plugin_RestrictedEditing;function RTE_Plugin_RestrictedEditing(){var N=this,c,g,d=!1,f="data-rte-editable";N.PluginName="RestrictedEditing",N.InitConfig=function(t){c=t,c.text_restrictedediting=c.text_restrictedediting||"Restricted editing",c.text_restrictedediting_mark=c.text_restrictedediting_mark||"Mark as editable",c.text_restrictedediting_next=c.text_restrictedediting_next||"Next editable region",c.text_restrictedediting_hint=c.text_restrictedediting_hint||"Lock the document; only marked regions accept input."},N.InitEditor=function(t){g=t,d=!!c.restrictedEditingMode,g.restrictedEditing={isEnabled:function(){return d},enable:b,disable:D,toggle:function(){return d?D():b()},markSelection:B,unmark:K,list:R,goToNext:function(){return x(1)},goToPrev:function(){return x(-1)}},g.attachEvent("exec_command_restrictedediting",function(r){r.returnValue=!0,r.stopBubble=!0,g.restrictedEditing.toggle()}),d&&b(),g.attachEvent("change",h),h()};var A=[];function _(t){for(var r=t&&t.nodeType===1?t:t&&t.parentNode;r&&r.nodeType===1;){if(r.getAttribute&&r.getAttribute(f)==="true")return r;r=r.parentNode}return null}function y(t){var r=t.getSelection();if(!r||r.rangeCount===0)return null;var e=r.getRangeAt(0),n=_(e.startContainer),i=_(e.endContainer);return n&&n===i?{region:n,range:e}:null}function w(t,r,e,n){var i=t.createRange();return i.selectNodeContents(r),n?i.setEnd(e.startContainer,e.startOffset):i.setStart(e.endContainer,e.endOffset),i.toString()===""}function k(t){var r=t.key||"";if(t.isComposing||r==="Backspace"||r==="Delete"||r==="Enter"||r==="Tab"||t.altKey&&(r==="ArrowUp"||r==="ArrowDown"))return!0;if(t.ctrlKey||t.metaKey){var e=r.toLowerCase();return e==="x"||e==="v"||e==="b"||e==="i"||e==="u"||e==="d"}return r.length===1&&!t.altKey}function O(t){if(!(!d||!k(t))){var r=t.target&&(t.target.ownerDocument||t.target);if(!(!r||!r.getSelection)){var e=y(r),n=!e;e&&e.range.collapsed&&(t.key==="Backspace"&&w(r,e.region,e.range,!0)&&(n=!0),t.key==="Delete"&&w(r,e.region,e.range,!1)&&(n=!0)),e&&t.altKey&&(t.key==="ArrowUp"||t.key==="ArrowDown")&&(n=!0),e&&(t.ctrlKey||t.metaKey)&&t.shiftKey&&String(t.key).toLowerCase()==="d"&&(n=!0),n&&(t.preventDefault(),t.stopImmediatePropagation())}}}function L(t){if(d){var r=t.inputType||"";if(r.indexOf("history")!==0){var e=t.target&&(t.target.ownerDocument||t.target);!e||!e.getSelection||y(e)||(t.preventDefault(),t.stopImmediatePropagation())}}}function I(){var t=v(),r=t&&t.ownerDocument&&t.ownerDocument.defaultView;!r||A.indexOf(r)>=0||(A.push(r),r.addEventListener("keydown",O,!0),r.addEventListener("beforeinput",L,!0),r.addEventListener("cut",function(e){d&&!y(e.target.ownerDocument||e.target)&&(e.preventDefault(),e.stopImmediatePropagation())},!0),r.addEventListener("paste",function(e){d&&!y(e.target.ownerDocument||e.target)&&(e.preventDefault(),e.stopImmediatePropagation())},!0),r.addEventListener("drop",function(e){d&&(e.preventDefault(),e.stopImmediatePropagation())},!0))}function v(){return g.getEditable?g.getEditable():null}function h(){var t=v();if(t){var r=t.ownerDocument;if(!(!r||r.getElementById("__rte_restrictedediting_styles"))){var e=r.createElement("style");e.id="__rte_restrictedediting_styles",e.textContent=["["+f+"='true']{background:rgba(34,197,94,.10);outline:1px dashed rgba(34,197,94,.55);outline-offset:1px;border-radius:2px;padding:1px 2px}","body.rte-restricted [contenteditable='false']{cursor:not-allowed}","body.rte-restricted ["+f+"='true']{background:rgba(34,197,94,.16)}"].join(`
2
+ `),(r.head||r.getElementsByTagName("head")[0]).appendChild(e),g&&typeof g.ensureStyleSheetLive=="function"&&g.ensureStyleSheetLive(r,e,"__rte_restrictedediting_styles",e.textContent)}}}function b(){var t=v();if(!t)return!1;d=!0,t.classList.add("rte-restricted");for(var r=0;r<t.children.length;r++){var e=t.children[r];e.querySelector("["+f+"='true']")?(e.setAttribute("contenteditable","true"),e.setAttribute(f+"-wrapper","true")):e.setAttribute("contenteditable","false")}return t.setAttribute("contenteditable","false"),I(),h(),!0}function D(){var t=v();if(!t)return!1;d=!1,t.classList.remove("rte-restricted");for(var r=t.querySelectorAll("[contenteditable]"),e=0;e<r.length;e++)r[e].removeAttribute("contenteditable"),r[e].removeAttribute(f+"-wrapper");return t.setAttribute("contenteditable","true"),!0}function B(t){t=t||{};var r=v();if(!r)return null;var e=r.ownerDocument,n=e.getSelection();if(!n||n.rangeCount===0)return null;var i=n.getRangeAt(0);if(i.collapsed)return null;var o=F(e,i);if(o){for(var a=[],u=o.length-1;u>=0;u--){var l=e.createRange();l.setStart(o[u].sn,o[u].so),l.setEnd(o[u].en,o[u].eo),P(l,o[u].block);var p=e.createElement("span");p.setAttribute(f,"true"),t.label&&p.setAttribute("aria-label",t.label),p.appendChild(l.extractContents()),l.insertNode(p),a.unshift(p)}if(!a.length)return null;var E=e.createRange();return E.setStart(a[0],0),E.setEnd(a[a.length-1],a[a.length-1].childNodes.length),n.removeAllRanges(),n.addRange(E),h(),d&&b(),a[0]}var s=e.createElement("span");s.setAttribute(f,"true"),t.label&&s.setAttribute("aria-label",t.label);try{i.surroundContents(s)}catch{var C=i.extractContents();s.appendChild(C),i.insertNode(s)}var S=e.createRange();return S.selectNodeContents(s),n.removeAllRanges(),n.addRange(S),h(),d&&b(),s}var m=/^(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)$/;function T(t){for(var r=t;r;r=r.parentNode)if(r.nodeType===1&&m.test(r.nodeName))return r;return null}function F(t,r){if(T(r.startContainer)===T(r.endContainer))return null;var e=r.commonAncestorContainer;e.nodeType!==1&&(e=e.parentNode);for(var n=[],i=null,o=t.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT,{acceptNode:function(C){return C.nodeType===1&&C.getAttribute("contenteditable")==="false"?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),a=o.nextNode();a;a=o.nextNode()){if(a.nodeType===1){m.test(a.nodeName)&&(i=null);continue}if(r.intersectsNode(a)){var u=a===r.startContainer?r.startOffset:0,l=a===r.endContainer?r.endOffset:a.data.length;if(!(l<=u)){var p=T(a);(!i||i.block!==p)&&(i={block:p,sn:a,so:u,text:""},n.push(i)),i.en=a,i.eo=l,i.text+=a.data.substring(u,l)}}}for(var E=[],s=0;s<n.length;s++)/\S/.test(n[s].text)&&E.push(n[s]);return E}function P(t,r){var e=t.startContainer;if(e.nodeType===3&&t.startOffset===0){for(;e.parentNode&&e.parentNode!==r&&!e.previousSibling&&!m.test(e.parentNode.nodeName);)e=e.parentNode;t.setStartBefore(e)}if(e=t.endContainer,e.nodeType===3&&t.endOffset===e.data.length){for(;e.parentNode&&e.parentNode!==r&&!e.nextSibling&&!m.test(e.parentNode.nodeName);)e=e.parentNode;t.setEndAfter(e)}}function K(t){if(t=t||(function(){var e=v(),n=e&&e.ownerDocument.getSelection();if(!n||n.rangeCount===0)return null;for(var i=n.anchorNode;i&&i!==e;){if(i.nodeType===1&&i.getAttribute&&i.getAttribute(f)==="true")return i;i=i.parentNode}return null})(),!t)return!1;for(var r=t.parentNode;t.firstChild;)r.insertBefore(t.firstChild,t);return r.removeChild(t),d&&b(),!0}function R(){var t=v();return t?Array.prototype.slice.call(t.querySelectorAll("["+f+"='true']")):[]}function x(t){var r=R();if(r.length===0)return null;var e=v(),n=e&&e.ownerDocument.getSelection(),i=null;if(n&&n.anchorNode)for(var o=n.anchorNode;o&&o!==e;){if(o.nodeType===1&&o.getAttribute&&o.getAttribute(f)==="true"){i=o;break}o=o.parentNode}var a=i?r.indexOf(i):-1,u=r[(a+t+r.length)%r.length];if(!u)return null;var l=e.ownerDocument.createRange();return l.selectNodeContents(u),l.collapse(!1),n.removeAllRanges(),n.addRange(l),u.scrollIntoView({block:"center",behavior:"smooth"}),u}}
@@ -75,7 +75,8 @@ function RTE_Plugin_RevisionHistory() {
75
75
 
76
76
  // Auto-snapshot on idle typing (opt-in).
77
77
  if (config.revisionHistoryAutoSnapshotMs > 0) {
78
- editor.getDocument().addEventListener("input", function () {
78
+ // 2026-10-17 bound through the core (follows the live editing document). tools/keyboard/plugin-listeners-live.mjs
79
+ (editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("input", fn, true); } : function (fn) { editor.getDocument().addEventListener("input", fn, true); })(function () {
79
80
  if (autoSnapshotTimer) clearTimeout(autoSnapshotTimer);
80
81
  autoSnapshotTimer = setTimeout(function () {
81
82
  var currentHtml = editor.getHTML();
@@ -83,16 +84,18 @@ function RTE_Plugin_RevisionHistory() {
83
84
  snapshot("Auto", { auto: true });
84
85
  }
85
86
  }, config.revisionHistoryAutoSnapshotMs);
86
- }, true);
87
+ });
87
88
  }
88
89
 
89
- // Keyboard shortcut: Ctrl/Cmd + Shift + H opens the browser.
90
- editor.getDocument().addEventListener("keydown", function (e) {
91
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "H" || e.key === "h")) {
90
+ // Keyboard shortcut: Ctrl/Cmd + Alt + Shift + H opens the browser (Google Docs' version-history keys).
91
+ // 2026-10-15 it was Ctrl+Shift+H, which is also Highlight: highlighting opened this modal over the page and,
92
+ // once the dialog took focus, the highlight could not be toggled off. tools/keyboard/shortcut-collisions.mjs
93
+ (editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("keydown", fn, true); } : function (fn) { editor.getDocument().addEventListener("keydown", fn, true); })(function (e) {
94
+ if ((e.ctrlKey || e.metaKey) && e.altKey && e.shiftKey && (e.key === "H" || e.key === "h" || e.code === "KeyH")) {
92
95
  e.preventDefault();
93
96
  openDialog();
94
97
  }
95
- }, true);
98
+ });
96
99
  };
97
100
 
98
101
  function appendToolbarCommand(toolbar, item) {
@@ -106,9 +109,50 @@ function RTE_Plugin_RevisionHistory() {
106
109
  return { id: "user", name: "User", color: "#64748b" };
107
110
  }
108
111
 
112
+ // 2026-10-03 One store per DOCUMENT. The key was "RTE.Revisions." + (aiToolkitPersistenceKey ||
113
+ // "default"), so without that key every editor and every document on a site shared one list: two
114
+ // editors on a page overwrote each other's saved versions, and a version taken on one document was
115
+ // listed - and restorable, replacing the content - on every other one (localdraft had the same
116
+ // defect). An explicit key wins (config.revisionHistoryKey, or aiToolkitPersistenceKey as
117
+ // before); otherwise the page (path + query, which tells /edit?id=1 from ?id=2) and, with several
118
+ // editors on it, the editor's own name. tools/revisions/revision-keys.mjs
119
+ var cachedStoreKey = null;
120
+ var LEGACY_STORE_KEY = "RTE.Revisions.default";
121
+
109
122
  function getStoreKey() {
110
- var key = config.aiToolkitPersistenceKey || "default";
111
- return "RTE.Revisions." + key;
123
+ if (config.revisionHistoryKey) return "RTE.Revisions." + config.revisionHistoryKey;
124
+ if (config.aiToolkitPersistenceKey) return "RTE.Revisions." + config.aiToolkitPersistenceKey;
125
+ if (cachedStoreKey) return cachedStoreKey;
126
+ var page = "default";
127
+ try { page = location.pathname + location.search; } catch (e) { }
128
+ var name = "";
129
+ try {
130
+ var inp = editor.input;
131
+ if (inp) name = inp.id || inp.getAttribute("name") || "";
132
+ if (!name && editor.container && editor.container.id) name = editor.container.id;
133
+ if (!name) {
134
+ var all = document.querySelectorAll(".richtexteditor");
135
+ for (var i = 1; i < all.length; i++) if (all[i] === editor.container) { name = "editor-" + i; break; }
136
+ }
137
+ } catch (e) { }
138
+ return (cachedStoreKey = "RTE.Revisions." + page + (name ? "#" + name : ""));
139
+ }
140
+
141
+ // Versions saved under the old shared key belong to this editor only when it is the page's only
142
+ // one (checked once every editor on the page exists); then they move to this editor's key.
143
+ function takeLegacyRevisions() {
144
+ if (config.revisionHistoryKey || config.aiToolkitPersistenceKey || getStore().length) return;
145
+ try {
146
+ if (!window.localStorage || document.querySelectorAll(".richtexteditor").length !== 1) return;
147
+ var key = getStoreKey(), raw = window.localStorage.getItem(LEGACY_STORE_KEY);
148
+ if (!raw || key === LEGACY_STORE_KEY || window.localStorage.getItem(key)) return;
149
+ var parsed = JSON.parse(raw);
150
+ if (!Array.isArray(parsed)) return;
151
+ window.localStorage.setItem(key, raw);
152
+ window.localStorage.removeItem(LEGACY_STORE_KEY);
153
+ editor.__revisions = parsed;
154
+ if (dialog && dialog.isConnected) renderDialog();
155
+ } catch (ignore) { }
112
156
  }
113
157
 
114
158
  function getStore() {
@@ -188,6 +232,9 @@ function RTE_Plugin_RevisionHistory() {
188
232
  }
189
233
 
190
234
  function restoreStore() {
235
+ // other editors on the page are created after this one: decide about the old shared
236
+ // versions once they all exist
237
+ setTimeout(takeLegacyRevisions, 0);
191
238
  try {
192
239
  if (!window.localStorage) return;
193
240
  var raw = window.localStorage.getItem(getStoreKey());
@@ -222,10 +269,16 @@ function RTE_Plugin_RevisionHistory() {
222
269
 
223
270
  // Prompt the user for a snapshot name via native window.prompt, then take a snapshot.
224
271
  // Returns the entry (or null if the user cancelled).
272
+ // 2026-10-18 the history's words read config.text_ keys (language packs), English as the fallback; {0} fills a
273
+ // template. tools/i18n/revision-history-language.mjs
274
+ function rhText(key, fallback, arg) {
275
+ var s = (config && config["text_" + key]) || fallback;
276
+ return arg === undefined ? s : s.split("{0}").join(String(arg));
277
+ }
225
278
  function promptAndSnapshot(defaultLabel) {
226
279
  var iframeDoc = editor.iframe ? editor.iframe.ownerDocument : document;
227
280
  var win = iframeDoc.defaultView || window;
228
- var value = win.prompt("Name this version (e.g. \"Draft sent to legal\"):", defaultLabel || "");
281
+ var value = win.prompt(rhText("rh_nameversion", "Name this version (e.g. \"Draft sent to legal\"):"), defaultLabel || "");
229
282
  if (value === null) return null;
230
283
  var trimmed = (value || "").trim();
231
284
  if (!trimmed) return null;
@@ -277,13 +330,31 @@ function RTE_Plugin_RevisionHistory() {
277
330
  opts = opts || {};
278
331
  var entry = findById(id);
279
332
  if (!entry) return false;
333
+ // 2026-10-03 Restoring replaces the content with setHTMLCode, which is not an undo step, and saved
334
+ // nothing first - restoring a version by mistake threw away everything written since it, for
335
+ // good. The current content is now kept as a (named) version first, unless the newest version
336
+ // already holds it; config.revisionHistorySnapshotBeforeRestore: false for the old behaviour.
337
+ // tools/revisions/restore-safety.mjs
338
+ var keep = config.revisionHistorySnapshotBeforeRestore !== false;
280
339
  if (!opts.skipConfirm) {
281
340
  var doc = editor.iframe ? editor.iframe.ownerDocument : document;
282
- if (!doc.defaultView.confirm("Restore this version? Your current content will be replaced (this itself is not auto-snapshotted).")) {
341
+ var message = keep
342
+ ? rhText("rh_restorekeep", "Restore this version? Your current content is kept as a version first, so you can go back to it.")
343
+ : rhText("rh_restorereplace", "Restore this version? Your current content will be replaced (this itself is not auto-snapshotted).");
344
+ if (!doc.defaultView.confirm(message)) {
283
345
  return false;
284
346
  }
285
347
  }
286
- if (editor.setHTMLCode) editor.setHTMLCode(entry.html);
348
+ if (keep) {
349
+ var current = editor.getHTML ? editor.getHTML() : (editor.getHTMLCode ? editor.getHTMLCode() : "");
350
+ var newest = null, store = getStore();
351
+ for (var i = 0; i < store.length; i++) if (!newest || (store[i].createdAt || 0) >= (newest.createdAt || 0)) newest = store[i];
352
+ if (current !== entry.html && (!newest || newest.html !== current)) {
353
+ snapshot(rhText("rh_beforerestore", "Before restoring {0}", entry.label), { beforeRestore: entry.id });
354
+ }
355
+ }
356
+ // undoable: Ctrl+Z after a restore gives back exactly the document it replaced (import-undo.mjs)
357
+ if (editor.setHTMLCode) editor.setHTMLCode(entry.html, { undoable: true });
287
358
  else if (editor.setHTML) editor.setHTML(entry.html);
288
359
  lastSnapshotHtml = entry.html;
289
360
  if (dialog && dialog.isConnected) closeDialog();
@@ -406,13 +477,68 @@ function RTE_Plugin_RevisionHistory() {
406
477
  panel.className = "rte-rev-panel";
407
478
  backdrop.appendChild(panel);
408
479
  dialog.appendChild(backdrop);
480
+ // 2026-10-15 keyboard: the dialog is modal, so focus moves in, Tab stays in, Escape closes, and focus goes back
481
+ // to where it was. Escape did nothing and the backdrop kept blocking the page. tools/a11y/revision-dialog-keyboard.mjs
482
+ dialogReturnFocus = host.activeElement;
483
+ dialog.addEventListener("keydown", onDialogKey, true);
409
484
  host.body.appendChild(dialog);
410
485
  renderDialog();
486
+ // the toolbar returns focus to the editor just after a command runs; move in after that, and once more
487
+ // if something took it back
488
+ setTimeout(focusFirstInDialog, 0);
489
+ setTimeout(function () { if (dialog && !dialog.contains(dialog.ownerDocument.activeElement)) focusFirstInDialog(); }, 250);
490
+ }
491
+
492
+ var dialogReturnFocus = null;
493
+ function dialogFocusables() {
494
+ if (!dialog) return [];
495
+ var all = dialog.querySelectorAll("button, input, select, textarea, a[href], [tabindex]:not([tabindex='-1'])");
496
+ var out = [];
497
+ for (var i = 0; i < all.length; i++) if (!all[i].disabled && all[i].offsetParent !== null) out.push(all[i]);
498
+ return out;
499
+ }
500
+ function focusFirstInDialog() {
501
+ var f = dialogFocusables();
502
+ if (f.length) { try { f[0].focus(); } catch (e) { /* ignore */ } }
503
+ }
504
+ // 2026-10-18 a dialog control runs its action on click, so Enter and Space work (they fire click, not mousedown;
505
+ // these ran on mousedown only and did nothing from the keyboard). mousedown still prevents default so a mouse
506
+ // press keeps the editor's selection. The action usually re-renders the dialog; after a keyboard press, focus
507
+ // goes back to the control with the same key. tools/a11y/revision-buttons-keyboard.mjs
508
+ function onActivate(el, key, fn) {
509
+ el.setAttribute("data-rte-rev-key", key);
510
+ el.addEventListener("mousedown", function (e) { e.preventDefault(); });
511
+ el.addEventListener("click", function (e) {
512
+ e.preventDefault();
513
+ var fromKeyboard = e.detail === 0;
514
+ fn();
515
+ if (!fromKeyboard || !dialog) return;
516
+ var all = dialog.querySelectorAll("[data-rte-rev-key]");
517
+ for (var i = 0; i < all.length; i++) {
518
+ if (all[i].getAttribute("data-rte-rev-key") === key) { try { all[i].focus(); } catch (x) { /* ignore */ } return; }
519
+ }
520
+ if (!dialog.contains(dialog.ownerDocument.activeElement)) focusFirstInDialog();
521
+ });
522
+ }
523
+ function onDialogKey(e) {
524
+ if (e.key === "Escape" || e.key === "Esc") { e.preventDefault(); e.stopPropagation(); closeDialog(); return; }
525
+ if (e.key !== "Tab") return;
526
+ var f = dialogFocusables();
527
+ if (!f.length) return;
528
+ var a = dialog.ownerDocument.activeElement, i = f.indexOf(a);
529
+ if (e.shiftKey && (i <= 0)) { e.preventDefault(); f[f.length - 1].focus(); }
530
+ else if (!e.shiftKey && (i === -1 || i === f.length - 1)) { e.preventDefault(); f[0].focus(); }
411
531
  }
412
532
 
413
533
  function closeDialog() {
414
534
  if (dialog && dialog.parentNode) dialog.parentNode.removeChild(dialog);
415
535
  dialog = null;
536
+ var back = dialogReturnFocus;
537
+ dialogReturnFocus = null;
538
+ try {
539
+ if (back && back.isConnected && back.tagName !== "IFRAME" && back !== back.ownerDocument.body) back.focus();
540
+ else if (typeof editor.focus === "function") editor.focus();
541
+ } catch (e) { /* ignore */ }
416
542
  }
417
543
 
418
544
  var dialogSelectedId = null;
@@ -433,17 +559,16 @@ function RTE_Plugin_RevisionHistory() {
433
559
  var title = host.createElement("div");
434
560
  title.id = "rte-rev-dialog-title";
435
561
  title.className = "rte-rev-title";
436
- title.textContent = "Revision history";
562
+ title.textContent = rhText("revisionhistory", "Revision history");
437
563
  header.appendChild(title);
438
564
  var actions = host.createElement("div");
439
565
  actions.className = "rte-rev-header-actions";
440
566
  var snapBtn = host.createElement("button");
441
567
  snapBtn.type = "button";
442
568
  snapBtn.className = "rte-rev-btn rte-rev-btn-ghost";
443
- snapBtn.textContent = "Save version now";
444
- snapBtn.addEventListener("mousedown", function (e) {
445
- e.preventDefault();
446
- var label = host.defaultView.prompt("Label this version (optional):", "");
569
+ snapBtn.textContent = rhText("rh_savenow", "Save version now");
570
+ onActivate(snapBtn, "save", function () {
571
+ var label = host.defaultView.prompt(rhText("rh_labelversion", "Label this version (optional):"), "");
447
572
  if (label === null) return; // cancelled
448
573
  snapshot(label || "");
449
574
  });
@@ -451,9 +576,9 @@ function RTE_Plugin_RevisionHistory() {
451
576
  var closeBtn = host.createElement("button");
452
577
  closeBtn.type = "button";
453
578
  closeBtn.className = "rte-rev-btn rte-rev-btn-ghost";
454
- closeBtn.textContent = "Close";
455
- closeBtn.setAttribute("aria-label", "Close revision history");
456
- closeBtn.addEventListener("mousedown", function (e) { e.preventDefault(); closeDialog(); });
579
+ closeBtn.textContent = rhText("close", "Close");
580
+ closeBtn.setAttribute("aria-label", rhText("rh_closehistory", "Close revision history"));
581
+ onActivate(closeBtn, "close", closeDialog);
457
582
  actions.appendChild(closeBtn);
458
583
  header.appendChild(actions);
459
584
  panel.appendChild(header);
@@ -464,13 +589,17 @@ function RTE_Plugin_RevisionHistory() {
464
589
  // Left: list
465
590
  var list = host.createElement("div");
466
591
  list.className = "rte-rev-list";
467
- list.setAttribute("role", "listbox");
468
- list.setAttribute("aria-label", "Saved revisions");
469
592
  var entries = editor.revisionHistory.list();
593
+ // 2026-10-15 a listbox only when it lists revisions: the empty message inside role=listbox left a listbox with
594
+ // no options (axe aria-required-children) that read as an empty list. tools/a11y/revision-list-roles.mjs
595
+ if (entries.length) {
596
+ list.setAttribute("role", "listbox");
597
+ list.setAttribute("aria-label", rhText("rh_saved", "Saved revisions"));
598
+ }
470
599
  if (!entries.length) {
471
600
  var empty = host.createElement("div");
472
601
  empty.className = "rte-rev-empty";
473
- empty.textContent = "No revisions yet. Click \"Save version now\" to capture the current document.";
602
+ empty.textContent = rhText("rh_empty", "No revisions yet. Click \"Save version now\" to capture the current document.");
474
603
  list.appendChild(empty);
475
604
  } else {
476
605
  if (!dialogSelectedId || !findById(dialogSelectedId)) {
@@ -488,7 +617,7 @@ function RTE_Plugin_RevisionHistory() {
488
617
  dot.style.background = (e.author && e.author.color) || "#64748b";
489
618
  var who = host.createElement("div");
490
619
  who.className = "rte-rev-row-who";
491
- who.textContent = (e.author && e.author.name) || "User";
620
+ who.textContent = (e.author && e.author.name) || rhText("rh_user", "User");
492
621
  var when = host.createElement("div");
493
622
  when.className = "rte-rev-row-when";
494
623
  when.textContent = formatDate(e.createdAt) + " \u00B7 " + (e.label || "");
@@ -498,8 +627,7 @@ function RTE_Plugin_RevisionHistory() {
498
627
  col.appendChild(when);
499
628
  row.appendChild(dot);
500
629
  row.appendChild(col);
501
- row.addEventListener("mousedown", function (ev) {
502
- ev.preventDefault();
630
+ onActivate(row, "row-" + e.id, function () {
503
631
  dialogSelectedId = e.id;
504
632
  renderDialog();
505
633
  });
@@ -518,21 +646,21 @@ function RTE_Plugin_RevisionHistory() {
518
646
  var tabs = host.createElement("div");
519
647
  tabs.className = "rte-rev-tabs";
520
648
  tabs.setAttribute("role", "tablist");
521
- tabs.setAttribute("aria-label", "Revision view");
649
+ tabs.setAttribute("aria-label", rhText("rh_view", "Revision view"));
522
650
  var tabPreview = host.createElement("button");
523
651
  tabPreview.type = "button";
524
652
  tabPreview.className = "rte-rev-tab" + (dialogView === "preview" ? " rte-rev-tab-active" : "");
525
653
  tabPreview.setAttribute("role", "tab");
526
654
  tabPreview.setAttribute("aria-selected", dialogView === "preview" ? "true" : "false");
527
- tabPreview.textContent = "Preview";
528
- tabPreview.addEventListener("mousedown", function (e) { e.preventDefault(); dialogView = "preview"; renderDialog(); });
655
+ tabPreview.textContent = rhText("rh_preview", "Preview");
656
+ onActivate(tabPreview, "tab-preview", function () { dialogView = "preview"; renderDialog(); });
529
657
  var tabDiff = host.createElement("button");
530
658
  tabDiff.type = "button";
531
659
  tabDiff.className = "rte-rev-tab" + (dialogView === "diff" ? " rte-rev-tab-active" : "");
532
660
  tabDiff.setAttribute("role", "tab");
533
661
  tabDiff.setAttribute("aria-selected", dialogView === "diff" ? "true" : "false");
534
- tabDiff.textContent = "Diff vs current";
535
- tabDiff.addEventListener("mousedown", function (e) { e.preventDefault(); dialogView = "diff"; renderDialog(); });
662
+ tabDiff.textContent = rhText("rh_diff", "Diff vs current");
663
+ onActivate(tabDiff, "tab-diff", function () { dialogView = "diff"; renderDialog(); });
536
664
  tabs.appendChild(tabPreview);
537
665
  tabs.appendChild(tabDiff);
538
666
  pane.appendChild(tabs);
@@ -542,7 +670,7 @@ function RTE_Plugin_RevisionHistory() {
542
670
  if (dialogView === "preview") {
543
671
  var iframe = host.createElement("iframe");
544
672
  iframe.className = "rte-rev-preview-frame";
545
- iframe.setAttribute("title", "Revision preview");
673
+ iframe.setAttribute("title", rhText("rh_previewframe", "Revision preview"));
546
674
  iframe.setAttribute("sandbox", "allow-same-origin");
547
675
  content.appendChild(iframe);
548
676
  // Write after appending so document is available.
@@ -576,18 +704,18 @@ function RTE_Plugin_RevisionHistory() {
576
704
  footer.className = "rte-rev-footer";
577
705
  var labelInfo = host.createElement("div");
578
706
  labelInfo.className = "rte-rev-footer-info";
579
- labelInfo.textContent = selected.label ? "Label: " + selected.label : "";
707
+ labelInfo.textContent = selected.label ? rhText("rh_label", "Label: {0}", selected.label) : "";
580
708
  var footerBtns = host.createElement("div");
581
709
  footerBtns.className = "rte-rev-footer-btns";
582
710
  var delBtn = host.createElement("button");
583
711
  delBtn.type = "button";
584
712
  delBtn.className = "rte-rev-btn rte-rev-btn-danger";
585
- delBtn.textContent = "Delete";
586
- delBtn.addEventListener("mousedown", function (e) { e.preventDefault(); deleteEntry(selected.id); });
713
+ delBtn.textContent = rhText("delete", "Delete");
714
+ onActivate(delBtn, "delete", function () { deleteEntry(selected.id); });
587
715
  var restoreBtn = host.createElement("button");
588
716
  restoreBtn.type = "button";
589
717
  restoreBtn.className = "rte-rev-btn rte-rev-btn-primary";
590
- restoreBtn.textContent = "Restore this version";
718
+ restoreBtn.textContent = rhText("rh_restore", "Restore this version");
591
719
  restoreBtn.addEventListener("mousedown", function (e) { e.preventDefault(); });
592
720
  restoreBtn.addEventListener("click", function (e) {
593
721
  e.preventDefault();
@@ -599,7 +727,10 @@ function RTE_Plugin_RevisionHistory() {
599
727
  footer.appendChild(footerBtns);
600
728
  pane.appendChild(footer);
601
729
  }
602
- body.appendChild(pane);
730
+ // 2026-10-18 nothing to preview: no empty pane (a blank half on desktop, a 360px block on a phone); the
731
+ // no-revisions message spans the dialog instead. tools/ui/revision-empty-state.mjs
732
+ if (entries.length) body.appendChild(pane);
733
+ else list.classList.add("rte-rev-list-solo");
603
734
  panel.appendChild(body);
604
735
  }
605
736
 
@@ -609,7 +740,7 @@ function RTE_Plugin_RevisionHistory() {
609
740
  var now = new Date();
610
741
  function pad(n) { return n < 10 ? "0" + n : "" + n; }
611
742
  var time = pad(d.getHours()) + ":" + pad(d.getMinutes());
612
- if (d.toDateString() === now.toDateString()) return "Today " + time;
743
+ if (d.toDateString() === now.toDateString()) return rhText("rh_today", "Today {0}", time);
613
744
  return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + time;
614
745
  }
615
746
 
@@ -646,6 +777,7 @@ function RTE_Plugin_RevisionHistory() {
646
777
  ".rte-rev-row-who{font-weight:720;font-size:12px;line-height:1.25;color:#172033}",
647
778
  ".rte-rev-row-when{margin-top:2px;font-size:11px;color:#637487;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
648
779
  ".rte-rev-pane{flex:1;display:flex;flex-direction:column;min-width:0;padding:10px}",
780
+ ".rte-rev-list.rte-rev-list-solo{flex:1 1 auto;width:auto;max-height:none;border-right:0;border-bottom:0}",
649
781
  ".rte-rev-tabs{display:flex;align-self:flex-start;gap:3px;padding:3px;margin:0 0 10px;border:1px solid rgba(117,137,163,.2);border-radius:7px;background:#eef3f8}",
650
782
  ".rte-rev-tab{padding:7px 12px;border:0;border-radius:6px;background:transparent;font:inherit;font-size:12px;font-weight:680;color:#637487;cursor:pointer}",
651
783
  ".rte-rev-tab:hover{color:#1d3557;background:rgba(255,255,255,.58)}",