@rathkit/rte-element 0.1.0-alpha.4 → 0.2.0-beta.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @rathkit/rte-element
2
2
 
3
- > **Alpha** (0.1.0-alpha.0). Not for production: the APIs will change between releases. Published to the `next` tag only.
3
+ > **Beta.** Not for production yet: the APIs may still change between releases. Published to the `next` tag only.
4
4
 
5
- The editor as custom elements, `<rk-editor>` and `<rk-toolbar>`, for any page or framework: form-associated (it submits its value, resets, restores), with properties, attributes and events.
5
+ The editor as custom elements, `<rte-editor>` and `<rte-toolbar>`, for any page or framework: form-associated (it submits its value, resets, restores), with properties, attributes and events.
6
6
 
7
7
  ## Install
8
8
 
@@ -20,10 +20,10 @@ This package has no dependencies of its own. It works with `@rathkit/rte-core`,
20
20
  </script>
21
21
 
22
22
  <form>
23
- <rk-editor name="body" label="Body"><p>Start here</p></rk-editor>
23
+ <rte-editor name="body" label="Body"><p>Start here</p></rte-editor>
24
24
  </form>
25
25
  <script>
26
- document.querySelector("rk-editor").addEventListener("rk-change", (e) => console.log(e.detail.html));
26
+ document.querySelector("rte-editor").addEventListener("rte-change", (e) => console.log(e.detail.html));
27
27
  </script>
