@richscripts/richtexteditor 2.5.4 → 2.6.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/README.md +29 -2
- package/package.json +1 -1
- package/richtexteditor/CHANGELOG.md +2714 -2201
- package/richtexteditor/all_plugins.js +2008 -1272
- package/richtexteditor/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/accessibilitychecker.js +60 -3
- package/richtexteditor/plugins/aitoolkit.js +24 -6
- package/richtexteditor/plugins/aitoolkit.min.css +1 -0
- package/richtexteditor/plugins/all_plugins.js +2008 -1272
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/documentimport.js +1279 -1263
- package/richtexteditor/plugins/layouttable.js +383 -0
- package/richtexteditor/plugins/shadowdom.js +252 -0
- package/richtexteditor/plugins/slashcommand.js +8 -0
- package/richtexteditor/rte-headless.js +360 -0
- package/richtexteditor/rte-watchdog.js +303 -0
- package/richtexteditor/rte-webcomponent.js +351 -0
- package/richtexteditor/rte.js +2 -2
- package/richtexteditor/rte_mobile.min.css +1 -0
- package/richtexteditor/rte_theme_default.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_content.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_preview.min.css +1 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RichTextEditor — Headless API (rte-headless.js)
|
|
3
|
+
* ------------------------------------------------------------------
|
|
4
|
+
* Optional, un-obfuscated companion to rte.js. It lets you run the editor with
|
|
5
|
+
* NO built-in toolbar/chrome and drive everything from your own UI — the same
|
|
6
|
+
* "bring your own interface" model as headless frameworks, but on top of the
|
|
7
|
+
* full RichTextEditor engine (commands, plugins, collaboration, AI, etc.).
|
|
8
|
+
*
|
|
9
|
+
* What you get:
|
|
10
|
+
* - a chrome-less editing surface (toolbar / sub-toolbar / status bar hidden)
|
|
11
|
+
* - run(command, value) -> dispatch any RTE command
|
|
12
|
+
* - isActive(name) / state() -> query active formatting for your buttons
|
|
13
|
+
* - can(command) -> queryCommandEnabled (undo/redo/...)
|
|
14
|
+
* - on("update" | "change" | "selectionchange" | "focus" | "blur", cb)
|
|
15
|
+
* - getHTML / setHTML / getText / setText / getMarkdown / setMarkdown / getJSON
|
|
16
|
+
*
|
|
17
|
+
* Example:
|
|
18
|
+
* var hl = RTEHeadless("#editor", { value: "<p>Hello</p>" });
|
|
19
|
+
* hl.on("update", function (s) {
|
|
20
|
+
* boldBtn.classList.toggle("is-active", s.bold);
|
|
21
|
+
* });
|
|
22
|
+
* boldBtn.addEventListener("mousedown", function (e) {
|
|
23
|
+
* e.preventDefault(); // keep the editor selection
|
|
24
|
+
* hl.run("bold");
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* Loads after rte-config.js + rte.js (+ optional plugins). Ships un-obfuscated
|
|
28
|
+
* on purpose, like rte-config.js — you are meant to read and extend it.
|
|
29
|
+
*/
|
|
30
|
+
(function (global) {
|
|
31
|
+
"use strict";
|
|
32
|
+
|
|
33
|
+
// logical state name -> how to query it on the editor document.
|
|
34
|
+
// "state" uses queryCommandState; "block" matches queryCommandValue("formatBlock").
|
|
35
|
+
var STATE_MAP = {
|
|
36
|
+
bold: { kind: "state", q: "bold" },
|
|
37
|
+
italic: { kind: "state", q: "italic" },
|
|
38
|
+
underline: { kind: "state", q: "underline" },
|
|
39
|
+
strike: { kind: "state", q: "strikethrough" },
|
|
40
|
+
strikethrough: { kind: "state", q: "strikethrough" },
|
|
41
|
+
subscript: { kind: "state", q: "subscript" },
|
|
42
|
+
superscript: { kind: "state", q: "superscript" },
|
|
43
|
+
ul: { kind: "state", q: "insertunorderedlist" },
|
|
44
|
+
unorderedlist: { kind: "state", q: "insertunorderedlist" },
|
|
45
|
+
ol: { kind: "state", q: "insertorderedlist" },
|
|
46
|
+
orderedlist: { kind: "state", q: "insertorderedlist" },
|
|
47
|
+
justifyleft: { kind: "state", q: "justifyleft" },
|
|
48
|
+
justifycenter: { kind: "state", q: "justifycenter" },
|
|
49
|
+
justifyright: { kind: "state", q: "justifyright" },
|
|
50
|
+
justifyfull: { kind: "state", q: "justifyfull" },
|
|
51
|
+
h1: { kind: "block", q: "h1" },
|
|
52
|
+
h2: { kind: "block", q: "h2" },
|
|
53
|
+
h3: { kind: "block", q: "h3" },
|
|
54
|
+
h4: { kind: "block", q: "h4" },
|
|
55
|
+
h5: { kind: "block", q: "h5" },
|
|
56
|
+
h6: { kind: "block", q: "h6" },
|
|
57
|
+
p: { kind: "block", q: "p" },
|
|
58
|
+
paragraph: { kind: "block", q: "p" },
|
|
59
|
+
blockquote: { kind: "block", q: "blockquote" },
|
|
60
|
+
pre: { kind: "block", q: "pre" }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// keys included in the snapshot returned by state() when no list is given.
|
|
64
|
+
var DEFAULT_STATE_KEYS = [
|
|
65
|
+
"bold", "italic", "underline", "strike", "ul", "ol",
|
|
66
|
+
"justifyleft", "justifycenter", "justifyright",
|
|
67
|
+
"blockquote", "h1", "h2", "h3", "p"
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function resolveTarget(target) {
|
|
71
|
+
if (typeof target === "string") {
|
|
72
|
+
return (typeof document !== "undefined") ? document.querySelector(target) : null;
|
|
73
|
+
}
|
|
74
|
+
return target || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function HeadlessController(editor, options) {
|
|
78
|
+
this.editor = editor; // escape hatch: the underlying RichTextEditor
|
|
79
|
+
this._opts = options || {};
|
|
80
|
+
this._handlers = {}; // event name -> [cb]
|
|
81
|
+
this._scheduled = false;
|
|
82
|
+
this._destroyed = false;
|
|
83
|
+
this._wire();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The contentEditable document we query formatting state against.
|
|
87
|
+
HeadlessController.prototype._doc = function () {
|
|
88
|
+
try {
|
|
89
|
+
var ifr = this.editor && this.editor.iframe;
|
|
90
|
+
if (!ifr) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return ifr.contentDocument || (ifr.contentWindow && ifr.contentWindow.document) || null;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
HeadlessController.prototype._wire = function () {
|
|
100
|
+
var self = this;
|
|
101
|
+
// Coalesce bursts into a single "update". setTimeout (not rAF) on purpose:
|
|
102
|
+
// requestAnimationFrame is paused in background/hidden tabs, which would
|
|
103
|
+
// latch _scheduled and silently drop every later update (collab, tests,
|
|
104
|
+
// off-screen editors). A ~16ms timer fires in every context.
|
|
105
|
+
function emitUpdate() {
|
|
106
|
+
if (self._destroyed || self._scheduled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
self._scheduled = true;
|
|
110
|
+
setTimeout(function () {
|
|
111
|
+
self._scheduled = false;
|
|
112
|
+
if (!self._destroyed) {
|
|
113
|
+
self._emit("update", self.state());
|
|
114
|
+
}
|
|
115
|
+
}, 16);
|
|
116
|
+
}
|
|
117
|
+
this._emitUpdate = emitUpdate;
|
|
118
|
+
|
|
119
|
+
["selectionchange", "change", "focus", "blur"].forEach(function (evt) {
|
|
120
|
+
try {
|
|
121
|
+
self.editor.attachEvent(evt, function (a, b) {
|
|
122
|
+
self._emit(evt, a, b);
|
|
123
|
+
if (evt === "selectionchange" || evt === "change") {
|
|
124
|
+
emitUpdate();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
} catch (e) { /* event surface differs by build; ignore */ }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Robust fallback: bind native listeners on the editor document so the
|
|
131
|
+
// "update" (active-state) snapshot stays in sync with EVERY caret/selection
|
|
132
|
+
// move and keystroke, even if a given build doesn't route them through the
|
|
133
|
+
// editor's own event hook. Coalesced by emitUpdate's per-frame guard.
|
|
134
|
+
var doc = this._doc();
|
|
135
|
+
if (doc && doc.addEventListener) {
|
|
136
|
+
// Only drive the coalesced "update" snapshot here; the editor's own
|
|
137
|
+
// hook owns the "change" event, so we don't double-emit it.
|
|
138
|
+
this._docListeners = [
|
|
139
|
+
["selectionchange", function () { emitUpdate(); }],
|
|
140
|
+
["mouseup", function () { emitUpdate(); }],
|
|
141
|
+
["keyup", function () { emitUpdate(); }],
|
|
142
|
+
["input", function () { emitUpdate(); }]
|
|
143
|
+
];
|
|
144
|
+
for (var i = 0; i < this._docListeners.length; i++) {
|
|
145
|
+
try { doc.addEventListener(this._docListeners[i][0], this._docListeners[i][1], true); } catch (e) {}
|
|
146
|
+
}
|
|
147
|
+
this._boundDoc = doc;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
HeadlessController.prototype.on = function (name, cb) {
|
|
152
|
+
if (typeof cb === "function") {
|
|
153
|
+
(this._handlers[name] = this._handlers[name] || []).push(cb);
|
|
154
|
+
}
|
|
155
|
+
return this;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
HeadlessController.prototype.off = function (name, cb) {
|
|
159
|
+
var list = this._handlers[name];
|
|
160
|
+
if (list) {
|
|
161
|
+
this._handlers[name] = cb ? list.filter(function (f) { return f !== cb; }) : [];
|
|
162
|
+
}
|
|
163
|
+
return this;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
HeadlessController.prototype._emit = function (name) {
|
|
167
|
+
var list = this._handlers[name];
|
|
168
|
+
if (!list || !list.length) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
172
|
+
for (var i = 0; i < list.length; i++) {
|
|
173
|
+
try {
|
|
174
|
+
list[i].apply(this, args);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
if (global.console) { global.console.error(e); }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Dispatch any RichTextEditor command (same names the toolbar uses), chainable.
|
|
182
|
+
HeadlessController.prototype.run = function (command, value) {
|
|
183
|
+
try {
|
|
184
|
+
this.editor.execCommand(command, value);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (global.console) { global.console.error(e); }
|
|
187
|
+
}
|
|
188
|
+
if (this._emitUpdate) { this._emitUpdate(); }
|
|
189
|
+
return this;
|
|
190
|
+
};
|
|
191
|
+
HeadlessController.prototype.cmd = HeadlessController.prototype.run;
|
|
192
|
+
|
|
193
|
+
HeadlessController.prototype.isActive = function (name) {
|
|
194
|
+
var spec = STATE_MAP[String(name || "").toLowerCase()];
|
|
195
|
+
var doc = this._doc();
|
|
196
|
+
if (!spec || !doc) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
if (spec.kind === "state") {
|
|
201
|
+
return !!doc.queryCommandState(spec.q);
|
|
202
|
+
}
|
|
203
|
+
var v = String(doc.queryCommandValue("formatBlock") || "").toLowerCase().replace(/[<>]/g, "");
|
|
204
|
+
return v === spec.q;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
HeadlessController.prototype.can = function (command) {
|
|
211
|
+
var doc = this._doc();
|
|
212
|
+
if (!doc) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return !!doc.queryCommandEnabled(command);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Snapshot of active formatting for binding a custom toolbar.
|
|
223
|
+
HeadlessController.prototype.state = function (keys) {
|
|
224
|
+
keys = keys || DEFAULT_STATE_KEYS;
|
|
225
|
+
var out = {};
|
|
226
|
+
for (var i = 0; i < keys.length; i++) {
|
|
227
|
+
out[keys[i]] = this.isActive(keys[i]);
|
|
228
|
+
}
|
|
229
|
+
out.canUndo = this.can("undo");
|
|
230
|
+
out.canRedo = this.can("redo");
|
|
231
|
+
return out;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// ---- content I/O (thin pass-throughs to the public editor API) ----
|
|
235
|
+
HeadlessController.prototype.getHTML = function () {
|
|
236
|
+
return this.editor.getHTMLCode ? this.editor.getHTMLCode() : "";
|
|
237
|
+
};
|
|
238
|
+
HeadlessController.prototype.setHTML = function (html) {
|
|
239
|
+
if (this.editor.setHTMLCode) { this.editor.setHTMLCode(html == null ? "" : html); }
|
|
240
|
+
if (this._emitUpdate) { this._emitUpdate(); }
|
|
241
|
+
return this;
|
|
242
|
+
};
|
|
243
|
+
HeadlessController.prototype.getText = function () {
|
|
244
|
+
return this.editor.getText ? this.editor.getText() : "";
|
|
245
|
+
};
|
|
246
|
+
HeadlessController.prototype.setText = function (text) {
|
|
247
|
+
if (this.editor.setText) { this.editor.setText(text == null ? "" : text); }
|
|
248
|
+
if (this._emitUpdate) { this._emitUpdate(); }
|
|
249
|
+
return this;
|
|
250
|
+
};
|
|
251
|
+
HeadlessController.prototype.getMarkdown = function () {
|
|
252
|
+
return this.editor.toMarkdown ? this.editor.toMarkdown() : "";
|
|
253
|
+
};
|
|
254
|
+
HeadlessController.prototype.setMarkdown = function (markdown) {
|
|
255
|
+
if (this.editor.fromMarkdown) { this.editor.fromMarkdown(markdown == null ? "" : markdown); }
|
|
256
|
+
if (this._emitUpdate) { this._emitUpdate(); }
|
|
257
|
+
return this;
|
|
258
|
+
};
|
|
259
|
+
HeadlessController.prototype.getJSON = function () {
|
|
260
|
+
return this.editor.getJSON ? this.editor.getJSON() : null; // requires the structured-content bridge
|
|
261
|
+
};
|
|
262
|
+
HeadlessController.prototype.getStatistics = function () {
|
|
263
|
+
return this.editor.getStatistics ? this.editor.getStatistics() : null;
|
|
264
|
+
};
|
|
265
|
+
HeadlessController.prototype.isEmpty = function () {
|
|
266
|
+
return String(this.getText() || "").replace(/\s+/g, "").length === 0;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
HeadlessController.prototype.focus = function () {
|
|
270
|
+
try { this.editor.focus(); } catch (e) { /* ignore */ }
|
|
271
|
+
return this;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
HeadlessController.prototype.destroy = function () {
|
|
275
|
+
this._destroyed = true;
|
|
276
|
+
this._handlers = {};
|
|
277
|
+
if (this._boundDoc && this._docListeners) {
|
|
278
|
+
for (var i = 0; i < this._docListeners.length; i++) {
|
|
279
|
+
try { this._boundDoc.removeEventListener(this._docListeners[i][0], this._docListeners[i][1], true); } catch (e) {}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this._docListeners = null;
|
|
283
|
+
this._boundDoc = null;
|
|
284
|
+
try { if (this.editor.prepareDestroy) { this.editor.prepareDestroy(); } } catch (e) { /* ignore */ }
|
|
285
|
+
return this;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
var _cssInjected = false;
|
|
289
|
+
function injectHeadlessCss() {
|
|
290
|
+
if (_cssInjected || typeof document === "undefined") {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
_cssInjected = true;
|
|
294
|
+
var css =
|
|
295
|
+
".rte-headless rte-toolbar,.rte-headless rte-subtoolbar,.rte-headless rte-bottom{display:none !important;}" +
|
|
296
|
+
".rte-headless{border:0 !important;background:transparent !important;}";
|
|
297
|
+
var style = document.createElement("style");
|
|
298
|
+
style.setAttribute("data-rte-headless", "");
|
|
299
|
+
style.appendChild(document.createTextNode(css));
|
|
300
|
+
(document.head || document.documentElement).appendChild(style);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Factory: build a chrome-less editor and return a HeadlessController.
|
|
304
|
+
function createHeadless(target, options) {
|
|
305
|
+
options = options || {};
|
|
306
|
+
if (typeof global.RichTextEditor !== "function") {
|
|
307
|
+
throw new Error("rte-headless: RichTextEditor (rte.js) must be loaded before rte-headless.js.");
|
|
308
|
+
}
|
|
309
|
+
var el = resolveTarget(target);
|
|
310
|
+
if (!el) {
|
|
311
|
+
throw new Error("rte-headless: target element not found.");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Start from the standard config so plugins/commands still work, then
|
|
315
|
+
// strip the chrome. A single-space toolbar definition parses to ZERO
|
|
316
|
+
// items (an empty string would fall back to the full toolbar).
|
|
317
|
+
var cfg = (typeof global.RTE_CreateConfig === "function") ? global.RTE_CreateConfig() : {};
|
|
318
|
+
if (options.config) {
|
|
319
|
+
for (var k in options.config) {
|
|
320
|
+
if (Object.prototype.hasOwnProperty.call(options.config, k)) {
|
|
321
|
+
cfg[k] = options.config[k];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
cfg.toolbar = "headless";
|
|
326
|
+
cfg.toolbar_headless = " ";
|
|
327
|
+
cfg.toolbarMobile = "headless";
|
|
328
|
+
if (options.showStatistics !== true) {
|
|
329
|
+
cfg.showStatistics = false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
injectHeadlessCss();
|
|
333
|
+
var editor = new global.RichTextEditor(el, cfg);
|
|
334
|
+
if (editor.container && editor.container.classList) {
|
|
335
|
+
editor.container.classList.add("rte-headless");
|
|
336
|
+
}
|
|
337
|
+
if (options.value != null && editor.setHTMLCode) {
|
|
338
|
+
editor.setHTMLCode(options.value);
|
|
339
|
+
}
|
|
340
|
+
return new HeadlessController(editor, options);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- exports ----
|
|
344
|
+
if (typeof global !== "undefined") {
|
|
345
|
+
global.RTEHeadless = createHeadless;
|
|
346
|
+
if (typeof global.RichTextEditor === "function") {
|
|
347
|
+
global.RichTextEditor.headless = createHeadless;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
351
|
+
module.exports = {
|
|
352
|
+
RTEHeadless: createHeadless,
|
|
353
|
+
_internals: {
|
|
354
|
+
STATE_MAP: STATE_MAP,
|
|
355
|
+
DEFAULT_STATE_KEYS: DEFAULT_STATE_KEYS,
|
|
356
|
+
HeadlessController: HeadlessController
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
})(typeof window !== "undefined" ? window : this);
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RichTextEditor — Watchdog (rte-watchdog.js)
|
|
3
|
+
* ------------------------------------------------------------------
|
|
4
|
+
* 2026-08-30. Optional, un-obfuscated companion to rte.js: keeps an editor alive
|
|
5
|
+
* across an unexpected error, and — more importantly — keeps the author's text.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists. CKEditor 5 ships `Watchdog`; nothing here did. The gap is not
|
|
8
|
+
* a feature-list checkbox, it is the worst five seconds a writer can have: an
|
|
9
|
+
* uncaught exception leaves the editing surface half-dead — keystrokes land but
|
|
10
|
+
* commands throw, or the iframe stops accepting input — and the page looks fine,
|
|
11
|
+
* so nobody reloads until an hour of writing is already gone.
|
|
12
|
+
*
|
|
13
|
+
* What it does:
|
|
14
|
+
* - snapshots the content on an interval and after every change, so there is
|
|
15
|
+
* always a recent known-good document to restore;
|
|
16
|
+
* - notices an uncaught error or rejection that came from the editor's own
|
|
17
|
+
* code, and re-checks the editing surface before believing it;
|
|
18
|
+
* - destroys and recreates the instance, then puts the snapshot back;
|
|
19
|
+
* - refuses to do that forever. Restarting into the same crash is a loop that
|
|
20
|
+
* eats the document a little more each pass, so after `crashNumberLimit`
|
|
21
|
+
* crashes without a quiet period in between it stops, reports
|
|
22
|
+
* "crashedPermanently", and holds the last snapshot for the host page to
|
|
23
|
+
* save somewhere it trusts.
|
|
24
|
+
*
|
|
25
|
+
* What it deliberately does NOT do: guess. An error thrown by unrelated page code
|
|
26
|
+
* is none of its business — restarting the editor because an analytics script
|
|
27
|
+
* threw would destroy the selection, the undo stack and any in-progress dialog
|
|
28
|
+
* for no reason. Only errors whose stack names an editor script, or a genuinely
|
|
29
|
+
* unresponsive editing surface, count as a crash. `getDiagnostics()` reports the
|
|
30
|
+
* errors it saw and ignored, so a real crash that was filtered out is findable
|
|
31
|
+
* rather than invisible.
|
|
32
|
+
*
|
|
33
|
+
* var wd = new RichTextEditorWatchdog();
|
|
34
|
+
* wd.on("restart", function (info) { console.log("recovered", info.crashes); });
|
|
35
|
+
* wd.on("crashedPermanently", function (info) { saveDraft(info.data); });
|
|
36
|
+
* wd.create(document.getElementById("host"), RTE_CreateConfig());
|
|
37
|
+
* wd.editor // the live instance — re-read it after a restart
|
|
38
|
+
* wd.getData() // last known-good HTML, valid even while crashed
|
|
39
|
+
*
|
|
40
|
+
* Loads after rte-config.js + rte.js. Un-obfuscated on purpose: recovery policy
|
|
41
|
+
* is something an integrator should be able to read and change.
|
|
42
|
+
*/
|
|
43
|
+
(function (global) {
|
|
44
|
+
"use strict";
|
|
45
|
+
|
|
46
|
+
var STATE_INITIALIZING = "initializing";
|
|
47
|
+
var STATE_READY = "ready";
|
|
48
|
+
var STATE_CRASHED = "crashed";
|
|
49
|
+
var STATE_PERMANENT = "crashedPermanently";
|
|
50
|
+
var STATE_DESTROYED = "destroyed";
|
|
51
|
+
|
|
52
|
+
function RichTextEditorWatchdog(options) {
|
|
53
|
+
if (!(this instanceof RichTextEditorWatchdog)) return new RichTextEditorWatchdog(options);
|
|
54
|
+
options = options || {};
|
|
55
|
+
|
|
56
|
+
this.state = STATE_INITIALIZING;
|
|
57
|
+
this.editor = null;
|
|
58
|
+
this.crashes = [];
|
|
59
|
+
|
|
60
|
+
this._host = null;
|
|
61
|
+
this._config = null;
|
|
62
|
+
this._data = "";
|
|
63
|
+
this._listeners = {};
|
|
64
|
+
this._ignoredErrors = [];
|
|
65
|
+
this._timer = null;
|
|
66
|
+
this._destroyed = false;
|
|
67
|
+
|
|
68
|
+
// How many crashes without a quiet stretch before giving up. CKEditor's
|
|
69
|
+
// default is 3 and the reasoning holds: one crash is an accident, three
|
|
70
|
+
// in a row is a bug that restarting will not fix.
|
|
71
|
+
this._crashNumberLimit = typeof options.crashNumberLimit === "number" ? options.crashNumberLimit : 3;
|
|
72
|
+
// A crash this long after the previous one starts the count again.
|
|
73
|
+
this._minimumNonErrorTimePeriod = typeof options.minimumNonErrorTimePeriod === "number"
|
|
74
|
+
? options.minimumNonErrorTimePeriod : 5000;
|
|
75
|
+
this._saveInterval = typeof options.saveInterval === "number" ? options.saveInterval : 5000;
|
|
76
|
+
|
|
77
|
+
// Swap these to supervise something other than a plain `new RichTextEditor`
|
|
78
|
+
// (a framework wrapper, a headless instance, a preconfigured factory).
|
|
79
|
+
this._creator = typeof options.creator === "function"
|
|
80
|
+
? options.creator
|
|
81
|
+
: function (host, config) { return new global.RichTextEditor(host, config); };
|
|
82
|
+
this._destructor = typeof options.destructor === "function"
|
|
83
|
+
? options.destructor
|
|
84
|
+
: function (editor) { if (editor && typeof editor.destroy === "function") editor.destroy(); };
|
|
85
|
+
|
|
86
|
+
// Which scripts count as "ours". An error is only a crash if its stack
|
|
87
|
+
// names one of them — see the comment at the top of the file.
|
|
88
|
+
this._ownScriptPattern = options.ownScriptPattern instanceof RegExp
|
|
89
|
+
? options.ownScriptPattern
|
|
90
|
+
: /(^|[\/\\])(rte|rte-headless|rte-watchdog|all_plugins)([.\-][\w.]*)?\.js|RTE_Plugin_|RichTextEditor/;
|
|
91
|
+
|
|
92
|
+
var self = this;
|
|
93
|
+
this._onError = function (event) { self._handleError(event && event.error, event && event.message, event); };
|
|
94
|
+
this._onRejection = function (event) {
|
|
95
|
+
var reason = event && event.reason;
|
|
96
|
+
self._handleError(reason instanceof Error ? reason : null, String(reason && reason.message || reason || ""), event);
|
|
97
|
+
};
|
|
98
|
+
global.addEventListener("error", this._onError);
|
|
99
|
+
global.addEventListener("unhandledrejection", this._onRejection);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var proto = RichTextEditorWatchdog.prototype;
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------------ events
|
|
105
|
+
|
|
106
|
+
proto.on = function (name, callback) {
|
|
107
|
+
if (typeof callback !== "function") return this;
|
|
108
|
+
(this._listeners[name] = this._listeners[name] || []).push(callback);
|
|
109
|
+
return this;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
proto.off = function (name, callback) {
|
|
113
|
+
var list = this._listeners[name];
|
|
114
|
+
if (!list) return this;
|
|
115
|
+
for (var i = list.length - 1; i >= 0; i--) if (list[i] === callback) list.splice(i, 1);
|
|
116
|
+
return this;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
proto._emit = function (name, payload) {
|
|
120
|
+
var list = (this._listeners[name] || []).slice();
|
|
121
|
+
for (var i = 0; i < list.length; i++) {
|
|
122
|
+
// A throwing listener must not take down the recovery it was told about.
|
|
123
|
+
try { list[i].call(this, payload); } catch (e) {}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ------------------------------------------------------------- lifecycle
|
|
128
|
+
|
|
129
|
+
proto.create = function (host, config) {
|
|
130
|
+
if (this._destroyed) throw new Error("This watchdog has been destroyed.");
|
|
131
|
+
this._host = host || this._host;
|
|
132
|
+
this._config = config || this._config;
|
|
133
|
+
return this._start();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
proto._start = function () {
|
|
137
|
+
var self = this;
|
|
138
|
+
this.editor = this._creator(this._host, this._config);
|
|
139
|
+
this.state = STATE_READY;
|
|
140
|
+
|
|
141
|
+
// Snapshot on change AND on a timer. Change alone misses the case that
|
|
142
|
+
// matters most — a crash mid-edit that stops change events firing — and
|
|
143
|
+
// the timer alone can be up to `saveInterval` stale.
|
|
144
|
+
try {
|
|
145
|
+
this.editor.attachEvent("change", function () { self._snapshot(); });
|
|
146
|
+
} catch (e) {}
|
|
147
|
+
this._snapshot();
|
|
148
|
+
if (this._saveInterval > 0) {
|
|
149
|
+
this._timer = global.setInterval(function () { self._snapshot(); }, this._saveInterval);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._emit("ready", { editor: this.editor });
|
|
153
|
+
return this.editor;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
proto.destroy = function () {
|
|
157
|
+
if (this._destroyed) return;
|
|
158
|
+
this._destroyed = true;
|
|
159
|
+
this.state = STATE_DESTROYED;
|
|
160
|
+
if (this._timer) { global.clearInterval(this._timer); this._timer = null; }
|
|
161
|
+
global.removeEventListener("error", this._onError);
|
|
162
|
+
global.removeEventListener("unhandledrejection", this._onRejection);
|
|
163
|
+
try { this._destructor(this.editor); } catch (e) {}
|
|
164
|
+
this.editor = null;
|
|
165
|
+
this._emit("destroy", {});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ------------------------------------------------------------------- data
|
|
169
|
+
|
|
170
|
+
proto.getData = function () { return this._data; };
|
|
171
|
+
|
|
172
|
+
proto._snapshot = function () {
|
|
173
|
+
if (!this.editor) return;
|
|
174
|
+
try {
|
|
175
|
+
var html = this.editor.getHTMLCode();
|
|
176
|
+
// A crashing editor can start returning "" or null. Overwriting a good
|
|
177
|
+
// snapshot with that is how a recovery feature loses the document it
|
|
178
|
+
// exists to protect, so empty never replaces non-empty.
|
|
179
|
+
if (typeof html === "string" && (html || !this._data)) this._data = html;
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
proto.getDiagnostics = function () {
|
|
184
|
+
return {
|
|
185
|
+
state: this.state,
|
|
186
|
+
crashes: this.crashes.slice(),
|
|
187
|
+
ignoredErrors: this._ignoredErrors.slice(),
|
|
188
|
+
dataLength: this._data.length
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ------------------------------------------------------------------ crash
|
|
193
|
+
|
|
194
|
+
// Is the editing surface still usable? An error is only half the evidence:
|
|
195
|
+
// plenty of exceptions are thrown by a feature and leave editing perfectly
|
|
196
|
+
// fine, and restarting for those costs the author their selection and undo
|
|
197
|
+
// history for nothing.
|
|
198
|
+
proto.isEditorResponsive = function () {
|
|
199
|
+
var editor = this.editor;
|
|
200
|
+
if (!editor) return false;
|
|
201
|
+
try {
|
|
202
|
+
var editable = editor.getEditable();
|
|
203
|
+
if (!editable || !editable.ownerDocument) return false;
|
|
204
|
+
// The iframe was removed, or its document was replaced under us.
|
|
205
|
+
if (!editable.ownerDocument.defaultView) return false;
|
|
206
|
+
if (!editable.ownerDocument.body || !editable.ownerDocument.body.contains(editable)) {
|
|
207
|
+
if (editable !== editable.ownerDocument.body) return false;
|
|
208
|
+
}
|
|
209
|
+
// Still attached to the page the host element lives in?
|
|
210
|
+
var frame = editable.ownerDocument.defaultView.frameElement;
|
|
211
|
+
if (frame && !frame.ownerDocument.documentElement.contains(frame)) return false;
|
|
212
|
+
if (typeof editor.getHTMLCode !== "function") return false;
|
|
213
|
+
editor.getHTMLCode();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
catch (e) { return false; }
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
proto._isOwnError = function (error, message) {
|
|
220
|
+
var stack = (error && error.stack) || "";
|
|
221
|
+
var where = stack + " " + (message || "") + " " + ((error && error.fileName) || "");
|
|
222
|
+
return this._ownScriptPattern.test(where);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
proto._handleError = function (error, message, event) {
|
|
226
|
+
if (this._destroyed || this.state === STATE_PERMANENT) return;
|
|
227
|
+
|
|
228
|
+
var mine = this._isOwnError(error, message);
|
|
229
|
+
// An error from elsewhere still gets a health check — a crash can surface
|
|
230
|
+
// as a foreign stack (a framework's error boundary, a rethrow) — but it
|
|
231
|
+
// only counts if the editing surface is genuinely broken.
|
|
232
|
+
var responsive = this.isEditorResponsive();
|
|
233
|
+
if (!mine && responsive) {
|
|
234
|
+
this._ignoredErrors.push({ message: String(message || (error && error.message) || ""), at: Date.now() });
|
|
235
|
+
if (this._ignoredErrors.length > 50) this._ignoredErrors.shift();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (mine && responsive) {
|
|
239
|
+
// Ours, but the editor still works: record it, do not restart.
|
|
240
|
+
this._ignoredErrors.push({
|
|
241
|
+
message: String(message || (error && error.message) || ""),
|
|
242
|
+
at: Date.now(), own: true, restarted: false
|
|
243
|
+
});
|
|
244
|
+
if (this._ignoredErrors.length > 50) this._ignoredErrors.shift();
|
|
245
|
+
this._emit("error", { error: error, message: message, restarting: false, event: event });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this._crash(error, message, event);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
proto._crash = function (error, message, event) {
|
|
253
|
+
var now = Date.now();
|
|
254
|
+
var last = this.crashes.length ? this.crashes[this.crashes.length - 1].at : 0;
|
|
255
|
+
// A crash after a long quiet stretch is a new incident, not a loop.
|
|
256
|
+
if (last && (now - last) > this._minimumNonErrorTimePeriod) this.crashes = [];
|
|
257
|
+
|
|
258
|
+
this.crashes.push({ at: now, message: String(message || (error && error.message) || "") });
|
|
259
|
+
this.state = STATE_CRASHED;
|
|
260
|
+
this._emit("error", { error: error, message: message, restarting: true, event: event });
|
|
261
|
+
|
|
262
|
+
if (this.crashes.length > this._crashNumberLimit) {
|
|
263
|
+
this.state = STATE_PERMANENT;
|
|
264
|
+
if (this._timer) { global.clearInterval(this._timer); this._timer = null; }
|
|
265
|
+
this._emit("crashedPermanently", { data: this._data, crashes: this.crashes.slice() });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.restart();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
proto.restart = function () {
|
|
273
|
+
if (this._destroyed) return null;
|
|
274
|
+
var data = this._data;
|
|
275
|
+
|
|
276
|
+
if (this._timer) { global.clearInterval(this._timer); this._timer = null; }
|
|
277
|
+
try { this._destructor(this.editor); } catch (e) {}
|
|
278
|
+
this.editor = null;
|
|
279
|
+
|
|
280
|
+
// The old instance leaves its chrome in the host element; a fresh editor
|
|
281
|
+
// built on top of that would stack two toolbars.
|
|
282
|
+
try { if (this._host) this._host.innerHTML = ""; } catch (e) {}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
this._start();
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
this.state = STATE_PERMANENT;
|
|
289
|
+
this._emit("crashedPermanently", { data: data, crashes: this.crashes.slice(), error: e });
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (data) {
|
|
294
|
+
try { this.editor.setHTMLCode(data); this._data = data; } catch (e) {}
|
|
295
|
+
}
|
|
296
|
+
this._emit("restart", { editor: this.editor, crashes: this.crashes.length, data: data });
|
|
297
|
+
return this.editor;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
global.RichTextEditorWatchdog = RichTextEditorWatchdog;
|
|
301
|
+
if (global.RichTextEditor) global.RichTextEditor.Watchdog = RichTextEditorWatchdog;
|
|
302
|
+
if (typeof module !== "undefined" && module.exports) module.exports = RichTextEditorWatchdog;
|
|
303
|
+
})(typeof window !== "undefined" ? window : this);
|