@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.
@@ -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);