28
28
  ```
29
29
 
@@ -1,8 +1,8 @@
1
1
  import type { Editor } from "@rathkit/rte-core/dom";
2
2
  import { type StandardOptions } from "@rathkit/rte-core/standard";
3
3
  /** Options an app can only pass as code (functions, plugin lists): everything in the standard editor's options except what attributes control. */
4
- export type RkEditorOptions = Omit<StandardOptions, "element" | "html" | "readOnly">;
5
- export declare class RkEditor extends HTMLElement {
4
+ export type RteElementOptions = Omit<StandardOptions, "element" | "html" | "readOnly" | "disabled">;
5
+ export declare class RteEditorElement extends HTMLElement {
6
6
  static formAssociated: boolean;
7
7
  static observedAttributes: string[];
8
8
  private internals;
@@ -13,9 +13,13 @@ export declare class RkEditor extends HTMLElement {
13
13
  private opts;
14
14
  private stop;
15
15
  private atFocus;
16
+ /** Whether a disabled `<fieldset>` around the element disables it. */
17
+ private formDisabled;
16
18
  private resolveReady;
17
19
  private ready;
18
20
  constructor();
21
+ /** Problems found before there is an editor to report them, on the same channel (`options.onWarning`, else the console). */
22
+ private warn;
19
23
  connectedCallback(): void;
20
24
  disconnectedCallback(): void;
21
25
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
@@ -33,16 +37,18 @@ export declare class RkEditor extends HTMLElement {
33
37
  set value(html: string);
34
38
  get readOnly(): boolean;
35
39
  set readOnly(value: boolean);
40
+ get disabled(): boolean;
41
+ set disabled(value: boolean);
36
42
  /** The editor inside, once it exists: for commands, plugins and subscriptions. */
37
43
  get editor(): Editor | undefined;
38
44
  /** Resolves with the editor once the element is connected and the editor is built. */
39
45
  whenReady(): Promise<Editor>;
40
46
  /** Options only code can give (an upload function, more plugins). Set them before the element is connected. */
41
- get options(): RkEditorOptions;
42
- set options(options: RkEditorOptions);
47
+ get options(): RteElementOptions;
48
+ set options(options: RteElementOptions);
43
49
  focus(): void;
44
50
  private changed;
45
- private lastSent;
51
+ private announce;
46
52
  /** Brings the form value and validity up to date. */
47
53
  private sync;
48
54
  private isEmpty;
@@ -1,5 +1,5 @@
1
1
  import { createEditor } from "@rathkit/rte-core/standard";
2
- export class RkEditor extends HTMLElement {
2
+ export class RteEditorElement extends HTMLElement {
3
3
  static formAssociated = true;
4
4
  static observedAttributes = ["value", "readonly", "disabled", "label", "required", "placeholder"];
5
5
  internals;
@@ -10,12 +10,19 @@ export class RkEditor extends HTMLElement {
10
10
  opts = {};
11
11
  stop;
12
12
  atFocus = "";
13
+ formDisabled = false;
13
14
  resolveReady;
14
15
  ready = new Promise((resolve) => { this.resolveReady = resolve; });
15
16
  constructor() {
16
17
  super();
17
18
  this.internals = typeof this.attachInternals === "function" ? this.attachInternals() : undefined;
18
19
  }
20
+ warn(warning) {
21
+ try {
22
+ (this.opts.onWarning ?? ((w) => console.warn(`[rte] ${w.code}: ${w.message}`)))(warning);
23
+ }
24
+ catch { }
25
+ }
19
26
  connectedCallback() {
20
27
  if (this.current)
21
28
  return;
@@ -27,30 +34,42 @@ export class RkEditor extends HTMLElement {
27
34
  this.defaultHtml = this.hasAttribute("value") ? (this.getAttribute("value") ?? "") : this.html;
28
35
  this.replaceChildren();
29
36
  const surface = document.createElement("div");
30
- surface.setAttribute("data-rk-surface", "");
37
+ surface.setAttribute("data-rte-surface", "");
31
38
  this.appendChild(surface);
32
39
  this.surface = surface;
33
40
  const label = this.getAttribute("label");
34
41
  if (label)
35
42
  surface.setAttribute("aria-label", label);
36
43
  else if (!this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby"))
37
- console.warn("[rte] <rk-editor> has no name: give it a label attribute.");
44
+ this.warn({ code: "missing-label", message: "<rte-editor> has no name: give it a label attribute." });
38
45
  const maxLength = Number.parseInt(this.getAttribute("maxlength") ?? "", 10);
39
46
  const spellcheck = this.getAttribute("spellcheck");
47
+ const profile = this.getAttribute("content");
48
+ const known = profile === "strict" || profile === "standard" || profile === "loose";
49
+ if (profile !== null && !known)
50
+ this.warn({ code: "bad-option", message: `<rte-editor content="${profile}"> is not a content profile: use strict, standard or loose.` });
40
51
  const editor = createEditor({
41
- ...this.opts, element: surface, html: this.html, readOnly: this.hasAttribute("readonly") || this.hasAttribute("disabled"),
52
+ ...this.opts, element: surface, html: this.html, readOnly: this.hasAttribute("readonly"), disabled: this.hasAttribute("disabled") || this.formDisabled,
42
53
  ...(this.hasAttribute("placeholder") ? { placeholder: this.getAttribute("placeholder") ?? "" } : {}),
54
+ ...(known ? { content: { ...this.opts.content, profile } } : {}),
43
55
  ...(Number.isFinite(maxLength) && maxLength >= 0 ? { maxLength } : {}),
44
56
  ...(spellcheck === "true" || spellcheck === "false" ? { spellcheck: spellcheck === "true" } : {}),
45
57
  ...(this.hasAttribute("autofocus") ? { autofocus: true } : {}),
46
58
  });
47
59
  this.current = editor;
48
- this.stop = editor.subscribe(() => this.changed());
60
+ const offs = [
61
+ editor.on("change", (html) => this.changed(html)),
62
+ editor.on("focus", () => this.announce("rte-focus", {})),
63
+ editor.on("blur", () => this.announce("rte-blur", {})),
64
+ editor.on("selection", (selection) => this.announce("rte-selection", { selection })),
65
+ ];
66
+ this.stop = () => { for (const off of offs)
67
+ off(); };
49
68
  surface.addEventListener("focusin", this.onFocusIn);
50
69
  surface.addEventListener("focusout", this.onFocusOut);
51
70
  this.sync();
52
71
  this.resolveReady?.(editor);
53
- this.dispatchEvent(new CustomEvent("rk-ready", { bubbles: true, composed: true, detail: { editor } }));
72
+ this.announce("rte-ready", { editor });
54
73
  }
55
74
  disconnectedCallback() {
56
75
  const editor = this.current;
@@ -71,7 +90,8 @@ export class RkEditor extends HTMLElement {
71
90
  this.value = value;
72
91
  }
73
92
  else if (name === "readonly" || name === "disabled") {
74
- this.current?.setReadOnly(this.hasAttribute("readonly") || this.hasAttribute("disabled"));
93
+ this.current?.setReadOnly(this.hasAttribute("readonly"));
94
+ this.current?.setDisabled(this.hasAttribute("disabled") || this.formDisabled);
75
95
  }
76
96
  else if (name === "placeholder") {
77
97
  this.current?.setPlaceholder(value ?? "");
@@ -85,7 +105,7 @@ export class RkEditor extends HTMLElement {
85
105
  }
86
106
  }
87
107
  formResetCallback() { this.value = this.defaultHtml; }
88
- formDisabledCallback(disabled) { this.current?.setReadOnly(disabled || this.hasAttribute("readonly")); }
108
+ formDisabledCallback(disabled) { this.formDisabled = disabled; this.current?.setDisabled(disabled || this.hasAttribute("disabled")); }
89
109
  formStateRestoreCallback(state) { if (typeof state === "string")
90
110
  this.value = state; }
91
111
  get form() { return this.internals?.form ?? null; }
@@ -105,25 +125,25 @@ export class RkEditor extends HTMLElement {
105
125
  }
106
126
  get readOnly() { return this.hasAttribute("readonly"); }
107
127
  set readOnly(value) { this.toggleAttribute("readonly", value); }
128
+ get disabled() { return this.hasAttribute("disabled") || this.formDisabled; }
129
+ set disabled(value) { this.toggleAttribute("disabled", value); }
108
130
  get editor() { return this.current; }
109
131
  whenReady() { return this.ready; }
110
132
  get options() { return this.opts; }
111
133
  set options(options) {
112
134
  if (this.current)
113
- console.warn("[rte] <rk-editor> options set after it was connected have no effect.");
135
+ this.warn({ code: "late-options", message: "<rte-editor> options set after it was connected have no effect." });
114
136
  this.opts = options;
115
137
  }
116
138
  focus() { this.current?.focus(); }
117
- changed() {
118
- const html = this.value;
119
- if (html === this.lastSent)
120
- return;
121
- this.lastSent = html;
139
+ changed(html) {
122
140
  this.sync();
123
- this.dispatchEvent(new CustomEvent("rk-change", { bubbles: true, composed: true, detail: { html } }));
141
+ this.announce("rte-change", { html });
124
142
  this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
125
143
  }
126
- lastSent = "";
144
+ announce(name, detail) {
145
+ this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
146
+ }
127
147
  sync() {
128
148
  const html = this.value;
129
149
  this.internals?.setFormValue(html);
@@ -132,7 +152,26 @@ export class RkEditor extends HTMLElement {
132
152
  else
133
153
  this.internals?.setValidity({});
134
154
  }
135
- isEmpty(html) { return html.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, "").trim() === "" && !html.includes("<img"); }
155
+ isEmpty(html) {
156
+ if (html.includes("<img"))
157
+ return false;
158
+ let text = "";
159
+ let open = -1;
160
+ for (let i = 0; i < html.length; i++) {
161
+ const ch = html[i];
162
+ if (open >= 0) {
163
+ if (ch === ">")
164
+ open = -1;
165
+ }
166
+ else if (ch === "<")
167
+ open = i;
168
+ else
169
+ text += ch;
170
+ }
171
+ if (open >= 0)
172
+ text += html.slice(open);
173
+ return text.replace(/&nbsp;/g, "").trim() === "";
174
+ }
136
175
  onFocusIn = () => { this.atFocus = this.value; };
137
176
  onFocusOut = () => { if (this.value !== this.atFocus)
138
177
  this.dispatchEvent(new Event("change", { bubbles: true })); };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { RkEditor, type RkEditorOptions } from "./editor-element.ts";
2
- import { RkToolbar } from "./toolbar-element.ts";
3
- export { RkEditor, RkToolbar, type RkEditorOptions };
1
+ import { RteEditorElement, type RteElementOptions } from "./editor-element.ts";
2
+ import { RteToolbarElement } from "./toolbar-element.ts";
3
+ export { RteEditorElement, RteToolbarElement, type RteElementOptions };
4
4
  /** Registers the elements. Safe to call more than once. */
5
5
  export declare function define(names?: {
6
6
  readonly editor?: string;
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { RkEditor } from "./editor-element.js";
2
- import { RkToolbar } from "./toolbar-element.js";
3
- export { RkEditor, RkToolbar };
1
+ import { RteEditorElement } from "./editor-element.js";
2
+ import { RteToolbarElement } from "./toolbar-element.js";
3
+ export { RteEditorElement, RteToolbarElement };
4
4
  export function define(names = {}) {
5
- const editor = names.editor ?? "rk-editor";
6
- const toolbar = names.toolbar ?? "rk-toolbar";
5
+ const editor = names.editor ?? "rte-editor";
6
+ const toolbar = names.toolbar ?? "rte-toolbar";
7
7
  if (!customElements.get(editor))
8
- customElements.define(editor, RkEditor);
8
+ customElements.define(editor, RteEditorElement);
9
9
  if (!customElements.get(toolbar))
10
- customElements.define(toolbar, RkToolbar);
10
+ customElements.define(toolbar, RteToolbarElement);
11
11
  }
@@ -1,4 +1,4 @@
1
- export declare class RkToolbar extends HTMLElement {
1
+ export declare class RteToolbarElement extends HTMLElement {
2
2
  static observedAttributes: string[];
3
3
  private toolbar;
4
4
  private unbind;
@@ -7,7 +7,7 @@ export declare class RkToolbar extends HTMLElement {
7
7
  connectedCallback(): void;
8
8
  disconnectedCallback(): void;
9
9
  attributeChangedCallback(): void;
10
- /** The editor this toolbar belongs to: the element named by `for`, else the nearest <rk-editor> beside or around it. */
10
+ /** The editor this toolbar belongs to: the element named by `for`, else the nearest <rte-editor> beside or around it. */
11
11
  private target;
12
12
  private teardown;
13
13
  private build;
@@ -1,5 +1,5 @@
1
1
  import { createToolbar } from "@rathkit/rte-headless";
2
- export class RkToolbar extends HTMLElement {
2
+ export class RteToolbarElement extends HTMLElement {
3
3
  static observedAttributes = ["for", "label", "slot-name"];
4
4
  toolbar;
5
5
  unbind;
@@ -13,7 +13,7 @@ export class RkToolbar extends HTMLElement {
13
13
  const id = this.getAttribute("for");
14
14
  if (id)
15
15
  return this.ownerDocument.getElementById(id);
16
- return (this.parentElement?.querySelector("rk-editor") ?? this.closest("rk-editor"));
16
+ return (this.parentElement?.querySelector("rte-editor") ?? this.closest("rte-editor"));
17
17
  }
18
18
  teardown() {
19
19
  this.unwatch?.();
@@ -27,7 +27,7 @@ export class RkToolbar extends HTMLElement {
27
27
  const host = this.target();
28
28
  if (!host)
29
29
  return;
30
- await customElements.whenDefined("rk-editor");
30
+ await customElements.whenDefined("rte-editor");
31
31
  const editor = await host.whenReady();
32
32
  if (ticket !== this.building || !this.isConnected)
33
33
  return;
@@ -44,7 +44,7 @@ export class RkToolbar extends HTMLElement {
44
44
  }
45
45
  else if (item.kind === "select") {
46
46
  const select = document.createElement("select");
47
- select.setAttribute("data-rk-item", item.id);
47
+ select.setAttribute("data-rte-item", item.id);
48
48
  for (const o of item.options) {
49
49
  const option = document.createElement("option");
50
50
  option.value = String(o.value);
@@ -56,13 +56,13 @@ export class RkToolbar extends HTMLElement {
56
56
  else if (item.kind !== "menu") {
57
57
  const button = document.createElement("button");
58
58
  button.type = "button";
59
- button.setAttribute("data-rk-item", item.id);
59
+ button.setAttribute("data-rte-item", item.id);
60
60
  button.textContent = toolbar.state(item.id)?.label ?? item.id;
61
61
  controls.push(button);
62
62
  }
63
63
  }
64
64
  this.replaceChildren(...controls);
65
- this.unbind = toolbar.bind(this, { onOpen: (item, control) => this.dispatchEvent(new CustomEvent("rk-open", { bubbles: true, composed: true, detail: { item, control } })) });
65
+ this.unbind = toolbar.bind(this, { onOpen: (item, control) => this.dispatchEvent(new CustomEvent("rte-open", { bubbles: true, composed: true, detail: { item, control } })) });
66
66
  };
67
67
  draw();
68
68
  this.unwatch = editor.ui.subscribe(this.getAttribute("slot-name") ?? "toolbar.main", draw);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rathkit/rte-element",
3
- "version": "0.1.0-alpha.4",
4
- "description": "The editor as a custom element, <rk-editor>, for any page or framework: form-associated, with properties, attributes and events.",
3
+ "version": "0.2.0-beta.0",
4
+ "description": "The editor as a custom element, <rte-editor>, for any page or framework: form-associated, with properties, attributes and events.",
5
5
  "keywords": [
6
6
  "rich-text",
7
7
  "editor",
@@ -50,8 +50,8 @@
50
50
  }
51
51
  },
52
52
  "peerDependencies": {
53
- "@rathkit/rte-core": ">=0.1.0-alpha.4 <0.2.0",
54
- "@rathkit/rte-headless": ">=0.1.0-alpha.4 <0.2.0"
53
+ "@rathkit/rte-core": ">=0.2.0-beta.0 <0.3.0",
54
+ "@rathkit/rte-headless": ">=0.2.0-beta.0 <0.3.0"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@rathkit/rte-core": {