@openleaf-editor/element 0.1.0-beta.2 → 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 +51 -2
- package/dist/form-bridge.d.ts +46 -0
- package/dist/form-bridge.d.ts.map +1 -0
- package/dist/form-bridge.js +158 -0
- package/dist/form-bridge.js.map +1 -0
- package/dist/index.d.ts +113 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1008 -200
- package/dist/index.js.map +1 -1
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -5,6 +5,17 @@ The `<openleaf-editor>` custom element: OpenLeaf's drop-in for CMS forms. HTML i
|
|
|
5
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
|
|
@@ -87,6 +98,44 @@ Whatever that function throws is shown to the author verbatim, so write the
|
|
|
87
98
|
message for them. For one editor with its own endpoint, set
|
|
88
99
|
`element.imageUploader` instead.
|
|
89
100
|
|
|
90
|
-
|
|
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
|
+
```
|
|
91
117
|
|
|
92
|
-
|
|
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
|
@@ -36,9 +36,15 @@
|
|
|
36
36
|
* autoresize grow the canvas with the document
|
|
37
37
|
* toolbar-overflow collapse overflowing groups into a More menu
|
|
38
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
|
|
39
42
|
* aria-label accessible name for the editable region
|
|
43
|
+
*
|
|
44
|
+
* See `docs/api-reference.md` for the properties, methods and events too.
|
|
40
45
|
*/
|
|
41
|
-
import {
|
|
46
|
+
import { type ToolbarHandle, type ImageUploader } from '@openleaf-editor/ui';
|
|
47
|
+
import type { Schema } from 'prosemirror-model';
|
|
42
48
|
import { EditorView } from 'prosemirror-view';
|
|
43
49
|
/**
|
|
44
50
|
* Emitted when the HTML source view opens and closes, carrying the textarea.
|
|
@@ -47,10 +53,23 @@ import { EditorView } from 'prosemirror-view';
|
|
|
47
53
|
* formatting, syntax highlighting -- without the element having to know anything
|
|
48
54
|
* about it. Names are defined here rather than imported so the element keeps no
|
|
49
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.
|
|
50
61
|
*/
|
|
51
62
|
export declare const SOURCE_OPEN_EVENT = "openleaf:source-open";
|
|
52
63
|
export declare const SOURCE_CLOSE_EVENT = "openleaf:source-close";
|
|
53
|
-
|
|
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 {
|
|
54
73
|
#private;
|
|
55
74
|
static get observedAttributes(): string[];
|
|
56
75
|
/**
|
|
@@ -77,17 +96,61 @@ export declare class OpenLeafEditor extends HTMLElement {
|
|
|
77
96
|
* nothing about this is visible to an author.
|
|
78
97
|
*/
|
|
79
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
|
+
*/
|
|
80
115
|
disconnectedCallback(): void;
|
|
81
116
|
/** Current document as an HTML string. */
|
|
82
117
|
get value(): string;
|
|
83
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);
|
|
84
130
|
/** Escape hatch for plugins and integrations that need the real view. */
|
|
85
131
|
get view(): EditorView | null;
|
|
86
132
|
/** The schema this editor was built with. */
|
|
87
|
-
get schema():
|
|
133
|
+
get schema(): Schema<string, string>;
|
|
88
134
|
/** The toolbar, for plugins pushing external state via setItemState. */
|
|
89
|
-
get
|
|
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. */
|
|
90
152
|
get sourceMode(): boolean;
|
|
153
|
+
set sourceMode(open: boolean);
|
|
91
154
|
}
|
|
92
155
|
/**
|
|
93
156
|
* Re-exported so the single-file bundle can offer paste normalization without a
|
|
@@ -101,6 +164,52 @@ export { normalizePastedHtml } from '@openleaf-editor/paste';
|
|
|
101
164
|
* `element.imageUploader` overrides it for one editor.
|
|
102
165
|
*/
|
|
103
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
|
+
}
|
|
104
213
|
/** Idempotent: safe to import twice, or alongside a bundled copy. */
|
|
105
214
|
export declare function defineOpenLeafEditor(tag?: string): void;
|
|
106
215
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|