@openleaf-editor/element 0.1.0-beta.1 → 0.1.0-beta.3

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
@@ -2,9 +2,20 @@
2
2
 
3
3
  The `<openleaf-editor>` custom element: OpenLeaf's drop-in for CMS forms. HTML in, HTML out, syncs to a textarea.
4
4
 
5
- This is a **beta** (`0.1.0-beta.1`). APIs may still change. It has not been
5
+ This is a **beta** (`0.1.0-beta.2`). APIs may still change. It has not been
6
6
  used in production, and it has not been driven by a real screen reader.
7
7
 
8
+ > **Editor output is untrusted input.** Whatever the editor produces — and
9
+ > whatever a user pasted into it — must be sanitized **on your server** before it
10
+ > is stored or rendered as HTML. Client-side sanitization is a user-experience
11
+ > feature, not a security control: anything the editor strips can be put back
12
+ > with developer tools, because the editor runs under the user's control.
13
+ >
14
+ > [`@openleaf-editor/sanitize`](https://github.com/PeytonNowlin/openleaf/tree/main/packages/sanitize) ships the
15
+ > canonical allowlist as data and generates configuration for DOMPurify, Python
16
+ > `bleach` and PHP HTMLPurifier from it, so client and server enforce the same
17
+ > rules. Read [SECURITY.md](https://github.com/PeytonNowlin/openleaf/blob/main/SECURITY.md) before you ship.
18
+
8
19
  ## Install
9
20
 
10
21
  ```bash
@@ -31,7 +42,9 @@ npm install @openleaf-editor/plugins-table@beta \
31
42
  @openleaf-editor/plugins-colour@beta \
32
43
  @openleaf-editor/plugins-highlight@beta \
33
44
  @openleaf-editor/plugins-import@beta \
34
- @openleaf-editor/plugins-import-docx@beta
45
+ @openleaf-editor/plugins-import-docx@beta \
46
+ @openleaf-editor/plugins-session@beta \
47
+ @openleaf-editor/plugins-insert@beta
35
48
  ```
36
49
 
37
50
  ```ts
@@ -40,12 +53,16 @@ import { installColourPicker } from '@openleaf-editor/plugins-colour'
40
53
  import { installSyntaxHighlighting } from '@openleaf-editor/plugins-highlight'
41
54
  import { installImport } from '@openleaf-editor/plugins-import'
42
55
  import { installDocxImport } from '@openleaf-editor/plugins-import-docx'
56
+ import { installSessionTools } from '@openleaf-editor/plugins-session'
57
+ import { installInsertTools } from '@openleaf-editor/plugins-insert'
43
58
 
44
59
  installTableEditing()
45
60
  installColourPicker()
46
61
  installSyntaxHighlighting()
47
62
  installImport()
48
63
  installDocxImport()
64
+ installSessionTools()
65
+ installInsertTools()
49
66
  ```
50
67
 
51
68
  **Keep every `@openleaf-editor/*` package on the same version.** They pin each
@@ -81,6 +98,44 @@ Whatever that function throws is shown to the author verbatim, so write the
81
98
  message for them. For one editor with its own endpoint, set
82
99
  `element.imageUploader` instead.
83
100
 
84
- Sanitize submitted HTML on the server. `@openleaf-editor/sanitize` ships the same allowlist as data — including the narrow `style` allowance that alignment and colour need. If you sanitize with DOMPurify, install `styleAttributeHook` as well; the config alone cannot filter CSS per element.
101
+ ## Sanitizing submitted HTML
102
+
103
+ Sanitize on the server — see the note at the top of this file for why.
104
+ `@openleaf-editor/sanitize` ships the same allowlist as data, including the
105
+ narrow `style` allowance that alignment and colour need.
106
+
107
+ With DOMPurify, this is the whole setup:
108
+
109
+ ```js
110
+ import DOMPurify from 'dompurify'
111
+ import { configureDOMPurify, DEFAULT_POLICY } from '@openleaf-editor/sanitize'
112
+
113
+ const purify = DOMPurify(window)
114
+ const config = configureDOMPurify(purify, DEFAULT_POLICY)
115
+ const clean = purify.sanitize(dirty, config)
116
+ ```
85
117
 
86
- See the [project README](https://github.com/PeytonNowlin/openleaf) for the rest.
118
+ One call, because the safe setup has to be atomic. `configureDOMPurify` installs
119
+ **both** hooks the policy needs and then returns a config that enables the
120
+ features they guard:
121
+
122
+ - `styleAttributeHook`, because `ALLOWED_ATTR` is global and DOMPurify filters no
123
+ CSS properties of its own — without it, permitting `style` permits
124
+ `position:fixed;inset:0`, a page-covering overlay that looks like your own UI.
125
+ - `embedHook`, because the policy allows an `<iframe>` only when its `src` is on a
126
+ closed list of player hosts, which no DOMPurify config can express. Enabling
127
+ iframes without it would let `<iframe src="https://evil.example">` through the
128
+ sanitizer this file recommends.
129
+
130
+ Reach for the individual hooks or `toDOMPurifyConfig` only if you manage
131
+ DOMPurify hooks yourself. Both fail closed — they drop styles and iframes rather
132
+ than trusting them — so the failure mode of getting it wrong is content loss
133
+ rather than XSS. That is the right direction, and it is still worth not doing.
134
+
135
+ ## More
136
+
137
+ - [API reference](https://github.com/PeytonNowlin/openleaf/blob/main/docs/api-reference.md) —
138
+ every attribute, property and `openleaf:*` event.
139
+ - [SECURITY.md](https://github.com/PeytonNowlin/openleaf/blob/main/SECURITY.md) —
140
+ the threat model and a baseline CSP.
141
+ - [Project README](https://github.com/PeytonNowlin/openleaf) for the rest.
@@ -0,0 +1,46 @@
1
+ /** Owns the custom element's textarea and form-submission contract. */
2
+ export declare class FormBridge {
3
+ #private;
4
+ private readonly host;
5
+ private readonly readValue;
6
+ private readonly writeValue;
7
+ constructor(host: HTMLElement, readValue: () => string, writeValue: (html: string) => void);
8
+ get textarea(): HTMLTextAreaElement | null;
9
+ bind(): HTMLTextAreaElement | null;
10
+ attach(): void;
11
+ detach(): void;
12
+ rebind(): void;
13
+ /**
14
+ * Write the document into the textarea.
15
+ *
16
+ * `value` is an optional already-serialized copy. The keystroke path has one
17
+ * in hand -- it is about to put the same string in the change event's detail
18
+ * -- and serializing the document twice per keystroke was measurable on a
19
+ * large post.
20
+ *
21
+ * An explicit sync also cancels a pending debounced one and clears the dirty
22
+ * flag: the write it was going to make has just happened.
23
+ */
24
+ sync(value?: string): void;
25
+ /**
26
+ * Record that the document changed, without serializing it.
27
+ *
28
+ * The serialization is what costs -- a `DOMSerializer` pass over the whole
29
+ * document and a ~0.33 MB string per keystroke on a 100-page document -- and
30
+ * nothing reads the result until one of the flush points below. So the change
31
+ * is only noted here, and the trailing timer exists for hosts that watch the
32
+ * textarea rather than the element.
33
+ */
34
+ markDirty(): void;
35
+ /**
36
+ * Write the document to the textarea if, and only if, it would differ.
37
+ *
38
+ * Two ways it can: the document changed since the last write, or something
39
+ * outside wrote to the textarea itself -- a script, a server re-render, a
40
+ * test. The second is why this cannot be a bare dirty check. Comparing
41
+ * against the string last written costs a string compare and no
42
+ * serialization, so the flush points stay free when nothing has moved.
43
+ */
44
+ flush(): void;
45
+ }
46
+ //# sourceMappingURL=form-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-bridge.d.ts","sourceRoot":"","sources":["../src/form-bridge.ts"],"names":[],"mappings":"AAYA,uEAAuE;AACvE,qBAAa,UAAU;;IASnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAFV,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,MAAM,MAAM,EACvB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAGrD,IAAI,QAAQ,IAAI,mBAAmB,GAAG,IAAI,CAEzC;IAED,IAAI,IAAI,mBAAmB,GAAG,IAAI;IAmBlC,MAAM,IAAI,IAAI;IAmBd,MAAM,IAAI,IAAI;IAWd,MAAM,IAAI,IAAI;IAMd;;;;;;;;;;OAUG;IACH,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAS1B;;;;;;;;OAQG;IACH,SAAS,IAAI,IAAI;IAUjB;;;;;;;;OAQG;IACH,KAAK,IAAI,IAAI;CA0Bd"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * How long a document change may sit unwritten before the textarea catches up.
3
+ *
4
+ * Nothing reads the textarea between edits -- `submit` and `formdata` both
5
+ * force a write first, and so does teardown -- so this exists only for a host
6
+ * that watches the textarea itself, with a MutationObserver or a polling
7
+ * autosave. Writing on every keystroke instead cost a full re-serialization of
8
+ * the document per character: 12 ms on a plain 100-page document, 74 ms with
9
+ * tables, against a 16.7 ms frame.
10
+ */
11
+ const SYNC_DELAY_MS = 300;
12
+ /** Owns the custom element's textarea and form-submission contract. */
13
+ export class FormBridge {
14
+ host;
15
+ readValue;
16
+ writeValue;
17
+ #textarea = null;
18
+ #form = null;
19
+ #dirty = false;
20
+ /** The last string this bridge wrote, so a foreign write is detectable. */
21
+ #written = null;
22
+ #timer = null;
23
+ constructor(host, readValue, writeValue) {
24
+ this.host = host;
25
+ this.readValue = readValue;
26
+ this.writeValue = writeValue;
27
+ }
28
+ get textarea() {
29
+ return this.#textarea;
30
+ }
31
+ bind() {
32
+ const id = this.host.getAttribute('for');
33
+ if (id) {
34
+ const root = this.host.getRootNode();
35
+ const element = root.getElementById?.(id);
36
+ if (element instanceof HTMLTextAreaElement)
37
+ this.#textarea = element;
38
+ else {
39
+ this.#textarea = null;
40
+ console.error(`<openleaf-editor for="${id}">: no <textarea id="${id}"> found. ` +
41
+ 'Content will not be submitted with the form.');
42
+ }
43
+ return this.#textarea;
44
+ }
45
+ this.#textarea = this.host.querySelector('textarea');
46
+ return this.#textarea;
47
+ }
48
+ attach() {
49
+ this.detach();
50
+ // Re-resolve a `for=` binding that could not be resolved at build time. A
51
+ // wrapper builds the element while it is still detached, and a detached
52
+ // root has no `getElementById`, so `bind()` found nothing and every later
53
+ // write went nowhere -- the editor looked right and the textarea it posts
54
+ // still held the server's original HTML.
55
+ //
56
+ // Safe here in a way `rebind()` is not: this only re-runs for an explicit
57
+ // `for`, whose id lookup is unambiguous, where the nested
58
+ // `querySelector('textarea')` can match the source box instead.
59
+ if (!this.#textarea && this.host.getAttribute('for'))
60
+ this.bind();
61
+ this.#form = this.#textarea?.form ?? this.host.closest('form');
62
+ this.#form?.addEventListener('submit', this.#onSubmit);
63
+ this.#form?.addEventListener('formdata', this.#onFormData);
64
+ this.#form?.addEventListener('reset', this.#onReset);
65
+ this.sync();
66
+ }
67
+ detach() {
68
+ // Flush first. A host that removes the editor and then reads the textarea
69
+ // -- a framework unmounting a component, a wizard swapping a step -- must
70
+ // not get the value as of the last debounce tick.
71
+ this.flush();
72
+ this.#form?.removeEventListener('submit', this.#onSubmit);
73
+ this.#form?.removeEventListener('formdata', this.#onFormData);
74
+ this.#form?.removeEventListener('reset', this.#onReset);
75
+ this.#form = null;
76
+ }
77
+ rebind() {
78
+ this.detach();
79
+ this.bind();
80
+ this.attach();
81
+ }
82
+ /**
83
+ * Write the document into the textarea.
84
+ *
85
+ * `value` is an optional already-serialized copy. The keystroke path has one
86
+ * in hand -- it is about to put the same string in the change event's detail
87
+ * -- and serializing the document twice per keystroke was measurable on a
88
+ * large post.
89
+ *
90
+ * An explicit sync also cancels a pending debounced one and clears the dirty
91
+ * flag: the write it was going to make has just happened.
92
+ */
93
+ sync(value) {
94
+ this.#cancel();
95
+ this.#dirty = false;
96
+ if (!this.#textarea)
97
+ return;
98
+ const html = value ?? this.readValue();
99
+ this.#textarea.value = html;
100
+ this.#written = html;
101
+ }
102
+ /**
103
+ * Record that the document changed, without serializing it.
104
+ *
105
+ * The serialization is what costs -- a `DOMSerializer` pass over the whole
106
+ * document and a ~0.33 MB string per keystroke on a 100-page document -- and
107
+ * nothing reads the result until one of the flush points below. So the change
108
+ * is only noted here, and the trailing timer exists for hosts that watch the
109
+ * textarea rather than the element.
110
+ */
111
+ markDirty() {
112
+ if (!this.#textarea)
113
+ return;
114
+ this.#dirty = true;
115
+ if (this.#timer !== null)
116
+ return;
117
+ this.#timer = setTimeout(() => {
118
+ this.#timer = null;
119
+ this.flush();
120
+ }, SYNC_DELAY_MS);
121
+ }
122
+ /**
123
+ * Write the document to the textarea if, and only if, it would differ.
124
+ *
125
+ * Two ways it can: the document changed since the last write, or something
126
+ * outside wrote to the textarea itself -- a script, a server re-render, a
127
+ * test. The second is why this cannot be a bare dirty check. Comparing
128
+ * against the string last written costs a string compare and no
129
+ * serialization, so the flush points stay free when nothing has moved.
130
+ */
131
+ flush() {
132
+ const foreign = this.#textarea !== null && this.#written !== null && this.#textarea.value !== this.#written;
133
+ if (this.#dirty || foreign)
134
+ this.sync();
135
+ else
136
+ this.#cancel();
137
+ }
138
+ #cancel() {
139
+ if (this.#timer === null)
140
+ return;
141
+ clearTimeout(this.#timer);
142
+ this.#timer = null;
143
+ }
144
+ #onSubmit = () => this.flush();
145
+ #onFormData = (event) => {
146
+ this.flush();
147
+ if (this.#textarea?.name)
148
+ event.formData.set(this.#textarea.name, this.#textarea.value);
149
+ };
150
+ #onReset = () => {
151
+ // Reset fires before controls restore their defaults.
152
+ queueMicrotask(() => {
153
+ if (this.#textarea)
154
+ this.writeValue(this.#textarea.value);
155
+ });
156
+ };
157
+ }
158
+ //# sourceMappingURL=form-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-bridge.js","sourceRoot":"","sources":["../src/form-bridge.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -20,14 +20,31 @@
20
20
  * reads `$_POST['body']` keeps working untouched.
21
21
  *
22
22
  * Attributes:
23
- * for id of the textarea to bind to
24
- * skin named appearance: midnight, paper, contrast, compact
25
- * theme light | dark | auto (default: follow the visitor's system)
26
- * toolbar space-separated item ids, `|` for a separator; `none` to omit
27
- * readonly render but do not allow editing
28
- * aria-label accessible name for the editable region
23
+ * for id of the textarea to bind to
24
+ * skin named appearance: midnight, paper, contrast, compact
25
+ * theme light | dark | auto (default: follow the visitor's system)
26
+ * toolbar space-separated item ids, `|` for a separator; `none` to omit
27
+ * toolbar2 a second toolbar, same grammar
28
+ * menubar space-separated menu ids, or omit to hide; `none` also hides
29
+ * contextmenu `none` to disable; default is link, image and table menus
30
+ * selection-toolbar floating bar for a non-empty selection; `none` disables
31
+ * insert-toolbar floating bar for an empty block; `none` disables
32
+ * formats `p.lead=Lead|h2=Section` entries for the formats dropdown
33
+ * content-css comma-separated URLs scoped onto the canvas
34
+ * lang UI locale, matched against registerTranslations()
35
+ * inline hide chrome until the editor is focused
36
+ * autoresize grow the canvas with the document
37
+ * toolbar-overflow collapse overflowing groups into a More menu
38
+ * readonly render but do not allow editing
39
+ * autolink `false` to stop URLs becoming links on space or Enter.
40
+ * Trailing prose punctuation is left outside the mark.
41
+ * visualaids `false` to hide the guides for invisible structure
42
+ * aria-label accessible name for the editable region
43
+ *
44
+ * See `docs/api-reference.md` for the properties, methods and events too.
29
45
  */
30
- import { Toolbar } from '@openleaf-editor/ui';
46
+ import { type ToolbarHandle, type ImageUploader } from '@openleaf-editor/ui';
47
+ import type { Schema } from 'prosemirror-model';
31
48
  import { EditorView } from 'prosemirror-view';
32
49
  /**
33
50
  * Emitted when the HTML source view opens and closes, carrying the textarea.
@@ -36,10 +53,23 @@ import { EditorView } from 'prosemirror-view';
36
53
  * formatting, syntax highlighting -- without the element having to know anything
37
54
  * about it. Names are defined here rather than imported so the element keeps no
38
55
  * dependency on any plugin.
56
+ *
57
+ * These fire on a REAL teardown, not on a DOM move. Moving the element keeps
58
+ * the whole session -- including source mode and the same textarea node -- so
59
+ * an enhancer that attached on open stays correctly attached, and gets its
60
+ * close only when the element is actually removed for good.
39
61
  */
40
62
  export declare const SOURCE_OPEN_EVENT = "openleaf:source-open";
41
63
  export declare const SOURCE_CLOSE_EVENT = "openleaf:source-close";
42
- export declare class OpenLeafEditor extends HTMLElement {
64
+ declare const HTMLElementBase: typeof HTMLElement;
65
+ export interface OpenLeafChangeDetail {
66
+ /** The document as HTML, already serialized. */
67
+ value: string;
68
+ }
69
+ export interface OpenLeafSourceDetail {
70
+ textarea: HTMLTextAreaElement;
71
+ }
72
+ export declare class OpenLeafEditor extends HTMLElementBase {
43
73
  #private;
44
74
  static get observedAttributes(): string[];
45
75
  /**
@@ -66,17 +96,61 @@ export declare class OpenLeafEditor extends HTMLElement {
66
96
  * nothing about this is visible to an author.
67
97
  */
68
98
  connectedCallback(): void;
99
+ /**
100
+ * Tear down -- but only once the element is really gone.
101
+ *
102
+ * Moving a node fires disconnect and then connect SYNCHRONOUSLY, so a guard
103
+ * in `connectedCallback` can never help: by the time it runs the view has
104
+ * already been destroyed. Deferring the decision by one microtask makes a
105
+ * move a no-op, which is what keeps undo history, selection and every
106
+ * plugin's state alive across a keyed-list reorder, an `insertBefore`
107
+ * shuffle or a drag-to-reorder.
108
+ *
109
+ * The limit is worth being precise about, because it is not "unmounting is
110
+ * safe now": this only covers a move completed within one task. Anything that
111
+ * parks the element in a detached container across ticks -- Vue's
112
+ * `<KeepAlive>` does exactly that -- is a real removal and tears down, which
113
+ * is why the rebuild path has to stay correct rather than merely unreachable.
114
+ */
69
115
  disconnectedCallback(): void;
70
116
  /** Current document as an HTML string. */
71
117
  get value(): string;
72
118
  set value(html: string);
119
+ /**
120
+ * Uploader for this editor alone, overriding `registerImageUploader`.
121
+ *
122
+ * An accessor rather than a class field, and that is load-bearing: a field
123
+ * initializer runs when the element upgrades, so it would overwrite an
124
+ * uploader assigned before the definition loaded -- exactly the case
125
+ * `#upgradeProperty` exists to rescue, defeated by the declaration meant to
126
+ * make the property visible.
127
+ */
128
+ get imageUploader(): ImageUploader | null;
129
+ set imageUploader(uploader: ImageUploader | null);
73
130
  /** Escape hatch for plugins and integrations that need the real view. */
74
131
  get view(): EditorView | null;
75
132
  /** The schema this editor was built with. */
76
- get schema(): import('prosemirror-model').Schema;
133
+ get schema(): Schema<string, string>;
77
134
  /** The toolbar, for plugins pushing external state via setItemState. */
78
- get toolbar(): Toolbar | null;
135
+ get toolbarInstance(): ToolbarHandle | null;
136
+ /** The `toolbar` attribute. Assigning reflects, as HTML properties should. */
137
+ get toolbar(): string | null;
138
+ set toolbar(layout: string | null);
139
+ /** The `toolbar2` attribute. */
140
+ get toolbar2(): string | null;
141
+ set toolbar2(layout: string | null);
142
+ /** The `menubar` attribute. */
143
+ get menubar(): string | null;
144
+ set menubar(menus: string | null);
145
+ /** The `formats` attribute. */
146
+ get formats(): string | null;
147
+ set formats(spec: string | null);
148
+ /** The `readonly` attribute, as the boolean every framework binds it as. */
149
+ get readOnly(): boolean;
150
+ set readOnly(value: boolean);
151
+ /** Whether the HTML source view is open. Assigning toggles it. */
79
152
  get sourceMode(): boolean;
153
+ set sourceMode(open: boolean);
80
154
  }
81
155
  /**
82
156
  * Re-exported so the single-file bundle can offer paste normalization without a
@@ -89,7 +163,53 @@ export { normalizePastedHtml } from '@openleaf-editor/paste';
89
163
  * without a build step: `OpenLeaf.registerImageUploader(fn)`. Setting
90
164
  * `element.imageUploader` overrides it for one editor.
91
165
  */
92
- export { registerImageUploader, type ImageUploadResult, type ImageUploader, } from '@openleaf-editor/ui';
166
+ export { registerFilePicker, registerImageClasses, registerImageList, registerImageUploader, registerLinkList, registerTranslations, setUiLocale, type FilePicker, type FilePickerKind, type ImageUploadResult, type ImageUploader, type ListedResource, type PickedResource, } from '@openleaf-editor/ui';
167
+ /**
168
+ * Re-exported because it is the documented way to add a toolbar control and it
169
+ * lived in a package no install command mentions.
170
+ *
171
+ * `registerToolbarItem` is exported from `@openleaf-editor/ui`, which is a
172
+ * transitive dependency of this package and appears in no `npm install` line in
173
+ * any README. So the one extension point an integrator is most likely to reach
174
+ * for was reachable only by guessing at a package name. It is already in this
175
+ * bundle; re-exporting it costs nothing and means `registerToolbarItem` is
176
+ * available wherever `<openleaf-editor>` is -- including from
177
+ * `window.OpenLeaf` in a plain `<script>` integration, which has no other route
178
+ * to it at all.
179
+ *
180
+ * `t` comes with it: a custom control's label is translated by the toolbar, but
181
+ * anything the control builds itself has to be translated by the control.
182
+ */
183
+ export { registerIcons, registerStyles, registerToolbarItem, t, type ToolbarContext, type ToolbarControl, type ToolbarItemSpec, type ToolbarSelectOption, } from '@openleaf-editor/ui';
184
+ declare global {
185
+ interface HTMLElementTagNameMap {
186
+ 'openleaf-editor': OpenLeafEditor;
187
+ }
188
+ interface HTMLElementEventMap {
189
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
190
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
191
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
192
+ }
193
+ /**
194
+ * The same three on `document` and `window`.
195
+ *
196
+ * `document.addEventListener` resolves against `DocumentEventMap`, not
197
+ * `HTMLElementEventMap`, so without these a delegated listener -- the ordinary
198
+ * way to watch every editor on a page, and the one `composed: true` now makes
199
+ * reliable from outside a shadow root -- was back to a bare `Event` and a cast
200
+ * to read `detail`.
201
+ */
202
+ interface DocumentEventMap {
203
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
204
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
205
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
206
+ }
207
+ interface WindowEventMap {
208
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
209
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
210
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
211
+ }
212
+ }
93
213
  /** Idempotent: safe to import twice, or alongside a bundled copy. */
94
214
  export declare function defineOpenLeafEditor(tag?: string): void;
95
215
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAcH,OAAO,EAEL,OAAO,EAYR,MAAM,qBAAqB,CAAA;AAK5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAI7C;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,yBAAyB,CAAA;AACvD,eAAO,MAAM,kBAAkB,0BAA0B,CAAA;AAEzD,qBAAa,cAAe,SAAQ,WAAW;;IAC7C,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAED;;;;OAIG;IACH,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA6C5C;;;;;;;;;;;;;;;;OAgBG;IACH,iBAAiB,IAAI,IAAI;IA2LzB,oBAAoB,IAAI,IAAI;IAiB5B,0CAA0C;IAC1C,IAAI,KAAK,IAAI,MAAM,CAIlB;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,MAAM,EAWrB;IAED,yEAAyE;IACzE,IAAI,IAAI,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,6CAA6C;IAC7C,IAAI,MAAM,IAAI,OAAO,mBAAmB,EAAE,MAAM,CAE/C;IAED,wEAAwE;IACxE,IAAI,OAAO,IAAI,OAAO,GAAG,IAAI,CAE5B;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;CAmNF;AAED;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAE5D;;;;GAIG;AACH,OAAO,EACL,qBAAqB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAA;AAE5B,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,GAAG,SAAoB,GAAG,IAAI,CAIlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}