@bloklabs/vue 1.16.1 → 2.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/README.md +20 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +146 -104
- package/package.json +3 -4
- package/types/index.d.ts +57 -3
- package/src/BlockPortalHost.ts +0 -46
- package/src/BlokContent.ts +0 -103
- package/src/BlokEditor.ts +0 -326
- package/src/block-portal-registry.ts +0 -93
- package/src/blocks-snapshot.ts +0 -17
- package/src/blok-instance.ts +0 -40
- package/src/config-keys.ts +0 -82
- package/src/content-baseline-map.ts +0 -38
- package/src/createVueBlock.ts +0 -740
- package/src/holder-map.ts +0 -19
- package/src/index.ts +0 -30
- package/src/provide-blok.ts +0 -65
- package/src/registry-map.ts +0 -21
- package/src/types.ts +0 -36
- package/src/useBlocks.ts +0 -182
- package/src/useBlok.ts +0 -686
- package/src/useBlokReady.ts +0 -70
package/src/BlockPortalHost.ts
DELETED
|
@@ -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
|
-
});
|
package/src/BlokContent.ts
DELETED
|
@@ -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
|
-
};
|
package/src/blocks-snapshot.ts
DELETED
|
@@ -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';
|
package/src/blok-instance.ts
DELETED
|
@@ -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
|
-
}
|