@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,316 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-08-02 Keyboard and state accessibility for the editor CHROME.
4
+ //
5
+ // a11yenhance.js names the editing region and dialoga11y.js names the dialogs.
6
+ // This closes what an audit of the running editor found still open — four
7
+ // findings, three of them WCAG Level A:
8
+ //
9
+ // 1. KEYBOARD TRAP (2.1.2, Level A). Tab inside the editing area is
10
+ // preventDefault-ed and inserts spaces, so a keyboard-only or screen
11
+ // reader user who enters the editor can never leave it. Reloading the page
12
+ // is the only way out. This is the most serious kind of accessibility
13
+ // defect: it does not degrade the experience, it ends it.
14
+ //
15
+ // 2. TOGGLE STATE NOT EXPOSED (4.1.2, Level A). Bold, italic, underline and
16
+ // the alignment buttons carry their state in a CSS class
17
+ // (rte-command-active / rte-command-deactive) and nowhere else. Sighted
18
+ // users see a highlighted button; assistive technology is told nothing.
19
+ // Measured: 0 of 6 toggle buttons exposed aria-pressed.
20
+ //
21
+ // 3. POPUP STATE NOT EXPOSED (4.1.2, Level A). Buttons carry
22
+ // aria-haspopup but never aria-expanded, so there is no way to know
23
+ // whether a menu is open. Measured: 0 of 7.
24
+ //
25
+ // 4. EVERY TOOLBAR BUTTON IS A TAB STOP (2.4.3, and plain usability).
26
+ // Measured 53. The ARIA Authoring Practices toolbar pattern is a single
27
+ // tab stop per toolbar with arrow keys moving between buttons. 53 presses
28
+ // of Tab to reach the text you came to write is not operable in any
29
+ // meaningful sense.
30
+ //
31
+ // All four are fixed here rather than in the core, so they ship without a
32
+ // re-obfuscation cycle.
33
+ //
34
+ // Config:
35
+ // config.keyboardA11y = false // opt out entirely
36
+ // config.a11yEscapeHint = "..." // wording appended to the editing area's name
37
+ // config.a11yRovingToolbar = false // keep every button as a tab stop
38
+ RTE_DefaultConfig.plugin_keyboarda11y = RTE_Plugin_KeyboardA11y;
39
+ if (typeof RTE_DefaultConfig.keyboardA11y === "undefined") RTE_DefaultConfig.keyboardA11y = true;
40
+
41
+ function RTE_Plugin_KeyboardA11y() {
42
+ var obj = this;
43
+ var config, editor;
44
+ var observers = [];
45
+ var popupOwner = null; // last activated [aria-haspopup]
46
+ var openPanels = []; // [{ panel, owner }]
47
+
48
+ obj.PluginName = "KeyboardA11y";
49
+
50
+ obj.InitConfig = function (argconfig) { config = argconfig; };
51
+
52
+ obj.InitEditor = function (argeditor) {
53
+ editor = argeditor;
54
+ if (config.keyboardA11y === false) return;
55
+
56
+ editor.focusToolbar = function () { return focusFirstToolbarButton(); };
57
+
58
+ setup();
59
+ try { editor.attachEvent("ready", setup); } catch (e) {}
60
+ // The toolbar is built asynchronously; a deferred pass catches the case
61
+ // where InitEditor runs before it exists.
62
+ setTimeout(setup, 0);
63
+ setTimeout(setup, 400);
64
+ };
65
+
66
+ function shell() {
67
+ try {
68
+ var ed = editor.getEditable();
69
+ if (!ed) return null;
70
+ var win = ed.ownerDocument.defaultView;
71
+ var node = (win && win.frameElement) ? win.frameElement : ed;
72
+ while (node && node.classList && !node.classList.contains("richtexteditor")) node = node.parentNode;
73
+ return (node && node.classList) ? node : null;
74
+ } catch (e) { return null; }
75
+ }
76
+
77
+ function setup() {
78
+ var root = shell();
79
+ if (!root) return;
80
+ bindEscapeHatch();
81
+ applyRovingTabindex(root);
82
+ syncToggleStates(root);
83
+ trackPopups(root);
84
+ watch(root);
85
+ }
86
+
87
+ // ---------------------------------------------------- 1. keyboard trap
88
+ //
89
+ // Tab keeps its editing meaning (indent, next table cell) because that is
90
+ // what writers expect and what every other editor does. The escape is a
91
+ // separate, documented key: Escape leaves the editing area and puts focus
92
+ // on the toolbar, from which Tab continues through the page normally.
93
+ //
94
+ // Escape is only intercepted when nothing is open — a dialog or dropdown
95
+ // must still get its own Escape first, or closing a colour picker would
96
+ // throw the user out of the editor.
97
+ function bindEscapeHatch() {
98
+ var ed;
99
+ try { ed = editor.getEditable(); } catch (e) { return; }
100
+ if (!ed || ed.__rteEscapeHatch) return;
101
+ ed.__rteEscapeHatch = true;
102
+
103
+ ed.addEventListener("keydown", function (e) {
104
+ if (e.key !== "Escape" && e.keyCode !== 27) return;
105
+ if (anythingOpen()) return; // let the panel close itself
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ if (!focusFirstToolbarButton()) focusAfterEditor();
109
+ }, false);
110
+
111
+ announceEscape(ed);
112
+ }
113
+
114
+ function anythingOpen() {
115
+ try {
116
+ var panels = document.querySelectorAll("rte-dropdown-panel, rte-dialog-float, rte-floatpanel");
117
+ for (var i = 0; i < panels.length; i++) {
118
+ var s = getComputedStyle(panels[i]);
119
+ if (s.display !== "none" && s.visibility !== "hidden") return true;
120
+ }
121
+ } catch (e) {}
122
+ return false;
123
+ }
124
+
125
+ // The way out has to be discoverable, or it may as well not exist. The
126
+ // editing region's accessible name is the one thing a screen reader always
127
+ // announces on entry, so the hint goes there rather than into the content.
128
+ function announceEscape(ed) {
129
+ var hint = config.a11yEscapeHint ||
130
+ "Press Escape to leave the editing area.";
131
+ var label = ed.getAttribute("aria-label") || "";
132
+ if (label.indexOf(hint) >= 0) return;
133
+ ed.setAttribute("aria-label", (label ? label.replace(/\s*$/, " ") : "") + hint);
134
+ }
135
+
136
+ function focusAfterEditor() {
137
+ var root = shell();
138
+ if (!root) return false;
139
+ var all = [].slice.call(document.querySelectorAll(
140
+ 'a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
141
+ )).filter(function (el) {
142
+ return !root.contains(el) && el.offsetParent !== null;
143
+ });
144
+ // The first focusable that follows the editor in document order.
145
+ for (var i = 0; i < all.length; i++) {
146
+ if (root.compareDocumentPosition(all[i]) & Node.DOCUMENT_POSITION_FOLLOWING) {
147
+ all[i].focus();
148
+ return true;
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+
154
+ // ------------------------------------------------- 4. roving tabindex
155
+ function toolbarsIn(root) {
156
+ return [].slice.call(root.querySelectorAll('[role="toolbar"]'));
157
+ }
158
+ // Toolbars nest — an overflow or ribbon toolbar sits inside the main one.
159
+ // A plain descendant query therefore makes the outer toolbar claim the
160
+ // inner one's buttons, and the two passes fight: the first run measured two
161
+ // tab stops in one toolbar and NONE in the other two, which left those
162
+ // buttons unreachable by keyboard altogether. Each button belongs to its
163
+ // NEAREST toolbar and to no other.
164
+ function buttonsIn(bar) {
165
+ return [].slice.call(bar.querySelectorAll('[role="button"]')).filter(function (b) {
166
+ if (b.getAttribute("aria-disabled") === "true") return false;
167
+ if (b.offsetParent === null) return false; // hidden: not reachable anyway
168
+ return (b.closest && b.closest('[role="toolbar"]')) === bar;
169
+ });
170
+ }
171
+
172
+ function applyRovingTabindex(root) {
173
+ if (config.a11yRovingToolbar === false) return;
174
+ var bars = toolbarsIn(root);
175
+ for (var i = 0; i < bars.length; i++) {
176
+ (function (bar) {
177
+ var btns = buttonsIn(bar);
178
+ if (!btns.length) return;
179
+ // Exactly one tab stop per toolbar, which is the ARIA pattern.
180
+ var current = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
181
+ for (var j = 0; j < btns.length; j++) btns[j].setAttribute("tabindex", btns[j] === current ? "0" : "-1");
182
+
183
+ if (bar.__rteRoving) return;
184
+ bar.__rteRoving = true;
185
+ bar.addEventListener("keydown", function (e) {
186
+ var list = buttonsIn(bar);
187
+ var at = list.indexOf(document.activeElement);
188
+ if (at < 0) return;
189
+ var rtl = (bar.closest && bar.closest('[dir="rtl"]')) ? true : false;
190
+ var next = null;
191
+ if (e.key === "ArrowRight") next = list[(at + (rtl ? -1 : 1) + list.length) % list.length];
192
+ else if (e.key === "ArrowLeft") next = list[(at + (rtl ? 1 : -1) + list.length) % list.length];
193
+ else if (e.key === "Home") next = list[0];
194
+ else if (e.key === "End") next = list[list.length - 1];
195
+ else return;
196
+ e.preventDefault();
197
+ for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === next ? "0" : "-1");
198
+ next.focus();
199
+ }, false);
200
+ // Clicking a button makes it the new tab stop, so returning by
201
+ // Tab lands where the user last was.
202
+ bar.addEventListener("focusin", function (e) {
203
+ var list = buttonsIn(bar);
204
+ if (list.indexOf(e.target) < 0) return;
205
+ for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === e.target ? "0" : "-1");
206
+ }, false);
207
+ })(bars[i]);
208
+ }
209
+ }
210
+
211
+ function focusFirstToolbarButton() {
212
+ var root = shell();
213
+ if (!root) return false;
214
+ var bars = toolbarsIn(root);
215
+ for (var i = 0; i < bars.length; i++) {
216
+ var btns = buttonsIn(bars[i]);
217
+ if (!btns.length) continue;
218
+ var target = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
219
+ target.focus();
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ // ------------------------------------------------- 2. aria-pressed
226
+ //
227
+ // The editor already tracks active state — it just keeps it in a class.
228
+ // Mirroring rather than recomputing means the announced state can never
229
+ // disagree with the highlighted button.
230
+ var TOGGLE_CMD = /^(bold|italic|underline|strikethrough|subscript|superscript|justifyleft|justifycenter|justifyright|justifyfull|insertorderedlist|insertunorderedlist|outdent|indent|blockquote|inlinecode|trackchanges|typewriter|focusmode|pagination|formattingmarks|linenumbers|permanentpen|rtlui)$/;
231
+
232
+ function syncToggleStates(root) {
233
+ var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
234
+ for (var i = 0; i < btns.length; i++) {
235
+ var cmd = (btns[i].getAttribute("rte-cmd-name") || "").toLowerCase();
236
+ if (!TOGGLE_CMD.test(cmd)) continue;
237
+ var active = btns[i].classList.contains("rte-command-active");
238
+ var pressed = active ? "true" : "false";
239
+ if (btns[i].getAttribute("aria-pressed") !== pressed) btns[i].setAttribute("aria-pressed", pressed);
240
+ }
241
+ }
242
+
243
+ // ------------------------------------------------- 3. aria-expanded
244
+ //
245
+ // Opening a menu inserts an <rte-dropdown-panel> elsewhere in the document
246
+ // and leaves the button untouched, so the button and its panel have to be
247
+ // correlated: remember which popup button was activated, then pair it with
248
+ // the panel that appears.
249
+ function trackPopups(root) {
250
+ var pops = [].slice.call(root.querySelectorAll("[aria-haspopup]"));
251
+ for (var i = 0; i < pops.length; i++) {
252
+ if (!pops[i].hasAttribute("aria-expanded")) pops[i].setAttribute("aria-expanded", "false");
253
+ if (pops[i].__rtePopupBound) continue;
254
+ pops[i].__rtePopupBound = true;
255
+ (function (el) {
256
+ function remember() { popupOwner = el; }
257
+ el.addEventListener("mousedown", remember, true);
258
+ el.addEventListener("keydown", function (e) {
259
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") remember();
260
+ }, true);
261
+ })(pops[i]);
262
+ }
263
+
264
+ if (trackPopups.bound) return;
265
+ trackPopups.bound = true;
266
+ var mo = new MutationObserver(function (records) {
267
+ for (var r = 0; r < records.length; r++) {
268
+ var rec = records[r];
269
+ for (var a = 0; a < rec.addedNodes.length; a++) {
270
+ var n = rec.addedNodes[a];
271
+ if (n.nodeType !== 1 || !isPanel(n)) continue;
272
+ if (popupOwner) {
273
+ popupOwner.setAttribute("aria-expanded", "true");
274
+ openPanels.push({ panel: n, owner: popupOwner });
275
+ popupOwner = null;
276
+ }
277
+ }
278
+ for (var d = 0; d < rec.removedNodes.length; d++) {
279
+ var m = rec.removedNodes[d];
280
+ if (m.nodeType !== 1 || !isPanel(m)) continue;
281
+ for (var k = openPanels.length - 1; k >= 0; k--) {
282
+ if (openPanels[k].panel === m) {
283
+ openPanels[k].owner.setAttribute("aria-expanded", "false");
284
+ openPanels.splice(k, 1);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ });
290
+ mo.observe(document.body, { childList: true, subtree: true });
291
+ observers.push(mo);
292
+ }
293
+ function isPanel(el) {
294
+ var t = (el.tagName || "").toLowerCase();
295
+ return t === "rte-dropdown-panel" || t === "rte-floatpanel" || t === "rte-dialog-float";
296
+ }
297
+
298
+ // Toolbar buttons are rebuilt and re-classed as the selection moves, so the
299
+ // mirrored state has to follow rather than be set once.
300
+ function watch(root) {
301
+ if (root.__rteA11yWatch) return;
302
+ root.__rteA11yWatch = true;
303
+ var pending = null;
304
+ var mo = new MutationObserver(function () {
305
+ if (pending) return;
306
+ pending = setTimeout(function () {
307
+ pending = null;
308
+ syncToggleStates(root);
309
+ applyRovingTabindex(root);
310
+ trackPopups(root);
311
+ }, 60);
312
+ });
313
+ mo.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "aria-disabled"] });
314
+ observers.push(mo);
315
+ }
316
+ }
@@ -0,0 +1,352 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Margin line numbers. Numbers every visual line in the left gutter,
4
+ // the way a word processor does for pleadings, statutes, transcripts and any
5
+ // document people cite by line ("page 4, lines 12-15").
6
+ //
7
+ // Court filing rules in several jurisdictions REQUIRE numbered lines, which is
8
+ // why every serious document editor has this and no browser editor does:
9
+ // CKEditor 5 and TinyMCE both document nothing equivalent. It pairs with
10
+ // pagination.js — with page view on, numbering can restart at each page, which
11
+ // is what the filing rules actually specify.
12
+ //
13
+ // Design notes:
14
+ // - VISUAL lines, not blocks. A paragraph that wraps over four lines gets four
15
+ // numbers, because that is what "line 12" means to the person citing it.
16
+ // Found via Range.getClientRects(), which returns one rect per line box.
17
+ // - Purely presentational. The overlay is contenteditable=false and stripped
18
+ // around every serialize (the pagination.js contract), so numbering the lines
19
+ // never changes a byte of the HTML you save.
20
+ // - Recompute is rAF-throttled and skipped when the geometry signature is
21
+ // unchanged, because this measures every line box in the document and would
22
+ // otherwise run on every keystroke.
23
+ RTE_DefaultConfig.plugin_linenumbers = RTE_Plugin_LineNumbers;
24
+
25
+ // "continuous" | "page" (restart each page, needs pagination) | "block"
26
+ if (typeof RTE_DefaultConfig.lineNumberRestart === "undefined") RTE_DefaultConfig.lineNumberRestart = "continuous";
27
+ // Show every Nth number. 1 = every line; 5 is the common pleading convention.
28
+ if (typeof RTE_DefaultConfig.lineNumberInterval === "undefined") RTE_DefaultConfig.lineNumberInterval = 1;
29
+ // First number.
30
+ if (typeof RTE_DefaultConfig.lineNumberStart === "undefined") RTE_DefaultConfig.lineNumberStart = 1;
31
+ // Gutter width in px.
32
+ if (typeof RTE_DefaultConfig.lineNumberGutter === "undefined") RTE_DefaultConfig.lineNumberGutter = 38;
33
+
34
+ function RTE_Plugin_LineNumbers() {
35
+ var obj = this;
36
+ var config, editor;
37
+ var boundDoc = null;
38
+ var active = false;
39
+ var overlay = null;
40
+ var raf = 0;
41
+ var lastSignature = "";
42
+ var wrapped = false;
43
+ var lineCount = 0;
44
+
45
+ obj.PluginName = "LineNumbers";
46
+
47
+ obj.InitConfig = function (argconfig) { config = argconfig; };
48
+
49
+ obj.InitEditor = function (argeditor) {
50
+ editor = argeditor;
51
+
52
+ editor.attachEvent("exec_command_linenumbers", function (state) {
53
+ state.returnValue = true;
54
+ obj.Toggle();
55
+ });
56
+
57
+ setup();
58
+ try { editor.attachEvent("ready", setup); } catch (e) {}
59
+ try { editor.attachEvent("aftersethtml", function () { setup(); schedule(); }); } catch (e) {}
60
+ setTimeout(setup, 0);
61
+
62
+ // Public API.
63
+ editor.setLineNumbers = function (on) { active = !!on; apply(); return active; };
64
+ editor.toggleLineNumbers = function () { return obj.Toggle(); };
65
+ editor.isLineNumbers = function () { return active; };
66
+ editor.getLineCount = function () { if (active) render(); return lineCount; };
67
+ editor.setLineNumberOptions = function (o) {
68
+ if (o && typeof o === "object") {
69
+ if (o.restart != null) config.lineNumberRestart = o.restart;
70
+ if (o.interval != null) config.lineNumberInterval = o.interval;
71
+ if (o.start != null) config.lineNumberStart = o.start;
72
+ if (o.gutter != null) config.lineNumberGutter = o.gutter;
73
+ }
74
+ lastSignature = "";
75
+ apply();
76
+ return {
77
+ restart: config.lineNumberRestart, interval: config.lineNumberInterval,
78
+ start: config.lineNumberStart, gutter: config.lineNumberGutter
79
+ };
80
+ };
81
+ editor.getLineNumbersCss = function () { return css(); };
82
+ };
83
+
84
+ obj.Toggle = function () { active = !active; apply(); return active; };
85
+
86
+ function setup() {
87
+ var doc = getDoc();
88
+ if (!doc) return;
89
+ injectStyles(doc);
90
+ wrapSerializers();
91
+ if (doc === boundDoc) return;
92
+ boundDoc = doc;
93
+ doc.addEventListener("input", schedule, true);
94
+ doc.addEventListener("keyup", schedule, true);
95
+ var win = doc.defaultView || doc.parentWindow;
96
+ if (win) win.addEventListener("resize", schedule);
97
+ }
98
+
99
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
100
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
101
+
102
+ function apply() {
103
+ var editable = getEditable();
104
+ if (editable && editable.classList) {
105
+ if (active) editable.classList.add("rte-linenumbers-on");
106
+ else editable.classList.remove("rte-linenumbers-on");
107
+ }
108
+ if (editable) {
109
+ // Reserve the gutter by padding the editable, so text does not sit
110
+ // under the numbers.
111
+ editable.style.paddingLeft = active ? (parseInt(config.lineNumberGutter, 10) + 12) + "px" : "";
112
+ }
113
+ if (active) { lastSignature = ""; render(); }
114
+ else removeOverlay();
115
+ }
116
+
117
+ function schedule() {
118
+ if (!active) return;
119
+ if (raf) return;
120
+ var doc = getDoc();
121
+ var win = (doc && (doc.defaultView || doc.parentWindow)) || window;
122
+ var rq = (win && typeof win.requestAnimationFrame === "function")
123
+ ? function (f) { return win.requestAnimationFrame(f); }
124
+ : function (f) { return setTimeout(f, 16); };
125
+ raf = rq(function () { raf = 0; render(); });
126
+ }
127
+
128
+ // ---- line discovery --------------------------------------------------
129
+
130
+ // One entry per VISUAL line: its top offset relative to the editable, and
131
+ // which top-level block it belongs to.
132
+ function measureLines() {
133
+ var doc = getDoc();
134
+ var editable = getEditable();
135
+ if (!doc || !editable) return [];
136
+ var lines = [];
137
+ var kids = editable.children;
138
+ var edRect = editable.getBoundingClientRect();
139
+ var scrollTop = editable.scrollTop || 0;
140
+
141
+ for (var i = 0; i < kids.length; i++) {
142
+ var block = kids[i];
143
+ if (block.nodeType !== 1) continue;
144
+ if (block.getAttribute && (block.getAttribute("data-rte-linenumbers") === "true" ||
145
+ block.getAttribute("data-rte-page-overlay") === "true")) continue;
146
+ if (!block.textContent && !block.querySelector("img,table,hr")) {
147
+ // Empty block still occupies one line.
148
+ var br = block.getBoundingClientRect();
149
+ if (br.height > 0) lines.push({ top: br.top - edRect.top + scrollTop, block: i });
150
+ continue;
151
+ }
152
+ var rects = lineRectsOf(doc, block);
153
+ for (var r = 0; r < rects.length; r++) {
154
+ lines.push({ top: rects[r] - edRect.top + scrollTop, block: i });
155
+ }
156
+ }
157
+ return lines;
158
+ }
159
+
160
+ // Distinct line-box tops inside one block. getClientRects() on a range over
161
+ // the block's contents yields a rect per line box; identical tops are the
162
+ // same line split across inline elements, so they collapse.
163
+ function lineRectsOf(doc, block) {
164
+ var tops = [];
165
+ var seen = {};
166
+ try {
167
+ var range = doc.createRange();
168
+ range.selectNodeContents(block);
169
+ var rects = range.getClientRects();
170
+ for (var i = 0; i < rects.length; i++) {
171
+ var rc = rects[i];
172
+ if (!rc || rc.height <= 0 || rc.width <= 0) continue;
173
+ // Round: sub-pixel differences within one line are common.
174
+ var key = Math.round(rc.top);
175
+ if (seen[key]) continue;
176
+ seen[key] = true;
177
+ tops.push(rc.top);
178
+ }
179
+ } catch (e) {}
180
+ if (!tops.length) {
181
+ var br = block.getBoundingClientRect();
182
+ if (br.height > 0) tops.push(br.top);
183
+ }
184
+ tops.sort(function (a, b) { return a - b; });
185
+ return tops;
186
+ }
187
+
188
+ // ---- render ----------------------------------------------------------
189
+
190
+ function render() {
191
+ if (!active) return;
192
+ var doc = getDoc();
193
+ var editable = getEditable();
194
+ if (!doc || !editable) return;
195
+
196
+ var lines = measureLines();
197
+ lineCount = lines.length;
198
+
199
+ var restart = String(config.lineNumberRestart || "continuous");
200
+ var interval = Math.max(1, parseInt(config.lineNumberInterval, 10) || 1);
201
+ var start = parseInt(config.lineNumberStart, 10);
202
+ if (isNaN(start)) start = 1;
203
+
204
+ // Page boundaries, only when both page view and pagination are present.
205
+ var breaks = [];
206
+ if (restart === "page") {
207
+ try {
208
+ if (typeof editor.isPageView === "function" && editor.isPageView() &&
209
+ typeof editor.getPageOfElement === "function") {
210
+ breaks = null; // signalled by using getPageOfElement per line below
211
+ }
212
+ } catch (e) {}
213
+ }
214
+
215
+ var sig = lines.length + "|" + restart + "|" + interval + "|" + start + "|" +
216
+ (lines.length ? Math.round(lines[lines.length - 1].top) : 0) + "|" + editable.offsetHeight;
217
+ if (sig === lastSignature && overlay && overlay.parentNode) return;
218
+ lastSignature = sig;
219
+
220
+ var ov = ensureOverlay(doc, editable);
221
+ while (ov.firstChild) ov.removeChild(ov.firstChild);
222
+ ov.style.width = parseInt(config.lineNumberGutter, 10) + "px";
223
+
224
+ var n = start;
225
+ var lastPage = null;
226
+ var lastBlock = null;
227
+ for (var i = 0; i < lines.length; i++) {
228
+ var line = lines[i];
229
+
230
+ if (restart === "block") {
231
+ if (lastBlock !== null && line.block !== lastBlock) n = start;
232
+ lastBlock = line.block;
233
+ } else if (restart === "page") {
234
+ var page = pageOfLine(editable, line);
235
+ if (lastPage !== null && page !== lastPage) n = start;
236
+ lastPage = page;
237
+ }
238
+
239
+ if ((n - start) % interval === 0) {
240
+ var tag = doc.createElement("div");
241
+ tag.className = "rte-linenumber";
242
+ tag.style.top = Math.round(line.top) + "px";
243
+ tag.textContent = String(n);
244
+ ov.appendChild(tag);
245
+ }
246
+ n++;
247
+ }
248
+ }
249
+
250
+ function pageOfLine(editable, line) {
251
+ // Resolve through the block the line belongs to; pagination measures
252
+ // top-level children, which is exactly what line.block indexes.
253
+ try {
254
+ var block = editable.children[line.block];
255
+ if (block && typeof editor.getPageOfElement === "function") {
256
+ var p = editor.getPageOfElement(block);
257
+ if (p) return p;
258
+ }
259
+ } catch (e) {}
260
+ return 1;
261
+ }
262
+
263
+ function ensureOverlay(doc, editable) {
264
+ if (overlay && overlay.parentNode) return overlay;
265
+ overlay = doc.createElement("div");
266
+ overlay.setAttribute("data-rte-linenumbers", "true");
267
+ overlay.setAttribute("contenteditable", "false");
268
+ overlay.setAttribute("aria-hidden", "true");
269
+ overlay.className = "rte-linenumber-gutter";
270
+ (editable.parentNode || doc.body || doc.documentElement).appendChild(overlay);
271
+ return overlay;
272
+ }
273
+
274
+ function removeOverlay() {
275
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
276
+ overlay = null;
277
+ lastSignature = "";
278
+ }
279
+
280
+ // ---- serialization safety -------------------------------------------
281
+
282
+ function stripFor() {
283
+ var editable = getEditable();
284
+ if (!editable) return function () {};
285
+ var doc = getDoc();
286
+ var nodes = doc ? doc.querySelectorAll("[data-rte-linenumbers]") : [];
287
+ var parked = [];
288
+ for (var i = 0; i < nodes.length; i++) {
289
+ var nd = nodes[i];
290
+ parked.push({ node: nd, parent: nd.parentNode, next: nd.nextSibling });
291
+ if (nd.parentNode) nd.parentNode.removeChild(nd);
292
+ }
293
+ // The gutter padding is ours too, and would otherwise leak into any
294
+ // serializer that reads inline styles off the editable.
295
+ var pad = editable.style.paddingLeft;
296
+ editable.style.paddingLeft = "";
297
+ return function restore() {
298
+ editable.style.paddingLeft = pad;
299
+ for (var j = 0; j < parked.length; j++) {
300
+ if (parked[j].parent) parked[j].parent.insertBefore(parked[j].node, parked[j].next || null);
301
+ }
302
+ };
303
+ }
304
+
305
+ function wrapSerializers() {
306
+ if (wrapped) return;
307
+ var names = ["getHTMLCode", "getJSON", "getHTMLContent", "getText"];
308
+ var did = false;
309
+ for (var i = 0; i < names.length; i++) {
310
+ (function (name) {
311
+ var orig = editor[name];
312
+ if (typeof orig !== "function" || orig.__rteLnWrapped) return;
313
+ var w = function () {
314
+ var restore = stripFor();
315
+ try { return orig.apply(editor, arguments); } finally { restore(); }
316
+ };
317
+ w.__rteLnWrapped = true;
318
+ editor[name] = w;
319
+ did = true;
320
+ })(names[i]);
321
+ }
322
+ if (did) wrapped = true;
323
+ }
324
+
325
+ // ---- styles ----------------------------------------------------------
326
+
327
+ function css() {
328
+ return (
329
+ ".rte-linenumber-gutter{position:absolute;left:0;top:0;pointer-events:none;" +
330
+ "user-select:none;z-index:2;}" +
331
+ ".rte-linenumber{position:absolute;right:6px;text-align:right;width:100%;" +
332
+ "font-size:11px;line-height:1;color:#8a94a6;font-variant-numeric:tabular-nums;" +
333
+ "font-family:Consolas,Menlo,monospace;}" +
334
+ ".rte-linenumbers-on{position:relative;}"
335
+ );
336
+ }
337
+
338
+ function injectStyles(doc) {
339
+ if (!doc) return;
340
+ var existing = doc.getElementById("rte-linenumber-styles");
341
+ var text = css();
342
+ if (existing) {
343
+ if (existing.getAttribute("data-css") === text) return;
344
+ existing.parentNode && existing.parentNode.removeChild(existing);
345
+ }
346
+ var st = doc.createElement("style");
347
+ st.id = "rte-linenumber-styles";
348
+ st.setAttribute("data-css", text);
349
+ st.appendChild(doc.createTextNode(text));
350
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
351
+ }
352
+ }