@richscripts/richtexteditor 2.4.0 → 2.4.2

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 (60) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +14 -2
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14982 -4831
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14982 -4831
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crdt-engine.js +2201 -198
  21. package/richtexteditor/plugins/crdt-engine.min.js +5 -5
  22. package/richtexteditor/plugins/crossreference.js +408 -0
  23. package/richtexteditor/plugins/documentimport.js +915 -21
  24. package/richtexteditor/plugins/documentoutline.js +5 -4
  25. package/richtexteditor/plugins/docxexport.js +960 -0
  26. package/richtexteditor/plugins/draghandle.js +309 -0
  27. package/richtexteditor/plugins/footnotes.js +398 -0
  28. package/richtexteditor/plugins/formatpainter.js +298 -0
  29. package/richtexteditor/plugins/formattingmarks.js +246 -0
  30. package/richtexteditor/plugins/html2pdf.js +108 -4
  31. package/richtexteditor/plugins/imageeditor.js +37 -7
  32. package/richtexteditor/plugins/insertgallery.js +553 -170
  33. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  34. package/richtexteditor/plugins/linenumbers.js +352 -0
  35. package/richtexteditor/plugins/linkchecker.js +396 -0
  36. package/richtexteditor/plugins/mentionplugin.js +4 -3
  37. package/richtexteditor/plugins/mergefields.js +328 -0
  38. package/richtexteditor/plugins/multilevellist.js +137 -0
  39. package/richtexteditor/plugins/pagination.js +505 -0
  40. package/richtexteditor/plugins/pdfexport.js +1137 -0
  41. package/richtexteditor/plugins/permanentpen.js +211 -0
  42. package/richtexteditor/plugins/revisionhistory.js +47 -34
  43. package/richtexteditor/plugins/rtlui.js +150 -0
  44. package/richtexteditor/plugins/sanitizer.js +510 -0
  45. package/richtexteditor/plugins/slashcommand.js +142 -17
  46. package/richtexteditor/plugins/spellcheck.js +6 -3
  47. package/richtexteditor/plugins/tableofcontents.js +353 -0
  48. package/richtexteditor/plugins/tabletools.js +342 -0
  49. package/richtexteditor/plugins/textdirection.js +313 -0
  50. package/richtexteditor/plugins/trackedchanges.js +37 -6
  51. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  52. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  53. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  54. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  55. package/richtexteditor/plugins/watermark.js +214 -0
  56. package/richtexteditor/plugins/wordexport.js +0 -0
  57. package/richtexteditor/plugins/yjscollab.js +49 -11
  58. package/richtexteditor/rte-config.js +44 -44
  59. package/richtexteditor/rte.js +91 -63
  60. package/richtexteditor/rte_theme_default.css +483 -384
