@bloklabs/vue 1.16.1 → 2.0.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.
@@ -1,46 +0,0 @@
1
- // src/vue/BlockPortalHost.ts
2
- import { defineComponent, h, Teleport, Fragment, onErrorCaptured, type PropType } from 'vue';
3
-
4
- import type { BlockPortalRegistry } from './block-portal-registry';
5
-
6
- /**
7
- * Wraps a single block's render so a throw inside it can't tear down the whole
8
- * editor's render tree. `onErrorCaptured` returning false stops propagation.
9
- * Authored as a render function (no `.vue` SFC) per the adapter invariant.
10
- */
11
- const BlockErrorBoundary = defineComponent({
12
- name: 'BlockErrorBoundary',
13
- setup(_props, { slots }) {
14
- onErrorCaptured(() => false);
15
-
16
- return () => slots.default?.();
17
- },
18
- });
19
-
20
- /**
21
- * The single shared render tree that teleports each registered Vue block into
22
- * its Blok-owned host element. Mounted once per editor (by `BlokContent`), it
23
- * iterates the reactive registry and emits one `<Teleport>` per entry — so all
24
- * blocks share one reconciler and the surrounding app's provide/inject context
25
- * (Teleport preserves the component render context, not the DOM target's).
26
- */
27
- export const BlockPortalHost = defineComponent({
28
- name: 'BlockPortalHost',
29
- props: {
30
- registry: { type: Object as PropType<BlockPortalRegistry>, required: true },
31
- },
32
- setup(props) {
33
- return () =>
34
- h(
35
- Fragment,
36
- // Iterating the reactive Map tracks it, so register/unregister re-renders.
37
- Array.from(props.registry.entries.entries()).map(([id, entry]) =>
38
- h(Teleport, { key: id, to: entry.hostEl }, [
39
- h(BlockErrorBoundary, null, {
40
- default: () => h(entry.component, entry.props),
41
- }),
42
- ])
43
- )
44
- );
45
- },
46
- });
@@ -1,103 +0,0 @@
1
- import {
2
- computed,
3
- defineComponent,
4
- h,
5
- onMounted,
6
- onBeforeUnmount,
7
- provide,
8
- ref,
9
- toRaw,
10
- watch,
11
- type PropType,
12
- } from 'vue';
13
- import { getHolder } from './holder-map';
14
- import { getRegistry } from './registry-map';
15
- import { BlockPortalHost } from './BlockPortalHost';
16
- import { BLOK_EDITOR_INSTANCE } from './blok-instance';
17
- import type { Blok } from '@/types';
18
-
19
- /**
20
- * Provides the DOM mount point for a Blok editor. Renders a `<div>` and adopts
21
- * the editor's detached holder element into it (the Vue analog of React's
22
- * `BlokContent`). When `editor` is null it renders an empty div for layout
23
- * stability. The holder subtree is owned entirely by core — Vue's renderer never
24
- * manages its children, so the container is always rendered with no vnode
25
- * children and the holder is appended/removed imperatively.
26
- *
27
- * `toRaw` unwraps the editor before the holder-map lookup: Vue stores the object
28
- * prop behind a reactive proxy, and the holder WeakMap is keyed by the raw Blok
29
- * instance (Risk R0 — Vue proxies must never be used as identity keys).
30
- */
31
- export const BlokContent = defineComponent({
32
- name: 'BlokContent',
33
- props: {
34
- editor: {
35
- type: Object as PropType<Blok | null>,
36
- default: null,
37
- },
38
- },
39
- setup(props) {
40
- const container = ref<HTMLDivElement | null>(null);
41
-
42
- // Publish the live instance to everything rendered beneath — including every
43
- // `createVueBlock` block, because <Teleport> preserves the COMPONENT render
44
- // context. That is what lets a block call `useBlocks(useBlokInstance())`
45
- // without the host prop-drilling the instance into it. `toRaw` for the same
46
- // reason the holder lookup uses it (Risk R0): the prop may arrive behind a
47
- // reactive proxy, and consumers compare the instance BY IDENTITY.
48
- provide(
49
- BLOK_EDITOR_INSTANCE,
50
- computed(() => (props.editor === null ? null : toRaw(props.editor)))
51
- );
52
-
53
- const adopt = (editor: Blok | null): void => {
54
- if (container.value === null || editor === null) {
55
- return;
56
- }
57
-
58
- const holder = getHolder(toRaw(editor));
59
-
60
- if (holder === undefined) {
61
- return;
62
- }
63
-
64
- container.value.appendChild(holder);
65
- };
66
-
67
- const detach = (editor: Blok | null): void => {
68
- if (editor === null) {
69
- return;
70
- }
71
-
72
- getHolder(toRaw(editor))?.remove();
73
- };
74
-
75
- onMounted(() => {
76
- adopt(props.editor);
77
- });
78
-
79
- watch(
80
- () => props.editor,
81
- (next, previous) => {
82
- detach(previous ?? null);
83
- adopt(next);
84
- }
85
- );
86
-
87
- onBeforeUnmount(() => {
88
- detach(props.editor);
89
- });
90
-
91
- return () => {
92
- const editorVal = props.editor;
93
- // The shared portal host for `createVueBlock` tools (when this editor has
94
- // one). It emits ONLY <Teleport> anchors — no real DOM in the container —
95
- // so the imperatively-appended holder is undisturbed and the container
96
- // stays a single root for attribute fallthrough. Registry-less editors
97
- // (vanilla-only) render the bare div exactly as before.
98
- const registry = editorVal === null ? undefined : getRegistry(toRaw(editorVal));
99
-
100
- return h('div', { ref: container }, registry === undefined ? [] : [h(BlockPortalHost, { registry })]);
101
- };
102
- },
103
- });
package/src/BlokEditor.ts DELETED
@@ -1,326 +0,0 @@
1
- import { computed, defineComponent, getCurrentInstance, h, onUpdated, ref, watch, type PropType } from 'vue';
2
- import { useBlok } from './useBlok';
3
- import { BlokContent } from './BlokContent';
4
- import { getContentBaseline } from './content-baseline-map';
5
- import { BLOK_EDITOR_CONFIG_KEYS } from './config-keys';
6
- import type {
7
- API,
8
- BlockMutationEvent,
9
- BlokConfig,
10
- EditorWidth,
11
- OutputData,
12
- ResolvedTheme,
13
- ThemeMode,
14
- } from '@/types';
15
- import type { UseBlokConfig } from './types';
16
-
17
- /**
18
- * Calls emit-mapped listeners read from a vnode's props. Vue stores one as a
19
- * function, or as an array when several are bound to the same event.
20
- * @param vnodeProps - the component vnode's props
21
- * @param keys - listener keys, e.g. `onSave`
22
- * @param data - the payload
23
- */
24
- const callVnodeListeners = (vnodeProps: Record<string, unknown> | null, keys: string[], data: OutputData): void => {
25
- const listeners: unknown[] = keys.flatMap((key) => vnodeProps?.[key] ?? []);
26
-
27
- for (const listener of listeners) {
28
- if (typeof listener === 'function') {
29
- (listener as (payload: OutputData) => void)(data);
30
- }
31
- }
32
- };
33
-
34
- /**
35
- * The blessed all-in-one component for embedding Blok in Vue. Wires `useBlok`
36
- * and `BlokContent`, maps Vue emits onto the core config callbacks (gated on
37
- * listener presence, since their mere presence makes core do extra work), and
38
- * threads the return-valued transform hooks through.
39
- *
40
- * Every declared prop is removed from `$attrs`, so any extra attribute
41
- * (`id`/`class`/`aria-*`/`data-*`) falls through to the editor container.
42
- */
43
- export const BlokEditor = defineComponent({
44
- name: 'BlokEditor',
45
- // Bind fallthrough attrs explicitly onto the container (below) instead of
46
- // relying on implicit double-fallthrough through BlokContent.
47
- inheritAttrs: false,
48
- // Reactive config + seed props. `default: undefined` on the boolean props keeps
49
- // an unset prop `undefined` (Vue otherwise coerces absent Boolean props to
50
- // false), so `provideBlok` defaults can apply and core can coerce.
51
- props: {
52
- autofocus: { type: Boolean as PropType<boolean | undefined>, default: undefined },
53
- defaultBlock: { type: String, default: undefined },
54
- dataModel: { type: String as PropType<BlokConfig['dataModel']>, default: undefined },
55
- placeholder: { type: [String, Boolean] as PropType<string | false>, default: undefined },
56
- sanitizer: { type: Object as PropType<BlokConfig['sanitizer']>, default: undefined },
57
- hideToolbar: { type: Boolean as PropType<boolean | undefined>, default: undefined },
58
- toolbarPosition: { type: String as PropType<BlokConfig['toolbarPosition']>, default: undefined },
59
- maxHistoryLength: { type: Number, default: undefined },
60
- historyDebounceTime: { type: Number, default: undefined },
61
- newGroupDelay: { type: Number, default: undefined },
62
- globalUndoRedo: { type: Boolean as PropType<boolean | undefined>, default: undefined },
63
- tools: { type: Object as PropType<BlokConfig['tools']>, default: undefined },
64
- data: { type: Object as PropType<OutputData>, default: undefined },
65
- minHeight: { type: Number, default: undefined },
66
- loader: { type: [Boolean, Object] as PropType<BlokConfig['loader']>, default: undefined },
67
- logLevel: { type: String as PropType<BlokConfig['logLevel']>, default: undefined },
68
- // [Boolean, Object]: the object form ({ hideControls }) must not be dropped
69
- // or dev-warned — it always means read-only enabled (reactive contract).
70
- readOnly: { type: [Boolean, Object] as PropType<BlokConfig['readOnly']>, default: undefined },
71
- i18n: { type: Object as PropType<BlokConfig['i18n']>, default: undefined },
72
- link: { type: Object as PropType<BlokConfig['link']>, default: undefined },
73
- linkPaste: { type: Object as PropType<BlokConfig['linkPaste']>, default: undefined },
74
- inlineToolbar: { type: [Array, Boolean] as PropType<string[] | boolean>, default: undefined },
75
- inlineEmoji: { type: Boolean as PropType<boolean | undefined>, default: undefined },
76
- find: { type: [Boolean, Object] as PropType<BlokConfig['find']>, default: undefined },
77
- tunes: { type: Array as PropType<string[]>, default: undefined },
78
- style: { type: Object as PropType<BlokConfig['style']>, default: undefined },
79
- theme: { type: String as PropType<ThemeMode>, default: undefined },
80
- scrollToBlock: { type: Object as PropType<BlokConfig['scrollToBlock']>, default: undefined },
81
- user: { type: Object as PropType<BlokConfig['user']>, default: undefined },
82
- resolveUser: { type: Function as PropType<BlokConfig['resolveUser']>, default: undefined },
83
- notifierPosition: { type: String as PropType<BlokConfig['notifierPosition']>, default: undefined },
84
- notifier: { type: Function as PropType<BlokConfig['notifier']>, default: undefined },
85
- width: { type: String as PropType<EditorWidth>, default: undefined },
86
- onBeforeRender: { type: Function as PropType<BlokConfig['onBeforeRender']>, default: undefined },
87
- onBeforePaste: { type: Function as PropType<BlokConfig['onBeforePaste']>, default: undefined },
88
- // Return-valued hook (the boolean is the "handled" signal), so it is a
89
- // declared prop copied into the config — not an emit-mapped callback.
90
- onEnter: { type: Function as PropType<BlokConfig['onEnter']>, default: undefined },
91
- // Fire-and-forget submit hook (Enter-to-send) — a declared prop copied into
92
- // config, delivering the serialized document.
93
- onSubmit: { type: Function as PropType<BlokConfig['onSubmit']>, default: undefined },
94
- // Fire-and-forget error channel — a plain declared prop copied into config.
95
- onError: { type: Function as PropType<BlokConfig['onError']>, default: undefined },
96
- onImageFailure: { type: Function as PropType<BlokConfig['onImageFailure']>, default: undefined },
97
- /** Host-supplied per-type block migrations applied at load. */
98
- migrations: { type: Object as PropType<BlokConfig['migrations']>, default: undefined },
99
- /** Editor-level uploader, routed by asset kind (images, video, audio, files). */
100
- uploader: { type: Object as PropType<BlokConfig['uploader']>, default: undefined },
101
- /** Convert uploaded photos and videos into several formats, rendered best-first. */
102
- media: { type: Object as PropType<BlokConfig['media']>, default: undefined },
103
- /** Base URL of a service speaking Blok's upload and unfurl contracts. */
104
- server: { type: String as PropType<BlokConfig['server']>, default: undefined },
105
- /** Endpoint in your app that mints a short-lived access pass for the current user. */
106
- ticket: { type: String as PropType<BlokConfig['ticket']>, default: undefined },
107
- /** Load on mount and save on change, against your own endpoint. */
108
- persistence: { type: Object as PropType<BlokConfig['persistence']>, default: undefined },
109
- /** Real-time multiplayer against the server's sync service. Mutually exclusive with persistence. */
110
- collaboration: { type: Object as PropType<BlokConfig['collaboration']>, default: undefined },
111
- /** Your app's id for this document; tabs showing the same id stay in sync. Mount-fixed. */
112
- documentId: { type: String as PropType<BlokConfig['documentId']>, default: undefined },
113
- /** Live sync between tabs of this browser. On by default. */
114
- tabSync: { type: [Boolean, Object] as PropType<BlokConfig['tabSync']>, default: undefined },
115
- /** Opt-in: clicks on the host page below the editor append a block. */
116
- captureClicksBelowEditor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
117
- /** Changing this prop's identity destroys and recreates the editor. */
118
- recreateKey: { type: null as unknown as PropType<unknown> },
119
- },
120
- emits: {
121
- ready: (_instance: unknown): boolean => true,
122
- change: (_payload: { api: API; event: BlockMutationEvent | BlockMutationEvent[] }): boolean => true,
123
- save: (_data: OutputData): boolean => true,
124
- 'update:data': (_data: OutputData): boolean => true,
125
- 'after-render': (_api: API): boolean => true,
126
- 'theme-change': (_theme: ResolvedTheme): boolean => true,
127
- 'blocks-rendered': (_payload: unknown): boolean => true,
128
- 'block-rendered': (_payload: unknown): boolean => true,
129
- },
130
- setup(props, { emit, expose, attrs }) {
131
- const instance = getCurrentInstance();
132
-
133
- /**
134
- * The `onXxx` keys currently on the component's vnode, as a sorted, stable
135
- * string. Emit-mapped listeners (`@change`, `@save`, `v-model:data`,
136
- * `@after-render`, …) are not declared props, so `vnode.props` is the only
137
- * place they exist.
138
- */
139
- const readListenerKeys = (): string => {
140
- const vnodeProps = instance?.vnode.props;
141
-
142
- if (vnodeProps == null) {
143
- return '';
144
- }
145
-
146
- return Object.keys(vnodeProps)
147
- .filter((key) => key.startsWith('on'))
148
- .sort()
149
- .join('|');
150
- };
151
-
152
- /**
153
- * Reactive mirror of `readListenerKeys()`. `vnode.props` is a plain object
154
- * Vue swaps out on each patch — reading it tracks nothing, so a host that
155
- * added or removed a listener WITHOUT also changing a reactive prop never
156
- * re-ran the config snapshot and the flip was silently lost (core reads
157
- * callback PRESENCE as intent: `onSave` arms the change pipeline).
158
- *
159
- * This ref is the missing reactive source. It is READ by `hasListener` (so
160
- * every watcher that snapshots the config depends on it) and WRITTEN only
161
- * from `onUpdated` — never from the snapshot itself, which would make the
162
- * read its own write and self-trigger an endless update loop. Storing the
163
- * key set rather than a counter makes "bump only on a genuine presence
164
- * change" structural: re-assigning an equal string is inert.
165
- *
166
- * One case stays out of reach, and it is Vue's, not the ref's:
167
- * `hasPropsChanged` skips keys that name a declared emit, so REPLACING one
168
- * emit listener with a different one while the vnode's prop count and every
169
- * other prop stay identical (`@change` out, `@save` in, nothing else moving)
170
- * skips the child update entirely — no hook fires anywhere. Anything that
171
- * also changes the prop count, including plain add/remove, updates.
172
- */
173
- const listenerKeys = ref(readListenerKeys());
174
-
175
- onUpdated(() => {
176
- listenerKeys.value = readListenerKeys();
177
- });
178
-
179
- /** Listener presence (the Vue analog of Angular's `output.observed`). */
180
- const hasListener = (handlerKey: string): boolean =>
181
- listenerKeys.value.split('|').includes(handlerKey);
182
-
183
- /**
184
- * Snapshot the props + gated emit-callbacks into a `UseBlokConfig`. App-wide
185
- * `provideBlok` defaults are merged in by `useBlok` (the lifecycle engine),
186
- * so this component stays thin and the escape-hatch path honors defaults too
187
- * — mirroring React, where only `useBlok` merges.
188
- */
189
- const buildConfig = (): UseBlokConfig => {
190
- const config: Record<string, unknown> = {};
191
-
192
- for (const key of BLOK_EDITOR_CONFIG_KEYS) {
193
- const value = props[key];
194
-
195
- if (value !== undefined) {
196
- config[key] = value;
197
- }
198
- }
199
-
200
- if (hasListener('onChange')) {
201
- config.onChange = (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
202
- emit('change', { api, event });
203
- };
204
- }
205
-
206
- // onSave makes core serialize on every change batch — wire it only when a
207
- // v-model:data binding or an explicit @save listener consumes it.
208
- if (hasListener('onSave') || hasListener('onUpdate:data')) {
209
- config.onSave = (data: OutputData): void => {
210
- // Vue drops emits once unmounted, but destroy()'s final save lands
211
- // after that, so call the listeners straight from the vnode.
212
- if (instance?.isUnmounted === true) {
213
- callVnodeListeners(instance.vnode.props, ['onUpdate:data', 'onSave'], data);
214
-
215
- return;
216
- }
217
-
218
- emit('update:data', data);
219
- emit('save', data);
220
- };
221
- }
222
-
223
- if (hasListener('onAfterRender')) {
224
- config.onAfterRender = (api: API): void => {
225
- emit('after-render', api);
226
- };
227
- }
228
-
229
- if (hasListener('onThemeChange')) {
230
- config.onThemeChange = (resolved: ResolvedTheme): void => {
231
- emit('theme-change', resolved);
232
- };
233
- }
234
-
235
- return config;
236
- };
237
-
238
- const editor = useBlok(
239
- () => buildConfig(),
240
- () => props.recreateKey
241
- );
242
-
243
- // Subscribe to the editor's rendered-lifecycle events once it exists (gated
244
- // on the emits being listened). `onCleanup` unsubscribes when the instance is
245
- // replaced or the component unmounts. `listenerKeys` is a source too, so a
246
- // listener added after mount subscribes instead of waiting for a recreate —
247
- // presence is re-read from scratch each run, and the cleanup keeps the
248
- // subscriptions balanced.
249
- watch([editor, listenerKeys], ([ed], _previous, onCleanup) => {
250
- if (ed === null) {
251
- return;
252
- }
253
-
254
- const subscriptions: Array<[string, (payload?: unknown) => void]> = [];
255
-
256
- if (hasListener('onBlocksRendered')) {
257
- const handler = (payload?: unknown): void => emit('blocks-rendered', payload);
258
-
259
- ed.on('blocks:rendered', handler);
260
- subscriptions.push(['blocks:rendered', handler]);
261
- }
262
-
263
- if (hasListener('onBlockRendered')) {
264
- const handler = (payload?: unknown): void => emit('block-rendered', payload);
265
-
266
- ed.on('block:rendered', handler);
267
- subscriptions.push(['block:rendered', handler]);
268
- }
269
-
270
- onCleanup(() => {
271
- for (const [name, handler] of subscriptions) {
272
- ed.off(name, handler);
273
- }
274
- });
275
- });
276
-
277
- // Emit `ready` once per editor, after the instance is published to the ref
278
- // (and to `expose` below), so a consumer reading the instance inside the
279
- // handler — or via a template ref — sees the live editor. The last-emitted
280
- // instance is held in an object to avoid `let` reassignment.
281
- const readyState: { last: typeof editor.value } = { last: null };
282
-
283
- watch(editor, (ed) => {
284
- if (ed !== null && ed !== readyState.last) {
285
- readyState.last = ed;
286
- emit('ready', ed);
287
- }
288
- });
289
-
290
- // Curated imperative facade (delegates to the live instance; no-ops until
291
- // ready). `instance` is live (null before ready, the Blok after).
292
- expose({
293
- instance: computed(() => editor.value),
294
- save: (): ReturnType<NonNullable<typeof editor.value>['save']> | undefined => editor.value?.save(),
295
- confirmLeave: (): Promise<boolean> => editor.value?.confirmLeave() ?? Promise.resolve(true),
296
- focus: (atEnd?: boolean): void => {
297
- editor.value?.focus(atEnd);
298
- },
299
- /**
300
- * Replace the editor content.
301
- *
302
- * Safe to mix with a controlled `data` prop: the rendered document
303
- * becomes the adapter's content baseline once the call lands, so a later
304
- * `data` change back to the previous document still re-renders instead of
305
- * being deduped against a baseline the editor no longer reflects.
306
- * Recorded only on the RESOLVED path — a failed render leaves the editor
307
- * on its previous content, and a baseline naming content it never showed
308
- * would dedupe away the correcting update.
309
- * @param data - the document to render
310
- */
311
- render: (data: OutputData): Promise<void> | undefined => {
312
- const ed = editor.value;
313
-
314
- if (ed === null) {
315
- return undefined;
316
- }
317
-
318
- return ed.render(data).then(() => {
319
- getContentBaseline(ed)?.markRendered(data);
320
- });
321
- },
322
- });
323
-
324
- return () => h(BlokContent, { editor: editor.value, ...attrs });
325
- },
326
- });
@@ -1,93 +0,0 @@
1
- // src/vue/block-portal-registry.ts
2
- import { markRaw, reactive, type Component } from 'vue';
3
-
4
- /**
5
- * Tool-config key carrying the editor's portal registry into a `createVueBlock`
6
- * tool. The tool is constructed by CORE (outside any Vue `setup`), so it cannot
7
- * `inject()` — `useBlok` injects the editor-scoped registry through each
8
- * vue-block tool's `config`, and the tool reads it here.
9
- */
10
- export const BLOK_PORTAL_REGISTRY_CONFIG_KEY = '__blokPortalRegistry';
11
-
12
- /**
13
- * One mounted Vue block in the portal registry. `hostEl` is the Blok-owned
14
- * `toolRenderedElement` (tagged `data-blok-mutation-free`) the component is
15
- * teleported into; `props` is a REACTIVE object so `setProps` updates the
16
- * rendered block in place (no remount).
17
- */
18
- export interface BlockPortalEntry {
19
- hostEl: HTMLElement;
20
- component: Component;
21
- props: Record<string, unknown>;
22
- }
23
-
24
- /**
25
- * Per-editor registry of Vue blocks. A single shared {@link BlockPortalHost}
26
- * renders one `<Teleport>` per entry, so every block lives in ONE render tree
27
- * (shared provide/inject + app plugins, one effect scope) — the TipTap
28
- * `VueNodeViewRenderer` pattern, not a Vue app per block.
29
- */
30
- export interface BlockPortalRegistry {
31
- /** Reactive set of entries the host iterates (keyed by block id). */
32
- readonly entries: ReadonlyMap<string, BlockPortalEntry>;
33
- /**
34
- * Register (or replace) the entry for `id`. Idempotent: a second register
35
- * for the same id replaces the entry rather than mounting a duplicate.
36
- */
37
- register(id: string, entry: BlockPortalEntry): void;
38
- /**
39
- * Remove the entry for `id`. Safe (no-op) when absent.
40
- *
41
- * Pass the caller's own `hostEl` to make the removal OWNERSHIP-CHECKED: the
42
- * entry is deleted only when it is still the one that host registered. Core
43
- * composes a replacement block (which registers under the SAME id) BEFORE it
44
- * destroys the block it replaces, so a superseded tool's `removed()`/
45
- * `destroy()` teardown arrives after the live entry is already in place —
46
- * without the check it deletes that live entry and the holder stays empty.
47
- */
48
- unregister(id: string, hostEl?: HTMLElement): void;
49
- /** Merge `props` into the live entry's reactive props (in-place update). */
50
- setProps(id: string, props: Record<string, unknown>): void;
51
- }
52
-
53
- /**
54
- * Create a fresh portal registry. One per editor instance (associated via the
55
- * registry map, mirroring the holder WeakMap), so multiple editors on a page
56
- * never cross-render each other's blocks.
57
- */
58
- export const createBlockPortalRegistry = (): BlockPortalRegistry => {
59
- // Vue's reactive Map tracks iteration (entries/size), so the host re-renders
60
- // on register/unregister.
61
- const entries = reactive(new Map<string, BlockPortalEntry>());
62
-
63
- return {
64
- entries: entries,
65
- register(id: string, entry: BlockPortalEntry): void {
66
- // markRaw so the reactive Map (which must still track add/delete) does NOT
67
- // deep-proxy the entry — a proxied component DEFINITION or DOM host would
68
- // break rendering ("received a Component made reactive"). Per-entry prop
69
- // reactivity, when needed, lives on the `props` object itself.
70
- // set() replaces an existing entry — idempotent, never duplicates.
71
- entries.set(id, markRaw(entry));
72
- },
73
- unregister(id: string, hostEl?: HTMLElement): void {
74
- // A superseded owner may not evict the entry that replaced it.
75
- if (hostEl !== undefined && entries.get(id)?.hostEl !== hostEl) {
76
- return;
77
- }
78
-
79
- entries.delete(id);
80
- },
81
- setProps(id: string, props: Record<string, unknown>): void {
82
- const entry = entries.get(id);
83
-
84
- if (entry === undefined) {
85
- return;
86
- }
87
-
88
- // Mutate the reactive props object in place so the mounted component
89
- // re-renders without being remounted (preserving its internal state).
90
- Object.assign(entry.props, props);
91
- },
92
- };
93
- };
@@ -1,17 +0,0 @@
1
- // src/vue/blocks-snapshot.ts
2
- //
3
- // The block-tree data types, pure flat-index resolvers, AND the `UseBlocksApi`
4
- // connection contract are framework-agnostic and live in the shared core module
5
- // (`components/utils/blocks-tree`). The Vue `useBlocks` is a thin reactivity
6
- // wrapper over the shared `createBlocksApiForEditor`, so it exposes the IDENTICAL
7
- // surface as the React adapter — this file re-exports those shapes (no Vue-local
8
- // redefinition, so the two adapters cannot drift).
9
- export type {
10
- BlockNode,
11
- CaretTarget,
12
- InsertPosition,
13
- InsertSpec,
14
- TreeInsertSpec,
15
- MoveTarget,
16
- UseBlocksApi,
17
- } from '@bloklabs/core/adapters';
@@ -1,40 +0,0 @@
1
- import { inject, shallowRef, type InjectionKey, type ShallowRef } from 'vue';
2
- import type { Blok } from '@/types';
3
-
4
- /**
5
- * Injection key carrying the LIVE editor instance down to everything
6
- * `BlokContent` renders — which, because `<Teleport>` preserves the COMPONENT
7
- * render context (not the DOM target's), includes every `createVueBlock` block
8
- * mounted through the editor's shared `BlockPortalHost`.
9
- *
10
- * The provided ref is null until the instance exists, matching `useBlok`'s own
11
- * pre-ready contract.
12
- */
13
- export const BLOK_EDITOR_INSTANCE: InjectionKey<Readonly<ShallowRef<Blok | null>>> =
14
- Symbol('BLOK_EDITOR_INSTANCE');
15
-
16
- /** Stable "no editor here" ref for components rendered outside a BlokContent. */
17
- const NO_EDITOR: Readonly<ShallowRef<Blok | null>> = shallowRef(null);
18
-
19
- /**
20
- * The live Blok instance a component is rendered inside, as a ref, or a ref of
21
- * null before it exists. Inside a `createVueBlock` block this is the block's OWN
22
- * editor — so a block can drive the tree it lives in without the host
23
- * prop-drilling the instance into it:
24
- *
25
- * ```ts
26
- * const blocks = useBlocks(useBlokInstance());
27
- * const children = computed(() => blocks.getChildren(ctx.block.id));
28
- * ```
29
- *
30
- * That pairing is also what makes a container block REACTIVE to its own child
31
- * tree: `useBlocks` refreshes on the editor's `block changed` event, which core
32
- * emits for every structural mutation — including children the adapter itself
33
- * never sees (a pasted paragraph, a Tab-indent from the keyboard).
34
- *
35
- * Call it inside `setup`. Outside a `BlokContent`/`BlokEditor` subtree it
36
- * returns a ref of null.
37
- */
38
- export function useBlokInstance(): Readonly<ShallowRef<Blok | null>> {
39
- return inject(BLOK_EDITOR_INSTANCE, NO_EDITOR);
40
- }