@richscripts/richtexteditor 2.4.0 → 2.4.1

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 (58) 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 +11 -1
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14959 -4832
  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 +14959 -4832
  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/crossreference.js +408 -0
  21. package/richtexteditor/plugins/documentimport.js +915 -21
  22. package/richtexteditor/plugins/documentoutline.js +5 -4
  23. package/richtexteditor/plugins/docxexport.js +960 -0
  24. package/richtexteditor/plugins/draghandle.js +309 -0
  25. package/richtexteditor/plugins/footnotes.js +398 -0
  26. package/richtexteditor/plugins/formatpainter.js +298 -0
  27. package/richtexteditor/plugins/formattingmarks.js +246 -0
  28. package/richtexteditor/plugins/html2pdf.js +108 -4
  29. package/richtexteditor/plugins/imageeditor.js +37 -7
  30. package/richtexteditor/plugins/insertgallery.js +553 -170
  31. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  32. package/richtexteditor/plugins/linenumbers.js +352 -0
  33. package/richtexteditor/plugins/linkchecker.js +396 -0
  34. package/richtexteditor/plugins/mentionplugin.js +4 -3
  35. package/richtexteditor/plugins/mergefields.js +328 -0
  36. package/richtexteditor/plugins/multilevellist.js +137 -0
  37. package/richtexteditor/plugins/pagination.js +505 -0
  38. package/richtexteditor/plugins/pdfexport.js +1137 -0
  39. package/richtexteditor/plugins/permanentpen.js +211 -0
  40. package/richtexteditor/plugins/revisionhistory.js +47 -34
  41. package/richtexteditor/plugins/rtlui.js +150 -0
  42. package/richtexteditor/plugins/sanitizer.js +486 -0
  43. package/richtexteditor/plugins/slashcommand.js +142 -17
  44. package/richtexteditor/plugins/spellcheck.js +6 -3
  45. package/richtexteditor/plugins/tableofcontents.js +353 -0
  46. package/richtexteditor/plugins/tabletools.js +342 -0
  47. package/richtexteditor/plugins/textdirection.js +313 -0
  48. package/richtexteditor/plugins/trackedchanges.js +37 -6
  49. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  50. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  51. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  52. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  53. package/richtexteditor/plugins/watermark.js +214 -0
  54. package/richtexteditor/plugins/wordexport.js +0 -0
  55. package/richtexteditor/plugins/yjscollab.js +49 -11
  56. package/richtexteditor/rte-config.js +44 -44
  57. package/richtexteditor/rte.js +91 -63
  58. package/richtexteditor/rte_theme_default.css +462 -384
