@slexkit/tiptap 0.3.3 → 0.5.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/README.md +29 -72
- package/dist/index.d.ts +2 -2
- package/dist/index.js +257 -43
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,88 +1,45 @@
|
|
|
1
1
|
# @slexkit/tiptap
|
|
2
2
|
|
|
3
|
-
Tiptap
|
|
4
|
-
|
|
5
|
-
```sh
|
|
6
|
-
npm install slexkit @slexkit/theme-shadcn @slexkit/tiptap @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-code-block @tiptap/markdown
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Import the SlexKit theme once in your app entry:
|
|
3
|
+
Official Tiptap code-block node view for HTML-first `slex` fences.
|
|
10
4
|
|
|
11
5
|
```ts
|
|
12
|
-
import "@slexkit/theme-shadcn/style.css";
|
|
13
|
-
import "@slexkit/tiptap/style.css";
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Minimal setup
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { Editor } from "@tiptap/core";
|
|
20
6
|
import StarterKit from "@tiptap/starter-kit";
|
|
21
7
|
import { createSlexKitTiptapExtension } from "@slexkit/tiptap";
|
|
8
|
+
import { createSlexKitMarkdownRuntimeHost } from "slexkit";
|
|
22
9
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
extensions: [
|
|
26
|
-
StarterKit.configure({ codeBlock: false }),
|
|
27
|
-
createSlexKitTiptapExtension({
|
|
28
|
-
artifactId: "note-42",
|
|
29
|
-
}),
|
|
30
|
-
],
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Only code blocks whose language is exactly `slex` are rendered by SlexKit. Other code blocks stay ordinary Tiptap code blocks.
|
|
35
|
-
|
|
36
|
-
## Markdown roundtrip
|
|
37
|
-
|
|
38
|
-
Add Tiptap's Markdown extension when loading or exporting Markdown:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
import { Markdown } from "@tiptap/markdown";
|
|
42
|
-
|
|
43
|
-
const editor = new Editor({
|
|
44
|
-
extensions: [
|
|
45
|
-
StarterKit.configure({ codeBlock: false }),
|
|
46
|
-
Markdown,
|
|
47
|
-
createSlexKitTiptapExtension({ artifactId: "doc" }),
|
|
48
|
-
],
|
|
49
|
-
content: markdown,
|
|
50
|
-
contentType: "markdown",
|
|
10
|
+
const runtimeHost = createSlexKitMarkdownRuntimeHost({
|
|
11
|
+
secureFrame: { runtimeUrl: "/assets/slexkit.runtime.js" },
|
|
51
12
|
});
|
|
52
13
|
|
|
53
|
-
const
|
|
14
|
+
const extensions = [
|
|
15
|
+
StarterKit.configure({ codeBlock: false }),
|
|
16
|
+
createSlexKitTiptapExtension({ runtimeHost }),
|
|
17
|
+
];
|
|
54
18
|
```
|
|
55
19
|
|
|
56
|
-
The
|
|
57
|
-
|
|
58
|
-
## Shared state across fences
|
|
59
|
-
|
|
60
|
-
All `slex` blocks in one editor share the same artifact runtime by default. A state-only fence can seed later renderable fences:
|
|
20
|
+
The extension replaces only code blocks whose language is exactly `slex`.
|
|
21
|
+
Ordinary code blocks retain normal Tiptap behavior and Markdown round-tripping.
|
|
61
22
|
|
|
62
23
|
````md
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"text:answer": { "$text": "'answer: ' + (g.value * 2)" }
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
24
|
+
~~~slex
|
|
25
|
+
<slex-input id="voltage" type="engineering" label="Voltage" value="12"></slex-input>
|
|
26
|
+
<slex-stat id="power" label="Power" unit="W"></slex-stat>
|
|
27
|
+
<script type="module">
|
|
28
|
+
const voltage = slex.root.querySelector("#voltage");
|
|
29
|
+
const power = slex.root.querySelector("#power");
|
|
30
|
+
const update = () => { power.value = voltage.valueAsNumber * 2; };
|
|
31
|
+
voltage.addEventListener("input", update);
|
|
32
|
+
update();
|
|
33
|
+
</script>
|
|
34
|
+
~~~
|
|
78
35
|
````
|
|
79
36
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
37
|
+
Each editor owns a stable artifact; each node view owns a stable block ID.
|
|
38
|
+
Updates use the runtime host's preview/commit protocol, so invalid source reports
|
|
39
|
+
a diagnostic without destroying the last committed iframe. The adapter never
|
|
40
|
+
interprets DSL objects, namespaces, state-only blocks, expression strings, or
|
|
41
|
+
handler strings.
|
|
85
42
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
43
|
+
Options include `artifactId`, `runtimeHost`, `theme`, `showSource`, labels,
|
|
44
|
+
`className`, and `onError`. Custom legacy hosts remain supported for complete
|
|
45
|
+
commits, though atomic preservation requires `updateArtifact`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
|
-
import { type SlexKitMarkdownRuntimeHost, type
|
|
2
|
+
import { type SlexKitMarkdownRuntimeHost, type HtmlTheme } from "slexkit";
|
|
3
3
|
export type SlexKitTiptapOptions = {
|
|
4
4
|
artifactId?: string | ((ctx: {
|
|
5
5
|
editor: Editor;
|
|
6
6
|
}) => string);
|
|
7
7
|
runtimeHost?: SlexKitMarkdownRuntimeHost;
|
|
8
|
-
theme?:
|
|
8
|
+
theme?: HtmlTheme;
|
|
9
9
|
showSource?: boolean;
|
|
10
10
|
sourceLabel?: string;
|
|
11
11
|
previewLabel?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import CodeBlock from "@tiptap/extension-code-block";
|
|
2
|
-
import { createSlexKitMarkdownRuntimeHost,
|
|
2
|
+
import { createSlexKitMarkdownRuntimeHost, inspectSlexSource, } from "slexkit";
|
|
3
3
|
const DEFAULT_OPTIONS = {
|
|
4
4
|
showSource: true,
|
|
5
5
|
sourceLabel: "Source",
|
|
@@ -20,26 +20,31 @@ const PREVIEW_INTERACTIVE_SELECTOR = [
|
|
|
20
20
|
"[role='tab']",
|
|
21
21
|
"[role='textbox']",
|
|
22
22
|
].join(",");
|
|
23
|
+
const TRANSIENT_POINTER_INTERACTIVE_SELECTOR = [
|
|
24
|
+
"a[href]",
|
|
25
|
+
"button",
|
|
26
|
+
"input[type='button']",
|
|
27
|
+
"input[type='checkbox']",
|
|
28
|
+
"input[type='image']",
|
|
29
|
+
"input[type='radio']",
|
|
30
|
+
"input[type='range']",
|
|
31
|
+
"input[type='reset']",
|
|
32
|
+
"input[type='submit']",
|
|
33
|
+
"[role='button']",
|
|
34
|
+
"[role='checkbox']",
|
|
35
|
+
"[role='radio']",
|
|
36
|
+
"[role='slider']",
|
|
37
|
+
"[role='switch']",
|
|
38
|
+
"[role='tab']",
|
|
39
|
+
].join(",");
|
|
23
40
|
const editorArtifactIds = new WeakMap();
|
|
24
41
|
let nextArtifactId = 0;
|
|
25
42
|
const sourceOpenStates = new Map();
|
|
26
43
|
const editorArtifactRuntimes = new WeakMap();
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!isRecord(value))
|
|
32
|
-
return false;
|
|
33
|
-
if ("layout" in value)
|
|
34
|
-
return isRecord(value.layout) && Object.keys(value.layout).length > 0;
|
|
35
|
-
if ("namespace" in value || "g" in value)
|
|
36
|
-
return false;
|
|
37
|
-
return Object.keys(value).some((key) => key.includes(":"));
|
|
38
|
-
}
|
|
39
|
-
function isStateOnlySource(value) {
|
|
40
|
-
return isRecord(value)
|
|
41
|
-
&& !isRenderableSource(value)
|
|
42
|
-
&& ("slex" in value || "namespace" in value || "g" in value);
|
|
44
|
+
function isIncrementalRuntimeHost(host) {
|
|
45
|
+
const candidate = host;
|
|
46
|
+
return typeof candidate.updateArtifact === "function"
|
|
47
|
+
&& typeof candidate.resetArtifact === "function";
|
|
43
48
|
}
|
|
44
49
|
function resolveArtifactId(editor, option) {
|
|
45
50
|
if (typeof option === "function")
|
|
@@ -82,6 +87,8 @@ function resolveArtifactRuntime(editor, artifactId, runtimeHost) {
|
|
|
82
87
|
runtimeHost: runtimeHost ?? createSlexKitMarkdownRuntimeHost(),
|
|
83
88
|
views: new Set(),
|
|
84
89
|
scheduled: false,
|
|
90
|
+
pending: Promise.resolve(),
|
|
91
|
+
nextBlockId: 0,
|
|
85
92
|
};
|
|
86
93
|
runtimes.set(artifactId, runtime);
|
|
87
94
|
}
|
|
@@ -93,8 +100,36 @@ function scheduleArtifactRender(runtime) {
|
|
|
93
100
|
runtime.scheduled = true;
|
|
94
101
|
queueMicrotask(() => {
|
|
95
102
|
runtime.scheduled = false;
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
const views = Array.from(runtime.views).sort(compareNodeViewOrder);
|
|
104
|
+
if (isIncrementalRuntimeHost(runtime.runtimeHost)) {
|
|
105
|
+
const prepared = views.map((view) => view.prepareIncrementalRender());
|
|
106
|
+
const phase = prepared.every((entry) => entry.valid)
|
|
107
|
+
? "commit"
|
|
108
|
+
: "preview";
|
|
109
|
+
const host = runtime.runtimeHost;
|
|
110
|
+
const update = async () => {
|
|
111
|
+
if (runtime.disposed)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
await host.updateArtifact(runtime.artifactId, prepared.map((entry) => entry.block), { phase });
|
|
115
|
+
if (phase === "commit") {
|
|
116
|
+
for (const entry of prepared) {
|
|
117
|
+
entry.view.finishIncrementalRender();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (phase === "commit") {
|
|
123
|
+
for (const entry of prepared) {
|
|
124
|
+
entry.view.reportIncrementalError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
runtime.pending = runtime.pending.then(update, update);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const view of views) {
|
|
98
133
|
view.renderWithRuntime(runtime.runtimeHost);
|
|
99
134
|
}
|
|
100
135
|
});
|
|
@@ -105,6 +140,7 @@ function unregisterArtifactRuntime(editor, runtime, view) {
|
|
|
105
140
|
scheduleArtifactRender(runtime);
|
|
106
141
|
return;
|
|
107
142
|
}
|
|
143
|
+
runtime.disposed = true;
|
|
108
144
|
runtime.runtimeHost.disposeArtifact(runtime.artifactId);
|
|
109
145
|
editorArtifactRuntimes.get(editor)?.delete(runtime.artifactId);
|
|
110
146
|
}
|
|
@@ -125,8 +161,17 @@ function focusPreviewInteractiveTarget(event) {
|
|
|
125
161
|
const target = event.target;
|
|
126
162
|
const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
127
163
|
const interactive = targetElement?.closest(PREVIEW_INTERACTIVE_SELECTOR);
|
|
128
|
-
if (interactive instanceof HTMLElement)
|
|
129
|
-
|
|
164
|
+
if (!(interactive instanceof HTMLElement))
|
|
165
|
+
return null;
|
|
166
|
+
interactive.focus({ preventScroll: true });
|
|
167
|
+
return interactive;
|
|
168
|
+
}
|
|
169
|
+
function shouldRestorePreviewFocusAfterPointer(element) {
|
|
170
|
+
return !!element.closest(TRANSIENT_POINTER_INTERACTIVE_SELECTOR);
|
|
171
|
+
}
|
|
172
|
+
function pointerIdFromEvent(event) {
|
|
173
|
+
const pointerEvent = event;
|
|
174
|
+
return typeof pointerEvent?.pointerId === "number" ? pointerEvent.pointerId : undefined;
|
|
130
175
|
}
|
|
131
176
|
function markPreviewInteractive(preview, force = false) {
|
|
132
177
|
if (preview.getAttribute("contenteditable") !== "false") {
|
|
@@ -134,18 +179,38 @@ function markPreviewInteractive(preview, force = false) {
|
|
|
134
179
|
}
|
|
135
180
|
preview.querySelectorAll(PREVIEW_INTERACTIVE_SELECTOR).forEach((element) => {
|
|
136
181
|
if (element instanceof HTMLElement && (force || element.getAttribute("contenteditable") !== "true")) {
|
|
137
|
-
if (force)
|
|
182
|
+
if (force && !element.contains(preview.ownerDocument.activeElement))
|
|
138
183
|
element.removeAttribute("contenteditable");
|
|
139
184
|
element.setAttribute("contenteditable", "true");
|
|
140
185
|
}
|
|
141
186
|
});
|
|
142
187
|
}
|
|
188
|
+
const previewMarks = new WeakMap();
|
|
189
|
+
function cancelPreviewInteractiveMark(preview) {
|
|
190
|
+
const pending = previewMarks.get(preview);
|
|
191
|
+
if (!pending)
|
|
192
|
+
return;
|
|
193
|
+
pending.cancelled = true;
|
|
194
|
+
if (pending.timer !== undefined)
|
|
195
|
+
preview.ownerDocument.defaultView?.clearTimeout(pending.timer);
|
|
196
|
+
if (pending.frame !== undefined)
|
|
197
|
+
preview.ownerDocument.defaultView?.cancelAnimationFrame(pending.frame);
|
|
198
|
+
previewMarks.delete(preview);
|
|
199
|
+
}
|
|
143
200
|
function schedulePreviewInteractiveMark(preview) {
|
|
144
201
|
markPreviewInteractive(preview);
|
|
145
|
-
|
|
202
|
+
if (previewMarks.has(preview))
|
|
203
|
+
return;
|
|
204
|
+
const pending = { cancelled: false };
|
|
205
|
+
previewMarks.set(preview, pending);
|
|
206
|
+
const mark = () => { if (!pending.cancelled)
|
|
207
|
+
markPreviewInteractive(preview, true); };
|
|
208
|
+
queueMicrotask(mark);
|
|
146
209
|
const win = preview.ownerDocument.defaultView;
|
|
147
|
-
win?.setTimeout(() =>
|
|
148
|
-
|
|
210
|
+
pending.timer = win?.setTimeout(() => { pending.timer = undefined; mark(); if (pending.frame === undefined)
|
|
211
|
+
previewMarks.delete(preview); }, 0);
|
|
212
|
+
pending.frame = win?.requestAnimationFrame(() => { pending.frame = undefined; mark(); if (pending.timer === undefined)
|
|
213
|
+
previewMarks.delete(preview); });
|
|
149
214
|
}
|
|
150
215
|
function renderError(error, source, container) {
|
|
151
216
|
const doc = container.ownerDocument || document;
|
|
@@ -192,15 +257,113 @@ class SlexKitCodeBlockNodeView {
|
|
|
192
257
|
editor;
|
|
193
258
|
options;
|
|
194
259
|
preview;
|
|
260
|
+
diagnostic;
|
|
195
261
|
runtime;
|
|
262
|
+
blockId;
|
|
196
263
|
interactiveObserver;
|
|
264
|
+
pendingFocusRestore;
|
|
265
|
+
pointerFocusContext;
|
|
266
|
+
focusTimers = new Set();
|
|
267
|
+
focusFrames = new Set();
|
|
268
|
+
clearKeyboardPointerIntent = () => {
|
|
269
|
+
this.pointerFocusContext = undefined;
|
|
270
|
+
this.pendingFocusRestore = undefined;
|
|
271
|
+
this.removeFocusRestoreListeners();
|
|
272
|
+
this.cancelFocusWork();
|
|
273
|
+
};
|
|
274
|
+
cancelFocusWork() {
|
|
275
|
+
const win = this.preview.ownerDocument.defaultView;
|
|
276
|
+
for (const timer of this.focusTimers)
|
|
277
|
+
win?.clearTimeout(timer);
|
|
278
|
+
for (const frame of this.focusFrames)
|
|
279
|
+
win?.cancelAnimationFrame(frame);
|
|
280
|
+
this.focusTimers.clear();
|
|
281
|
+
this.focusFrames.clear();
|
|
282
|
+
}
|
|
197
283
|
renderToken = 0;
|
|
284
|
+
focusPreviewPointerTarget = (event) => {
|
|
285
|
+
const restoreEditor = this.editor.isFocused;
|
|
286
|
+
const interactive = focusPreviewInteractiveTarget(event);
|
|
287
|
+
this.pointerFocusContext = { restoreEditor, expiresAt: Date.now() + 500,
|
|
288
|
+
scope: interactive?.closest(".slex-select,.slex-checkbox,.slex-switch,.slex-tabs") ?? interactive ?? undefined };
|
|
289
|
+
if (!interactive || !shouldRestorePreviewFocusAfterPointer(interactive))
|
|
290
|
+
return;
|
|
291
|
+
this.pendingFocusRestore = {
|
|
292
|
+
target: interactive,
|
|
293
|
+
restoreEditor,
|
|
294
|
+
pointerId: typeof event.pointerId === "number" ? event.pointerId : null,
|
|
295
|
+
};
|
|
296
|
+
this.addFocusRestoreListeners();
|
|
297
|
+
};
|
|
298
|
+
clearPointerFocusedPreviewTarget = (event) => {
|
|
299
|
+
const context = this.pointerFocusContext;
|
|
300
|
+
if (!context || Date.now() > context.expiresAt)
|
|
301
|
+
return;
|
|
302
|
+
const target = event.target;
|
|
303
|
+
const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
304
|
+
const interactive = targetElement?.closest(TRANSIENT_POINTER_INTERACTIVE_SELECTOR);
|
|
305
|
+
if (!(interactive instanceof HTMLElement))
|
|
306
|
+
return;
|
|
307
|
+
if (context.scope && !context.scope.contains(interactive))
|
|
308
|
+
return;
|
|
309
|
+
if (this.pendingFocusRestore?.target === interactive)
|
|
310
|
+
return;
|
|
311
|
+
this.queuePreviewFocusCleanup({
|
|
312
|
+
target: interactive,
|
|
313
|
+
restoreEditor: context.restoreEditor,
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
finishPreviewPointerFocus = (event) => {
|
|
317
|
+
const pending = this.pendingFocusRestore;
|
|
318
|
+
if (!pending)
|
|
319
|
+
return;
|
|
320
|
+
const pointerId = pointerIdFromEvent(event);
|
|
321
|
+
if (pointerId !== undefined
|
|
322
|
+
&& pending.pointerId !== null
|
|
323
|
+
&& pointerId !== pending.pointerId) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.pendingFocusRestore = undefined;
|
|
327
|
+
this.removeFocusRestoreListeners();
|
|
328
|
+
this.queuePreviewFocusCleanup(pending);
|
|
329
|
+
};
|
|
330
|
+
queuePreviewFocusCleanup(pending) {
|
|
331
|
+
const doc = this.preview.ownerDocument || document;
|
|
332
|
+
const win = doc.defaultView;
|
|
333
|
+
const cleanupFocus = () => {
|
|
334
|
+
if (!this.dom.isConnected)
|
|
335
|
+
return;
|
|
336
|
+
const active = doc.activeElement;
|
|
337
|
+
const clearedTarget = active === pending.target;
|
|
338
|
+
if (clearedTarget)
|
|
339
|
+
pending.target.blur();
|
|
340
|
+
const nextActive = doc.activeElement;
|
|
341
|
+
const canRestoreEditor = pending.restoreEditor
|
|
342
|
+
&& !this.editor.isDestroyed
|
|
343
|
+
&& (clearedTarget || nextActive === doc.body || nextActive === doc.documentElement);
|
|
344
|
+
if (canRestoreEditor) {
|
|
345
|
+
this.editor.commands.focus(undefined, { scrollIntoView: false });
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
if (!win)
|
|
349
|
+
return;
|
|
350
|
+
for (const ms of [0, 50]) {
|
|
351
|
+
const timer = win.setTimeout(() => { this.focusTimers.delete(timer); cleanupFocus(); }, ms);
|
|
352
|
+
this.focusTimers.add(timer);
|
|
353
|
+
}
|
|
354
|
+
const frame = win.requestAnimationFrame(() => { this.focusFrames.delete(frame); cleanupFocus(); });
|
|
355
|
+
this.focusFrames.add(frame);
|
|
356
|
+
}
|
|
357
|
+
cancelPreviewPointerFocus = (event) => {
|
|
358
|
+
this.finishPreviewPointerFocus(event);
|
|
359
|
+
};
|
|
198
360
|
constructor(node, props, options) {
|
|
199
361
|
this.node = node;
|
|
200
362
|
this.options = options;
|
|
201
363
|
this.editor = props.editor;
|
|
202
364
|
this.artifactId = resolveArtifactId(props.editor, options.artifactId);
|
|
203
365
|
this.runtime = resolveArtifactRuntime(props.editor, this.artifactId, options.runtimeHost);
|
|
366
|
+
this.blockId = `tiptap-block-${++this.runtime.nextBlockId}`;
|
|
204
367
|
this.runtime.views.add(this);
|
|
205
368
|
const doc = props.view.dom.ownerDocument || document;
|
|
206
369
|
this.dom = createElement(doc, "div", ["slex-tiptap-block", options.className].filter(Boolean).join(" "));
|
|
@@ -210,8 +373,11 @@ class SlexKitCodeBlockNodeView {
|
|
|
210
373
|
if (label)
|
|
211
374
|
label.textContent = options.previewLabel;
|
|
212
375
|
this.preview = createElement(doc, "div", "slex-tiptap-preview");
|
|
376
|
+
this.diagnostic = createElement(doc, "div", "slex-tiptap-diagnostic");
|
|
213
377
|
this.preview.setAttribute("contenteditable", "false");
|
|
214
|
-
|
|
378
|
+
doc.addEventListener("keydown", this.clearKeyboardPointerIntent, { capture: true });
|
|
379
|
+
this.preview.addEventListener("pointerdown", this.focusPreviewPointerTarget, { capture: true });
|
|
380
|
+
this.preview.addEventListener("focusin", this.clearPointerFocusedPreviewTarget, { capture: true });
|
|
215
381
|
this.preview.addEventListener("mousedown", focusPreviewInteractiveTarget, { capture: true });
|
|
216
382
|
this.preview.addEventListener("click", focusPreviewInteractiveTarget, { capture: true });
|
|
217
383
|
if (typeof MutationObserver !== "undefined") {
|
|
@@ -259,13 +425,13 @@ class SlexKitCodeBlockNodeView {
|
|
|
259
425
|
sourceToggle.addEventListener("click", stopSourceClick, { capture: true });
|
|
260
426
|
setSourceOpen(sourceOpen);
|
|
261
427
|
sourceShell.append(sourceToggle, pre);
|
|
262
|
-
this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
|
|
428
|
+
this.dom.append(...[label, this.diagnostic, this.preview, sourceShell].filter((element) => !!element));
|
|
263
429
|
}
|
|
264
430
|
else {
|
|
265
431
|
const sourceShell = createElement(doc, "div", "slex-tiptap-source slex-tiptap-source-hidden");
|
|
266
432
|
sourceShell.hidden = true;
|
|
267
433
|
sourceShell.append(pre);
|
|
268
|
-
this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
|
|
434
|
+
this.dom.append(...[label, this.diagnostic, this.preview, sourceShell].filter((element) => !!element));
|
|
269
435
|
}
|
|
270
436
|
scheduleArtifactRender(this.runtime);
|
|
271
437
|
}
|
|
@@ -277,7 +443,16 @@ class SlexKitCodeBlockNodeView {
|
|
|
277
443
|
return true;
|
|
278
444
|
}
|
|
279
445
|
destroy() {
|
|
446
|
+
this.renderToken += 1;
|
|
447
|
+
this.cancelFocusWork();
|
|
448
|
+
this.preview.ownerDocument.removeEventListener("keydown", this.clearKeyboardPointerIntent, { capture: true });
|
|
449
|
+
cancelPreviewInteractiveMark(this.preview);
|
|
280
450
|
this.interactiveObserver?.disconnect();
|
|
451
|
+
this.removeFocusRestoreListeners();
|
|
452
|
+
this.preview.removeEventListener("pointerdown", this.focusPreviewPointerTarget, { capture: true });
|
|
453
|
+
this.preview.removeEventListener("mousedown", focusPreviewInteractiveTarget, { capture: true });
|
|
454
|
+
this.preview.removeEventListener("click", focusPreviewInteractiveTarget, { capture: true });
|
|
455
|
+
this.preview.removeEventListener("focusin", this.clearPointerFocusedPreviewTarget, { capture: true });
|
|
281
456
|
this.preview.replaceChildren();
|
|
282
457
|
unregisterArtifactRuntime(this.editor, this.runtime, this);
|
|
283
458
|
}
|
|
@@ -297,6 +472,8 @@ class SlexKitCodeBlockNodeView {
|
|
|
297
472
|
const target = mutation.target;
|
|
298
473
|
if (this.preview.contains(target))
|
|
299
474
|
return true;
|
|
475
|
+
if (this.diagnostic.contains(target))
|
|
476
|
+
return true;
|
|
300
477
|
if (target === this.contentDOM || this.contentDOM.contains(target))
|
|
301
478
|
return false;
|
|
302
479
|
const targetElement = target instanceof Element ? target : target.parentElement;
|
|
@@ -315,41 +492,78 @@ class SlexKitCodeBlockNodeView {
|
|
|
315
492
|
scheduleArtifactRender(this.runtime);
|
|
316
493
|
});
|
|
317
494
|
}
|
|
495
|
+
addFocusRestoreListeners() {
|
|
496
|
+
const doc = this.preview.ownerDocument || document;
|
|
497
|
+
doc.addEventListener("pointerup", this.finishPreviewPointerFocus, { capture: true });
|
|
498
|
+
doc.addEventListener("pointercancel", this.cancelPreviewPointerFocus, { capture: true });
|
|
499
|
+
doc.defaultView?.addEventListener("blur", this.cancelPreviewPointerFocus);
|
|
500
|
+
}
|
|
501
|
+
removeFocusRestoreListeners() {
|
|
502
|
+
const doc = this.preview.ownerDocument || document;
|
|
503
|
+
doc.removeEventListener("pointerup", this.finishPreviewPointerFocus, { capture: true });
|
|
504
|
+
doc.removeEventListener("pointercancel", this.cancelPreviewPointerFocus, { capture: true });
|
|
505
|
+
doc.defaultView?.removeEventListener("blur", this.cancelPreviewPointerFocus);
|
|
506
|
+
}
|
|
318
507
|
renderWithRuntime(runtimeHost) {
|
|
319
508
|
const source = this.source();
|
|
320
509
|
this.dom.hidden = false;
|
|
321
510
|
this.preview.hidden = false;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this.options.onError?.(parsed.error, source);
|
|
511
|
+
const inspected = inspectSlexSource(source);
|
|
512
|
+
if (!inspected.ok) {
|
|
513
|
+
renderError(inspected.error, source, this.diagnostic);
|
|
514
|
+
this.options.onError?.(inspected.error, source);
|
|
327
515
|
return;
|
|
328
516
|
}
|
|
329
517
|
try {
|
|
518
|
+
runtimeHost.disposeBlock(this.preview);
|
|
330
519
|
runtimeHost.mountBlock({
|
|
331
520
|
artifactId: this.artifactId,
|
|
521
|
+
blockId: this.blockId,
|
|
332
522
|
source,
|
|
333
523
|
container: this.preview,
|
|
334
524
|
theme: this.options.theme,
|
|
335
525
|
});
|
|
526
|
+
this.diagnostic.replaceChildren();
|
|
336
527
|
schedulePreviewInteractiveMark(this.preview);
|
|
337
528
|
}
|
|
338
529
|
catch (error) {
|
|
339
|
-
renderError(error, source, this.
|
|
530
|
+
renderError(error, source, this.diagnostic);
|
|
340
531
|
this.options.onError?.(error, source);
|
|
341
532
|
return;
|
|
342
533
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const placeholder = createElement(this.preview.ownerDocument || document, "div", "slex-tiptap-state");
|
|
350
|
-
placeholder.textContent = "State block";
|
|
351
|
-
this.preview.append(placeholder);
|
|
534
|
+
}
|
|
535
|
+
prepareIncrementalRender() {
|
|
536
|
+
const source = this.source();
|
|
537
|
+
const inspected = inspectSlexSource(source);
|
|
538
|
+
if (inspected.ok) {
|
|
539
|
+
this.diagnostic.replaceChildren();
|
|
352
540
|
}
|
|
541
|
+
else {
|
|
542
|
+
renderError(inspected.error, source, this.diagnostic);
|
|
543
|
+
this.options.onError?.(inspected.error, source);
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
view: this,
|
|
547
|
+
block: {
|
|
548
|
+
artifactId: this.artifactId,
|
|
549
|
+
blockId: this.blockId,
|
|
550
|
+
source,
|
|
551
|
+
container: this.preview,
|
|
552
|
+
theme: this.options.theme,
|
|
553
|
+
},
|
|
554
|
+
valid: inspected.ok,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
finishIncrementalRender() {
|
|
558
|
+
this.dom.hidden = false;
|
|
559
|
+
this.preview.hidden = false;
|
|
560
|
+
this.diagnostic.replaceChildren();
|
|
561
|
+
schedulePreviewInteractiveMark(this.preview);
|
|
562
|
+
}
|
|
563
|
+
reportIncrementalError(error) {
|
|
564
|
+
const source = this.source();
|
|
565
|
+
renderError(error, source, this.diagnostic);
|
|
566
|
+
this.options.onError?.(error, source);
|
|
353
567
|
}
|
|
354
568
|
}
|
|
355
569
|
export function createSlexKitTiptapExtension(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slexkit/tiptap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Tiptap extension for SlexKit fenced UI blocks.",
|
|
5
5
|
"author": "SlexKit contributors",
|
|
6
6
|
"type": "module",
|
|
@@ -48,9 +48,12 @@
|
|
|
48
48
|
"@tiptap/extension-code-block": "^3.0.0",
|
|
49
49
|
"@tiptap/markdown": "^3.0.0",
|
|
50
50
|
"@tiptap/pm": "^3.0.0",
|
|
51
|
-
"slexkit": "^0.
|
|
51
|
+
"slexkit": "^0.5.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
|
+
"slexkit": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
54
57
|
"@tiptap/markdown": {
|
|
55
58
|
"optional": true
|
|
56
59
|
}
|
|
@@ -60,7 +63,6 @@
|
|
|
60
63
|
"@tiptap/extension-code-block": "^3.27.1",
|
|
61
64
|
"@tiptap/markdown": "^3.27.1",
|
|
62
65
|
"@tiptap/pm": "^3.27.1",
|
|
63
|
-
"@tiptap/starter-kit": "^3.27.1"
|
|
64
|
-
"slexkit": "file:../.."
|
|
66
|
+
"@tiptap/starter-kit": "^3.27.1"
|
|
65
67
|
}
|
|
66
68
|
}
|