@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.
- package/migration/README.md +100 -0
- package/migration/cli.js +294 -0
- package/migration/index.d.ts +18 -0
- package/migration/index.js +242 -0
- package/migration/report-schema-v2.json +127 -0
- package/package.json +11 -1
- package/richtexteditor/CHANGELOG.md +326 -1
- package/richtexteditor/all_plugins.js +14959 -4832
- package/richtexteditor/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/accessibilitychecker.js +8 -7
- package/richtexteditor/plugins/aitoolkit.css +17 -8
- package/richtexteditor/plugins/aitoolkit.js +8 -8
- package/richtexteditor/plugins/all_plugins.js +14959 -4832
- package/richtexteditor/plugins/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/autocorrect.js +353 -0
- package/richtexteditor/plugins/bookmarkcard.js +12 -1
- package/richtexteditor/plugins/changecase.js +238 -0
- package/richtexteditor/plugins/comments.js +15 -28
- package/richtexteditor/plugins/contentminimap.js +6 -5
- package/richtexteditor/plugins/crossreference.js +408 -0
- package/richtexteditor/plugins/documentimport.js +915 -21
- package/richtexteditor/plugins/documentoutline.js +5 -4
- package/richtexteditor/plugins/docxexport.js +960 -0
- package/richtexteditor/plugins/draghandle.js +309 -0
- package/richtexteditor/plugins/footnotes.js +398 -0
- package/richtexteditor/plugins/formatpainter.js +298 -0
- package/richtexteditor/plugins/formattingmarks.js +246 -0
- package/richtexteditor/plugins/html2pdf.js +108 -4
- package/richtexteditor/plugins/imageeditor.js +37 -7
- package/richtexteditor/plugins/insertgallery.js +553 -170
- package/richtexteditor/plugins/keyboarda11y.js +316 -0
- package/richtexteditor/plugins/linenumbers.js +352 -0
- package/richtexteditor/plugins/linkchecker.js +396 -0
- package/richtexteditor/plugins/mentionplugin.js +4 -3
- package/richtexteditor/plugins/mergefields.js +328 -0
- package/richtexteditor/plugins/multilevellist.js +137 -0
- package/richtexteditor/plugins/pagination.js +505 -0
- package/richtexteditor/plugins/pdfexport.js +1137 -0
- package/richtexteditor/plugins/permanentpen.js +211 -0
- package/richtexteditor/plugins/revisionhistory.js +47 -34
- package/richtexteditor/plugins/rtlui.js +150 -0
- package/richtexteditor/plugins/sanitizer.js +486 -0
- package/richtexteditor/plugins/slashcommand.js +142 -17
- package/richtexteditor/plugins/spellcheck.js +6 -3
- package/richtexteditor/plugins/tableofcontents.js +353 -0
- package/richtexteditor/plugins/tabletools.js +342 -0
- package/richtexteditor/plugins/textdirection.js +313 -0
- package/richtexteditor/plugins/trackedchanges.js +37 -6
- package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
- package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
- package/richtexteditor/plugins/watermark.js +214 -0
- package/richtexteditor/plugins/wordexport.js +0 -0
- package/richtexteditor/plugins/yjscollab.js +49 -11
- package/richtexteditor/rte-config.js +44 -44
- package/richtexteditor/rte.js +91 -63
- 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
|
+
}
|