@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,398 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Footnotes. Inline reference markers plus an auto-numbered notes
4
+ // section at the end of the document — the citation apparatus legal briefs,
5
+ // academic papers, standards and long-form publishing are written with.
6
+ //
7
+ // Both premium competitors charge for this:
8
+ // - CKEditor 5 footnotes: "To use this premium feature, you need to activate
9
+ // it with proper credentials"; available in the Essential, Professional and
10
+ // Custom plans.
11
+ // - TinyMCE footnotes: "This plugin is only available for paid TinyMCE
12
+ // subscriptions" (premium plugin, TinyMCE 6.2+).
13
+ // Froala, Quill and Lexical ship nothing equivalent.
14
+ //
15
+ // Design notes:
16
+ // - The marker is an ATOMIC contenteditable=false <sup>, so arrow keys and
17
+ // backspace treat it as one unit and the caret can never land inside the
18
+ // number and corrupt it.
19
+ // - Numbers are written as LITERAL TEXT, not CSS counters. multilevellist.js
20
+ // deliberately went the other way, but the trade-off inverts here: a
21
+ // footnote's whole job is to survive leaving the editor (publishing, PDF,
22
+ // an RSS feed, someone else's page), and a CSS-counter number is invisible
23
+ // the moment the stylesheet does not travel with it. So we renumber on
24
+ // every mutation instead — see syncFootnotes().
25
+ // - Marker order is DOCUMENT order, and the <li> notes are re-sorted to match,
26
+ // so cutting a paragraph and pasting it earlier renumbers both ends.
27
+ // - Deleting a marker deletes its note; deleting the last marker removes the
28
+ // whole section. No orphans are left in the saved HTML.
29
+ // - The notes section IS content and is meant to persist in getHTMLCode()
30
+ // output — same contract as multilevellist's root class, and unlike the
31
+ // presentational overlays (pagination/typewriter) which are stripped around
32
+ // every serialize.
33
+ RTE_DefaultConfig.plugin_footnotes = RTE_Plugin_Footnotes;
34
+
35
+ // Heading rendered above the notes section. Set to "" to omit the heading.
36
+ if (typeof RTE_DefaultConfig.footnotesTitle === "undefined") RTE_DefaultConfig.footnotesTitle = "Footnotes";
37
+ // "decimal" | "lower-roman" | "upper-roman" | "lower-alpha" | "upper-alpha".
38
+ if (typeof RTE_DefaultConfig.footnotesNumbering === "undefined") RTE_DefaultConfig.footnotesNumbering = "decimal";
39
+ // Prefix/suffix wrapped around the marker number, e.g. "[" and "]".
40
+ if (typeof RTE_DefaultConfig.footnotesMarkerPrefix === "undefined") RTE_DefaultConfig.footnotesMarkerPrefix = "";
41
+ if (typeof RTE_DefaultConfig.footnotesMarkerSuffix === "undefined") RTE_DefaultConfig.footnotesMarkerSuffix = "";
42
+
43
+ function RTE_Plugin_Footnotes() {
44
+ var obj = this;
45
+ var config, editor;
46
+ var boundDoc = null;
47
+ var syncQueued = false;
48
+
49
+ obj.PluginName = "Footnotes";
50
+
51
+ obj.InitConfig = function (argconfig) { config = argconfig; };
52
+
53
+ obj.InitEditor = function (argeditor) {
54
+ editor = argeditor;
55
+
56
+ editor.attachEvent("exec_command_insertfootnote", function (state) {
57
+ state.returnValue = true;
58
+ obj.InsertFootnote();
59
+ });
60
+
61
+ setup();
62
+ try { editor.attachEvent("ready", setup); } catch (e) {}
63
+ // Content replaced wholesale (setHTMLCode / load): re-bind and renumber,
64
+ // because the incoming HTML may already carry markers from a prior save.
65
+ try { editor.attachEvent("aftersethtml", function () { setup(); queueSync(); }); } catch (e) {}
66
+ setTimeout(function () { setup(); queueSync(); }, 0);
67
+
68
+ // Public API.
69
+ editor.insertFootnote = function (text) { return obj.InsertFootnote(text); };
70
+ editor.getFootnotes = function () { return obj.List(); };
71
+ editor.syncFootnotes = function () { return syncFootnotes(); };
72
+ editor.getFootnotesCss = function () { return css(); };
73
+ };
74
+
75
+ function setup() {
76
+ var doc = getDoc();
77
+ if (!doc) return;
78
+ injectStyles(doc);
79
+ if (doc !== boundDoc) { bindEditable(); boundDoc = doc; }
80
+ }
81
+
82
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
83
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
84
+
85
+ function bindEditable() {
86
+ var editable = getEditable();
87
+ if (!editable) return;
88
+ // Any content mutation can add, remove or move a marker. Renumbering is
89
+ // cheap (a querySelectorAll plus text writes) so we just resync rather
90
+ // than trying to detect which edit mattered.
91
+ editable.addEventListener("keyup", queueSync);
92
+ editable.addEventListener("cut", queueSync);
93
+ editable.addEventListener("paste", queueSync);
94
+ editable.addEventListener("drop", queueSync);
95
+ // Clicking a marker jumps to its note, and clicking the backlink returns.
96
+ editable.addEventListener("click", function (e) {
97
+ var ref = closestClass(e.target, "rte-fn-ref", editable);
98
+ var back = closestClass(e.target, "rte-fn-back", editable);
99
+ var target = null;
100
+ if (ref) target = noteFor(ref.getAttribute("data-fn-id"));
101
+ else if (back) target = refFor(back.getAttribute("data-fn-id"));
102
+ if (!target) return;
103
+ e.preventDefault();
104
+ try { target.scrollIntoView({ block: "center" }); } catch (e2) { target.scrollIntoView(); }
105
+ flash(target);
106
+ });
107
+ }
108
+
109
+ // Coalesce bursts of edits into one renumber. setTimeout, not rAF: the
110
+ // editor may live in a background tab or a hidden iframe, where rAF never
111
+ // fires and the numbering would silently stop updating.
112
+ function queueSync() {
113
+ if (syncQueued) return;
114
+ syncQueued = true;
115
+ setTimeout(function () { syncQueued = false; syncFootnotes(); }, 0);
116
+ }
117
+
118
+ function closestClass(node, cls, root) {
119
+ while (node && node !== root) {
120
+ if (node.nodeType === 1 && node.classList && node.classList.contains(cls)) return node;
121
+ node = node.parentNode;
122
+ }
123
+ return null;
124
+ }
125
+
126
+ function newId() {
127
+ return "fn" + Math.floor(Math.random() * 1e9).toString(36) + (idSeq++).toString(36);
128
+ }
129
+ var idSeq = 0;
130
+
131
+ // ---- lookups ---------------------------------------------------------
132
+
133
+ function refs() {
134
+ var editable = getEditable();
135
+ if (!editable) return [];
136
+ // Document order is exactly what querySelectorAll returns, which is what
137
+ // the numbering is defined by.
138
+ return Array.prototype.slice.call(editable.querySelectorAll("sup.rte-fn-ref[data-fn-id]"));
139
+ }
140
+
141
+ function section() {
142
+ var editable = getEditable();
143
+ return editable ? editable.querySelector("section.rte-footnotes") : null;
144
+ }
145
+
146
+ function noteFor(id) {
147
+ var sec = section();
148
+ return sec ? sec.querySelector('li.rte-fn-note[data-fn-id="' + cssEscape(id) + '"]') : null;
149
+ }
150
+
151
+ function refFor(id) {
152
+ var editable = getEditable();
153
+ return editable ? editable.querySelector('sup.rte-fn-ref[data-fn-id="' + cssEscape(id) + '"]') : null;
154
+ }
155
+
156
+ function cssEscape(s) { return String(s).replace(/["\\]/g, "\\$&"); }
157
+
158
+ // ---- insert ----------------------------------------------------------
159
+
160
+ obj.InsertFootnote = function (text) {
161
+ var doc = getDoc();
162
+ var editable = getEditable();
163
+ if (!doc || !editable) return null;
164
+
165
+ var id = newId();
166
+ var sup = doc.createElement("sup");
167
+ sup.className = "rte-fn-ref";
168
+ sup.setAttribute("data-fn-id", id);
169
+ sup.setAttribute("contenteditable", "false");
170
+ sup.setAttribute("id", "fnref-" + id);
171
+ sup.textContent = "0"; // placeholder; syncFootnotes writes the real number
172
+
173
+ // Drop the marker at the caret. insertElement keeps the editor's own
174
+ // undo/selection bookkeeping honest, which hand-splicing would not.
175
+ var placed = false;
176
+ try {
177
+ if (typeof editor.insertElement === "function") { editor.insertElement(sup); placed = true; }
178
+ } catch (e) {}
179
+ if (!placed) {
180
+ try {
181
+ var sel = editor.getSelection();
182
+ if (sel && sel.rangeCount) {
183
+ var range = sel.getRangeAt(0);
184
+ range.collapse(false);
185
+ range.insertNode(sup);
186
+ range.setStartAfter(sup);
187
+ range.collapse(true);
188
+ sel.removeAllRanges();
189
+ sel.addRange(range);
190
+ placed = true;
191
+ }
192
+ } catch (e) {}
193
+ }
194
+ if (!placed) { editable.appendChild(sup); }
195
+
196
+ var note = ensureSection(doc).querySelector("ol.rte-fn-list");
197
+ var li = doc.createElement("li");
198
+ li.className = "rte-fn-note";
199
+ li.setAttribute("data-fn-id", id);
200
+ li.setAttribute("id", "fn-" + id);
201
+ // The note text sits DIRECTLY in the <li>. An earlier revision wrapped it
202
+ // in <span class="rte-fn-text">, which was actively harmful: an empty
203
+ // inline element is a caret trap — the caret at offset 0 of an empty span
204
+ // resolves to the parent, so the first thing the user typed landed
205
+ // OUTSIDE the span and getFootnotes() reported the note as empty while
206
+ // the note visibly had text.
207
+ if (text != null && String(text) !== "") li.appendChild(doc.createTextNode(String(text)));
208
+ var back = doc.createElement("a");
209
+ back.className = "rte-fn-back";
210
+ back.setAttribute("data-fn-id", id);
211
+ back.setAttribute("href", "#fnref-" + id);
212
+ back.setAttribute("contenteditable", "false");
213
+ back.setAttribute("title", "Back to reference");
214
+ back.textContent = "\u21a9";
215
+ li.appendChild(back);
216
+ note.appendChild(li);
217
+
218
+ syncFootnotes();
219
+ // Put the caret at the START of the new note so the user can just type
220
+ // the citation. Offset 0 of the <li> itself, which is a block and so
221
+ // holds a caret reliably, unlike an empty inline wrapper.
222
+ try {
223
+ var r = doc.createRange();
224
+ r.setStart(li, 0);
225
+ r.collapse(true);
226
+ var s2 = editor.getSelection();
227
+ s2.removeAllRanges();
228
+ s2.addRange(r);
229
+ if (typeof editor.focus === "function") editor.focus();
230
+ } catch (e) {}
231
+ fireChange();
232
+ return id;
233
+ };
234
+
235
+ function ensureSection(doc) {
236
+ var editable = getEditable();
237
+ var sec = section();
238
+ if (sec) return sec;
239
+ sec = doc.createElement("section");
240
+ sec.className = "rte-footnotes";
241
+ var title = String(config.footnotesTitle == null ? "" : config.footnotesTitle);
242
+ if (title) {
243
+ var h = doc.createElement("h2");
244
+ h.className = "rte-fn-title";
245
+ h.textContent = title;
246
+ sec.appendChild(h);
247
+ }
248
+ var ol = doc.createElement("ol");
249
+ ol.className = "rte-fn-list";
250
+ sec.appendChild(ol);
251
+ editable.appendChild(sec);
252
+ return sec;
253
+ }
254
+
255
+ // ---- the renumber / reconcile pass -----------------------------------
256
+
257
+ // Single source of truth: the markers present in the document, in order.
258
+ // Everything else (note order, note existence, both sets of numbers) is
259
+ // derived from that list, so there is no state to drift.
260
+ function syncFootnotes() {
261
+ var doc = getDoc();
262
+ var sec = section();
263
+ var list = refs();
264
+
265
+ if (!list.length) {
266
+ // Last marker went away: drop the section rather than leaving an
267
+ // empty "Footnotes" heading in the saved HTML.
268
+ if (sec && sec.parentNode) sec.parentNode.removeChild(sec);
269
+ return [];
270
+ }
271
+ if (!doc) return [];
272
+ if (!sec) sec = ensureSection(doc);
273
+ var ol = sec.querySelector("ol.rte-fn-list");
274
+ if (!ol) { ol = doc.createElement("ol"); ol.className = "rte-fn-list"; sec.appendChild(ol); }
275
+
276
+ var seen = {};
277
+ var out = [];
278
+ for (var i = 0; i < list.length; i++) {
279
+ var ref = list[i];
280
+ var id = ref.getAttribute("data-fn-id");
281
+ // A copy/paste of a marker duplicates its id; re-key the duplicate so
282
+ // the two markers do not fight over one note.
283
+ if (seen[id]) {
284
+ var fresh = newId();
285
+ ref.setAttribute("data-fn-id", fresh);
286
+ ref.setAttribute("id", "fnref-" + fresh);
287
+ id = fresh;
288
+ }
289
+ seen[id] = true;
290
+
291
+ var num = String(i + 1);
292
+ var label = String(config.footnotesMarkerPrefix || "") + num + String(config.footnotesMarkerSuffix || "");
293
+ if (ref.textContent !== label) ref.textContent = label;
294
+ ref.setAttribute("data-fn-number", num);
295
+ if (ref.getAttribute("contenteditable") !== "false") ref.setAttribute("contenteditable", "false");
296
+ if (ref.getAttribute("id") !== "fnref-" + id) ref.setAttribute("id", "fnref-" + id);
297
+
298
+ var note = ol.querySelector('li.rte-fn-note[data-fn-id="' + cssEscape(id) + '"]');
299
+ if (!note) {
300
+ // Marker with no note — a paste from elsewhere, or a note the
301
+ // user deleted by hand. Give it an empty note to write into.
302
+ note = doc.createElement("li");
303
+ note.className = "rte-fn-note";
304
+ note.setAttribute("data-fn-id", id);
305
+ note.setAttribute("id", "fn-" + id);
306
+ }
307
+ note.setAttribute("data-fn-number", num);
308
+ // Append in marker order — this both inserts new notes and re-sorts
309
+ // existing ones to match a reordered document.
310
+ ol.appendChild(note);
311
+ out.push({ id: id, number: i + 1, text: noteText(note) });
312
+ }
313
+
314
+ // Drop notes whose marker is gone.
315
+ var notes = Array.prototype.slice.call(ol.querySelectorAll("li.rte-fn-note"));
316
+ for (var j = 0; j < notes.length; j++) {
317
+ if (!seen[notes[j].getAttribute("data-fn-id")]) ol.removeChild(notes[j]);
318
+ }
319
+
320
+ if (ol.getAttribute("data-numbering") !== config.footnotesNumbering) {
321
+ ol.setAttribute("data-numbering", config.footnotesNumbering);
322
+ }
323
+ return out;
324
+ }
325
+
326
+ // Read the WHOLE <li> minus the backlink rather than one designated child.
327
+ // The user can type anywhere in the note, and documents saved by an earlier
328
+ // revision still carry a .rte-fn-text wrapper, so anything that trusts a
329
+ // single container reports empty text for notes that plainly have some.
330
+ function noteText(note) {
331
+ if (!note) return "";
332
+ var clone = note.cloneNode(true);
333
+ var backs = clone.querySelectorAll ? clone.querySelectorAll("a.rte-fn-back") : [];
334
+ for (var i = 0; i < backs.length; i++) {
335
+ if (backs[i].parentNode) backs[i].parentNode.removeChild(backs[i]);
336
+ }
337
+ return (clone.textContent || "").replace(/\u21a9/g, "").trim();
338
+ }
339
+
340
+ obj.List = function () {
341
+ var sec = section();
342
+ if (!sec) return [];
343
+ var out = [];
344
+ var notes = sec.querySelectorAll("li.rte-fn-note");
345
+ for (var i = 0; i < notes.length; i++) {
346
+ out.push({
347
+ id: notes[i].getAttribute("data-fn-id"),
348
+ number: parseInt(notes[i].getAttribute("data-fn-number"), 10) || (i + 1),
349
+ text: noteText(notes[i])
350
+ });
351
+ }
352
+ return out;
353
+ };
354
+
355
+ function flash(el) {
356
+ if (!el || !el.classList) return;
357
+ el.classList.add("rte-fn-flash");
358
+ setTimeout(function () { try { el.classList.remove("rte-fn-flash"); } catch (e) {} }, 900);
359
+ }
360
+
361
+ function fireChange() {
362
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
363
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
364
+ }
365
+
366
+ // ---- styles ----------------------------------------------------------
367
+
368
+ function css() {
369
+ var numbering = String(config.footnotesNumbering || "decimal");
370
+ return (
371
+ "sup.rte-fn-ref{cursor:pointer;font-size:.72em;line-height:0;vertical-align:super;" +
372
+ "color:#1474ea;font-weight:700;padding:0 .12em;user-select:none;}" +
373
+ "sup.rte-fn-ref:hover{text-decoration:underline;}" +
374
+ "section.rte-footnotes{margin-top:2em;padding-top:.85em;border-top:1px solid #ddd;" +
375
+ "font-size:.88em;color:#333;}" +
376
+ "section.rte-footnotes h2.rte-fn-title{font-size:1em;font-weight:700;margin:0 0 .5em;}" +
377
+ "ol.rte-fn-list{list-style:" + numbering + ";padding-left:1.6em;margin:0;}" +
378
+ "ol.rte-fn-list>li{margin:.25em 0;}" +
379
+ "a.rte-fn-back{text-decoration:none;color:#1474ea;cursor:pointer;user-select:none;}" +
380
+ ".rte-fn-flash{background:#fff3b0;transition:background .5s ease;}"
381
+ );
382
+ }
383
+
384
+ function injectStyles(doc) {
385
+ if (!doc) return;
386
+ var existing = doc.getElementById("rte-footnotes-styles");
387
+ var text = css();
388
+ if (existing) {
389
+ if (existing.getAttribute("data-css") === text) return;
390
+ existing.parentNode && existing.parentNode.removeChild(existing);
391
+ }
392
+ var st = doc.createElement("style");
393
+ st.id = "rte-footnotes-styles";
394
+ st.setAttribute("data-css", text);
395
+ st.appendChild(doc.createTextNode(text));
396
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
397
+ }
398
+ }