@thomasfosterau/prosekit-flowchart 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 +180 -2
- package/dist/EditableNode.svelte +111 -0
- package/dist/EditableNode.svelte.d.ts +35 -0
- package/dist/Flowchart.svelte +170 -0
- package/dist/Flowchart.svelte.d.ts +27 -0
- package/dist/FlowchartEditDialog.svelte +188 -0
- package/dist/FlowchartEditDialog.svelte.d.ts +42 -0
- package/dist/FlowchartNodeView.svelte +71 -0
- package/dist/FlowchartNodeView.svelte.d.ts +16 -0
- package/dist/flowchart-node-view.d.ts +25 -0
- package/dist/flowchart-node-view.js +35 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/node/data.d.ts +28 -0
- package/dist/node/data.js +57 -0
- package/dist/node/flowchart-node.d.ts +63 -0
- package/dist/node/flowchart-node.js +100 -0
- package/dist/node/html.d.ts +31 -0
- package/dist/node/html.js +46 -0
- package/dist/node/index.d.ts +16 -0
- package/dist/node/index.js +16 -0
- package/dist/node/markdown.d.ts +31 -0
- package/dist/node/markdown.js +34 -0
- package/dist/node/overrides.d.ts +43 -0
- package/dist/node/overrides.js +42 -0
- package/dist/node/schema.d.ts +62 -0
- package/dist/node/schema.js +50 -0
- package/dist/node/svg.d.ts +41 -0
- package/dist/node/svg.js +124 -0
- package/dist/toolbar.d.ts +35 -0
- package/dist/toolbar.js +74 -0
- package/package.json +68 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!-- @component The modal that hosts the editable `Flowchart` canvas, loaded on open; Save hands the diagram to `onSave`. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
FlowchartEditDialog — the modal that hosts the full WYSIWYG canvas.
|
|
5
|
+
|
|
6
|
+
Opens either from the bindable `open` prop or from the imperative
|
|
7
|
+
`open(data)` instance method the node view uses. On Save it pulls the current
|
|
8
|
+
diagram out of `Flowchart` with `getSnapshot()` and hands it to `onSave`,
|
|
9
|
+
which may be async; Cancel discards.
|
|
10
|
+
|
|
11
|
+
## bits-ui, not a native `<dialog>`
|
|
12
|
+
|
|
13
|
+
The app this is ported from used a native `<dialog class="modal">`. bits-ui's
|
|
14
|
+
`Dialog` is used instead because it owns the whole modal contract — the focus
|
|
15
|
+
trap, the initial focus move, `aria-modal`, the `aria-labelledby` /
|
|
16
|
+
`aria-describedby` wiring to `Dialog.Title` and `Dialog.Description`, the
|
|
17
|
+
scroll lock and the focus return — and because a native `<dialog>`'s built-in
|
|
18
|
+
Escape handling cannot be gated: it closes the dialog out from under an
|
|
19
|
+
in-flight save. Here Escape goes through `onOpenChange`, which refuses to
|
|
20
|
+
close while `pending`, so a save in flight is never abandoned halfway.
|
|
21
|
+
|
|
22
|
+
## The canvas is loaded on open, not on import
|
|
23
|
+
|
|
24
|
+
`Flowchart.svelte` is reached through a dynamic `import()` in the effect
|
|
25
|
+
below, and the type-only import is what keeps that true. It is the package's
|
|
26
|
+
heaviest module — `@xyflow/svelte` plus its stylesheet — and the node view
|
|
27
|
+
that mounts this dialog renders every flowchart in a document. A static
|
|
28
|
+
import here would put the whole canvas engine in the chunk of any page with
|
|
29
|
+
an editor on it, which is precisely what the SVG preview exists to avoid.
|
|
30
|
+
|
|
31
|
+
Affordances inside the canvas: the "Add node" button appends a node; drag
|
|
32
|
+
from a node's bottom handle to another's top to connect; double-click a node
|
|
33
|
+
to edit its label; select a node or edge and press Delete to remove it.
|
|
34
|
+
-->
|
|
35
|
+
<script lang="ts">import { Dialog } from "bits-ui";
|
|
36
|
+
import { EMPTY_FLOWCHART } from "./node/data.js";
|
|
37
|
+
// The `open` PROP is bound to `isOpen` because this component also exports an
|
|
38
|
+
// `open(data)` METHOD (the node view's entry point) and the two cannot share
|
|
39
|
+
// a name in one scope.
|
|
40
|
+
let { open: isOpen = $bindable(false), initialData = EMPTY_FLOWCHART, onSave, titleLabel = "Edit flowchart", addNodeLabel = "Add node", helpText = "Drag from a node's bottom to another's top to connect. Double-click a node to edit its label. Select and press Delete to remove.", saveLabel = "Save", savingLabel = "Saving…", cancelLabel = "Cancel", closeLabel = "Close", failureMessage = "Failed to save diagram.", newNodeLabel = "New", nodeLabelLabel = "Node label", untitledLabel = "Untitled", class: className, id, ...rest } = $props();
|
|
41
|
+
// A `bind:this` target, so the declared type must literally be `T |
|
|
42
|
+
// undefined`: Svelte assigns the component instance itself into it, and
|
|
43
|
+
// `undefined` before mount and after unmount.
|
|
44
|
+
let flowchart = $state();
|
|
45
|
+
/** The canvas component itself, once the dialog has been opened at least once. */
|
|
46
|
+
let FlowchartCanvas = $state();
|
|
47
|
+
let pending = $state(false);
|
|
48
|
+
let errorMessage = $state("");
|
|
49
|
+
/**
|
|
50
|
+
* The payload queued by `open(data)` for the canvas that does not exist yet.
|
|
51
|
+
* Deliberately NOT `$state`: it is read and cleared inside the effect below,
|
|
52
|
+
* and making it reactive would make that effect invalidate itself.
|
|
53
|
+
*/
|
|
54
|
+
let queued;
|
|
55
|
+
/**
|
|
56
|
+
* Open the dialog on the given payload (default: `initialData`).
|
|
57
|
+
*
|
|
58
|
+
* The imperative entry point, kept for the ProseKit node view, which has an
|
|
59
|
+
* instance to reach through `bind:this` but no place to thread a bound prop.
|
|
60
|
+
*/
|
|
61
|
+
export function open(data) {
|
|
62
|
+
errorMessage = "";
|
|
63
|
+
queued = data ?? initialData;
|
|
64
|
+
isOpen = true;
|
|
65
|
+
}
|
|
66
|
+
// Pull the canvas in the first time the dialog opens. Everything it drags
|
|
67
|
+
// behind it — `@xyflow/svelte` and its stylesheet — stays out of the chunk
|
|
68
|
+
// that merely renders the node view's SVG preview.
|
|
69
|
+
async function loadCanvas() {
|
|
70
|
+
FlowchartCanvas = (await import("./Flowchart.svelte")).default;
|
|
71
|
+
}
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (!isOpen || FlowchartCanvas !== undefined) return;
|
|
74
|
+
void loadCanvas();
|
|
75
|
+
});
|
|
76
|
+
// Seed the canvas only once the dialog's content has mounted: `fitView` needs
|
|
77
|
+
// a real layout box, and bits-ui does not render `Dialog.Content` until the
|
|
78
|
+
// dialog is open. The effect re-runs when `bind:this` fills `flowchart` in;
|
|
79
|
+
// `queueMicrotask` then defers past the mount effect that measures it.
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if (!isOpen) return;
|
|
82
|
+
const canvas = flowchart;
|
|
83
|
+
if (canvas === undefined) return;
|
|
84
|
+
const payload = queued ?? initialData;
|
|
85
|
+
queued = undefined;
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
canvas.resetTo(payload);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* bits-ui's request to change the open state — Escape, an outside click, or
|
|
92
|
+
* the close button. Refused while a save is in flight, so the dialog cannot
|
|
93
|
+
* disappear while `onSave` is still running.
|
|
94
|
+
*/
|
|
95
|
+
function handleOpenChange(next) {
|
|
96
|
+
if (!next && pending) return;
|
|
97
|
+
if (!next) errorMessage = "";
|
|
98
|
+
isOpen = next;
|
|
99
|
+
}
|
|
100
|
+
function close() {
|
|
101
|
+
handleOpenChange(false);
|
|
102
|
+
}
|
|
103
|
+
async function save() {
|
|
104
|
+
const snapshot = flowchart?.getSnapshot();
|
|
105
|
+
if (snapshot === undefined) return;
|
|
106
|
+
pending = true;
|
|
107
|
+
errorMessage = "";
|
|
108
|
+
try {
|
|
109
|
+
await onSave(snapshot);
|
|
110
|
+
isOpen = false;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
errorMessage = error instanceof Error ? error.message : failureMessage;
|
|
113
|
+
} finally {
|
|
114
|
+
pending = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function addNode() {
|
|
118
|
+
flowchart?.addNode(newNodeLabel);
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<Dialog.Root bind:open={() => isOpen, handleOpenChange}>
|
|
123
|
+
<Dialog.Portal>
|
|
124
|
+
<!-- daisyUI's `modal-backdrop` is a child of an open `.modal`; this is the
|
|
125
|
+
same scrim as an element bits-ui can dismiss from. -->
|
|
126
|
+
<Dialog.Overlay class="bg-base-300/60 fixed inset-0 z-40" />
|
|
127
|
+
<Dialog.Content
|
|
128
|
+
{...rest}
|
|
129
|
+
{...(id === undefined ? {} : { id })}
|
|
130
|
+
class={[
|
|
131
|
+
'modal-box fixed top-1/2 left-1/2 z-50 max-h-[calc(100svh-2rem)] w-[calc(100vw-2rem)] max-w-6xl -translate-x-1/2 -translate-y-1/2',
|
|
132
|
+
className,
|
|
133
|
+
]}
|
|
134
|
+
>
|
|
135
|
+
<div class="flex items-center justify-between gap-3">
|
|
136
|
+
<Dialog.Title class="text-lg font-bold">{titleLabel}</Dialog.Title>
|
|
137
|
+
<div class="flex items-center gap-2">
|
|
138
|
+
<button type="button" class="btn btn-sm btn-primary" disabled={pending} onclick={addNode}>
|
|
139
|
+
{addNodeLabel}
|
|
140
|
+
</button>
|
|
141
|
+
<Dialog.Close>
|
|
142
|
+
{#snippet child({ props })}
|
|
143
|
+
<button
|
|
144
|
+
{...props}
|
|
145
|
+
type="button"
|
|
146
|
+
class="btn btn-sm btn-ghost btn-square"
|
|
147
|
+
disabled={pending}
|
|
148
|
+
aria-label={closeLabel}
|
|
149
|
+
>
|
|
150
|
+
<span aria-hidden="true">✕</span>
|
|
151
|
+
</button>
|
|
152
|
+
{/snippet}
|
|
153
|
+
</Dialog.Close>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<Dialog.Description class="text-base-content/70 mt-1 text-sm">
|
|
157
|
+
{helpText}
|
|
158
|
+
</Dialog.Description>
|
|
159
|
+
<div class="border-base-300 mt-4 rounded border" style="height: 32rem;">
|
|
160
|
+
{#if FlowchartCanvas}
|
|
161
|
+
<!-- `nodeLabelLabel` and `untitledLabel` are forwarded rather than
|
|
162
|
+
left to the canvas's own defaults: the nodes an author edits in
|
|
163
|
+
here are the dialog's surface, and an application that
|
|
164
|
+
translated the dialog would otherwise find English inside it. -->
|
|
165
|
+
<FlowchartCanvas
|
|
166
|
+
bind:this={flowchart}
|
|
167
|
+
data={initialData}
|
|
168
|
+
interactive
|
|
169
|
+
class="h-full"
|
|
170
|
+
{nodeLabelLabel}
|
|
171
|
+
{untitledLabel}
|
|
172
|
+
/>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
{#if errorMessage !== ''}
|
|
176
|
+
<p class="text-error pt-3 text-sm" role="alert">{errorMessage}</p>
|
|
177
|
+
{/if}
|
|
178
|
+
<div class="modal-action">
|
|
179
|
+
<button type="button" class="btn btn-ghost" disabled={pending} onclick={close}>
|
|
180
|
+
{cancelLabel}
|
|
181
|
+
</button>
|
|
182
|
+
<button type="button" class="btn btn-primary" disabled={pending} onclick={save}>
|
|
183
|
+
{pending ? savingLabel : saveLabel}
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</Dialog.Content>
|
|
187
|
+
</Dialog.Portal>
|
|
188
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { FlowchartData } from './node/schema.ts';
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'id'> {
|
|
4
|
+
/** The dialog element's `id`. */
|
|
5
|
+
id?: string | undefined;
|
|
6
|
+
/** Whether the dialog is shown. Bindable. */
|
|
7
|
+
open?: boolean | undefined;
|
|
8
|
+
/** The payload the canvas opens with, when `open(data)` is not given one. */
|
|
9
|
+
initialData?: FlowchartData | undefined;
|
|
10
|
+
/** Called with the snapshot on Save; may be async, and may reject. */
|
|
11
|
+
onSave: (data: FlowchartData) => void | Promise<void>;
|
|
12
|
+
/** Heading — the dialog's accessible name. */
|
|
13
|
+
titleLabel?: string | undefined;
|
|
14
|
+
/** The button that appends a node to the canvas. */
|
|
15
|
+
addNodeLabel?: string | undefined;
|
|
16
|
+
/** The supporting sentence under the heading — the accessible description. */
|
|
17
|
+
helpText?: string | undefined;
|
|
18
|
+
/** The confirm button, at rest. */
|
|
19
|
+
saveLabel?: string | undefined;
|
|
20
|
+
/** The confirm button, while `onSave` is in flight. */
|
|
21
|
+
savingLabel?: string | undefined;
|
|
22
|
+
/** The discard button. */
|
|
23
|
+
cancelLabel?: string | undefined;
|
|
24
|
+
/** Accessible name of the corner close button. */
|
|
25
|
+
closeLabel?: string | undefined;
|
|
26
|
+
/** Shown when `onSave` rejects with something that is not an `Error`. */
|
|
27
|
+
failureMessage?: string | undefined;
|
|
28
|
+
/** Label given to a node added by the "Add node" button. */
|
|
29
|
+
newNodeLabel?: string | undefined;
|
|
30
|
+
/** Accessible name of a node's label field, forwarded to the canvas. */
|
|
31
|
+
nodeLabelLabel?: string | undefined;
|
|
32
|
+
/** Shown, and committed, when a node's label is blank; forwarded to the canvas. */
|
|
33
|
+
untitledLabel?: string | undefined;
|
|
34
|
+
/** Extra classes on the `modal-box`; merged last, so they win. */
|
|
35
|
+
class?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The modal that hosts the editable `Flowchart` canvas, loaded on open; Save hands the diagram to `onSave`. */
|
|
38
|
+
declare const FlowchartEditDialog: import("svelte").Component<Props, {
|
|
39
|
+
open: (data?: FlowchartData) => void;
|
|
40
|
+
}, "open">;
|
|
41
|
+
type FlowchartEditDialog = ReturnType<typeof FlowchartEditDialog>;
|
|
42
|
+
export default FlowchartEditDialog;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!-- @component The ProseKit node view for a `flowchart` node: previews it with the server-rendered SVG and edits it in `FlowchartEditDialog`. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
FlowchartNodeView — the Svelte node view ProseKit mounts for a `flowchart`
|
|
5
|
+
node.
|
|
6
|
+
|
|
7
|
+
The preview is the SAME server-renderable SVG a reader page gets
|
|
8
|
+
(`generateFlowchartSvg`, from this package's `./node` entry), so the editor
|
|
9
|
+
and the published article cannot drift, and so the `@xyflow/svelte` bundle
|
|
10
|
+
only loads when the author actually opens the edit dialog.
|
|
11
|
+
|
|
12
|
+
## The one place stores are allowed
|
|
13
|
+
|
|
14
|
+
This workspace's Svelte code is runes-only. ProseKit's node-view contract is
|
|
15
|
+
the documented exception: the adapter hands the view a `Writable<Node>` and a
|
|
16
|
+
`Writable<boolean>` plus a `setAttrs` callback, and they are consumed here
|
|
17
|
+
through Svelte's `$store` auto-subscription. Nothing else in this package
|
|
18
|
+
touches a store.
|
|
19
|
+
-->
|
|
20
|
+
<script lang="ts">import FlowchartEditDialog from "./FlowchartEditDialog.svelte";
|
|
21
|
+
import { EMPTY_FLOWCHART } from "./node/data.js";
|
|
22
|
+
import { generateFlowchartSvg } from "./node/svg.js";
|
|
23
|
+
// `node` is read as `$node` (store auto-subscription), which fallow's
|
|
24
|
+
// component-prop check does not follow.
|
|
25
|
+
// fallow-ignore-next-line unused-component-prop
|
|
26
|
+
let { node, selected, setAttrs, editLabel = "Edit" } = $props();
|
|
27
|
+
// A `bind:this` target, so the declared type must literally be `T |
|
|
28
|
+
// undefined`: Svelte assigns the component instance itself into it, and
|
|
29
|
+
// `undefined` before mount and after unmount.
|
|
30
|
+
let dialog = $state();
|
|
31
|
+
const data = $derived($node.attrs.data ?? EMPTY_FLOWCHART);
|
|
32
|
+
const svg = $derived(generateFlowchartSvg(data));
|
|
33
|
+
function openEditor() {
|
|
34
|
+
dialog?.open(data);
|
|
35
|
+
}
|
|
36
|
+
function handleSave(next) {
|
|
37
|
+
setAttrs({ data: next });
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
class="flowchart-node-view group relative my-3 rounded border p-2"
|
|
43
|
+
class:border-primary={$selected}
|
|
44
|
+
class:border-base-300={!$selected}
|
|
45
|
+
contenteditable="false"
|
|
46
|
+
data-prosemirror-node-name="flowchart"
|
|
47
|
+
>
|
|
48
|
+
<div class="flowchart-preview overflow-hidden">
|
|
49
|
+
<!-- The SVG comes from this package's own escape-aware serialiser
|
|
50
|
+
(`./node/svg.ts`), which XML-escapes every label; it is never
|
|
51
|
+
user-supplied HTML. -->
|
|
52
|
+
{@html svg}
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="btn btn-xs btn-primary absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
|
|
57
|
+
class:opacity-100={$selected}
|
|
58
|
+
onclick={openEditor}
|
|
59
|
+
>
|
|
60
|
+
{editLabel}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<FlowchartEditDialog bind:this={dialog} initialData={data} onSave={handleSave} />
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.flowchart-preview :global(svg) {
|
|
68
|
+
max-height: 20rem;
|
|
69
|
+
display: block;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Attrs, Node as ProseMirrorNode } from '@prosekit/pm/model';
|
|
2
|
+
import type { Writable } from 'svelte/store';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The ProseMirror node this view renders, as ProseKit's store. */
|
|
5
|
+
node: Writable<ProseMirrorNode>;
|
|
6
|
+
/** Whether ProseMirror currently has this node selected. */
|
|
7
|
+
selected: Writable<boolean>;
|
|
8
|
+
/** Writes attributes back onto the node — the save channel. */
|
|
9
|
+
setAttrs: (attrs: Attrs) => void;
|
|
10
|
+
/** The button that opens the edit dialog. */
|
|
11
|
+
editLabel?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** The ProseKit node view for a `flowchart` node: previews it with the server-rendered SVG and edits it in `FlowchartEditDialog`. */
|
|
14
|
+
declare const FlowchartNodeView: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type FlowchartNodeView = ReturnType<typeof FlowchartNodeView>;
|
|
16
|
+
export default FlowchartNodeView;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ProseKit node-view extension for the `flowchart` block.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `./node/flowchart-node.ts` — which registers the node spec
|
|
5
|
+
* and the `updateFlowchartData` command — because the view pulls in Svelte, the
|
|
6
|
+
* edit dialog and (through it) `@xyflow/svelte`, none of which may reach the
|
|
7
|
+
* `./node` entry. A consuming app composes the two at editor-instantiation
|
|
8
|
+
* time: `defineFlowchart()` for the schema, `defineFlowchartNodeView()` for the
|
|
9
|
+
* UI.
|
|
10
|
+
*
|
|
11
|
+
* `defineSvelteNodeView` is a no-op on the server, so this is safe to include
|
|
12
|
+
* in an extension that is also built server-side.
|
|
13
|
+
*
|
|
14
|
+
* @module @thomasfosterau/prosekit-flowchart/flowchart-node-view
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Registers {@link FlowchartNodeView} as the `flowchart` node's view, hosted in
|
|
18
|
+
* a `div`.
|
|
19
|
+
*
|
|
20
|
+
* Both mutation guards return `true` on purpose: the rendered diagram rewrites
|
|
21
|
+
* its own DOM (the SVG preview, and xyflow's canvas while the dialog is open),
|
|
22
|
+
* and without them ProseMirror would read those rewrites as document edits and
|
|
23
|
+
* fight the view for control of the node.
|
|
24
|
+
*/
|
|
25
|
+
export declare function defineFlowchartNodeView(): import("@prosekit/core").Extension<import("@prosekit/core").ExtensionTyping<any, any, any>>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ProseKit node-view extension for the `flowchart` block.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `./node/flowchart-node.ts` — which registers the node spec
|
|
5
|
+
* and the `updateFlowchartData` command — because the view pulls in Svelte, the
|
|
6
|
+
* edit dialog and (through it) `@xyflow/svelte`, none of which may reach the
|
|
7
|
+
* `./node` entry. A consuming app composes the two at editor-instantiation
|
|
8
|
+
* time: `defineFlowchart()` for the schema, `defineFlowchartNodeView()` for the
|
|
9
|
+
* UI.
|
|
10
|
+
*
|
|
11
|
+
* `defineSvelteNodeView` is a no-op on the server, so this is safe to include
|
|
12
|
+
* in an extension that is also built server-side.
|
|
13
|
+
*
|
|
14
|
+
* @module @thomasfosterau/prosekit-flowchart/flowchart-node-view
|
|
15
|
+
*/
|
|
16
|
+
import { defineSvelteNodeView } from 'prosekit/svelte';
|
|
17
|
+
import FlowchartNodeView from './FlowchartNodeView.svelte';
|
|
18
|
+
/**
|
|
19
|
+
* Registers {@link FlowchartNodeView} as the `flowchart` node's view, hosted in
|
|
20
|
+
* a `div`.
|
|
21
|
+
*
|
|
22
|
+
* Both mutation guards return `true` on purpose: the rendered diagram rewrites
|
|
23
|
+
* its own DOM (the SVG preview, and xyflow's canvas while the dialog is open),
|
|
24
|
+
* and without them ProseMirror would read those rewrites as document edits and
|
|
25
|
+
* fight the view for control of the node.
|
|
26
|
+
*/
|
|
27
|
+
export function defineFlowchartNodeView() {
|
|
28
|
+
return defineSvelteNodeView({
|
|
29
|
+
name: 'flowchart',
|
|
30
|
+
component: FlowchartNodeView,
|
|
31
|
+
as: 'div',
|
|
32
|
+
ignoreMutation: () => true,
|
|
33
|
+
stopEvent: () => true,
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/prosekit-flowchart` — the flowchart canvas, edit dialog
|
|
3
|
+
* and ProseKit node view on `@xyflow/svelte`.
|
|
4
|
+
*
|
|
5
|
+
* `Flowchart.svelte` carries this package's only side-effecting import,
|
|
6
|
+
* `@xyflow/svelte/dist/style.css`. The ProseKit node spec, its Effect Schema
|
|
7
|
+
* payload and the server SVG renderer live under `./node`, which imports
|
|
8
|
+
* neither Svelte nor xyflow and is a separate entry point — nothing from it is
|
|
9
|
+
* re-exported here, so a server bundle never reaches this module.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
export { default as EditableNode } from './EditableNode.svelte';
|
|
14
|
+
export { default as Flowchart } from './Flowchart.svelte';
|
|
15
|
+
export { defineFlowchartNodeView } from './flowchart-node-view.ts';
|
|
16
|
+
export { default as FlowchartEditDialog } from './FlowchartEditDialog.svelte';
|
|
17
|
+
export { default as FlowchartNodeView } from './FlowchartNodeView.svelte';
|
|
18
|
+
export { flowchartSlashCommand, flowchartToolbarItem } from './toolbar.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/prosekit-flowchart` — the flowchart canvas, edit dialog
|
|
3
|
+
* and ProseKit node view on `@xyflow/svelte`.
|
|
4
|
+
*
|
|
5
|
+
* `Flowchart.svelte` carries this package's only side-effecting import,
|
|
6
|
+
* `@xyflow/svelte/dist/style.css`. The ProseKit node spec, its Effect Schema
|
|
7
|
+
* payload and the server SVG renderer live under `./node`, which imports
|
|
8
|
+
* neither Svelte nor xyflow and is a separate entry point — nothing from it is
|
|
9
|
+
* re-exported here, so a server bundle never reaches this module.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
export { default as EditableNode } from './EditableNode.svelte';
|
|
14
|
+
export { default as Flowchart } from './Flowchart.svelte';
|
|
15
|
+
export { defineFlowchartNodeView } from "./flowchart-node-view.js";
|
|
16
|
+
export { default as FlowchartEditDialog } from './FlowchartEditDialog.svelte';
|
|
17
|
+
export { default as FlowchartNodeView } from './FlowchartNodeView.svelte';
|
|
18
|
+
export { flowchartSlashCommand, flowchartToolbarItem } from "./toolbar.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flowchart payload's parser and empty-state default.
|
|
3
|
+
*
|
|
4
|
+
* `FlowchartPosition` is the one convenience type `./schema.ts` deliberately
|
|
5
|
+
* doesn't export on its own (see that module's doc comment); `FlowchartNode`,
|
|
6
|
+
* `FlowchartEdge` and `FlowchartData` are re-exported from `./schema.ts`
|
|
7
|
+
* directly by the package barrel, since each already merges the Effect Schema
|
|
8
|
+
* value and its decoded type under one name.
|
|
9
|
+
*
|
|
10
|
+
* Ported verbatim (semantics unchanged) from Website's
|
|
11
|
+
* `src/editor/flowchart/types.ts`.
|
|
12
|
+
*
|
|
13
|
+
* @module @thomasfosterau/prosekit-flowchart/node/data
|
|
14
|
+
*/
|
|
15
|
+
import { FlowchartData as FlowchartDataSchema, FlowchartNode as FlowchartNodeSchema } from './schema.ts';
|
|
16
|
+
/** Position of a node on the flowchart canvas. */
|
|
17
|
+
export type FlowchartPosition = FlowchartNodeSchema['position'];
|
|
18
|
+
/** Empty flowchart payload — used as the default attr value and on parse failure. */
|
|
19
|
+
export declare const EMPTY_FLOWCHART: FlowchartDataSchema;
|
|
20
|
+
/**
|
|
21
|
+
* Parses a JSON string into a `FlowchartData` payload, returning
|
|
22
|
+
* {@link EMPTY_FLOWCHART} on any failure. Validates the shape just enough to
|
|
23
|
+
* make downstream rendering safe.
|
|
24
|
+
*
|
|
25
|
+
* Nodes and edges are validated individually against `FlowchartNode` /
|
|
26
|
+
* `FlowchartEdge` — a single malformed entry is dropped, not the whole array.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseFlowchartData(input: string): FlowchartDataSchema;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flowchart payload's parser and empty-state default.
|
|
3
|
+
*
|
|
4
|
+
* `FlowchartPosition` is the one convenience type `./schema.ts` deliberately
|
|
5
|
+
* doesn't export on its own (see that module's doc comment); `FlowchartNode`,
|
|
6
|
+
* `FlowchartEdge` and `FlowchartData` are re-exported from `./schema.ts`
|
|
7
|
+
* directly by the package barrel, since each already merges the Effect Schema
|
|
8
|
+
* value and its decoded type under one name.
|
|
9
|
+
*
|
|
10
|
+
* Ported verbatim (semantics unchanged) from Website's
|
|
11
|
+
* `src/editor/flowchart/types.ts`.
|
|
12
|
+
*
|
|
13
|
+
* @module @thomasfosterau/prosekit-flowchart/node/data
|
|
14
|
+
*/
|
|
15
|
+
import * as Arr from 'effect/Array';
|
|
16
|
+
import * as Option from 'effect/Option';
|
|
17
|
+
import * as Predicate from 'effect/Predicate';
|
|
18
|
+
import * as Schema from 'effect/Schema';
|
|
19
|
+
import { FlowchartData as FlowchartDataSchema, FlowchartEdge as FlowchartEdgeSchema, FlowchartNode as FlowchartNodeSchema, } from "./schema.js";
|
|
20
|
+
/**
|
|
21
|
+
* Parses a JSON string to `unknown`, or `Option.none()` when it is not JSON.
|
|
22
|
+
*
|
|
23
|
+
* Built once at module scope, alongside the per-element decoders below:
|
|
24
|
+
* `parseFlowchartData` runs per flowchart block on every server render, so
|
|
25
|
+
* none of these decoders must be rebuilt per call.
|
|
26
|
+
*/
|
|
27
|
+
const decodeJson = /* @__PURE__ */ Schema.decodeUnknownOption(
|
|
28
|
+
/* @__PURE__ */ Schema.fromJsonString(Schema.Unknown));
|
|
29
|
+
/** Decodes a single node, dropping it (via `Option.none()`) if malformed. */
|
|
30
|
+
const decodeFlowchartNode = /* @__PURE__ */ Schema.decodeUnknownOption(FlowchartNodeSchema);
|
|
31
|
+
/** Decodes a single edge, dropping it (via `Option.none()`) if malformed. */
|
|
32
|
+
const decodeFlowchartEdge = /* @__PURE__ */ Schema.decodeUnknownOption(FlowchartEdgeSchema);
|
|
33
|
+
/** Empty flowchart payload — used as the default attr value and on parse failure. */
|
|
34
|
+
export const EMPTY_FLOWCHART = { nodes: [], edges: [] };
|
|
35
|
+
/**
|
|
36
|
+
* Parses a JSON string into a `FlowchartData` payload, returning
|
|
37
|
+
* {@link EMPTY_FLOWCHART} on any failure. Validates the shape just enough to
|
|
38
|
+
* make downstream rendering safe.
|
|
39
|
+
*
|
|
40
|
+
* Nodes and edges are validated individually against `FlowchartNode` /
|
|
41
|
+
* `FlowchartEdge` — a single malformed entry is dropped, not the whole array.
|
|
42
|
+
*/
|
|
43
|
+
export function parseFlowchartData(input) {
|
|
44
|
+
const decoded = decodeJson(input);
|
|
45
|
+
if (Option.isNone(decoded))
|
|
46
|
+
return { ...EMPTY_FLOWCHART };
|
|
47
|
+
const parsed = decoded.value;
|
|
48
|
+
if (!Predicate.isObjectOrArray(parsed))
|
|
49
|
+
return { ...EMPTY_FLOWCHART };
|
|
50
|
+
const nodes = Predicate.hasProperty(parsed, 'nodes') && Array.isArray(parsed.nodes)
|
|
51
|
+
? Arr.getSomes(parsed.nodes.map((node) => decodeFlowchartNode(node)))
|
|
52
|
+
: [];
|
|
53
|
+
const edges = Predicate.hasProperty(parsed, 'edges') && Array.isArray(parsed.edges)
|
|
54
|
+
? Arr.getSomes(parsed.edges.map((edge) => decodeFlowchartEdge(edge)))
|
|
55
|
+
: [];
|
|
56
|
+
return { nodes, edges };
|
|
57
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowchart block node for the ProseKit editor.
|
|
3
|
+
*
|
|
4
|
+
* Stores the diagram payload as a single `data` attribute containing a
|
|
5
|
+
* {@link FlowchartData}. The DOM representation is
|
|
6
|
+
* `<div data-flowchart>{JSON}</div>` so SSR and paste both round-trip the
|
|
7
|
+
* data through textContent.
|
|
8
|
+
*
|
|
9
|
+
* Ported verbatim from Website's `src/editor/flowchart/flowchart-node.ts`.
|
|
10
|
+
* The Svelte-backed node view lives at the flowchart package's root; this
|
|
11
|
+
* module only registers the schema-side node spec and the editing command.
|
|
12
|
+
*
|
|
13
|
+
* @module @thomasfosterau/prosekit-flowchart/node/flowchart-node
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Schema-side extension for the flowchart block: registers the node spec and
|
|
17
|
+
* the `updateFlowchartData` editing command. The Svelte-backed node view
|
|
18
|
+
* lives at this package's root and is composed in by the consuming app's
|
|
19
|
+
* editor component at instantiation time.
|
|
20
|
+
*/
|
|
21
|
+
export declare function defineFlowchart(): import("@prosekit/core").Union<readonly [import("@prosekit/core").Extension<{
|
|
22
|
+
Nodes: {
|
|
23
|
+
flowchart: {
|
|
24
|
+
data: {
|
|
25
|
+
readonly nodes: readonly {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly position: {
|
|
28
|
+
readonly x: number;
|
|
29
|
+
readonly y: number;
|
|
30
|
+
};
|
|
31
|
+
readonly data: {
|
|
32
|
+
readonly label: string;
|
|
33
|
+
};
|
|
34
|
+
}[];
|
|
35
|
+
readonly edges: readonly {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly source: string;
|
|
38
|
+
readonly target: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}>, import("@prosekit/core").Extension<{
|
|
44
|
+
Commands: {
|
|
45
|
+
updateFlowchartData: [pos: number, data: {
|
|
46
|
+
readonly nodes: readonly {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly position: {
|
|
49
|
+
readonly x: number;
|
|
50
|
+
readonly y: number;
|
|
51
|
+
};
|
|
52
|
+
readonly data: {
|
|
53
|
+
readonly label: string;
|
|
54
|
+
};
|
|
55
|
+
}[];
|
|
56
|
+
readonly edges: readonly {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly source: string;
|
|
59
|
+
readonly target: string;
|
|
60
|
+
}[];
|
|
61
|
+
}];
|
|
62
|
+
};
|
|
63
|
+
}>]>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowchart block node for the ProseKit editor.
|
|
3
|
+
*
|
|
4
|
+
* Stores the diagram payload as a single `data` attribute containing a
|
|
5
|
+
* {@link FlowchartData}. The DOM representation is
|
|
6
|
+
* `<div data-flowchart>{JSON}</div>` so SSR and paste both round-trip the
|
|
7
|
+
* data through textContent.
|
|
8
|
+
*
|
|
9
|
+
* Ported verbatim from Website's `src/editor/flowchart/flowchart-node.ts`.
|
|
10
|
+
* The Svelte-backed node view lives at the flowchart package's root; this
|
|
11
|
+
* module only registers the schema-side node spec and the editing command.
|
|
12
|
+
*
|
|
13
|
+
* @module @thomasfosterau/prosekit-flowchart/node/flowchart-node
|
|
14
|
+
*/
|
|
15
|
+
import * as Option from 'effect/Option';
|
|
16
|
+
import * as Schema from 'effect/Schema';
|
|
17
|
+
import { defineCommands, defineNodeSpec, union } from 'prosekit/core';
|
|
18
|
+
import { EMPTY_FLOWCHART, parseFlowchartData } from "./data.js";
|
|
19
|
+
/**
|
|
20
|
+
* JSON encoder for the `data` attr payload. Built once at module scope —
|
|
21
|
+
* `toDOM` runs per flowchart node on every editor render.
|
|
22
|
+
*/
|
|
23
|
+
const encodeJson = /* @__PURE__ */ Schema.encodeSync(
|
|
24
|
+
/* @__PURE__ */ Schema.fromJsonString(Schema.Unknown));
|
|
25
|
+
/**
|
|
26
|
+
* JSON encoding with HTML-sensitive characters unicode-escaped so the result
|
|
27
|
+
* can be embedded in an HTML attribute value without breaking out, regardless
|
|
28
|
+
* of which quote the serializer chooses.
|
|
29
|
+
*/
|
|
30
|
+
function htmlSafeStringify(value) {
|
|
31
|
+
return encodeJson(value)
|
|
32
|
+
.replace(/</g, '\\u003c')
|
|
33
|
+
.replace(/>/g, '\\u003e')
|
|
34
|
+
.replace(/&/g, '\\u0026')
|
|
35
|
+
.replace(/'/g, '\\u0027');
|
|
36
|
+
}
|
|
37
|
+
function defineFlowchartNodeSpec() {
|
|
38
|
+
return defineNodeSpec({
|
|
39
|
+
name: 'flowchart',
|
|
40
|
+
group: 'block',
|
|
41
|
+
atom: true,
|
|
42
|
+
draggable: true,
|
|
43
|
+
selectable: true,
|
|
44
|
+
isolating: true,
|
|
45
|
+
attrs: {
|
|
46
|
+
data: { default: EMPTY_FLOWCHART },
|
|
47
|
+
},
|
|
48
|
+
parseDOM: [
|
|
49
|
+
{
|
|
50
|
+
tag: 'div[data-flowchart]',
|
|
51
|
+
getAttrs: (el) => {
|
|
52
|
+
// Prefer the `data-payload` attribute (current emit format). Fall
|
|
53
|
+
// back to text content for older serialised documents where the
|
|
54
|
+
// JSON lived inside the element.
|
|
55
|
+
const payload = el.getAttribute('data-payload') ?? el.textContent ?? '';
|
|
56
|
+
return { data: parseFlowchartData(payload) };
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
toDOM: (node) => {
|
|
61
|
+
// ProseMirror `attrs` is a foreign plain object; convert at the
|
|
62
|
+
// boundary rather than pushing `Option` into the node spec.
|
|
63
|
+
// SAFETY: `data` is only ever written by this spec's default
|
|
64
|
+
// (`EMPTY_FLOWCHART`), by `parseDOM` (`parseFlowchartData`) and by
|
|
65
|
+
// `updateFlowchartData`, all of which produce a `FlowchartData`; a
|
|
66
|
+
// caller-supplied nullish `data` is still caught below.
|
|
67
|
+
const attrs = node.attrs;
|
|
68
|
+
const data = Option.getOrElse(Option.fromNullishOr(attrs.data), () => EMPTY_FLOWCHART);
|
|
69
|
+
// Persist the JSON in an attribute so the element's inner content is
|
|
70
|
+
// free for the post-rendered SVG (see `./html.ts`). The JSON is
|
|
71
|
+
// unicode-escaped so user-supplied labels containing `<`, `>`, `&`, `'`
|
|
72
|
+
// can never break out of the attribute context. The empty text child
|
|
73
|
+
// ensures happy-dom emits a proper closing tag.
|
|
74
|
+
return ['div', { 'data-flowchart': '', 'data-payload': htmlSafeStringify(data) }, ''];
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function defineFlowchartCommands() {
|
|
79
|
+
return defineCommands({
|
|
80
|
+
updateFlowchartData: (pos, data) => (state, dispatch) => {
|
|
81
|
+
const node = state.doc.nodeAt(pos);
|
|
82
|
+
if (!node || node.type.name !== 'flowchart')
|
|
83
|
+
return false;
|
|
84
|
+
if (dispatch) {
|
|
85
|
+
const tr = state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, data });
|
|
86
|
+
dispatch(tr);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Schema-side extension for the flowchart block: registers the node spec and
|
|
94
|
+
* the `updateFlowchartData` editing command. The Svelte-backed node view
|
|
95
|
+
* lives at this package's root and is composed in by the consuming app's
|
|
96
|
+
* editor component at instantiation time.
|
|
97
|
+
*/
|
|
98
|
+
export function defineFlowchart() {
|
|
99
|
+
return union(defineFlowchartNodeSpec(), defineFlowchartCommands());
|
|
100
|
+
}
|