@openpresentation/opf-editor 0.29.0 → 0.30.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.
@@ -0,0 +1,370 @@
1
+ // CE-28: the "expand editor" part. `createExpandController` makes the editor's host element fill the viewport for editing (a fixed overlay driven by
2
+ // `data-opf-expanded` and src/styles/expand.css; the element is never moved, so the editor keeps its state), and `createExpandButton` is the toolbar
3
+ // control for it. This is not the presenter (src/presenter.js), which shows slides on black. Both share one idea with it: the Fullscreen API is only a
4
+ // bonus on top of the overlay, because it is refused without a user gesture, in an iframe without `allow="fullscreen"` and on iPhone Safari.
5
+ // Plain DOM, no framework, no network. `target.ownerDocument` is the document, so it works in iframes. Icons are the Lucide Maximize2 and Minimize2
6
+ // paths (ISC license), inlined.
7
+ import { normalizeIcons, resolveIcon } from "./icons.js";
8
+
9
+ // Each icon is a list of [tag, attributes]; a host replaces one with `icons: { expand, collapse }`.
10
+ const ICON_NAMES = ["expand", "collapse"];
11
+ const ICONS = {
12
+ expand: ["M15 3h6v6", "m21 3-7 7", "m3 21 7-7", "M9 21H3v-6"].map((d) => ["path", { d }]),
13
+ collapse: ["m14 10 7-7", "M20 10h-6V4", "m3 21 7-7", "M4 14h6v6"].map((d) => ["path", { d }]),
14
+ };
15
+
16
+ const DEFAULT_LABELS = {
17
+ /** The accessible name of the button while the editor is not expanded. */
18
+ expand: "Expand editor",
19
+ /** The accessible name of the button while the editor is expanded. */
20
+ collapse: "Exit expanded editor",
21
+ /** The key named in the tooltip of the expanded button: "Exit expanded editor (Esc)". */
22
+ escape: "Esc",
23
+ /** The status line when the editor expands. */
24
+ expanded: "Editor expanded. Press Escape to exit.",
25
+ /** The status line when it returns. */
26
+ collapsed: "Editor restored.",
27
+ };
28
+
29
+ // ---- per-document state -------------------------------------------------------------------------------------------------------------------
30
+ // The scroll lock and "who is expanded" belong to the document, not to one controller: any number of editors can be mounted on a page.
31
+ const documentStates = new WeakMap(); // Document -> { active, locks, saved }
32
+ function stateOf(doc) {
33
+ let state = documentStates.get(doc);
34
+ if (!state) documentStates.set(doc, state = { active: null, locks: 0, saved: null });
35
+ return state;
36
+ }
37
+ const SCROLL_PROPS = ["overflow-x", "overflow-y"];
38
+ // Page scroll is locked with `overflow: hidden !important` on the document element. What the page had there (value and priority of the shorthand
39
+ // and of both longhands) is saved by the first lock and written back by the last unlock, so a page that set `overflow: scroll` gets exactly that.
40
+ function lockScroll(doc) {
41
+ const state = stateOf(doc);
42
+ const style = doc.documentElement?.style;
43
+ if (!style) return;
44
+ if (state.locks++ > 0) return;
45
+ state.saved = { shorthand: [style.getPropertyValue("overflow"), style.getPropertyPriority("overflow")], longhands: SCROLL_PROPS.map((name) => [name, style.getPropertyValue(name), style.getPropertyPriority(name)]) };
46
+ style.setProperty("overflow", "hidden", "important");
47
+ }
48
+ function unlockScroll(doc) {
49
+ const state = stateOf(doc);
50
+ const style = doc.documentElement?.style;
51
+ if (!style || state.locks === 0) return;
52
+ if (--state.locks > 0) return;
53
+ const { shorthand, longhands } = state.saved;
54
+ state.saved = null;
55
+ style.removeProperty("overflow");
56
+ if (shorthand[0]) style.setProperty("overflow", shorthand[0], shorthand[1]);
57
+ else for (const [name, value, priority] of longhands) if (value) style.setProperty(name, value, priority);
58
+ if (!doc.documentElement.getAttribute("style")) doc.documentElement.removeAttribute("style");
59
+ }
60
+
61
+ const fullscreenElementOf = (doc) => doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null;
62
+ const NON_TEXT_INPUTS = new Set(["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit"]);
63
+ // What owns Escape while the focus is in it: typing, an editor, a menu, a dialog. A dialog or menu that contains the target (the editor lives in a
64
+ // modal) does not count: it is the editor's surroundings, not something open over it.
65
+ const OWNS_ESCAPE = '[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"],.cm-editor,[role="textbox"],[role="combobox"],[role="listbox"],[role="menu"],[role="menubar"],[role="dialog"],[role="alertdialog"],[aria-modal="true"],dialog[open]';
66
+ function ownsEscape(node, target) {
67
+ if (!node || typeof node.closest !== "function") return false;
68
+ if (node.isContentEditable) return true;
69
+ const tag = node.tagName;
70
+ if (tag === "TEXTAREA" || tag === "SELECT") return true;
71
+ if (tag === "INPUT" && !NON_TEXT_INPUTS.has((node.getAttribute("type") ?? "text").toLowerCase())) return true;
72
+ // A trigger whose popup is open (a menu button) closes the popup first.
73
+ if (node.hasAttribute("aria-haspopup") && node.getAttribute("aria-expanded") === "true") return true;
74
+ let hit = node.closest(OWNS_ESCAPE);
75
+ if (!hit) {
76
+ try { hit = node.closest("[popover]:popover-open"); } catch { hit = null; }
77
+ }
78
+ return Boolean(hit) && !hit.contains(target);
79
+ }
80
+
81
+ // controller -> { labels, subscribe(fn), hint(element) }: what the button needs from the controller without widening the public API.
82
+ const internals = new WeakMap();
83
+
84
+ /**
85
+ * Make `target` (the editor's host element) fill the viewport on request.
86
+ *
87
+ * Expanding sets `data-opf-expanded` on `target` (src/styles/expand.css turns it into a fixed overlay), locks page scroll on the document element,
88
+ * moves the focus into `target`, and listens for Escape. Collapsing undoes each of those and returns the focus to the control that expanded. The
89
+ * element keeps its place in the DOM, so the editor keeps its state. Only one controller per document is expanded at a time: expanding another
90
+ * collapses the first (its `onChange` is called).
91
+ *
92
+ * Options: `onChange({ expanded })`, `fullscreen` (also ask the browser for full screen on `target`; best effort, the overlay is the fallback, and
93
+ * the editor collapses when the browser leaves full screen by itself), `labels` (`expand`, `collapse`, `escape`, `expanded`, `collapsed`) and
94
+ * `onStatus(message)` (screen-reader status lines for the two transitions), `inertOutside` (default true: while expanded, everything on the page
95
+ * outside `target` is made `inert`, so keyboard and screen-reader users cannot reach what the overlay hides) and `keep` (elements, or a function
96
+ * returning them, that stay interactive: a toolbar the host mounted outside `target`; read when the editor expands).
97
+ *
98
+ * Inert is applied to the siblings of `target` and of each of its ancestors, up to `body`; a sibling that contains a kept element is entered instead.
99
+ * Elements that are already `inert`, `script`, `style` and `template` are left alone, and so is a running or mounted presenter (`.opf-presenter`).
100
+ * Exactly the elements this controller marked are restored on collapse, destroy and hand-over. It is a snapshot: an element added to the page while
101
+ * the editor is expanded (a menu appended to `body`) is not inert.
102
+ *
103
+ * Returns `{ expanded, expand(on = true), collapse(), toggle(), destroy() }`. The three methods return the resulting state.
104
+ */
105
+ export function createExpandController(target, options = {}) {
106
+ if (!target?.ownerDocument) throw new TypeError("A DOM element is required.");
107
+ const doc = target.ownerDocument;
108
+ const labels = { ...DEFAULT_LABELS, ...(options.labels ?? {}) };
109
+ const { onChange, onStatus } = options;
110
+ const wantsFullscreen = options.fullscreen === true;
111
+ const inertOutside = options.inertOutside !== false;
112
+ let inertMarks = [];
113
+ const subscribers = new Set();
114
+ const yielded = new WeakSet(); // Escape events a typing field, menu, dialog or the presenter owns (decided before anyone handles them)
115
+ let expanded = false, destroyed = false, enteredFullscreen = false, addedTabindex = false, returnTo = null, hinted = null;
116
+
117
+ const slot = { target, release: (releaseOptions) => leave({ ...releaseOptions, restoreFocus: false }) };
118
+ let handedOffFrom = null; // the target of the controller this one replaced while it was in full screen
119
+
120
+ function announce() {
121
+ for (const subscriber of [...subscribers]) subscriber(expanded);
122
+ if (typeof onChange === "function") onChange({ expanded });
123
+ if (typeof onStatus === "function") onStatus(expanded ? labels.expanded : labels.collapsed);
124
+ }
125
+
126
+ // ---- focus ------------------------------------------------------------------------------------------------------------------------------
127
+ const isRealFocus = (node) => node && node !== doc.body && node !== doc.documentElement;
128
+ function moveFocusIn() {
129
+ const active = doc.activeElement;
130
+ // The control that expanded, when it lives in the editor, keeps the focus (it is now the "exit" control); anything else in the editor too.
131
+ if (hinted && target.contains(hinted) && typeof hinted.focus === "function") hinted.focus({ preventScroll: true });
132
+ else if (!(isRealFocus(active) && target.contains(active))) {
133
+ if (!target.hasAttribute("tabindex")) { target.setAttribute("tabindex", "-1"); addedTabindex = true; }
134
+ target.focus({ preventScroll: true });
135
+ }
136
+ }
137
+ // Only when nobody took the focus since (the host may move it in `onChange`). While the page is still full screen the page behind takes no
138
+ // focus, so this runs again when the exit has finished.
139
+ function restoreFocus() {
140
+ const now = doc.activeElement;
141
+ if (returnTo?.isConnected && typeof returnTo.focus === "function" && (!isRealFocus(now) || target.contains(now))) returnTo.focus({ preventScroll: true });
142
+ }
143
+
144
+ // ---- full screen ------------------------------------------------------------------------------------------------------------------------
145
+ function exitFullscreen(then) {
146
+ let result;
147
+ try { result = (doc.exitFullscreen ?? doc.webkitExitFullscreen)?.call(doc); } catch { result = undefined; }
148
+ Promise.resolve(result).then(() => then?.(), () => then?.());
149
+ }
150
+ // The previous editor kept the screen for this one to take over; when this one did not get it, the previous one's element must not keep it.
151
+ function reclaimHandedOff() {
152
+ const previous = handedOffFrom;
153
+ handedOffFrom = null;
154
+ if (previous && fullscreenElementOf(doc) === previous) exitFullscreen(restoreFocus);
155
+ }
156
+ function requestFullscreen() {
157
+ const request = target.requestFullscreen ?? target.webkitRequestFullscreen;
158
+ if (typeof request !== "function" || (doc.fullscreenEnabled ?? doc.webkitFullscreenEnabled) === false) return reclaimHandedOff();
159
+ let attempt;
160
+ try { attempt = Promise.resolve(request.call(target, { navigationUI: "hide" })); } catch (error) { attempt = Promise.reject(error); }
161
+ attempt.then(
162
+ () => {
163
+ reclaimHandedOff();
164
+ if (fullscreenElementOf(doc) !== target) return;
165
+ // Collapsed (or destroyed) while the request was pending: the screen is ours now, so give it back.
166
+ if (!expanded) exitFullscreen(restoreFocus);
167
+ else enteredFullscreen = true;
168
+ },
169
+ reclaimHandedOff, // refused (no gesture, an iframe without allow="fullscreen", iPhone): the overlay is the expanded editor
170
+ );
171
+ }
172
+ // The browser leaves full screen by itself (Esc, a swipe, the host): the expanded editor ends with it. An exit this controller asked for clears
173
+ // `enteredFullscreen` first, so its event is not read as the user's. Another element taking the screen (the presenter) leaves the overlay in place.
174
+ function onFullscreenChange() {
175
+ if (!expanded) return;
176
+ const holder = fullscreenElementOf(doc);
177
+ if (holder === target) { if (wantsFullscreen) enteredFullscreen = true; return; }
178
+ if (holder) { if (!target.contains(holder)) enteredFullscreen = false; return; }
179
+ if (enteredFullscreen) leave({ exitFullscreen: false });
180
+ }
181
+
182
+ // ---- inert -----------------------------------------------------------------------------------------------------------------------------
183
+ const INERT_SKIP = new Set(["SCRIPT", "STYLE", "TEMPLATE"]);
184
+ function applyInert() {
185
+ const root = doc.body;
186
+ if (!inertOutside || !root || !root.contains(target) || target === root) return;
187
+ const wanted = typeof options.keep === "function" ? options.keep() : options.keep;
188
+ // A mounted presenter lives outside the editor and must stay usable when it is opened from the expanded editor.
189
+ const keeps = [...(wanted ? Array.from(wanted) : []), ...doc.querySelectorAll(".opf-presenter")].filter((node) => node?.nodeType === 1 && !target.contains(node) && !node.contains(target));
190
+ const visit = (node) => {
191
+ if (INERT_SKIP.has(node.tagName) || node.hasAttribute("inert") || keeps.includes(node)) return;
192
+ if (keeps.some((kept) => node.contains(kept))) { for (const child of node.children) visit(child); return; }
193
+ node.setAttribute("inert", "");
194
+ inertMarks.push(node);
195
+ };
196
+ for (let current = target; current !== root; current = current.parentElement) {
197
+ for (const sibling of current.parentElement.children) if (sibling !== current) visit(sibling);
198
+ }
199
+ }
200
+ function removeInert() {
201
+ for (const node of inertMarks) node.removeAttribute("inert");
202
+ inertMarks = [];
203
+ }
204
+
205
+ // ---- Escape -----------------------------------------------------------------------------------------------------------------------------
206
+ // Two listeners: the capture one decides whether Escape belongs to something else (the state of a menu or the presenter is gone once their own
207
+ // handler ran); the bubbling one collapses unless the event was handled by then.
208
+ function onKeydownCapture(event) {
209
+ if (event.key !== "Escape") return;
210
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
211
+ const presenterRunning = doc.querySelector(".opf-presenter:not([hidden])");
212
+ if (presenterRunning || [event.target, path[0], doc.activeElement].some((node) => ownsEscape(node, target))) yielded.add(event);
213
+ }
214
+ function onKeydown(event) {
215
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
216
+ if (yielded.has(event)) return;
217
+ event.preventDefault(); // handled: nobody else (the host's own Escape handler) should act on it too
218
+ leave();
219
+ }
220
+
221
+ // ---- state ------------------------------------------------------------------------------------------------------------------------------
222
+ function enter() {
223
+ const state = stateOf(doc);
224
+ handedOffFrom = null;
225
+ if (state.active && state.active !== slot) {
226
+ handedOffFrom = wantsFullscreen ? state.active.target : null;
227
+ state.active.release({ exitFullscreen: !wantsFullscreen });
228
+ }
229
+ state.active = slot;
230
+ const active = doc.activeElement;
231
+ returnTo = hinted ?? (isRealFocus(active) ? active : null);
232
+ expanded = true;
233
+ target.setAttribute("data-opf-expanded", "true");
234
+ lockScroll(doc);
235
+ applyInert();
236
+ doc.addEventListener("keydown", onKeydownCapture, true);
237
+ doc.addEventListener("keydown", onKeydown, false);
238
+ doc.addEventListener("fullscreenchange", onFullscreenChange);
239
+ doc.addEventListener("webkitfullscreenchange", onFullscreenChange);
240
+ moveFocusIn();
241
+ if (wantsFullscreen) requestFullscreen();
242
+ }
243
+ function leave({ exitFullscreen: exit = true, restoreFocus: refocus = true, silent = false } = {}) {
244
+ if (!expanded) return false;
245
+ expanded = false;
246
+ const state = stateOf(doc);
247
+ if (state.active === slot) state.active = null;
248
+ target.removeAttribute("data-opf-expanded");
249
+ if (addedTabindex) { target.removeAttribute("tabindex"); addedTabindex = false; }
250
+ unlockScroll(doc);
251
+ removeInert();
252
+ doc.removeEventListener("keydown", onKeydownCapture, true);
253
+ doc.removeEventListener("keydown", onKeydown, false);
254
+ if (enteredFullscreen) {
255
+ enteredFullscreen = false;
256
+ if (exit && fullscreenElementOf(doc) === target) exitFullscreen(refocus ? restoreFocus : undefined);
257
+ }
258
+ if (refocus) restoreFocus();
259
+ if (!silent) announce();
260
+ returnTo = null;
261
+ hinted = null;
262
+ return true;
263
+ }
264
+ function expand(on = true) {
265
+ if (destroyed) return false;
266
+ if (on === false) return collapse();
267
+ if (!expanded) { enter(); announce(); }
268
+ return expanded;
269
+ }
270
+ function collapse() {
271
+ if (destroyed) return false;
272
+ leave();
273
+ return expanded;
274
+ }
275
+
276
+ const controller = {
277
+ get expanded() { return expanded; },
278
+ expand,
279
+ collapse,
280
+ toggle() { return expanded ? collapse() : expand(true); },
281
+ destroy() {
282
+ if (destroyed) return;
283
+ leave({ silent: true });
284
+ destroyed = true;
285
+ doc.removeEventListener("fullscreenchange", onFullscreenChange);
286
+ doc.removeEventListener("webkitfullscreenchange", onFullscreenChange);
287
+ subscribers.clear();
288
+ },
289
+ };
290
+ internals.set(controller, {
291
+ labels,
292
+ subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); },
293
+ // The button says which control is acting: Safari does not focus a button on click, so `activeElement` cannot name it.
294
+ hint(element) { hinted = element; },
295
+ });
296
+ return controller;
297
+ }
298
+
299
+ // ---- the button ---------------------------------------------------------------------------------------------------------------------------
300
+ function icon(doc, name, given) {
301
+ return resolveIcon(doc, given[name], ICONS[name], { name, className: "opf-expand-icon", size: 16 });
302
+ }
303
+
304
+ /**
305
+ * Mount the expand button in `container`: an icon-only toggle (Lucide Maximize2 while the editor is not expanded, Minimize2 while it is) with
306
+ * `aria-pressed`, the name "Expand editor" or "Exit expanded editor", and a tooltip that names the Escape key. It follows the controller, so it
307
+ * is right when the editor collapses another way (Escape, the browser leaving full screen).
308
+ *
309
+ * Options: `controller` (from `createExpandController`, required), `density` (`"comfortable"` 32px, or `"touch"` 40px; a coarse pointer makes it
310
+ * 44px either way) and `labels` (overrides the controller's).
311
+ *
312
+ * Returns `{ element, setDensity(density), setDisabled(disabled), menuItems(), destroy() }`. A disabled button stays focusable (`aria-disabled`)
313
+ * and cannot expand the editor, but it can always exit it.
314
+ */
315
+ export function createExpandButton(container, options = {}) {
316
+ if (!container?.ownerDocument) throw new TypeError("A DOM container is required.");
317
+ const { controller } = options;
318
+ const link = internals.get(controller);
319
+ if (!link) throw new TypeError("createExpandButton needs a controller from createExpandController.");
320
+ const doc = container.ownerDocument;
321
+ const labels = { ...link.labels, ...(options.labels ?? {}) };
322
+ const icons = normalizeIcons(options.icons, ICON_NAMES, "createExpandButton icons");
323
+ let disabled = false, destroyed = false;
324
+
325
+ const button = doc.createElement("button");
326
+ button.type = "button";
327
+ button.className = "opf-expand-button opf-ui";
328
+ button.dataset.density = options.density === "touch" ? "touch" : "comfortable";
329
+ button.dataset.action = "expand";
330
+ let shown = null;
331
+ const paint = () => {
332
+ const on = controller.expanded;
333
+ const label = on ? labels.collapse : labels.expand;
334
+ button.setAttribute("aria-pressed", on ? "true" : "false");
335
+ button.setAttribute("aria-label", label);
336
+ button.title = on ? `${label} (${labels.escape})` : label;
337
+ if (on) button.setAttribute("aria-keyshortcuts", "Escape"); else button.removeAttribute("aria-keyshortcuts");
338
+ // Disabled stops expanding only: an expanded editor must always be able to exit.
339
+ if (disabled && !on) button.setAttribute("aria-disabled", "true"); else button.removeAttribute("aria-disabled");
340
+ if (shown !== on) {
341
+ shown = on;
342
+ button.replaceChildren(icon(doc, on ? "collapse" : "expand", icons));
343
+ }
344
+ };
345
+ button.addEventListener("click", () => {
346
+ if (destroyed || (disabled && !controller.expanded)) return;
347
+ link.hint(button);
348
+ controller.toggle();
349
+ });
350
+ const unsubscribe = link.subscribe(paint);
351
+ paint();
352
+ container.append(button);
353
+
354
+ return {
355
+ element: button,
356
+ setDensity(density) { button.dataset.density = density === "touch" ? "touch" : "comfortable"; },
357
+ setDisabled(value) { disabled = Boolean(value); paint(); },
358
+ /** What the toolbar's overflow menu offers: one toggle item. */
359
+ menuItems() {
360
+ const on = controller.expanded;
361
+ return [{ id: "expand", label: on ? labels.collapse : labels.expand, pressed: on, ...(disabled && !on ? { disabled: true } : {}), run: () => { controller.toggle(); } }];
362
+ },
363
+ destroy() {
364
+ if (destroyed) return;
365
+ destroyed = true;
366
+ unsubscribe();
367
+ button.remove();
368
+ },
369
+ };
370
+ }
@@ -0,0 +1,2 @@
1
+ // Importing this module defines `<opf-editor>` (see element.d.ts). It exports nothing.
2
+ export {};
@@ -0,0 +1,7 @@
1
+ // CE-30: `import "@openpresentation/opf-editor/element/define"` defines `<opf-editor>` and nothing else. It is the one entry of the element with a
2
+ // side effect (listed in the package's `sideEffects`), for a page that wants the tag from a single import line. On a server there is no
3
+ // `customElements`, so importing it there does nothing. A page that needs another tag name, or default `fonts` and `catalogs`, imports
4
+ // `defineOpfEditorElement` from `@openpresentation/opf-editor/element` and calls it.
5
+ import { defineOpfEditorElement } from "./element.js";
6
+
7
+ if (typeof customElements !== "undefined" && typeof HTMLElement !== "undefined") defineOpfEditorElement();
@@ -0,0 +1,91 @@
1
+ import type { Catalog } from "@openpresentation/opf";
2
+ import type { OpfEditor, OpfEditorEvents, OpfEditorFontsOption, OpfEditorOptions } from "./workspace.js";
3
+
4
+ /** Every workspace event the element re-dispatches: the keys of `OpfEditorEvents` (the chrome's `deckName`, `expand`, `dirty`, `save` and `review-open` among them). */
5
+ export declare const OPF_EDITOR_EVENTS: readonly string[];
6
+ /** The DOM event type of a workspace event name: `opfEventType("layoutStudio")` is `"opf-layout-studio"`. */
7
+ export declare function opfEventType(name: string): string;
8
+
9
+ type KebabCase<S extends string> = S extends `${infer Head}${infer Tail}` ? `${Head extends Lowercase<Head> ? Head : `-${Lowercase<Head>}`}${KebabCase<Tail>}` : S;
10
+
11
+ /**
12
+ * The DOM events of `<opf-editor>`: one `opf-<name>` for each workspace event (`opf-ready`, `opf-change`, `opf-slide`, `opf-layout-studio`, ...), a
13
+ * `CustomEvent` that bubbles and is composed, whose `detail` is the workspace event's detail.
14
+ */
15
+ export type OpfEditorElementEventMap = {
16
+ [Name in keyof OpfEditorEvents as `opf-${KebabCase<Name & string>}`]: CustomEvent<OpfEditorEvents[Name]>;
17
+ } & {
18
+ /**
19
+ * `opf-error` is also the element's own error channel: a `src` that could not be fetched or read (`source: "src"`), inline or property text that is
20
+ * not a deck (`"inline"`, `"document"`), a document the editor refused (`"document"`), an attribute the editor rejected (`"layout"`, `"theme"`,
21
+ * `"slide"`, ...) and an editor that could not be created, for example an unknown `layout` (`"mount"`). The workspace's own errors carry no `source`.
22
+ */
23
+ "opf-error": CustomEvent<OpfEditorEvents["error"] & { source?: string }>;
24
+ };
25
+
26
+ /** What `defineOpfEditorElement` takes as the defaults of every element of that name. A property on an element wins. */
27
+ export interface OpfEditorElementDefaults {
28
+ /**
29
+ * The fonts the editor draws with: the renderer's handle, a lease, a promise or a function of either, or the URL of a font root (a string, as in
30
+ * `<opf-deck fonts="/opf-fonts">`, loaded with the renderer's `loadPreviewFonts`). Without it (and without a `fonts` property or attribute) the editor
31
+ * draws with estimated layout and system fonts.
32
+ */
33
+ fonts?: OpfEditorFontsOption | string;
34
+ /** The host's catalogs, for the session and every draw (for example `[gallery]`). */
35
+ catalogs?: readonly Catalog[];
36
+ }
37
+
38
+ /**
39
+ * `<opf-editor>`: the editor in one tag. The editor mounts in a `div` inside the element (light DOM), once the element is connected and the
40
+ * page has parsed its content, and is destroyed one microtask after the element leaves the document (unless it came back: a move in the DOM
41
+ * keeps the editor). Attributes `layout`, `toolbar`, `readonly`, `theme`, `density`, `slide` and `src` are observed; see docs/web-component.md.
42
+ */
43
+ export interface OpfEditorElement extends HTMLElement {
44
+ /** The editor handle once it is created, else `null` (before mount, after removal, or when it could not be created). */
45
+ readonly editor: OpfEditor | null;
46
+ /** Settles with the handle once the editor has mounted and drawn; rejects when it could not be created (`opf-error` says why). Read it again after a remount. */
47
+ readonly ready: Promise<OpfEditor>;
48
+ /**
49
+ * The document. Reading it gives the live document of a mounted editor (frozen, replaced on every change) or the one the editor will open.
50
+ * Setting an object opens it (one undoable change when the editor is mounted); a string is JSON, YAML or Markdown text.
51
+ * Wins over `src` and over an inline `<script>`.
52
+ */
53
+ document: unknown;
54
+ /** More `createOpfEditor` options. Read when the editor mounts; setting a different object on a mounted element remounts it, keeping the document. */
55
+ options: Partial<OpfEditorOptions>;
56
+ /** A fonts handle (or what `OpfEditorFontsOption` allows), or the URL of a font root; the `fonts` attribute is a font root. Default: `defineOpfEditorElement`'s. */
57
+ fonts: OpfEditorFontsOption | string | undefined;
58
+ catalogs: readonly Catalog[] | undefined;
59
+ /** The attributes as properties. `readOnly` is the `readonly` attribute. */
60
+ layout: string | null;
61
+ toolbar: string | null;
62
+ theme: "light" | "dark" | "auto" | string | null;
63
+ density: "comfortable" | "touch" | "auto" | string | null;
64
+ src: string | null;
65
+ readOnly: boolean;
66
+ /** The slide on show (0-based); setting it navigates. */
67
+ slide: number;
68
+
69
+ addEventListener<K extends keyof OpfEditorElementEventMap>(type: K, listener: (this: OpfEditorElement, event: OpfEditorElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
70
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
71
+ removeEventListener<K extends keyof OpfEditorElementEventMap>(type: K, listener: (this: OpfEditorElement, event: OpfEditorElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
72
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
73
+ }
74
+
75
+ export interface OpfEditorElementConstructor {
76
+ new (): OpfEditorElement;
77
+ readonly prototype: OpfEditorElement;
78
+ readonly observedAttributes: readonly string[];
79
+ }
80
+
81
+ /**
82
+ * Define the custom element (default tag `opf-editor`) and return its class. Safe to call twice: it returns the class already defined under
83
+ * that name, and throws when the name belongs to another element. Needs a browser (`customElements`); importing this module does not.
84
+ */
85
+ export declare function defineOpfEditorElement(name?: string, defaults?: OpfEditorElementDefaults): OpfEditorElementConstructor;
86
+
87
+ declare global {
88
+ interface HTMLElementTagNameMap {
89
+ "opf-editor": OpfEditorElement;
90
+ }
91
+ }