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