@richscripts/richtexteditor 2.5.4 → 2.6.0
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 +142 -142
- package/richtexteditor/CHANGELOG.md +2714 -2201
- package/richtexteditor/all_plugins.js +729 -9
- 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 +729 -9
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/layouttable.js +383 -0
- package/richtexteditor/plugins/shadowdom.js +252 -0
- package/richtexteditor/plugins/slashcommand.js +8 -0
- package/richtexteditor/rte-watchdog.js +303 -0
- package/richtexteditor/rte-webcomponent.js +351 -0
- 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,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);
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RichTextEditor — Web Component (rte-webcomponent.js)
|
|
3
|
+
* ------------------------------------------------------------------
|
|
4
|
+
* 2026-08-30. Optional, un-obfuscated companion to rte.js: registers
|
|
5
|
+
* <rich-text-editor> as a custom element.
|
|
6
|
+
*
|
|
7
|
+
* Why. We ship first-class React, Vue, Angular and Svelte bindings and a
|
|
8
|
+
* headless core — and nothing at all for the pages that use none of those.
|
|
9
|
+
* TinyMCE ships <tinymce-editor> for exactly that audience: Rails and Laravel
|
|
10
|
+
* templates, Blazor, Django, plain HTML, htmx, and any framework whose bindings
|
|
11
|
+
* we will never write. One custom element covers all of them, because every
|
|
12
|
+
* framework knows how to render an unknown tag with attributes.
|
|
13
|
+
*
|
|
14
|
+
* <script src="richtexteditor/rte-config.js"></script>
|
|
15
|
+
* <script src="richtexteditor/rte.js"></script>
|
|
16
|
+
* <script src="richtexteditor/rte-webcomponent.js"></script>
|
|
17
|
+
*
|
|
18
|
+
* <form method="post">
|
|
19
|
+
* <rich-text-editor name="body" value="<p>Hello</p>"></rich-text-editor>
|
|
20
|
+
* <button>Save</button>
|
|
21
|
+
* </form>
|
|
22
|
+
*
|
|
23
|
+
* That form posts `body=<p>Hello</p>` with no glue code — the element is
|
|
24
|
+
* form-associated, so the browser submits it like a <textarea>. `required`,
|
|
25
|
+
* form reset and `form.elements.body` work for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* Attributes: value, name, toolbar, height, width, readonly, disabled,
|
|
28
|
+
* required, shadow, config (JSON)
|
|
29
|
+
* Properties: .value, .editor, .ready (a Promise), .form, .name
|
|
30
|
+
* Events: input, change (both bubble and cross a shadow boundary)
|
|
31
|
+
* Methods: focus(), checkValidity(), reportValidity()
|
|
32
|
+
*
|
|
33
|
+
* Design notes:
|
|
34
|
+
* - LIGHT DOM by default. A shadow root would encapsulate the chrome away from
|
|
35
|
+
* the page's own theme overrides, which integrators legitimately rely on,
|
|
36
|
+
* and it is the more surprising default. `shadow` opts in, and then
|
|
37
|
+
* plugins/shadowdom.js copies the editor's stylesheets across the boundary —
|
|
38
|
+
* without that plugin a shadowed instance renders a 43,000-pixel toolbar,
|
|
39
|
+
* measured, so this element refuses to enable `shadow` silently if the
|
|
40
|
+
* plugin is absent: it warns and stays in the light DOM rather than
|
|
41
|
+
* rendering something visibly broken.
|
|
42
|
+
* - `value` is a PROPERTY first. Reading the attribute back after the user
|
|
43
|
+
* types is the classic custom-element bug — the attribute holds the initial
|
|
44
|
+
* value only, exactly like <input value>, and the property holds the truth.
|
|
45
|
+
* - The editor is created in connectedCallback and destroyed in
|
|
46
|
+
* disconnectedCallback, because SPA routers move elements in and out of the
|
|
47
|
+
* DOM constantly and a leaked iframe per navigation is a real memory bug.
|
|
48
|
+
* Moving an element (rather than removing it) re-creates it with the value
|
|
49
|
+
* preserved.
|
|
50
|
+
*/
|
|
51
|
+
(function (global) {
|
|
52
|
+
"use strict";
|
|
53
|
+
|
|
54
|
+
if (!global.customElements || !global.HTMLElement) return;
|
|
55
|
+
if (global.customElements.get("rich-text-editor")) return;
|
|
56
|
+
|
|
57
|
+
var OBSERVED = ["value", "readonly", "disabled", "height", "width", "toolbar", "name"];
|
|
58
|
+
|
|
59
|
+
function RichTextEditorElement() {
|
|
60
|
+
var self = Reflect.construct(HTMLElement, [], RichTextEditorElement);
|
|
61
|
+
self._editor = null;
|
|
62
|
+
self._value = "";
|
|
63
|
+
self._host = null;
|
|
64
|
+
self._ready = null;
|
|
65
|
+
self._resolveReady = null;
|
|
66
|
+
self._isReady = false;
|
|
67
|
+
self._everConnected = false;
|
|
68
|
+
self._internals = null;
|
|
69
|
+
self._fallbackInput = null;
|
|
70
|
+
try { if (self.attachInternals) self._internals = self.attachInternals(); } catch (e) {}
|
|
71
|
+
return self;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
RichTextEditorElement.prototype = Object.create(HTMLElement.prototype);
|
|
75
|
+
RichTextEditorElement.prototype.constructor = RichTextEditorElement;
|
|
76
|
+
Object.setPrototypeOf(RichTextEditorElement, HTMLElement);
|
|
77
|
+
|
|
78
|
+
Object.defineProperty(RichTextEditorElement, "observedAttributes", {
|
|
79
|
+
get: function () { return OBSERVED; }
|
|
80
|
+
});
|
|
81
|
+
// Makes the browser submit, reset and validate this element like a textarea.
|
|
82
|
+
Object.defineProperty(RichTextEditorElement, "formAssociated", { get: function () { return true; } });
|
|
83
|
+
|
|
84
|
+
var proto = RichTextEditorElement.prototype;
|
|
85
|
+
|
|
86
|
+
// ------------------------------------------------------------- properties
|
|
87
|
+
|
|
88
|
+
Object.defineProperty(proto, "value", {
|
|
89
|
+
get: function () {
|
|
90
|
+
if (this._editor) {
|
|
91
|
+
try { return this._editor.getHTMLCode(); } catch (e) {}
|
|
92
|
+
}
|
|
93
|
+
return this._value;
|
|
94
|
+
},
|
|
95
|
+
set: function (html) {
|
|
96
|
+
this._value = html == null ? "" : String(html);
|
|
97
|
+
if (this._editor) {
|
|
98
|
+
try { this._editor.setHTMLCode(this._value); } catch (e) {}
|
|
99
|
+
}
|
|
100
|
+
this._syncFormValue();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
Object.defineProperty(proto, "editor", { get: function () { return this._editor; } });
|
|
105
|
+
|
|
106
|
+
// Resolves when the editing surface exists — the only safe moment to call
|
|
107
|
+
// editor APIs from page script.
|
|
108
|
+
//
|
|
109
|
+
// `_isReady` is not redundant with `_ready`: the element becomes ready during
|
|
110
|
+
// connectedCallback, which runs BEFORE any page script can touch `.ready`.
|
|
111
|
+
// Without the flag, the resolve fired into nothing and the promise handed out
|
|
112
|
+
// afterwards never settled — an await that hangs forever. Caught by the first
|
|
113
|
+
// run of the test suite, which simply stopped.
|
|
114
|
+
Object.defineProperty(proto, "ready", {
|
|
115
|
+
get: function () {
|
|
116
|
+
var self = this;
|
|
117
|
+
if (this._isReady) return Promise.resolve(this._editor);
|
|
118
|
+
if (!this._ready) {
|
|
119
|
+
this._ready = new Promise(function (resolve) { self._resolveReady = resolve; });
|
|
120
|
+
}
|
|
121
|
+
return this._ready;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
Object.defineProperty(proto, "name", {
|
|
126
|
+
get: function () { return this.getAttribute("name") || ""; },
|
|
127
|
+
set: function (v) { this.setAttribute("name", v); }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Object.defineProperty(proto, "form", {
|
|
131
|
+
get: function () { return this._internals ? this._internals.form : this.closest("form"); }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ------------------------------------------------------------- lifecycle
|
|
135
|
+
|
|
136
|
+
proto.connectedCallback = function () {
|
|
137
|
+
if (this._editor) return;
|
|
138
|
+
if (!global.RichTextEditor) {
|
|
139
|
+
console.error("<rich-text-editor>: rte.js must be loaded before rte-webcomponent.js.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Only on the FIRST connect. A re-connect (an SPA router moving the
|
|
143
|
+
// element, a drag between containers) must keep what the user typed —
|
|
144
|
+
// the attribute still holds the ORIGINAL seed, so re-reading it here
|
|
145
|
+
// silently threw the edits away and restored stale text. Found by the
|
|
146
|
+
// unmount/remount check in the test suite.
|
|
147
|
+
if (!this._everConnected && this.hasAttribute("value")) this._value = this.getAttribute("value") || "";
|
|
148
|
+
this._everConnected = true;
|
|
149
|
+
|
|
150
|
+
var mount = this;
|
|
151
|
+
if (this.hasAttribute("shadow")) {
|
|
152
|
+
// Refuse to render something visibly broken: without the shadow-DOM
|
|
153
|
+
// style plugin the toolbar is unstyled and 43,000 px tall.
|
|
154
|
+
if (this._shadowStylingAvailable()) {
|
|
155
|
+
var root = this.shadowRoot || this.attachShadow({ mode: "open" });
|
|
156
|
+
mount = document.createElement("div");
|
|
157
|
+
root.appendChild(mount);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.warn("<rich-text-editor shadow>: plugins/shadowdom.js is not loaded, so the " +
|
|
161
|
+
"editor would render unstyled inside a shadow root. Staying in the light DOM.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this._host = document.createElement("div");
|
|
166
|
+
mount.appendChild(this._host);
|
|
167
|
+
|
|
168
|
+
var config = this._buildConfig();
|
|
169
|
+
this._editor = new global.RichTextEditor(this._host, config);
|
|
170
|
+
this._bind();
|
|
171
|
+
this._syncFormValue();
|
|
172
|
+
this._waitForReady();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
proto.disconnectedCallback = function () {
|
|
176
|
+
// An SPA router unmounts and remounts constantly; a leaked iframe per
|
|
177
|
+
// navigation is a real memory bug, not a theoretical one.
|
|
178
|
+
this._value = this.value;
|
|
179
|
+
if (this._editor) {
|
|
180
|
+
try { if (typeof this._editor.destroy === "function") this._editor.destroy(); } catch (e) {}
|
|
181
|
+
this._editor = null;
|
|
182
|
+
}
|
|
183
|
+
if (this._host && this._host.parentNode) this._host.parentNode.removeChild(this._host);
|
|
184
|
+
this._host = null;
|
|
185
|
+
this._ready = null;
|
|
186
|
+
this._resolveReady = null;
|
|
187
|
+
this._isReady = false;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
proto.attributeChangedCallback = function (name, oldValue, newValue) {
|
|
191
|
+
if (oldValue === newValue) return;
|
|
192
|
+
if (name === "value") {
|
|
193
|
+
// Like <input value>, the attribute seeds the initial value; once the
|
|
194
|
+
// user has typed, the property is the truth and setting the attribute
|
|
195
|
+
// again is an explicit reset by the page.
|
|
196
|
+
this.value = newValue || "";
|
|
197
|
+
}
|
|
198
|
+
else if (name === "readonly" || name === "disabled") {
|
|
199
|
+
this._applyReadOnly();
|
|
200
|
+
}
|
|
201
|
+
else if (name === "name") {
|
|
202
|
+
this._syncFormValue();
|
|
203
|
+
}
|
|
204
|
+
else if (this._editor) {
|
|
205
|
+
// height / width / toolbar need a rebuild; they are construction-time
|
|
206
|
+
// config, and pretending otherwise would silently do nothing.
|
|
207
|
+
var value = this.value;
|
|
208
|
+
this.disconnectedCallback();
|
|
209
|
+
this._value = value;
|
|
210
|
+
this.connectedCallback();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Called by the browser when the containing form is reset.
|
|
215
|
+
proto.formResetCallback = function () {
|
|
216
|
+
this.value = this.getAttribute("value") || "";
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
proto.formDisabledCallback = function () { this._applyReadOnly(); };
|
|
220
|
+
|
|
221
|
+
// -------------------------------------------------------------- behaviour
|
|
222
|
+
|
|
223
|
+
proto.focus = function () {
|
|
224
|
+
try {
|
|
225
|
+
if (this._editor && this._editor.getEditable) {
|
|
226
|
+
var editable = this._editor.getEditable();
|
|
227
|
+
if (editable && editable.ownerDocument && editable.ownerDocument.defaultView) {
|
|
228
|
+
editable.ownerDocument.defaultView.focus();
|
|
229
|
+
editable.focus();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (e) {}
|
|
235
|
+
HTMLElement.prototype.focus.call(this);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
proto.checkValidity = function () { return this._internals ? this._internals.checkValidity() : true; };
|
|
239
|
+
proto.reportValidity = function () { return this._internals ? this._internals.reportValidity() : true; };
|
|
240
|
+
|
|
241
|
+
proto._buildConfig = function () {
|
|
242
|
+
var config = global.RTE_CreateConfig ? global.RTE_CreateConfig() : {};
|
|
243
|
+
var raw = this.getAttribute("config");
|
|
244
|
+
if (raw) {
|
|
245
|
+
try {
|
|
246
|
+
var extra = JSON.parse(raw);
|
|
247
|
+
for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) config[k] = extra[k];
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
console.error("<rich-text-editor>: the config attribute is not valid JSON.", e);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (this.hasAttribute("toolbar")) config.toolbar = this.getAttribute("toolbar");
|
|
254
|
+
if (this.hasAttribute("height")) config.height = this.getAttribute("height");
|
|
255
|
+
if (this.hasAttribute("width")) config.width = this.getAttribute("width");
|
|
256
|
+
if (this._isReadOnly()) config.readOnly = true;
|
|
257
|
+
if (this._value) config.value = this._value;
|
|
258
|
+
return config;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
proto._isReadOnly = function () {
|
|
262
|
+
return this.hasAttribute("readonly") || this.hasAttribute("disabled")
|
|
263
|
+
|| !!(this._internals && this._internals.form && this._internals.form.disabled);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
proto._applyReadOnly = function () {
|
|
267
|
+
if (!this._editor) return;
|
|
268
|
+
var readOnly = this._isReadOnly();
|
|
269
|
+
try {
|
|
270
|
+
if (typeof this._editor.setReadOnly === "function") this._editor.setReadOnly(readOnly);
|
|
271
|
+
else this._editor.readOnly = readOnly;
|
|
272
|
+
}
|
|
273
|
+
catch (e) {}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
proto._shadowStylingAvailable = function () {
|
|
277
|
+
return !!(global.RTE_DefaultConfig && global.RTE_DefaultConfig.plugin_shadowdom);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
proto._bind = function () {
|
|
281
|
+
var self = this;
|
|
282
|
+
var emit = function (type) {
|
|
283
|
+
self._syncFormValue();
|
|
284
|
+
// composed:true so a listener outside a shadow boundary still hears it.
|
|
285
|
+
self.dispatchEvent(new Event(type, { bubbles: true, composed: true }));
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
this._editor.attachEvent("change", function () { emit("input"); });
|
|
289
|
+
}
|
|
290
|
+
catch (e) {}
|
|
291
|
+
try {
|
|
292
|
+
this._editor.attachEvent("blur", function () { emit("change"); });
|
|
293
|
+
}
|
|
294
|
+
catch (e) {}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
proto._waitForReady = function () {
|
|
298
|
+
var self = this;
|
|
299
|
+
var tries = 0;
|
|
300
|
+
var timer = setInterval(function () {
|
|
301
|
+
var alive = false;
|
|
302
|
+
try { alive = !!(self._editor && self._editor.getEditable()); } catch (e) {}
|
|
303
|
+
if (alive || ++tries > 200) {
|
|
304
|
+
clearInterval(timer);
|
|
305
|
+
if (self._value) { try { self._editor.setHTMLCode(self._value); } catch (e) {} }
|
|
306
|
+
self._applyReadOnly();
|
|
307
|
+
self._syncFormValue();
|
|
308
|
+
self._isReady = true;
|
|
309
|
+
if (self._resolveReady) self._resolveReady(self._editor);
|
|
310
|
+
}
|
|
311
|
+
}, 25);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Form participation. ElementInternals is the modern path; the hidden input
|
|
315
|
+
// keeps older browsers submitting the right thing instead of nothing at all.
|
|
316
|
+
proto._syncFormValue = function () {
|
|
317
|
+
var name = this.getAttribute("name");
|
|
318
|
+
var value = this.value;
|
|
319
|
+
if (this._internals && this._internals.setFormValue) {
|
|
320
|
+
try { this._internals.setFormValue(name ? value : null); } catch (e) {}
|
|
321
|
+
this._syncValidity(value);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!name) return;
|
|
325
|
+
if (!this._fallbackInput) {
|
|
326
|
+
this._fallbackInput = document.createElement("input");
|
|
327
|
+
this._fallbackInput.type = "hidden";
|
|
328
|
+
this.appendChild(this._fallbackInput);
|
|
329
|
+
}
|
|
330
|
+
this._fallbackInput.name = name;
|
|
331
|
+
this._fallbackInput.value = value;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
proto._syncValidity = function (value) {
|
|
335
|
+
if (!this._internals || !this._internals.setValidity) return;
|
|
336
|
+
var required = this.hasAttribute("required");
|
|
337
|
+
// "Empty" for a rich-text field means no text and no media — an empty
|
|
338
|
+
// <p> is what the editor puts in a blank document, and treating that as
|
|
339
|
+
// filled makes `required` meaningless.
|
|
340
|
+
var empty = !String(value || "").replace(/<[^>]*>/g, "").replace(/ |\s/g, "")
|
|
341
|
+
&& !/<(img|table|iframe|video|audio|hr)\b/i.test(value || "");
|
|
342
|
+
try {
|
|
343
|
+
if (required && empty) this._internals.setValidity({ valueMissing: true }, "Please fill out this field.", this._host || this);
|
|
344
|
+
else this._internals.setValidity({});
|
|
345
|
+
}
|
|
346
|
+
catch (e) {}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
global.customElements.define("rich-text-editor", RichTextEditorElement);
|
|
350
|
+
global.RichTextEditorElement = RichTextEditorElement;
|
|
351
|
+
})(typeof window !== "undefined" ? window : this);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media(max-width:768px),(pointer:coarse){.richtexteditor.rte-mobile rte-toolbar.rte-toolbar-mobile{display:flex!important;align-items:center;flex-wrap:nowrap!important;gap:2px;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;white-space:nowrap;-webkit-overflow-scrolling:touch;touch-action:pan-x;scrollbar-width:none;position:sticky;top:0;z-index:5;padding:2px 4px;background:#fff;background-clip:padding-box}.richtexteditor.rte-mobile rte-toolbar.rte-toolbar-mobile::-webkit-scrollbar{display:none}.richtexteditor.rte-mobile rte-toolbar.rte-toolbar-mobile rte-toolbar-group{display:inline-flex;align-items:center;flex:0 0 auto;min-width:0}.richtexteditor.rte-mobile rte-toolbar-button,.richtexteditor.rte-mobile rte-toolbar-splitbutton,.richtexteditor.rte-mobile rte-toolbar-dropdown,.richtexteditor.rte-mobile rte-toolbar-arrowbutton{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;width:auto;height:auto;padding:10px;border-radius:8px;color:var(--rte-icon-color, #5f6368);background-clip:padding-box;transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease}.richtexteditor.rte-mobile rte-toolbar-splitbutton-direct,.richtexteditor.rte-mobile rte-toolbar-splitbutton-dropdown{display:inline-flex;align-items:center;align-self:stretch;justify-content:center;min-height:24px}.richtexteditor.rte-mobile rte-toolbar-button svg,.richtexteditor.rte-mobile rte-toolbar-splitbutton svg,.richtexteditor.rte-mobile rte-toolbar-dropdown svg,.richtexteditor.rte-mobile rte-toolbar-arrowbutton svg{display:block!important;flex:0 0 auto;width:20px!important;height:20px!important;margin:0!important;align-self:center!important;color:currentColor;pointer-events:none}.richtexteditor.rte-mobile rte-toolbar-button:active,.richtexteditor.rte-mobile rte-toolbar-splitbutton:active,.richtexteditor.rte-mobile rte-toolbar-dropdown:active,.richtexteditor.rte-mobile rte-toolbar-arrowbutton:active{color:#1559d6;background-color:#f3f8ff;border-color:#2563eb24}.richtexteditor.rte-mobile rte-toolbar-button.rte-command-active,.richtexteditor.rte-mobile rte-toolbar-splitbutton.rte-command-active,.richtexteditor.rte-mobile rte-toolbar-dropdown.rte-command-active,.richtexteditor.rte-mobile rte-toolbar-arrowbutton.rte-command-active{color:#1559d6;fill:currentColor;background-color:#edf6ff;border-color:#2563eb38;box-shadow:inset 0 0 0 1px #2563eb1f}.richtexteditor.rte-mobile rte-toolbar-button:focus-visible,.richtexteditor.rte-mobile rte-toolbar-splitbutton:focus-visible,.richtexteditor.rte-mobile rte-toolbar-dropdown:focus-visible,.richtexteditor.rte-mobile rte-toolbar-arrowbutton:focus-visible{outline:none;border-color:#2563eb;box-shadow:0 0 0 2px #2563eb2e;background-color:#eef4ff}.richtexteditor.rte-mobile rte-dropdown-panel,.richtexteditor.rte-mobile rte-popup{position:fixed!important;inset:auto 0 0!important;box-sizing:border-box;width:100vw;max-height:min(60vh,calc(100vh - 24px));border-top-left-radius:8px;border-top-right-radius:8px;border:1px solid #e2e8f0;border-bottom:none;background:#fff;background-clip:padding-box;box-shadow:0 -12px 32px #0f172a1f;padding:10px 8px 22px;overflow-y:auto;overscroll-behavior:contain;z-index:2147483647}.richtexteditor.rte-mobile rte-floattoolbar,.richtexteditor.rte-mobile rte-floatparagraph{display:none!important}.richtexteditor.rte-mobile~.aitoolkit-dialog,body.rte-mobile .aitoolkit-dialog{left:8px!important;right:8px!important;width:auto!important;max-width:none!important}.richtexteditor.rte-mobile~.aitoolkit-review-drawer,body.rte-mobile .aitoolkit-review-drawer,.richtexteditor.rte-mobile~.comments-sidebar,body.rte-mobile .comments-sidebar{inset:auto 0 0!important;box-sizing:border-box;width:auto!important;max-height:min(55vh,calc(100vh - 24px));border-top-left-radius:8px;border-top-right-radius:8px;border-top:1px solid #e2e8f0;background-clip:padding-box;overflow:hidden}.richtexteditor.rte-mobile iframe,.richtexteditor.rte-mobile .rte-editable{font-size:16px}.rte-dark.richtexteditor.rte-mobile rte-toolbar.rte-toolbar-mobile{background:var(--rte-surface-raised, #1e293b)}.rte-dark.richtexteditor.rte-mobile rte-toolbar-button,.rte-dark.richtexteditor.rte-mobile rte-toolbar-splitbutton,.rte-dark.richtexteditor.rte-mobile rte-toolbar-dropdown,.rte-dark.richtexteditor.rte-mobile rte-toolbar-arrowbutton{color:var(--rte-icon-color, #cbd5e1)}.rte-dark.richtexteditor.rte-mobile rte-toolbar-button.rte-command-active,.rte-dark.richtexteditor.rte-mobile rte-toolbar-splitbutton.rte-command-active,.rte-dark.richtexteditor.rte-mobile rte-toolbar-dropdown.rte-command-active,.rte-dark.richtexteditor.rte-mobile rte-toolbar-arrowbutton.rte-command-active{color:#bfdbfe;background-color:#1e3a5f;border-color:#60a5fa75;box-shadow:inset 0 0 0 1px #60a5fa3d}@media(prefers-color-scheme:dark){.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar.rte-toolbar-mobile{background:var(--rte-surface-raised, #1e293b)}.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-button,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-splitbutton,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-dropdown,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-arrowbutton{color:var(--rte-icon-color, #cbd5e1)}.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-button.rte-command-active,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-splitbutton.rte-command-active,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-dropdown.rte-command-active,.richtexteditor.rte-mobile:not(.rte-light) rte-toolbar-arrowbutton.rte-command-active{color:#bfdbfe;background-color:#1e3a5f;border-color:#60a5fa75;box-shadow:inset 0 0 0 1px #60a5fa3d}}}.rte-force-mobile .richtexteditor rte-toolbar.rte-toolbar-mobile{display:flex!important}.rte-force-mobile .richtexteditor rte-toolbar.rte-toolbar-desktop{display:none!important}
|