@@ -0,0 +1,211 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Permanent pen. Turns on a fixed character format that applies to
4
+ // everything typed from then on, until switched off — the marker you pick up to
5
+ // annotate a document in a colour that is obviously not the original text.
6
+ //
7
+ // TinyMCE Permanent Pen: "This plugin is only available for paid TinyMCE
8
+ // subscriptions".
9
+ //
10
+ // Design notes:
11
+ // - The pen writes into a span that it OWNS and keeps extending, rather than
12
+ // re-wrapping every keystroke. Wrapping per character produces one span per
13
+ // letter, which bloats the HTML and makes the text impossible to edit
14
+ // sensibly afterwards.
15
+ // - It re-arms on selection changes. Click somewhere else and keep typing and
16
+ // the pen still applies, which is the entire point of "permanent" — but the
17
+ // new run gets its own span rather than reaching back to the old one.
18
+ // - Pen output is REAL CONTENT. Unlike the format painter's transient state,
19
+ // annotations written with the pen are meant to persist in the saved HTML.
20
+ RTE_DefaultConfig.plugin_permanentpen = RTE_Plugin_PermanentPen;
21
+
22
+ // The format the pen writes. Any CSS the host wants; these are the properties a
23
+ // reviewer's marker usually needs.
24
+ if (typeof RTE_DefaultConfig.permanentPenStyle === "undefined") {
25
+ RTE_DefaultConfig.permanentPenStyle = {
26
+ "color": "#c81e1e",
27
+ "font-weight": "700"
28
+ };
29
+ }
30
+ // Class placed on every run the pen writes, so a host can style or find them.
31
+ if (typeof RTE_DefaultConfig.permanentPenClass === "undefined") RTE_DefaultConfig.permanentPenClass = "rte-pen";
32
+
33
+ function RTE_Plugin_PermanentPen() {
34
+ var obj = this;
35
+ var config, editor;
36
+ var boundDoc = null;
37
+ var active = false;
38
+ var currentRun = null; // the span this pen stroke is currently extending
39
+
40
+ obj.PluginName = "PermanentPen";
41
+
42
+ obj.InitConfig = function (argconfig) { config = argconfig; };
43
+
44
+ obj.InitEditor = function (argeditor) {
45
+ editor = argeditor;
46
+
47
+ editor.attachEvent("exec_command_permanentpen", function (state) {
48
+ state.returnValue = true;
49
+ obj.Toggle();
50
+ });
51
+
52
+ setup();
53
+ try { editor.attachEvent("ready", setup); } catch (e) {}
54
+ try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
55
+ setTimeout(setup, 0);
56
+
57
+ // Public API.
58
+ editor.setPermanentPen = function (on) { return apply(!!on); };
59
+ editor.togglePermanentPen = function () { return apply(!active); };
60
+ editor.isPermanentPenActive = function () { return active; };
61
+ editor.setPermanentPenStyle = function (style) {
62
+ if (style && typeof style === "object") { config.permanentPenStyle = style; currentRun = null; }
63
+ return config.permanentPenStyle;
64
+ };
65
+ editor.getPermanentPenStyle = function () {
66
+ var s = config.permanentPenStyle || {}, out = {};
67
+ for (var k in s) if (s.hasOwnProperty(k)) out[k] = s[k];
68
+ return out;
69
+ };
70
+ };
71
+
72
+ function setup() {
73
+ var doc = getDoc();
74
+ if (!doc || doc === boundDoc) return;
75
+ boundDoc = doc;
76
+ var editable = getEditable();
77
+ if (!editable) return;
78
+ // beforeinput fires while the caret is still where the text will land,
79
+ // which is when the run has to exist for the character to go into it.
80
+ editable.addEventListener("beforeinput", onBeforeInput, true);
81
+ // Moving the caret ends the current stroke: the next typing starts a new
82
+ // run instead of teleporting text into the old one.
83
+ editable.addEventListener("mouseup", function () { currentRun = null; });
84
+ editable.addEventListener("keydown", function (e) {
85
+ if (/^Arrow|^Home|^End|^Page/.test(e.key)) currentRun = null;
86
+ });
87
+ editable.addEventListener("blur", function () { currentRun = null; });
88
+ }
89
+
90
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
91
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
92
+
93
+ function apply(on) {
94
+ var wasActive = active;
95
+ active = !!on;
96
+ var editable = getEditable();
97
+ if (editable && editable.classList) {
98
+ if (active) editable.classList.add("rte-pen-active");
99
+ else editable.classList.remove("rte-pen-active");
100
+ }
101
+ // Switching OFF has to move the caret out of the run. Typing continues
102
+ // in whatever element the caret is in, so leaving it inside the styled
103
+ // span means the pen visibly refuses to turn off — the user keeps typing
104
+ // red text after pressing the button that was supposed to stop that.
105
+ if (wasActive && !active) stepOutOfRun();
106
+ currentRun = null;
107
+ return active;
108
+ }
109
+
110
+ function stepOutOfRun() {
111
+ var doc = getDoc();
112
+ if (!doc) return;
113
+ var sel;
114
+ try { sel = editor.getSelection(); } catch (e) { return; }
115
+ if (!sel || sel.rangeCount === 0) return;
116
+ var run = closestPenRun(sel.getRangeAt(0).startContainer);
117
+ if (!run || !run.parentNode) return;
118
+ tidyRun(run);
119
+ try {
120
+ var r = doc.createRange();
121
+ r.setStartAfter(run);
122
+ r.collapse(true);
123
+ sel.removeAllRanges();
124
+ sel.addRange(r);
125
+ } catch (e) {}
126
+ }
127
+
128
+ // The run is seeded with a zero-width space so the caret has somewhere to
129
+ // live inside an otherwise empty inline element. Once the run has real text
130
+ // that placeholder is just litter in the saved HTML, so drop it.
131
+ function tidyRun(run) {
132
+ if (!run) return;
133
+ var text = run.textContent || "";
134
+ if (text.replace(/​/g, "") === "") {
135
+ // Nothing was ever typed: remove the empty run entirely.
136
+ if (run.parentNode) run.parentNode.removeChild(run);
137
+ return;
138
+ }
139
+ var walker = run.ownerDocument.createTreeWalker(run, 4 /* SHOW_TEXT */, null, false);
140
+ var n;
141
+ while ((n = walker.nextNode())) {
142
+ if (n.nodeValue.indexOf("​") >= 0) n.nodeValue = n.nodeValue.replace(/​/g, "");
143
+ }
144
+ }
145
+
146
+ function onBeforeInput(e) {
147
+ if (!active) return;
148
+ // Only ordinary typing. Deletions, formatting commands and paste keep
149
+ // their normal behaviour — a pen that hijacked paste would be a menace.
150
+ if (e.inputType && e.inputType !== "insertText" && e.inputType !== "insertCompositionText") return;
151
+
152
+ var doc = getDoc();
153
+ if (!doc) return;
154
+ var sel;
155
+ try { sel = editor.getSelection(); } catch (x) { return; }
156
+ if (!sel || sel.rangeCount === 0) return;
157
+ var range = sel.getRangeAt(0);
158
+ if (!range.collapsed) return; // replacing a selection: let it be
159
+
160
+ // Already inside our current run — nothing to do, the character will
161
+ // land in it naturally.
162
+ if (currentRun && containsNode(currentRun, range.startContainer)) return;
163
+
164
+ // Already inside an identically-styled pen run (e.g. the user clicked
165
+ // back into an annotation): adopt it rather than nesting a new span.
166
+ var existing = closestPenRun(range.startContainer);
167
+ if (existing && sameStyle(existing)) { currentRun = existing; return; }
168
+
169
+ var span = doc.createElement("span");
170
+ span.className = String(config.permanentPenClass || "rte-pen");
171
+ var style = config.permanentPenStyle || {};
172
+ for (var k in style) if (style.hasOwnProperty(k)) span.style.setProperty(k, style[k]);
173
+ // A zero-width space gives the caret something real to sit inside; an
174
+ // empty inline element cannot hold a caret, so the first character would
175
+ // land outside the span and the pen would appear not to work.
176
+ span.appendChild(doc.createTextNode("​"));
177
+
178
+ range.insertNode(span);
179
+ var r = doc.createRange();
180
+ r.setStart(span.firstChild, 1); // after the ZWSP
181
+ r.collapse(true);
182
+ sel.removeAllRanges();
183
+ sel.addRange(r);
184
+ currentRun = span;
185
+ }
186
+
187
+ function containsNode(root, node) {
188
+ while (node) { if (node === root) return true; node = node.parentNode; }
189
+ return false;
190
+ }
191
+
192
+ function closestPenRun(node) {
193
+ var editable = getEditable();
194
+ var cls = String(config.permanentPenClass || "rte-pen");
195
+ var n = node && node.nodeType === 3 ? node.parentNode : node;
196
+ while (n && n !== editable) {
197
+ if (n.nodeType === 1 && n.classList && n.classList.contains(cls)) return n;
198
+ n = n.parentNode;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function sameStyle(el) {
204
+ var style = config.permanentPenStyle || {};
205
+ for (var k in style) {
206
+ if (!style.hasOwnProperty(k)) continue;
207
+ if (el.style.getPropertyValue(k) !== String(style[k])) return false;
208
+ }
209
+ return true;
210
+ }
211
+ }
@@ -23,6 +23,8 @@ function RTE_Plugin_RevisionHistory() {
23
23
  config.revisionHistoryMaxEntries = config.revisionHistoryMaxEntries || 50;
24
24
  config.revisionHistoryAutoSnapshotMs = config.revisionHistoryAutoSnapshotMs || 0;
25
25
  config.revisionHistoryUrl = config.revisionHistoryUrl || "";
26
+ config.revisionHistoryRequestHeaders = config.revisionHistoryRequestHeaders || null;
27
+ config.onRevisionHistorySyncError = config.onRevisionHistorySyncError || null;
26
28
  config.text_revisionhistory = config.text_revisionhistory || "Revision history";
27
29
 
28
30
  appendToolbarCommand("toolbar_default", "#{revisionhistory}");
@@ -143,14 +145,46 @@ function RTE_Plugin_RevisionHistory() {
143
145
  var xhr = new XMLHttpRequest();
144
146
  xhr.open("POST", config.revisionHistoryUrl, true);
145
147
  xhr.setRequestHeader("Content-Type", "application/json");
148
+ var headers = resolveRequestHeaders();
149
+ for (var name in headers) {
150
+ if (!Object.prototype.hasOwnProperty.call(headers, name)) continue;
151
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue;
152
+ var value = String(headers[name]);
153
+ if (/\r|\n/.test(value)) continue;
154
+ try { xhr.setRequestHeader(name, value); } catch (ignore) { }
155
+ }
156
+ xhr.onerror = function () { reportRemoteFailure({ reason: "network", status: xhr.status || 0 }); };
157
+ xhr.onload = function () {
158
+ if (xhr.status < 200 || xhr.status >= 300) {
159
+ reportRemoteFailure({ reason: "http", status: xhr.status, responseText: (xhr.responseText || "").slice(0, 512) });
160
+ }
161
+ };
146
162
  xhr.send(JSON.stringify({
147
163
  documentKey: config.aiToolkitPersistenceKey || "",
148
164
  savedAt: Date.now(),
149
165
  revisions: store
150
166
  }));
151
167
  } catch (err) {
152
- console.warn("revisionhistory: remote POST failed:", err);
168
+ reportRemoteFailure({ reason: "exception", error: err });
169
+ }
170
+ }
171
+
172
+ function resolveRequestHeaders() {
173
+ var source = config.revisionHistoryRequestHeaders;
174
+ if (typeof source === "function") {
175
+ try { source = source(editor); }
176
+ catch (err) {
177
+ reportRemoteFailure({ reason: "header-provider", error: err });
178
+ return {};
179
+ }
153
180
  }
181
+ return source && typeof source === "object" ? source : {};
182
+ }
183
+
184
+ function reportRemoteFailure(detail) {
185
+ console.warn("revisionhistory: remote persistence failed:", detail && detail.reason, detail && detail.status);
186
+ try { typeof config.onRevisionHistorySyncError === "function" && config.onRevisionHistorySyncError(detail); } catch (ignore) { }
187
+ try { typeof editor.fireEvent === "function" && editor.fireEvent("revision_history_sync_error", detail); } catch (ignore2) { }
154
188
  }
155
189
 
156
190
  function restoreStore() {
@@ -346,8 +380,7 @@ function RTE_Plugin_RevisionHistory() {
346
380
  var host = editor.iframe.ownerDocument;
347
381
  if (!host || !host.body) return;
348
382
  dialog = host.createElement("div");
349
- // Body-appended modal — mirror the editor's forced dark mode (automatic
350
- // dark is handled by a prefers-color-scheme media query in the styles).
383
+ // Body-appended modal mirrors only an editor explicitly forced dark.
351
384
  var __revDark = false;
352
385
  try {
353
386
  var __h = editor.container || (editor.getEditable && editor.getEditable().closest && editor.getEditable().closest(".richtexteditor"));
@@ -585,11 +618,12 @@ function RTE_Plugin_RevisionHistory() {
585
618
  style.textContent = [
586
619
  ".rte-rev-dialog{position:fixed;inset:0;z-index:2147483600;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:13px;color:#172033}",
587
620
  ".rte-rev-backdrop{position:absolute;inset:0;background:linear-gradient(135deg,rgba(12,18,32,.44),rgba(40,54,77,.26));display:flex;align-items:center;justify-content:center;padding:18px}",
588
- ".rte-rev-panel{background:linear-gradient(180deg,#ffffff 0%,#f8fbff 100%);border:1px solid rgba(117,137,163,.28);border-radius:20px;width:min(960px,calc(100vw - 28px));max-height:min(82vh,740px);display:flex;flex-direction:column;box-shadow:0 24px 60px rgba(35,48,72,.26);overflow:hidden}",
621
+ "/* 2026-07-12 revision dialog precision */",
622
+ ".rte-rev-panel{background:linear-gradient(180deg,#ffffff 0%,#f8fbff 100%);border:1px solid rgba(117,137,163,.28);border-radius:8px;width:min(960px,calc(100vw - 28px));max-height:min(82vh,740px);display:flex;flex-direction:column;box-shadow:0 16px 40px rgba(35,48,72,.18);overflow:hidden}",
589
623
  ".rte-rev-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid rgba(117,137,163,.18);background:rgba(255,255,255,.72)}",
590
624
  ".rte-rev-title{font-size:16px;font-weight:760;letter-spacing:-.01em;color:#172033}",
591
625
  ".rte-rev-header-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
592
- ".rte-rev-btn{padding:8px 12px;border-radius:999px;border:1px solid rgba(116,135,162,.25);font:inherit;font-size:12px;font-weight:680;line-height:1;cursor:pointer;background:#fff;color:#223652;box-shadow:0 1px 2px rgba(32,45,66,.06);transition:background .14s ease,border-color .14s ease,box-shadow .14s ease,transform .14s ease}",
626
+ ".rte-rev-btn{padding:8px 12px;border-radius:7px;border:1px solid rgba(116,135,162,.25);font:inherit;font-size:12px;font-weight:680;line-height:1;cursor:pointer;background:#fff;color:#223652;box-shadow:0 1px 2px rgba(32,45,66,.06);transition:background .14s ease,border-color .14s ease,box-shadow .14s ease,transform .14s ease}",
593
627
  ".rte-rev-btn:hover{background:#f5f8fc;border-color:rgba(81,111,151,.42);box-shadow:0 5px 14px rgba(32,45,66,.09);transform:translateY(-1px)}",
594
628
  ".rte-rev-btn:focus-visible{outline:2px solid rgba(56,122,255,.34);outline-offset:2px}",
595
629
  ".rte-rev-btn-ghost{background:#fff;color:#34506f}",
@@ -598,8 +632,8 @@ function RTE_Plugin_RevisionHistory() {
598
632
  ".rte-rev-btn-danger{background:#fff5f5;color:#9f1d1d;border-color:rgba(190,45,45,.22)}",
599
633
  ".rte-rev-body{display:flex;flex:1;min-height:0;background:linear-gradient(90deg,#f8fbff 0%,#fff 42%)}",
600
634
  ".rte-rev-list{width:258px;flex:0 0 258px;border-right:1px solid rgba(117,137,163,.18);overflow-y:auto;background:rgba(244,248,253,.78);padding:10px}",
601
- ".rte-rev-empty{padding:16px;border:1px dashed rgba(99,118,145,.28);border-radius:14px;background:#fff;color:#5b6c81;font-size:12px;line-height:1.5}",
602
- ".rte-rev-row{display:flex;align-items:center;gap:10px;width:100%;padding:10px;border:1px solid transparent;border-radius:14px;background:transparent;text-align:left;cursor:pointer;font:inherit;color:inherit}",
635
+ ".rte-rev-empty{padding:16px;border:1px dashed rgba(99,118,145,.28);border-radius:8px;background:#fff;color:#5b6c81;font-size:12px;line-height:1.5}",
636
+ ".rte-rev-row{display:flex;align-items:center;gap:10px;width:100%;padding:10px;border:1px solid transparent;border-radius:8px;background:transparent;text-align:left;cursor:pointer;font:inherit;color:inherit}",
603
637
  ".rte-rev-row:hover{background:#fff;border-color:rgba(93,122,161,.24);box-shadow:0 8px 18px rgba(33,48,70,.08)}",
604
638
  ".rte-rev-row-active{background:#eef6ff;border-color:rgba(37,99,235,.28);box-shadow:0 8px 22px rgba(37,99,235,.12)}",
605
639
  ".rte-rev-dot{width:9px;height:9px;border-radius:50%;flex:0 0 9px;box-shadow:0 0 0 3px rgba(255,255,255,.95),0 0 0 4px rgba(96,116,140,.18)}",
@@ -607,12 +641,12 @@ function RTE_Plugin_RevisionHistory() {
607
641
  ".rte-rev-row-who{font-weight:720;font-size:12px;line-height:1.25;color:#172033}",
608
642
  ".rte-rev-row-when{margin-top:2px;font-size:11px;color:#637487;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
609
643
  ".rte-rev-pane{flex:1;display:flex;flex-direction:column;min-width:0;padding:10px}",
610
- ".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:999px;background:#eef3f8}",
611
- ".rte-rev-tab{padding:7px 12px;border:0;border-radius:999px;background:transparent;font:inherit;font-size:12px;font-weight:680;color:#637487;cursor:pointer}",
644
+ ".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}",
645
+ ".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}",
612
646
  ".rte-rev-tab:hover{color:#1d3557;background:rgba(255,255,255,.58)}",
613
647
  ".rte-rev-tab:focus-visible{outline:2px solid rgba(56,122,255,.34);outline-offset:2px}",
614
648
  ".rte-rev-tab-active{color:#163966;background:#fff;box-shadow:0 2px 7px rgba(35,48,72,.08)}",
615
- ".rte-rev-content{flex:1;overflow:auto;padding:0;border:1px solid rgba(117,137,163,.18);border-radius:16px;background:#fff;min-height:300px}",
649
+ ".rte-rev-content{flex:1;overflow:auto;padding:0;border:1px solid rgba(117,137,163,.18);border-radius:8px;background:#fff;min-height:300px}",
616
650
  ".rte-rev-preview-frame{width:100%;height:100%;min-height:330px;border:0;display:block;background:#fff}",
617
651
  ".rte-rev-diff{margin:0;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.55;white-space:pre-wrap;color:#172033;background:#fbfdff;min-height:100%}",
618
652
  ".rte-rev-diff-line{display:block}",
@@ -622,30 +656,9 @@ function RTE_Plugin_RevisionHistory() {
622
656
  ".rte-rev-footer{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 0 0;background:transparent}",
623
657
  ".rte-rev-footer-info{font-size:12px;color:#637487;min-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
624
658
  ".rte-rev-footer-btns{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}",
625
- "@media(max-width:720px){.rte-rev-backdrop{padding:10px;align-items:flex-start}.rte-rev-panel{width:calc(100vw - 20px);max-height:calc(100vh - 20px);border-radius:16px}.rte-rev-header{align-items:flex-start;flex-direction:column}.rte-rev-body{flex-direction:column}.rte-rev-list{width:auto;flex:0 0 auto;max-height:178px;border-right:0;border-bottom:1px solid rgba(117,137,163,.18)}.rte-rev-pane{min-height:360px}.rte-rev-footer{align-items:stretch;flex-direction:column}.rte-rev-footer-info{white-space:normal}.rte-rev-footer-btns{justify-content:stretch}.rte-rev-footer-btns .rte-rev-btn{flex:1}}",
626
- // Dark mode — modal is body-appended; auto via prefers-color-scheme +
627
- // explicit .rte-rev-dark class when the editor is forced dark. The
628
- // preview iframe stays white (it renders the actual document).
629
- "@media (prefers-color-scheme:dark){",
630
- " .rte-rev-dialog{color:#e2e8f0}",
631
- " .rte-rev-panel{background:linear-gradient(180deg,#1e293b,#0f172a);border-color:#334155}",
632
- " .rte-rev-header{background:rgba(30,41,59,.72);border-color:#334155}",
633
- " .rte-rev-title,.rte-rev-row-who{color:#e2e8f0}",
634
- " .rte-rev-btn,.rte-rev-btn-ghost{background:#0f172a;color:#cbd5e1;border-color:#334155}",
635
- " .rte-rev-btn:hover{background:#334155}",
636
- " .rte-rev-btn-primary,.rte-rev-btn-primary:hover{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border-color:#3b82f6}",
637
- " .rte-rev-body{background:#0f172a}",
638
- " .rte-rev-list{background:rgba(15,23,42,.6);border-color:#334155}",
639
- " .rte-rev-empty{background:#1e293b;color:#94a3b8;border-color:#334155}",
640
- " .rte-rev-row:hover{background:#1e293b;border-color:#334155}",
641
- " .rte-rev-row-active{background:#1e3a8a;border-color:#3b82f6}",
642
- " .rte-rev-row-when,.rte-rev-footer-info{color:#94a3b8}",
643
- " .rte-rev-tabs{background:#1e293b;border-color:#334155}",
644
- " .rte-rev-tab{color:#94a3b8}.rte-rev-tab-active{color:#fff;background:#1d4ed8}",
645
- " .rte-rev-diff{background:#0f172a;color:#cbd5e1}.rte-rev-diff-eq{color:#94a3b8}",
646
- " .rte-rev-diff-add{background:rgba(16,185,129,.16);color:#6ee7b7}",
647
- " .rte-rev-diff-del{background:rgba(244,63,94,.16);color:#fda4af}",
648
- "}",
659
+ "@media(max-width:720px){.rte-rev-backdrop{padding:10px;align-items:flex-start}.rte-rev-panel{width:calc(100vw - 20px);max-height:calc(100vh - 20px);border-radius:8px}.rte-rev-header{align-items:flex-start;flex-direction:column}.rte-rev-body{flex-direction:column}.rte-rev-list{width:auto;flex:0 0 auto;max-height:178px;border-right:0;border-bottom:1px solid rgba(117,137,163,.18)}.rte-rev-pane{min-height:360px}.rte-rev-footer{align-items:stretch;flex-direction:column}.rte-rev-footer-info{white-space:normal}.rte-rev-footer-btns{justify-content:stretch}.rte-rev-footer-btns .rte-rev-btn{flex:1}}",
660
+ // Revision history stays light unless the editor explicitly adds
661
+ // rte-rev-dark. The preview frame still renders document content.
649
662
  ".rte-rev-dark{color:#e2e8f0}",
650
663
  ".rte-rev-dark .rte-rev-panel{background:linear-gradient(180deg,#1e293b,#0f172a);border-color:#334155}",
651
664
  ".rte-rev-dark .rte-rev-header{background:rgba(30,41,59,.72);border-color:#334155}",
@@ -0,0 +1,150 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-31 Right-to-left EDITOR CHROME. textdirection.js flips the document;
4
+ // this flips the editor around it — toolbar, dropdowns, menus, dialogs — so an
5
+ // Arabic or Hebrew user is not typing right-to-left text inside a left-to-right
6
+ // application.
7
+ //
8
+ // Deliberately a plugin rather than an edit to rte_theme_default.css:
9
+ // - the theme is 146 KB, shared by every tier, and cached behind a fixed
10
+ // "?v=" query, so changing it means a cache-buster bump across the site
11
+ // - LTR users get zero new CSS; every rule here is scoped under [dir="rtl"]
12
+ // - it rides the existing plugin bundle, which is already audited for drift
13
+ //
14
+ // Why this is mostly small: the chrome is flexbox with NO floats, no
15
+ // text-align:left/right and no left:0/right:0 anchors, so `direction: rtl`
16
+ // reverses the layout on its own. What remains is the handful of PHYSICAL
17
+ // margins/paddings the theme still uses, which have to be mirrored by hand.
18
+ //
19
+ // Icons are deliberately NOT mirrored. Word does not flip its toolbar icons in
20
+ // RTL either: bold, italic and the alignment glyphs mean the same thing in both
21
+ // directions, and flipping them makes the toolbar harder to read, not easier.
22
+ // The only icons that SHOULD flip are the ones that encode reading order --
23
+ // indent/outdent and the menu disclosure arrow -- and those are handled below.
24
+ RTE_DefaultConfig.plugin_rtlui = RTE_Plugin_RtlUi;
25
+
26
+ // "auto" = mirror when the document/base direction is RTL
27
+ // true = always mirror | false = never
28
+ if (typeof RTE_DefaultConfig.rtlUserInterface === "undefined") RTE_DefaultConfig.rtlUserInterface = "auto";
29
+
30
+ function RTE_Plugin_RtlUi() {
31
+ var obj = this;
32
+ var config, editor;
33
+ var applied = false;
34
+
35
+ obj.PluginName = "RtlUi";
36
+
37
+ obj.InitConfig = function (argconfig) { config = argconfig; };
38
+
39
+ obj.InitEditor = function (argeditor) {
40
+ editor = argeditor;
41
+
42
+ editor.attachEvent("exec_command_rtlui", function (state) {
43
+ state.returnValue = true;
44
+ obj.Toggle();
45
+ });
46
+
47
+ setup();
48
+ try { editor.attachEvent("ready", setup); } catch (e) {}
49
+ try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
50
+ setTimeout(setup, 0);
51
+
52
+ editor.setRtlUserInterface = function (on) { return apply(!!on); };
53
+ editor.toggleRtlUserInterface = function () { return apply(!applied); };
54
+ editor.isRtlUserInterface = function () { return applied; };
55
+ editor.getRtlUiCss = function () { return css(); };
56
+ };
57
+
58
+ obj.Toggle = function () { return apply(!applied); };
59
+
60
+ function setup() {
61
+ injectStyles();
62
+ if (config.rtlUserInterface === true) apply(true);
63
+ else if (config.rtlUserInterface === false) apply(false);
64
+ else apply(detect());
65
+ }
66
+
67
+ // "auto": follow the document's own direction.
68
+ function detect() {
69
+ try {
70
+ if (typeof editor.getBaseDirection === "function" && editor.getBaseDirection() === "rtl") return true;
71
+ var ed = editor.getEditable();
72
+ if (ed) {
73
+ var doc = ed.ownerDocument;
74
+ var win = doc && (doc.defaultView || doc.parentWindow);
75
+ if (win && win.getComputedStyle(ed).direction === "rtl") return true;
76
+ }
77
+ } catch (e) {}
78
+ return false;
79
+ }
80
+
81
+ // The editor shell lives in the HOST document, not the editable iframe, so
82
+ // the class and the stylesheet both belong there.
83
+ function shell() {
84
+ try {
85
+ var ed = editor.getEditable();
86
+ if (!ed) return null;
87
+ // The iframe's frameElement walks us back out to the host page.
88
+ var win = ed.ownerDocument.defaultView;
89
+ var node = (win && win.frameElement) ? win.frameElement : ed;
90
+ while (node && node.classList && !node.classList.contains("richtexteditor")) node = node.parentNode;
91
+ return (node && node.classList) ? node : null;
92
+ } catch (e) { return null; }
93
+ }
94
+
95
+ function apply(on) {
96
+ var el = shell();
97
+ // Set the flag only once the change has actually landed. Setting it
98
+ // first meant isRtlUserInterface() reported success when the shell could
99
+ // not be resolved (the editor not yet built, or a host that reparents
100
+ // it) -- a state that reads as "mirrored" while nothing is mirrored.
101
+ if (!el) return applied;
102
+ if (on) el.setAttribute("dir", "rtl");
103
+ else el.removeAttribute("dir");
104
+ applied = !!on;
105
+ return applied;
106
+ }
107
+
108
+ function css() {
109
+ return [
110
+ // Flexbox does the bulk of the work once direction is set.
111
+ '.richtexteditor[dir="rtl"]{direction:rtl;}',
112
+ // Menus and dropdown panels open from the correct edge.
113
+ '.richtexteditor[dir="rtl"] rte-dropdown,',
114
+ '.richtexteditor[dir="rtl"] rte-submenu,',
115
+ '.richtexteditor[dir="rtl"] rte-floatpanel{direction:rtl;text-align:right;}',
116
+ '.richtexteditor[dir="rtl"] rte-menuitem{flex-direction:row-reverse;}',
117
+ // Mirror the theme's remaining PHYSICAL spacing.
118
+ '.richtexteditor[dir="rtl"] rte-menutext{margin-left:0;margin-right:3px;}',
119
+ '.richtexteditor[dir="rtl"] rte-menuarrow{margin-right:0;margin-left:4px;}',
120
+ '.richtexteditor[dir="rtl"] rte-ribbon-group-right{margin-left:2px;margin-right:5px;}',
121
+ '.richtexteditor[dir="rtl"] rte-toolbar-arrowbutton{padding-right:0;padding-left:12px;}',
122
+ '.richtexteditor[dir="rtl"] rte-toolbar-dropdown-input{padding-left:0;padding-right:3px;}',
123
+ '.richtexteditor[dir="rtl"] rte-dialog-line-target rte-dialog-input-label{padding-left:0;padding-right:20px;}',
124
+ // margin-*:auto is a push-to-the-far-edge idiom; the edge changes.
125
+ '.richtexteditor[dir="rtl"] .rte-find-replace-all{margin-left:0;margin-right:auto;}',
126
+ '.richtexteditor[dir="rtl"] .rte-gallery-browser-footer-text{margin-right:0;margin-left:auto;}',
127
+ // Status bar and tag list read from the right.
128
+ '.richtexteditor[dir="rtl"] rte-bottom,',
129
+ '.richtexteditor[dir="rtl"] rte-taglist,',
130
+ '.richtexteditor[dir="rtl"] rte-textcounter{direction:rtl;}',
131
+ // Resize grip moves to the opposite corner.
132
+ '.richtexteditor[dir="rtl"] rte-resizecorner{transform:scaleX(-1);}',
133
+ // ONLY the icons that encode reading order flip. Bold/italic/align
134
+ // glyphs deliberately do not.
135
+ '.richtexteditor[dir="rtl"] [class*="indent"] svg,',
136
+ '.richtexteditor[dir="rtl"] [class*="outdent"] svg,',
137
+ '.richtexteditor[dir="rtl"] rte-menuarrow svg{transform:scaleX(-1);}'
138
+ ].join("\n");
139
+ }
140
+
141
+ function injectStyles() {
142
+ // Host document: this styles the editor chrome, not the content.
143
+ var doc = document;
144
+ if (doc.getElementById("rte-rtl-ui-styles")) return;
145
+ var st = doc.createElement("style");
146
+ st.id = "rte-rtl-ui-styles";
147
+ st.appendChild(doc.createTextNode(css()));
148
+ (doc.head || doc.documentElement).appendChild(st);
149
+ }
150
+ }