@@ -0,0 +1,342 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Table sorting and row numbering.
4
+ //
5
+ // TinyMCE sells both as the premium "Enhanced Tables" plugin: "This plugin is
6
+ // only available for paid TinyMCE subscriptions". It adds sorting by column and
7
+ // an automatic row-numbering column on top of the standard table plugin.
8
+ //
9
+ // Design notes:
10
+ // - Column type is DETECTED, not assumed. A column of prices sorts numerically
11
+ // even when the cells read "$1,240.00" or "(500)"; a column of dates sorts
12
+ // chronologically rather than as strings. Text sorting a numeric column is
13
+ // the classic wrong answer that puts 10 before 9.
14
+ // - The sort REFUSES on a table with merged body cells rather than doing it
15
+ // badly. A rowspan crossing rows has no meaning once the rows move; silently
16
+ // shredding a table the user spent an hour on is far worse than declining.
17
+ // - <thead> and <tfoot> never move. If a table has no <thead>, a first row
18
+ // that looks like a header (all <th>) is pinned anyway.
19
+ // - The sort is STABLE, so re-sorting by a second column preserves the order
20
+ // of the first — which is how people expect to sort by two things.
21
+ RTE_DefaultConfig.plugin_tabletools = RTE_Plugin_TableTools;
22
+
23
+ // "numeric" | "upper-alpha" | "lower-alpha" | "upper-roman" | "lower-roman"
24
+ if (typeof RTE_DefaultConfig.tableNumberSeries === "undefined") RTE_DefaultConfig.tableNumberSeries = "numeric";
25
+ // Heading placed above the numbering column.
26
+ if (typeof RTE_DefaultConfig.tableNumberHeader === "undefined") RTE_DefaultConfig.tableNumberHeader = "#";
27
+
28
+ function RTE_Plugin_TableTools() {
29
+ var obj = this;
30
+ var config, editor;
31
+
32
+ obj.PluginName = "TableTools";
33
+
34
+ obj.InitConfig = function (argconfig) { config = argconfig; };
35
+
36
+ obj.InitEditor = function (argeditor) {
37
+ editor = argeditor;
38
+
39
+ editor.attachEvent("exec_command_sorttable", function (state) {
40
+ state.returnValue = true;
41
+ obj.SortAtCaret(state && state.value === "desc" ? "desc" : "asc");
42
+ });
43
+ editor.attachEvent("exec_command_tablerownumbers", function (state) {
44
+ state.returnValue = true;
45
+ obj.ToggleRowNumbers();
46
+ });
47
+
48
+ // Public API.
49
+ editor.sortTableByColumn = function (index, dir) { return obj.Sort(tableAtCaret(), index, dir); };
50
+ editor.sortTableAtCaret = function (dir) { return obj.SortAtCaret(dir); };
51
+ editor.canSortTable = function () { return canSort(tableAtCaret()); };
52
+ editor.addTableRowNumbers = function (opts) { return obj.AddRowNumbers(opts); };
53
+ editor.removeTableRowNumbers = function () { return obj.RemoveRowNumbers(); };
54
+ editor.hasTableRowNumbers = function () {
55
+ var t = tableAtCaret();
56
+ return !!(t && t.querySelector("[data-rte-rownum]"));
57
+ };
58
+ };
59
+
60
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
61
+
62
+ // ---- locating ---------------------------------------------------------
63
+
64
+ function tableAtCaret() {
65
+ try {
66
+ var sel = editor.getSelection();
67
+ if (!sel || sel.rangeCount === 0) return null;
68
+ var n = sel.getRangeAt(0).startContainer;
69
+ if (n && n.nodeType === 3) n = n.parentNode;
70
+ var editable = getEditable();
71
+ while (n && n !== editable) {
72
+ if (n.nodeName === "TABLE") return n;
73
+ n = n.parentNode;
74
+ }
75
+ return null;
76
+ } catch (e) { return null; }
77
+ }
78
+
79
+ function cellAtCaret() {
80
+ try {
81
+ var sel = editor.getSelection();
82
+ if (!sel || sel.rangeCount === 0) return null;
83
+ var n = sel.getRangeAt(0).startContainer;
84
+ if (n && n.nodeType === 3) n = n.parentNode;
85
+ var editable = getEditable();
86
+ while (n && n !== editable) {
87
+ if (n.nodeName === "TD" || n.nodeName === "TH") return n;
88
+ n = n.parentNode;
89
+ }
90
+ return null;
91
+ } catch (e) { return null; }
92
+ }
93
+
94
+ // Rows that are allowed to move: everything except thead/tfoot and a
95
+ // header-looking first row.
96
+ function bodyRows(table) {
97
+ if (!table) return [];
98
+ var all = Array.prototype.slice.call(table.rows || []);
99
+ var out = [];
100
+ for (var i = 0; i < all.length; i++) {
101
+ var r = all[i];
102
+ var section = r.parentNode ? r.parentNode.nodeName : "";
103
+ if (section === "THEAD" || section === "TFOOT") continue;
104
+ // No <thead>: treat an all-<th> first row as the header anyway.
105
+ if (out.length === 0 && i === 0 && r.cells.length && allHeaderCells(r)) continue;
106
+ out.push(r);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function allHeaderCells(row) {
112
+ for (var i = 0; i < row.cells.length; i++) if (row.cells[i].nodeName !== "TH") return false;
113
+ return true;
114
+ }
115
+
116
+ // ---- safety -----------------------------------------------------------
117
+
118
+ function canSort(table) {
119
+ if (!table) return { ok: false, reason: "The caret is not inside a table." };
120
+ var rows = bodyRows(table);
121
+ if (rows.length < 2) return { ok: false, reason: "The table has fewer than two sortable rows." };
122
+ for (var i = 0; i < rows.length; i++) {
123
+ for (var c = 0; c < rows[i].cells.length; c++) {
124
+ var cell = rows[i].cells[c];
125
+ if ((cell.rowSpan || 1) > 1 || (cell.colSpan || 1) > 1) {
126
+ return {
127
+ ok: false,
128
+ reason: "This table has merged cells. Sorting would move rows out from under a rowspan and break the table, so it has been left alone."
129
+ };
130
+ }
131
+ }
132
+ }
133
+ return { ok: true, reason: "" };
134
+ }
135
+
136
+ // ---- value typing -----------------------------------------------------
137
+
138
+ // Detect what a cell actually holds so the comparison matches the data.
139
+ function cellValue(text) {
140
+ var t = String(text == null ? "" : text).trim();
141
+ if (!t) return { type: "empty", v: "" };
142
+ if (/\d/.test(t)) {
143
+ // Strip currency, spaces, thousands separators; (500) is accounting
144
+ // notation for negative.
145
+ var num = t.replace(/[\s ,]/g, "").replace(/^\(([^)]*)\)$/, "-$1").replace(/[^0-9.eE+-]/g, "");
146
+ if (num && /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(num)) {
147
+ return { type: "number", v: parseFloat(num) };
148
+ }
149
+ var d = Date.parse(t);
150
+ if (!isNaN(d)) return { type: "date", v: d };
151
+ }
152
+ return { type: "text", v: t.toLowerCase() };
153
+ }
154
+
155
+ // The column's type is whatever most of its non-empty cells are.
156
+ function columnType(rows, index) {
157
+ var counts = { number: 0, date: 0, text: 0 };
158
+ for (var i = 0; i < rows.length; i++) {
159
+ var cell = rows[i].cells[index];
160
+ if (!cell) continue;
161
+ var val = cellValue(cell.textContent);
162
+ if (val.type === "empty") continue;
163
+ counts[val.type]++;
164
+ }
165
+ if (counts.number >= counts.date && counts.number >= counts.text && counts.number > 0) return "number";
166
+ if (counts.date >= counts.text && counts.date > 0) return "date";
167
+ return "text";
168
+ }
169
+
170
+ // ---- sort -------------------------------------------------------------
171
+
172
+ obj.SortAtCaret = function (dir) {
173
+ var table = tableAtCaret();
174
+ var cell = cellAtCaret();
175
+ if (!table || !cell) return false;
176
+ return obj.Sort(table, cell.cellIndex, dir);
177
+ };
178
+
179
+ obj.Sort = function (table, index, dir) {
180
+ var check = canSort(table);
181
+ if (!check.ok) return check;
182
+ var descending = String(dir).toLowerCase() === "desc";
183
+ var rows = bodyRows(table);
184
+ var type = columnType(rows, index);
185
+
186
+ // Decorate with the original position so the sort is stable — the
187
+ // Array.prototype.sort in older engines is not guaranteed to be.
188
+ var decorated = [];
189
+ for (var i = 0; i < rows.length; i++) {
190
+ var cell = rows[i].cells[index];
191
+ decorated.push({ row: rows[i], order: i, val: cellValue(cell ? cell.textContent : "") });
192
+ }
193
+
194
+ decorated.sort(function (a, b) {
195
+ // Empty cells sink to the bottom in both directions: a blank is
196
+ // "no value", not "the smallest value".
197
+ if (a.val.type === "empty" && b.val.type === "empty") return a.order - b.order;
198
+ if (a.val.type === "empty") return 1;
199
+ if (b.val.type === "empty") return -1;
200
+
201
+ var cmp;
202
+ if (type === "number" || type === "date") {
203
+ var av = a.val.type === type ? a.val.v : Number.POSITIVE_INFINITY;
204
+ var bv = b.val.type === type ? b.val.v : Number.POSITIVE_INFINITY;
205
+ cmp = av < bv ? -1 : (av > bv ? 1 : 0);
206
+ } else {
207
+ var as = String(a.val.v), bs = String(b.val.v);
208
+ cmp = as.localeCompare ? as.localeCompare(bs, undefined, { numeric: true, sensitivity: "base" })
209
+ : (as < bs ? -1 : (as > bs ? 1 : 0));
210
+ }
211
+ if (cmp === 0) return a.order - b.order; // stable
212
+ return descending ? -cmp : cmp;
213
+ });
214
+
215
+ // Re-append in the new order. Appending a row already in the table moves
216
+ // it, so this reorders in place without cloning (which would drop any
217
+ // event handlers or editor state attached to the cells).
218
+ var parent = decorated.length ? decorated[0].row.parentNode : null;
219
+ if (!parent) return { ok: false, reason: "The table has no sortable rows." };
220
+
221
+ // Moving a row that contains the caret collapses the selection, so a
222
+ // second sort would find no cell and silently do nothing. Remember the
223
+ // cell and put the caret back into it afterwards.
224
+ var caretCell = cellAtCaret();
225
+ for (var j = 0; j < decorated.length; j++) parent.appendChild(decorated[j].row);
226
+ restoreCaret(caretCell);
227
+
228
+ renumber(table); // numbering is positional, so it follows the sort
229
+ fireChange();
230
+ return { ok: true, reason: "", sortedBy: index, direction: descending ? "desc" : "asc", detectedType: type };
231
+ };
232
+
233
+ // ---- row numbering ----------------------------------------------------
234
+
235
+ function seriesLabel(n, series) {
236
+ switch (String(series)) {
237
+ case "upper-alpha": return alpha(n).toUpperCase();
238
+ case "lower-alpha": return alpha(n);
239
+ case "upper-roman": return roman(n);
240
+ case "lower-roman": return roman(n).toLowerCase();
241
+ default: return String(n);
242
+ }
243
+ }
244
+
245
+ function alpha(n) {
246
+ var s = "";
247
+ while (n > 0) { var m = (n - 1) % 26; s = String.fromCharCode(97 + m) + s; n = Math.floor((n - 1) / 26); }
248
+ return s || "a";
249
+ }
250
+
251
+ function roman(n) {
252
+ var map = [[1000,"M"],[900,"CM"],[500,"D"],[400,"CD"],[100,"C"],[90,"XC"],
253
+ [50,"L"],[40,"XL"],[10,"X"],[9,"IX"],[5,"V"],[4,"IV"],[1,"I"]];
254
+ var out = "";
255
+ for (var i = 0; i < map.length; i++) while (n >= map[i][0]) { out += map[i][1]; n -= map[i][0]; }
256
+ return out || "I";
257
+ }
258
+
259
+ obj.AddRowNumbers = function (opts) {
260
+ var table = tableAtCaret();
261
+ if (!table) return false;
262
+ if (table.querySelector("[data-rte-rownum]")) { renumber(table); return true; }
263
+ opts = opts || {};
264
+ var series = opts.series || config.tableNumberSeries;
265
+ var doc = table.ownerDocument;
266
+
267
+ // Header cell in every header row so the columns stay aligned.
268
+ var all = Array.prototype.slice.call(table.rows || []);
269
+ var body = bodyRows(table);
270
+ var bodySet = {};
271
+ for (var b = 0; b < body.length; b++) bodySet[rowKey(body[b])] = true;
272
+
273
+ for (var i = 0; i < all.length; i++) {
274
+ var row = all[i];
275
+ var isBody = bodySet[rowKey(row)];
276
+ var cell = doc.createElement(isBody ? "td" : "th");
277
+ cell.setAttribute("data-rte-rownum", isBody ? "value" : "header");
278
+ if (!isBody) cell.textContent = String(config.tableNumberHeader == null ? "#" : config.tableNumberHeader);
279
+ row.insertBefore(cell, row.firstChild);
280
+ }
281
+ table.setAttribute("data-rte-rownum-series", series);
282
+ renumber(table);
283
+ fireChange();
284
+ return true;
285
+ };
286
+
287
+ // rows have no stable identity across arrays, so key on position
288
+ var keySeq = 0;
289
+ function rowKey(row) {
290
+ if (!row.__rteKey) row.__rteKey = "r" + (keySeq++);
291
+ return row.__rteKey;
292
+ }
293
+
294
+ obj.RemoveRowNumbers = function () {
295
+ var table = tableAtCaret();
296
+ if (!table) return false;
297
+ var cells = table.querySelectorAll("[data-rte-rownum]");
298
+ if (!cells.length) return false;
299
+ for (var i = 0; i < cells.length; i++) if (cells[i].parentNode) cells[i].parentNode.removeChild(cells[i]);
300
+ table.removeAttribute("data-rte-rownum-series");
301
+ fireChange();
302
+ return true;
303
+ };
304
+
305
+ obj.ToggleRowNumbers = function () {
306
+ var table = tableAtCaret();
307
+ if (!table) return false;
308
+ return table.querySelector("[data-rte-rownum]") ? obj.RemoveRowNumbers() : obj.AddRowNumbers();
309
+ };
310
+
311
+ // Numbering is positional: after a sort, row 1 is whatever is now on top.
312
+ function renumber(table) {
313
+ if (!table) return;
314
+ var series = table.getAttribute("data-rte-rownum-series") || config.tableNumberSeries;
315
+ var rows = bodyRows(table);
316
+ var n = 1;
317
+ for (var i = 0; i < rows.length; i++) {
318
+ var cell = rows[i].querySelector("[data-rte-rownum='value']");
319
+ if (!cell) continue;
320
+ cell.textContent = seriesLabel(n++, series);
321
+ }
322
+ }
323
+
324
+ // Put a collapsed caret back into a cell that has just been moved.
325
+ function restoreCaret(cell) {
326
+ if (!cell || !cell.parentNode) return;
327
+ try {
328
+ var doc = cell.ownerDocument;
329
+ var r = doc.createRange();
330
+ r.selectNodeContents(cell);
331
+ r.collapse(true);
332
+ var sel = editor.getSelection();
333
+ sel.removeAllRanges();
334
+ sel.addRange(r);
335
+ } catch (e) {}
336
+ }
337
+
338
+ function fireChange() {
339
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
340
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
341
+ }
342
+ }
@@ -0,0 +1,313 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-28 Text direction / bidirectional text. Arabic, Hebrew, Persian and
4
+ // Urdu are written right-to-left, and until now this editor had no way to say so
5
+ // — no direction command, no toolbar item, not even a language string. Seventy-two
6
+ // translation files do not help if the text still runs the wrong way.
7
+ //
8
+ // Design notes:
9
+ // - `dir` is CONTENT, not presentation. It must persist in getHTMLCode(), and
10
+ // it is deliberately NOT stripped around serialize the way the page-view
11
+ // overlay or the watermark are. A document whose direction disappeared on
12
+ // save would be unreadable.
13
+ // - Flipping direction SWAPS an explicit physical alignment. `dir` alone only
14
+ // changes the DEFAULT alignment, so a paragraph explicitly set to
15
+ // text-align:left stays hard-left in RTL and looks broken. Word swaps it;
16
+ // so do we.
17
+ // - Detection uses the first-strong algorithm, the same rule the HTML spec
18
+ // defines for dir="auto": the paragraph's direction is that of its first
19
+ // strongly-directional character, ignoring digits and punctuation.
20
+ // - Mixed content is isolated, not just marked. Dropping an LTR URL into an
21
+ // RTL sentence without isolation lets the bidi algorithm reorder the
22
+ // surrounding punctuation, which is how "(https://example.com)" ends up
23
+ // rendering with its brackets swapped.
24
+ //
25
+ // Not covered here: mirroring the editor's own toolbar/chrome for RTL locales.
26
+ // That is theme work in rte_theme_default.css, not document direction.
27
+ RTE_DefaultConfig.plugin_textdirection = RTE_Plugin_TextDirection;
28
+
29
+ // "rtl" | "ltr" | null (leave the document alone)
30
+ if (typeof RTE_DefaultConfig.defaultTextDirection === "undefined") RTE_DefaultConfig.defaultTextDirection = null;
31
+ // Detect direction from content when HTML is loaded or pasted.
32
+ if (typeof RTE_DefaultConfig.textDirectionAutoDetect === "undefined") RTE_DefaultConfig.textDirectionAutoDetect = false;
33
+ // Swap an explicit left/right alignment when direction flips.
34
+ if (typeof RTE_DefaultConfig.textDirectionSwapAlign === "undefined") RTE_DefaultConfig.textDirectionSwapAlign = true;
35
+
36
+ function RTE_Plugin_TextDirection() {
37
+ var obj = this;
38
+ var config, editor;
39
+ var boundDoc = null;
40
+
41
+ var BLOCKS = { P: 1, DIV: 1, LI: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1,
42
+ BLOCKQUOTE: 1, TD: 1, TH: 1, PRE: 1, UL: 1, OL: 1, TABLE: 1, SECTION: 1, ARTICLE: 1 };
43
+
44
+ // Strongly right-to-left ranges: Hebrew, Arabic (+ supplement, extended),
45
+ // Syriac, Thaana, NKo, Samaritan, Mandaic, and the Arabic presentation forms.
46
+ var RTL_RE = /[֐-׿؀-ۿ܀-ݏݐ-ݿހ-޿߀-߿ࠀ-࠿ࡀ-࡟ࢠ-ࣿיִ-﷿ﹰ-]/;
47
+ // Strongly left-to-right: Latin, Greek, Cyrillic, Armenian, and CJK/Kana
48
+ // (which are LTR for bidi purposes).
49
+ var LTR_RE = /[A-Za-zÀ-ʯͰ-֏ऀ-႟Ḁ-῿Ⰰ-ⷿ⺀-꓏가-힯]/;
50
+
51
+ obj.PluginName = "TextDirection";
52
+
53
+ obj.InitConfig = function (argconfig) { config = argconfig; };
54
+
55
+ obj.InitEditor = function (argeditor) {
56
+ editor = argeditor;
57
+
58
+ editor.attachEvent("exec_command_textdirection", function (state) {
59
+ state.returnValue = true;
60
+ var v = state && state.value;
61
+ if (v === "rtl" || v === "ltr") obj.Apply(v);
62
+ else obj.Toggle();
63
+ });
64
+
65
+ setup();
66
+ try { editor.attachEvent("ready", setup); } catch (e) {}
67
+ try { editor.attachEvent("aftersethtml", function () { setup(); afterLoad(); }); } catch (e) {}
68
+ setTimeout(function () { setup(); afterLoad(); }, 0);
69
+
70
+ // Public API.
71
+ editor.setTextDirection = function (dir) { return obj.Apply(dir); };
72
+ editor.toggleTextDirection = function () { return obj.Toggle(); };
73
+ editor.getTextDirection = function () { return obj.Current(); };
74
+ editor.setBaseDirection = function (dir) { return obj.Base(dir); };
75
+ editor.getBaseDirection = function () {
76
+ var ed = getEditable();
77
+ return ed ? (ed.getAttribute("dir") || "ltr") : "ltr";
78
+ };
79
+ editor.detectTextDirection = function (text) { return firstStrong(String(text == null ? "" : text)); };
80
+ editor.autoDetectTextDirection = function () { return obj.AutoDetect(); };
81
+ editor.isolateBidi = function (html, dir) { return obj.Isolate(html, dir); };
82
+ editor.insertIsolated = function (html, dir) { return obj.InsertIsolated(html, dir); };
83
+ editor.getTextDirectionCss = function () { return css(); };
84
+ };
85
+
86
+ function setup() {
87
+ var doc = getDoc();
88
+ if (!doc || doc === boundDoc) return;
89
+ boundDoc = doc;
90
+ injectStyles(doc);
91
+ }
92
+
93
+ function afterLoad() {
94
+ var ed = getEditable();
95
+ if (!ed) return;
96
+ if (config.defaultTextDirection === "rtl" || config.defaultTextDirection === "ltr") {
97
+ if (!ed.getAttribute("dir")) obj.Base(config.defaultTextDirection);
98
+ }
99
+ if (config.textDirectionAutoDetect) obj.AutoDetect();
100
+ }
101
+
102
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
103
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
104
+
105
+ // ---- detection -------------------------------------------------------
106
+
107
+ // First-strong: the direction of the first strongly-directional character.
108
+ // Digits, punctuation and whitespace are neutral and skipped, which is why
109
+ // "123 שלום" is RTL and "123 hello" is LTR.
110
+ function firstStrong(text) {
111
+ for (var i = 0; i < text.length; i++) {
112
+ var ch = text.charAt(i);
113
+ if (RTL_RE.test(ch)) return "rtl";
114
+ if (LTR_RE.test(ch)) return "ltr";
115
+ }
116
+ return "neutral";
117
+ }
118
+
119
+ // ---- selection -------------------------------------------------------
120
+
121
+ function blocksInSelection() {
122
+ var ed = getEditable();
123
+ var out = [];
124
+ try {
125
+ var sel = editor.getSelection();
126
+ if (!sel || sel.rangeCount === 0) return out;
127
+ var range = sel.getRangeAt(0);
128
+ var start = blockOf(range.startContainer);
129
+ var end = blockOf(range.endContainer);
130
+ if (!start) return out;
131
+ if (start === end || range.collapsed) return [start];
132
+
133
+ // Walk top-level blocks between the two endpoints.
134
+ var all = ed.querySelectorAll("*");
135
+ var collecting = false;
136
+ for (var i = 0; i < all.length; i++) {
137
+ var el = all[i];
138
+ if (!BLOCKS[el.nodeName]) continue;
139
+ if (el === start) collecting = true;
140
+ if (collecting && !containsBlockChild(el)) out.push(el);
141
+ if (el === end) break;
142
+ }
143
+ if (!out.length) out.push(start);
144
+ } catch (e) {}
145
+ return out;
146
+ }
147
+
148
+ // Only leaf-ish blocks get dir, so setting it on a <li> does not also stamp
149
+ // the parent <ul> and double-apply.
150
+ function containsBlockChild(el) {
151
+ for (var i = 0; i < el.children.length; i++) {
152
+ if (BLOCKS[el.children[i].nodeName]) return true;
153
+ }
154
+ return false;
155
+ }
156
+
157
+ function blockOf(node) {
158
+ var ed = getEditable();
159
+ var n = node && node.nodeType === 3 ? node.parentNode : node;
160
+ while (n && n !== ed) {
161
+ if (n.nodeType === 1 && BLOCKS[n.nodeName]) return n;
162
+ n = n.parentNode;
163
+ }
164
+ return ed && ed.firstElementChild ? ed.firstElementChild : null;
165
+ }
166
+
167
+ // ---- apply -----------------------------------------------------------
168
+
169
+ obj.Current = function () {
170
+ var b = blocksInSelection()[0];
171
+ if (!b) return editor.getBaseDirection ? editor.getBaseDirection() : "ltr";
172
+ var d = b.getAttribute("dir");
173
+ if (d) return d;
174
+ try {
175
+ var doc = getDoc();
176
+ var win = doc && (doc.defaultView || doc.parentWindow);
177
+ if (win) return win.getComputedStyle(b).direction || "ltr";
178
+ } catch (e) {}
179
+ return "ltr";
180
+ };
181
+
182
+ obj.Apply = function (dir) {
183
+ dir = (dir === "rtl") ? "rtl" : (dir === "auto" ? "auto" : "ltr");
184
+ var blocks = blocksInSelection();
185
+ if (!blocks.length) return false;
186
+ for (var i = 0; i < blocks.length; i++) {
187
+ var b = blocks[i];
188
+ var was = b.getAttribute("dir") || "ltr";
189
+ b.setAttribute("dir", dir);
190
+ if (config.textDirectionSwapAlign !== false && dir !== "auto" && was !== dir) swapAlign(b, dir);
191
+ }
192
+ fireChange();
193
+ return dir;
194
+ };
195
+
196
+ obj.Toggle = function () {
197
+ return obj.Apply(obj.Current() === "rtl" ? "ltr" : "rtl");
198
+ };
199
+
200
+ // `dir` only changes the DEFAULT alignment. An explicit text-align:left
201
+ // survives the flip and pins the text to the wrong edge, so swap the
202
+ // physical value to the mirror side.
203
+ function swapAlign(el, dir) {
204
+ var cur = (el.style && el.style.textAlign) || el.getAttribute("align") || "";
205
+ if (!cur) return;
206
+ var next = null;
207
+ if (cur === "left") next = "right";
208
+ else if (cur === "right") next = "left";
209
+ if (!next) return;
210
+ // Landing on the new direction's natural side means the explicit value
211
+ // is now redundant; drop it so the block follows `dir` from here on.
212
+ var natural = (dir === "rtl") ? "right" : "left";
213
+ if (el.style) el.style.textAlign = (next === natural) ? "" : next;
214
+ if (el.getAttribute("align")) el.removeAttribute("align");
215
+ }
216
+
217
+ obj.Base = function (dir) {
218
+ var ed = getEditable();
219
+ if (!ed) return false;
220
+ dir = (dir === "rtl") ? "rtl" : (dir === "auto" ? "auto" : "ltr");
221
+ ed.setAttribute("dir", dir);
222
+ fireChange();
223
+ return dir;
224
+ };
225
+
226
+ // Stamp each block with the direction its own text implies.
227
+ obj.AutoDetect = function () {
228
+ var ed = getEditable();
229
+ if (!ed) return [];
230
+ var out = [];
231
+ var all = ed.querySelectorAll("*");
232
+ for (var i = 0; i < all.length; i++) {
233
+ var el = all[i];
234
+ if (!BLOCKS[el.nodeName] || containsBlockChild(el)) continue;
235
+ var d = firstStrong(el.textContent || "");
236
+ if (d === "neutral") continue; // nothing to go on: leave it
237
+ if (el.getAttribute("dir") === d) continue;
238
+ el.setAttribute("dir", d);
239
+ out.push({ tag: el.nodeName, dir: d });
240
+ }
241
+ if (out.length) fireChange();
242
+ return out;
243
+ };
244
+
245
+ // ---- bidi isolation --------------------------------------------------
246
+
247
+ // <bdi> isolates a run so the surrounding text's direction cannot reorder
248
+ // it. Without this, an LTR URL inside an RTL sentence drags the adjacent
249
+ // punctuation to the wrong side.
250
+ obj.Isolate = function (html, dir) {
251
+ var d = (dir === "rtl" || dir === "ltr") ? dir : "auto";
252
+ return '<bdi dir="' + d + '">' + String(html == null ? "" : html) + "</bdi>";
253
+ };
254
+
255
+ obj.InsertIsolated = function (html, dir) {
256
+ var doc = getDoc();
257
+ if (!doc) return false;
258
+ var span = doc.createElement("bdi");
259
+ span.setAttribute("dir", (dir === "rtl" || dir === "ltr") ? dir : "auto");
260
+ span.innerHTML = String(html == null ? "" : html);
261
+ try {
262
+ if (typeof editor.insertElement === "function") { editor.insertElement(span); fireChange(); return true; }
263
+ } catch (e) {}
264
+ try {
265
+ var sel = editor.getSelection();
266
+ if (sel && sel.rangeCount) {
267
+ var r = sel.getRangeAt(0);
268
+ r.collapse(false);
269
+ r.insertNode(span);
270
+ r.setStartAfter(span); r.collapse(true);
271
+ sel.removeAllRanges(); sel.addRange(r);
272
+ fireChange();
273
+ return true;
274
+ }
275
+ } catch (e) {}
276
+ return false;
277
+ };
278
+
279
+ function fireChange() {
280
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
281
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
282
+ }
283
+
284
+ // ---- styles ----------------------------------------------------------
285
+
286
+ function css() {
287
+ return (
288
+ // Lists and quotes carry their decoration on the physical side, so
289
+ // they need flipping too or bullets sit on the wrong edge.
290
+ "[dir='rtl'] ul,[dir='rtl'] ol{padding-right:1.6em;padding-left:0;}" +
291
+ "[dir='rtl'] blockquote{border-right:3px solid #ddd;border-left:0;" +
292
+ "padding-right:.9em;padding-left:0;margin-right:0;}" +
293
+ "[dir='rtl'] table{direction:rtl;}" +
294
+ // Isolation should be visually invisible.
295
+ "bdi{unicode-bidi:isolate;}"
296
+ );
297
+ }
298
+
299
+ function injectStyles(doc) {
300
+ if (!doc) return;
301
+ var existing = doc.getElementById("rte-textdirection-styles");
302
+ var text = css();
303
+ if (existing) {
304
+ if (existing.getAttribute("data-css") === text) return;
305
+ existing.parentNode && existing.parentNode.removeChild(existing);
306
+ }
307
+ var st = doc.createElement("style");
308
+ st.id = "rte-textdirection-styles";
309
+ st.setAttribute("data-css", text);
310
+ st.appendChild(doc.createTextNode(text));
311
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
312
+ }
313
+ }