@thomasfosterau/prosekit-svelte 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +255 -2
- package/dist/editor/BlockEditor.svelte +66 -0
- package/dist/editor/BlockEditor.svelte.d.ts +42 -0
- package/dist/editor/Editor.svelte +60 -0
- package/dist/editor/Editor.svelte.d.ts +57 -0
- package/dist/editor/InlineEditor.svelte +55 -0
- package/dist/editor/InlineEditor.svelte.d.ts +36 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/messages.svelte.d.ts +162 -0
- package/dist/messages.svelte.js +114 -0
- package/dist/toolbar/BlockToolbar.svelte +162 -0
- package/dist/toolbar/BlockToolbar.svelte.d.ts +52 -0
- package/dist/toolbar/ImageDialog.svelte +86 -0
- package/dist/toolbar/ImageDialog.svelte.d.ts +31 -0
- package/dist/toolbar/LinkDialog.svelte +113 -0
- package/dist/toolbar/LinkDialog.svelte.d.ts +40 -0
- package/dist/toolbar/SlashMenu.svelte +112 -0
- package/dist/toolbar/SlashMenu.svelte.d.ts +37 -0
- package/dist/toolbar/items.d.ts +125 -0
- package/dist/toolbar/items.js +262 -0
- package/package.json +65 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<!-- @component The link prompt as a bits-ui modal; the URL is checked with `isSafeHref` before it submits. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
LinkDialog — the link prompt, as a modal.
|
|
5
|
+
|
|
6
|
+
It replaces a `window.prompt` chain. A prompt cannot be styled, cannot be
|
|
7
|
+
validated before it closes, cannot be cancelled halfway through a
|
|
8
|
+
two-question sequence without leaving the first answer applied, and is
|
|
9
|
+
blocked outright in some embedding contexts. The dialog also gives the
|
|
10
|
+
href somewhere to be checked: `isSafeHref` runs on submit and the failure
|
|
11
|
+
is announced in a `role="alert"` rather than silently swallowed.
|
|
12
|
+
|
|
13
|
+
ARIA contract: bits-ui's. `role="dialog"`, `aria-modal`, the
|
|
14
|
+
`aria-labelledby` wiring to `Dialog.Title`, the focus trap, Escape, the
|
|
15
|
+
scroll lock and the focus return to whatever opened it are all its. What is
|
|
16
|
+
local: the fields, the validity check, the `aria-describedby` from the URL
|
|
17
|
+
input to the alert, and a `<form>` — so Enter in either input submits,
|
|
18
|
+
which is what anyone typing a URL expects.
|
|
19
|
+
-->
|
|
20
|
+
<script lang="ts">import { isSafeHref } from "@thomasfosterau/prosekit-utilities";
|
|
21
|
+
import { Dialog } from "bits-ui";
|
|
22
|
+
import { getEditorMessages } from "../messages.svelte.js";
|
|
23
|
+
const messages = getEditorMessages();
|
|
24
|
+
let { open = $bindable(false), href = "", title = "", onSubmit, onRemove, titleLabel = messages.linkDialogTitle, hrefLabel = messages.linkHrefLabel, linkTitleLabel = messages.linkTitleLabel, submitLabel = messages.linkSubmitLabel, removeLabel = messages.linkRemoveLabel, cancelLabel = messages.linkCancelLabel, closeLabel = messages.linkCloseLabel, invalidHrefMessage = messages.linkInvalidHrefMessage, class: className, id, ...rest } = $props();
|
|
25
|
+
const uid = $props.id();
|
|
26
|
+
const hrefId = `${uid}-href`;
|
|
27
|
+
const titleId = `${uid}-title`;
|
|
28
|
+
const errorId = `${uid}-error`;
|
|
29
|
+
let hrefValue = $state("");
|
|
30
|
+
let titleValue = $state("");
|
|
31
|
+
let invalid = $state(false);
|
|
32
|
+
// Seeded on every open, from whatever the caller last set: the toolbar
|
|
33
|
+
// reads the selection's link mark before it opens the dialog, so the form
|
|
34
|
+
// edits the existing link rather than starting blank over the top of one.
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!open) return;
|
|
37
|
+
hrefValue = href;
|
|
38
|
+
titleValue = title;
|
|
39
|
+
invalid = false;
|
|
40
|
+
});
|
|
41
|
+
function submit(event) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
const candidate = hrefValue.trim();
|
|
44
|
+
if (!isSafeHref(candidate)) {
|
|
45
|
+
invalid = true;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const trimmedTitle = titleValue.trim();
|
|
49
|
+
onSubmit({
|
|
50
|
+
href: candidate,
|
|
51
|
+
title: trimmedTitle.length > 0 ? trimmedTitle : null
|
|
52
|
+
});
|
|
53
|
+
open = false;
|
|
54
|
+
}
|
|
55
|
+
function remove() {
|
|
56
|
+
onRemove?.();
|
|
57
|
+
open = false;
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<Dialog.Root bind:open>
|
|
62
|
+
<Dialog.Portal>
|
|
63
|
+
<Dialog.Overlay class="bg-base-300/60 fixed inset-0 z-40" />
|
|
64
|
+
<Dialog.Content
|
|
65
|
+
{...rest}
|
|
66
|
+
{...(id === undefined ? {} : { id })}
|
|
67
|
+
class={[
|
|
68
|
+
'modal-box fixed left-1/2 top-1/2 z-50 w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2',
|
|
69
|
+
className,
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
<div class="flex items-start justify-between gap-4">
|
|
73
|
+
<Dialog.Title class="text-lg font-semibold">{titleLabel}</Dialog.Title>
|
|
74
|
+
<Dialog.Close class="btn btn-sm btn-ghost -mr-1 -mt-1">{closeLabel}</Dialog.Close>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<form class="mt-4 flex flex-col gap-3" onsubmit={submit}>
|
|
78
|
+
<label class="flex flex-col gap-1 text-sm" for={hrefId}>
|
|
79
|
+
{hrefLabel}
|
|
80
|
+
<input
|
|
81
|
+
id={hrefId}
|
|
82
|
+
class="input input-bordered w-full"
|
|
83
|
+
type="text"
|
|
84
|
+
bind:value={hrefValue}
|
|
85
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
86
|
+
aria-describedby={invalid ? errorId : undefined}
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
|
|
90
|
+
<label class="flex flex-col gap-1 text-sm" for={titleId}>
|
|
91
|
+
{linkTitleLabel}
|
|
92
|
+
<input id={titleId} class="input input-bordered w-full" type="text" bind:value={titleValue} />
|
|
93
|
+
</label>
|
|
94
|
+
|
|
95
|
+
{#if invalid}
|
|
96
|
+
<p id={errorId} role="alert" class="text-error text-sm">{invalidHrefMessage}</p>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<div class="modal-action">
|
|
100
|
+
{#if onRemove}
|
|
101
|
+
<button type="button" class="btn btn-sm btn-ghost" onclick={remove}>
|
|
102
|
+
{removeLabel}
|
|
103
|
+
</button>
|
|
104
|
+
{/if}
|
|
105
|
+
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (open = false)}>
|
|
106
|
+
{cancelLabel}
|
|
107
|
+
</button>
|
|
108
|
+
<button type="submit" class="btn btn-sm btn-primary">{submitLabel}</button>
|
|
109
|
+
</div>
|
|
110
|
+
</form>
|
|
111
|
+
</Dialog.Content>
|
|
112
|
+
</Dialog.Portal>
|
|
113
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'id' | 'title'> {
|
|
3
|
+
/** The dialog element's `id`. */
|
|
4
|
+
id?: string | undefined;
|
|
5
|
+
/** Whether the dialog is shown. Bindable. */
|
|
6
|
+
open?: boolean | undefined;
|
|
7
|
+
/** The URL the form opens with — the selection's link, when there is one. */
|
|
8
|
+
href?: string | undefined;
|
|
9
|
+
/** The title the form opens with. */
|
|
10
|
+
title?: string | undefined;
|
|
11
|
+
/** Called with the accepted attributes when the form is submitted. */
|
|
12
|
+
onSubmit: (attrs: {
|
|
13
|
+
href: string;
|
|
14
|
+
title: string | null;
|
|
15
|
+
}) => void;
|
|
16
|
+
/** Called when the remove button is used. Omitted, no remove button. */
|
|
17
|
+
onRemove?: (() => void) | undefined;
|
|
18
|
+
/** The dialog's heading, and its accessible name. */
|
|
19
|
+
titleLabel?: string | undefined;
|
|
20
|
+
/** Label of the URL field. */
|
|
21
|
+
hrefLabel?: string | undefined;
|
|
22
|
+
/** Label of the optional title field. */
|
|
23
|
+
linkTitleLabel?: string | undefined;
|
|
24
|
+
/** The submit button. */
|
|
25
|
+
submitLabel?: string | undefined;
|
|
26
|
+
/** The remove button. */
|
|
27
|
+
removeLabel?: string | undefined;
|
|
28
|
+
/** The cancel button. */
|
|
29
|
+
cancelLabel?: string | undefined;
|
|
30
|
+
/** Accessible name of the corner close button. */
|
|
31
|
+
closeLabel?: string | undefined;
|
|
32
|
+
/** Announced when the URL is rejected. */
|
|
33
|
+
invalidHrefMessage?: string | undefined;
|
|
34
|
+
/** Extra classes on the dialog box. */
|
|
35
|
+
class?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The link prompt as a bits-ui modal; the URL is checked with `isSafeHref` before it submits. */
|
|
38
|
+
declare const LinkDialog: import("svelte").Component<Props, {}, "open">;
|
|
39
|
+
type LinkDialog = ReturnType<typeof LinkDialog>;
|
|
40
|
+
export default LinkDialog;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!-- @component The insert menu: a bits-ui `DropdownMenu` of `SlashCommand` entries. `BlockToolbar` renders it; a custom toolbar can too. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
SlashMenu — the insert menu, on bits-ui's `DropdownMenu`.
|
|
5
|
+
|
|
6
|
+
"Slash" is the name the applications this package was extracted from used.
|
|
7
|
+
There is no `/` trigger inside the document: this is a button that opens a
|
|
8
|
+
menu of block insertions.
|
|
9
|
+
|
|
10
|
+
ARIA contract: bits-ui's. `aria-haspopup="menu"` and `aria-expanded` on the
|
|
11
|
+
trigger, `role="menu"` on the content, `role="menuitem"` on each entry,
|
|
12
|
+
arrow-key roving focus, typeahead, Escape to close and focus return. The
|
|
13
|
+
applications' version was a `role="listbox"` of plain `<button>`s — an
|
|
14
|
+
invalid parent/child pairing — with no `aria-expanded`, no focus
|
|
15
|
+
management and no Escape, which is the whole reason this is a bits-ui
|
|
16
|
+
primitive and not a `{#if}`.
|
|
17
|
+
|
|
18
|
+
The daisyUI `menu` styling goes on a `<ul>` INSIDE the content, and that
|
|
19
|
+
list and its `<li>` wrappers carry `role="none"` so the presentation layer
|
|
20
|
+
never comes between `role="menu"` and its items in the accessibility tree.
|
|
21
|
+
|
|
22
|
+
`BlockToolbar` renders this with its own `Toolbar.Button` as the trigger,
|
|
23
|
+
so the insert button is a toolbar item like any other. Exported for a
|
|
24
|
+
custom toolbar that wants the same menu.
|
|
25
|
+
-->
|
|
26
|
+
<script lang="ts">import { DropdownMenu } from "bits-ui";
|
|
27
|
+
import { useEditor } from "prosekit/svelte";
|
|
28
|
+
import { getEditorMessages } from "../messages.svelte.js";
|
|
29
|
+
const messages = getEditorMessages();
|
|
30
|
+
let { commands, open = $bindable(false), label = messages.insertLabel, menuLabel = messages.insertMenuLabel, labelledBy, dialogs, class: className, trigger, id, ...rest } = $props();
|
|
31
|
+
// `update: true` so the store re-emits on every editor state change; the
|
|
32
|
+
// commands are read at the moment they run, not captured. ProseKit hands
|
|
33
|
+
// the editor in as a Svelte store — that is its contract, and the one
|
|
34
|
+
// place this package touches one.
|
|
35
|
+
const editorStore = useEditor({ update: true });
|
|
36
|
+
// Set by `select`, read by `onCloseAutoFocus` below. Closing is what moves
|
|
37
|
+
// focus, and it happens after the handler returns.
|
|
38
|
+
let returnFocusToEditor = false;
|
|
39
|
+
function select(command) {
|
|
40
|
+
let opening = false;
|
|
41
|
+
command.run({
|
|
42
|
+
editor: $editorStore,
|
|
43
|
+
dialogs: {
|
|
44
|
+
link: () => {
|
|
45
|
+
opening = true;
|
|
46
|
+
dialogs?.link();
|
|
47
|
+
},
|
|
48
|
+
image: () => {
|
|
49
|
+
opening = true;
|
|
50
|
+
dialogs?.image();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Pulling focus back into the document is right after an insertion and
|
|
55
|
+
// wrong when a dialog is opening: it would take focus off the dialog
|
|
56
|
+
// before its trap is set up.
|
|
57
|
+
returnFocusToEditor = !opening;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Redirects bits-ui's focus return.
|
|
61
|
+
*
|
|
62
|
+
* On close it puts focus back on the trigger, which is right for Escape
|
|
63
|
+
* and wrong after an insertion — the caret belongs where the block just
|
|
64
|
+
* landed. Preventing the default and focusing the document is the
|
|
65
|
+
* primitive's own seam for exactly this.
|
|
66
|
+
*/
|
|
67
|
+
function closeAutoFocus(event) {
|
|
68
|
+
if (!returnFocusToEditor) return;
|
|
69
|
+
returnFocusToEditor = false;
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
$editorStore.focus();
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<DropdownMenu.Root bind:open>
|
|
76
|
+
<DropdownMenu.Trigger>
|
|
77
|
+
{#snippet child({ props })}
|
|
78
|
+
{#if trigger}
|
|
79
|
+
{@render trigger({ props })}
|
|
80
|
+
{:else}
|
|
81
|
+
<button {...props} type="button" class="btn btn-sm btn-ghost border-base-300 border">
|
|
82
|
+
{label}
|
|
83
|
+
</button>
|
|
84
|
+
{/if}
|
|
85
|
+
{/snippet}
|
|
86
|
+
</DropdownMenu.Trigger>
|
|
87
|
+
<DropdownMenu.Portal>
|
|
88
|
+
<DropdownMenu.Content
|
|
89
|
+
{...rest}
|
|
90
|
+
{...(id === undefined ? {} : { id })}
|
|
91
|
+
side="bottom"
|
|
92
|
+
align="start"
|
|
93
|
+
sideOffset={6}
|
|
94
|
+
aria-label={menuLabel}
|
|
95
|
+
aria-labelledby={labelledBy || undefined}
|
|
96
|
+
onCloseAutoFocus={closeAutoFocus}
|
|
97
|
+
class="z-50"
|
|
98
|
+
>
|
|
99
|
+
<ul role="none" class={['menu bg-base-100 rounded-box w-56 p-2 shadow-md', className]}>
|
|
100
|
+
{#each commands as command (command.id)}
|
|
101
|
+
<li role="none">
|
|
102
|
+
<DropdownMenu.Item onSelect={() => select(command)}>
|
|
103
|
+
{#snippet child({ props })}
|
|
104
|
+
<button {...props} type="button">{command.label}</button>
|
|
105
|
+
{/snippet}
|
|
106
|
+
</DropdownMenu.Item>
|
|
107
|
+
</li>
|
|
108
|
+
{/each}
|
|
109
|
+
</ul>
|
|
110
|
+
</DropdownMenu.Content>
|
|
111
|
+
</DropdownMenu.Portal>
|
|
112
|
+
</DropdownMenu.Root>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { SlashCommand, ToolbarRunContext } from './items.ts';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'id' | 'dir' | 'aria-label' | 'aria-labelledby'> {
|
|
5
|
+
/** The menu element's `id`. */
|
|
6
|
+
id?: string | undefined;
|
|
7
|
+
/** The entries. */
|
|
8
|
+
commands: ReadonlyArray<SlashCommand>;
|
|
9
|
+
/** Whether the menu is open. Bindable. */
|
|
10
|
+
open?: boolean | undefined;
|
|
11
|
+
/** Visible text and accessible name of the trigger. */
|
|
12
|
+
label?: string | undefined;
|
|
13
|
+
/** Accessible name of the open menu. */
|
|
14
|
+
menuLabel?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The `id` (or space-separated `id`s) of the visible element that names
|
|
17
|
+
* the open menu, rendered as `aria-labelledby`. Wins over `menuLabel`,
|
|
18
|
+
* which stays the fallback name if no element has the id.
|
|
19
|
+
*/
|
|
20
|
+
labelledBy?: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The dialogs a command may ask for. Omitted — a menu mounted outside a
|
|
23
|
+
* toolbar — `dialogs.link()` / `dialogs.image()` do nothing, and a
|
|
24
|
+
* command that calls one is inert rather than broken.
|
|
25
|
+
*/
|
|
26
|
+
dialogs?: ToolbarRunContext['dialogs'] | undefined;
|
|
27
|
+
/** Extra classes on the `<ul class="menu">`. */
|
|
28
|
+
class?: string | undefined;
|
|
29
|
+
/** The opening control — the caller's own element, spreading `props`. */
|
|
30
|
+
trigger?: Snippet<[{
|
|
31
|
+
props: Record<string, unknown>;
|
|
32
|
+
}]> | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The insert menu: a bits-ui `DropdownMenu` of `SlashCommand` entries. `BlockToolbar` renders it; a custom toolbar can too. */
|
|
35
|
+
declare const SlashMenu: import("svelte").Component<Props, {}, "open">;
|
|
36
|
+
type SlashMenu = ReturnType<typeof SlashMenu>;
|
|
37
|
+
export default SlashMenu;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toolbar's data model, and the default sets built from it.
|
|
3
|
+
*
|
|
4
|
+
* `BlockToolbar` renders a list rather than a hard-coded row of buttons.
|
|
5
|
+
* The applications this package was extracted from hard-coded theirs, and
|
|
6
|
+
* the cost showed: one of them carried five buttons for node types its
|
|
7
|
+
* grammar no longer had, and neither could add an item without editing the
|
|
8
|
+
* component. Here an item is a value — an application appends its own,
|
|
9
|
+
* reorders, drops one, or passes a list of its own making.
|
|
10
|
+
*
|
|
11
|
+
* An item's `run` receives a {@link ToolbarRunContext} rather than the
|
|
12
|
+
* editor alone, because two of the default items open a dialog the toolbar
|
|
13
|
+
* owns. `dialogs.link()` / `dialogs.image()` are how an item asks for one
|
|
14
|
+
* without importing it.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { CommandAction, Editor, MarkAction, NodeAction } from 'prosekit/core';
|
|
19
|
+
import { type EditorMessages } from '../messages.svelte.ts';
|
|
20
|
+
/**
|
|
21
|
+
* The editor as a toolbar item sees it: any grammar, with `commands`,
|
|
22
|
+
* `nodes` and `marks` read by name.
|
|
23
|
+
*
|
|
24
|
+
* `Editor` (whose extension type defaults to `any`) cannot be used directly. ProseKit derives those three from
|
|
25
|
+
* the extension's own typing, and the derivation collapses to `never` for
|
|
26
|
+
* `any` — so `editor.commands.toggleBold()` in an application's own item
|
|
27
|
+
* would not compile at all. Widening the three back to records is what makes
|
|
28
|
+
* a set of items generic over the grammar writable; a real `Editor<E>` is
|
|
29
|
+
* assignable to it, and an item that names a command the grammar lacks gets
|
|
30
|
+
* `undefined` rather than a type error, which is the honest outcome.
|
|
31
|
+
*/
|
|
32
|
+
export type ToolbarEditor = Omit<Editor, 'commands' | 'marks' | 'nodes'> & {
|
|
33
|
+
readonly commands: Record<string, CommandAction>;
|
|
34
|
+
readonly marks: Record<string, MarkAction>;
|
|
35
|
+
readonly nodes: Record<string, NodeAction>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* What a toolbar item's `run` is handed.
|
|
39
|
+
*
|
|
40
|
+
* `editor` is the live editor from the enclosing `<ProseKit>`; `dialogs`
|
|
41
|
+
* opens one of the toolbar's own dialogs. Calling a dialog opener also tells
|
|
42
|
+
* the toolbar not to pull focus back into the document, which would take it
|
|
43
|
+
* straight off the dialog that is about to open.
|
|
44
|
+
*/
|
|
45
|
+
export interface ToolbarRunContext {
|
|
46
|
+
/** The editor the toolbar is attached to. */
|
|
47
|
+
readonly editor: ToolbarEditor;
|
|
48
|
+
/** The dialogs the toolbar renders, for items that need one. */
|
|
49
|
+
readonly dialogs: {
|
|
50
|
+
/** Opens the link dialog, seeded from the selection's link mark. */
|
|
51
|
+
readonly link: () => void;
|
|
52
|
+
/** Opens the image dialog. */
|
|
53
|
+
readonly image: () => void;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One button of {@link BlockToolbar}.
|
|
58
|
+
*
|
|
59
|
+
* `kind: 'toggle'` is what makes the button carry `aria-pressed`; an action
|
|
60
|
+
* (insert a rule, open a dialog, undo) has no pressed state and must not
|
|
61
|
+
* claim one. `isActive` is read on every editor update, so it must be cheap
|
|
62
|
+
* and must not throw when the schema lacks the node or mark it asks about.
|
|
63
|
+
*/
|
|
64
|
+
export interface ToolbarItem {
|
|
65
|
+
/** Stable key for the `{#each}` and for an application replacing an item. */
|
|
66
|
+
readonly id: string;
|
|
67
|
+
/** Visible text and accessible name. */
|
|
68
|
+
readonly label: string;
|
|
69
|
+
/** `'toggle'` renders `aria-pressed`; `'action'` (the default) does not. */
|
|
70
|
+
readonly kind?: ('toggle' | 'action') | undefined;
|
|
71
|
+
/** Whether the item's state is on. Only read for `kind: 'toggle'`. */
|
|
72
|
+
readonly isActive?: ((editor: ToolbarEditor) => boolean) | undefined;
|
|
73
|
+
/** Whether the item can be used. `false` renders a `disabled` button. */
|
|
74
|
+
readonly isEnabled?: ((editor: ToolbarEditor) => boolean) | undefined;
|
|
75
|
+
/** What the button does. */
|
|
76
|
+
readonly run: (context: ToolbarRunContext) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A divider between groups of items, allowed anywhere in `items`.
|
|
80
|
+
*
|
|
81
|
+
* It renders as `role="separator"` and is skipped by the toolbar's arrow-key
|
|
82
|
+
* navigation — a separator is a landmark for the eye, not a stop for the
|
|
83
|
+
* keyboard.
|
|
84
|
+
*/
|
|
85
|
+
export interface ToolbarSeparator {
|
|
86
|
+
/** Stable key for the `{#each}`. */
|
|
87
|
+
readonly id: string;
|
|
88
|
+
/** The discriminant. */
|
|
89
|
+
readonly separator: true;
|
|
90
|
+
}
|
|
91
|
+
/** One entry of the insert menu. */
|
|
92
|
+
export interface SlashCommand {
|
|
93
|
+
/** Stable key for the `{#each}`. */
|
|
94
|
+
readonly id: string;
|
|
95
|
+
/** Visible text and accessible name. */
|
|
96
|
+
readonly label: string;
|
|
97
|
+
/** What choosing the entry does. */
|
|
98
|
+
readonly run: (context: ToolbarRunContext) => void;
|
|
99
|
+
}
|
|
100
|
+
/** Narrows an `items` entry to the separator arm. */
|
|
101
|
+
export declare function isToolbarSeparator(entry: ToolbarItem | ToolbarSeparator): entry is ToolbarSeparator;
|
|
102
|
+
/**
|
|
103
|
+
* The default toolbar: the nineteen items the applications shipped, in their
|
|
104
|
+
* order, with two separators marking the mark / block / history groups.
|
|
105
|
+
*
|
|
106
|
+
* The twentieth control those toolbars carried was a button that toggled an
|
|
107
|
+
* ad-hoc "slash" panel; it is the insert menu now, and `BlockToolbar`
|
|
108
|
+
* renders it itself from {@link createDefaultSlashCommands}.
|
|
109
|
+
*
|
|
110
|
+
* Every `run` goes through the editor's own `commands`, `marks` and `nodes`,
|
|
111
|
+
* so the set works against any extension that registers those node and mark
|
|
112
|
+
* types and quietly does nothing against one that does not.
|
|
113
|
+
*
|
|
114
|
+
* @param messages - Labels to use instead of the English defaults. A partial
|
|
115
|
+
* record; unlisted keys keep their default.
|
|
116
|
+
*/
|
|
117
|
+
export declare function createDefaultToolbarItems(messages?: Partial<EditorMessages>): ReadonlyArray<ToolbarItem | ToolbarSeparator>;
|
|
118
|
+
/**
|
|
119
|
+
* The default insert menu: the ten entries the applications shipped, in
|
|
120
|
+
* their order.
|
|
121
|
+
*
|
|
122
|
+
* @param messages - Labels to use instead of the English defaults. A partial
|
|
123
|
+
* record; unlisted keys keep their default.
|
|
124
|
+
*/
|
|
125
|
+
export declare function createDefaultSlashCommands(messages?: Partial<EditorMessages>): ReadonlyArray<SlashCommand>;
|