@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,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(/&nbsp;|\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);