@bloklabs/vue 1.6.2 → 1.8.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/dist/index.cjs +1 -1
- package/dist/index.mjs +328 -160
- package/package.json +2 -2
- package/src/BlokContent.ts +24 -1
- package/src/BlokEditor.ts +78 -8
- package/src/block-portal-registry.ts +17 -3
- package/src/blok-instance.ts +40 -0
- package/src/config-keys.ts +1 -0
- package/src/content-baseline-map.ts +38 -0
- package/src/createVueBlock.ts +389 -27
- package/src/index.ts +5 -0
- package/src/useBlok.ts +157 -14
- package/types/index.d.ts +139 -2
package/src/createVueBlock.ts
CHANGED
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
type VNodeChild,
|
|
15
15
|
} from 'vue';
|
|
16
16
|
|
|
17
|
+
import type { API } from '@/types';
|
|
17
18
|
import type { BlockAPI } from '@/types/api';
|
|
18
19
|
import type {
|
|
20
|
+
BlockOrigin,
|
|
21
|
+
BlockToolConstructable,
|
|
19
22
|
BlockToolConstructorOptions,
|
|
20
23
|
BlockToolData,
|
|
21
24
|
ToolboxConfig,
|
|
22
25
|
} from '@/types/tools';
|
|
23
|
-
import { DATA_ATTR , deepEqual , mountChildBlocks } from '@bloklabs/core/adapters';
|
|
26
|
+
import { BlockChildrenMounted, DATA_ATTR , deepEqual , mountChildBlocks } from '@bloklabs/core/adapters';
|
|
24
27
|
import { fillDefaults, type PropSchema } from '@bloklabs/core/adapters';
|
|
25
28
|
|
|
26
29
|
import {
|
|
@@ -30,6 +33,121 @@ import {
|
|
|
30
33
|
|
|
31
34
|
export type { PropSchema, PropSchemaEntry } from '@bloklabs/core/adapters';
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Every STATIC member of core's block-tool contract a Vue block may declare for
|
|
38
|
+
* itself — `ownsChildren`, `keepsChildrenOnEnter`, `conversionConfig`,
|
|
39
|
+
* `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`, and whatever core adds
|
|
40
|
+
* next. Derived from
|
|
41
|
+
* `BlockToolConstructable` rather than enumerated, so a new core static needs no
|
|
42
|
+
* adapter change to become reachable.
|
|
43
|
+
*
|
|
44
|
+
* `toolbox` and `isReadOnlySupported` are excluded because the factory owns
|
|
45
|
+
* them: `toolbox` is authored as {@link CreateVueBlockSpec.toolbox}, and
|
|
46
|
+
* in-place read-only support is unconditional.
|
|
47
|
+
*/
|
|
48
|
+
export type BlockToolStatics = Omit<BlockToolConstructable, 'toolbox' | 'isReadOnlySupported'>;
|
|
49
|
+
|
|
50
|
+
/** Statics the generated class owns; an authored `statics` bag can never take them over. */
|
|
51
|
+
const RESERVED_STATICS: readonly string[] = ['toolbox', 'isReadOnlySupported', '__isBlokVueBlock'];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Origins that mean "the author just made this block" — the only ones that fire
|
|
55
|
+
* {@link CreateVueBlockSpec.onCreated}. Written as an allow-list so a future
|
|
56
|
+
* core origin fails CLOSED (no creation signal) instead of silently opting in.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` is included because core always supplies an origin: an absent one
|
|
59
|
+
* means a host hand-built the constructor options, which is an explicit
|
|
60
|
+
* creation — the same reading core's own container tools apply.
|
|
61
|
+
*
|
|
62
|
+
* Note what is NOT the axis here: `origin === 'user'`. A container gated on that
|
|
63
|
+
* alone would refuse to seed for `api.blocks.insert('steps')` and for a
|
|
64
|
+
* turn-into, leaving an empty, unusable container.
|
|
65
|
+
*/
|
|
66
|
+
const CREATION_ORIGINS: ReadonlySet<BlockOrigin | undefined> = new Set<BlockOrigin | undefined>([
|
|
67
|
+
undefined,
|
|
68
|
+
'user',
|
|
69
|
+
'api',
|
|
70
|
+
'convert',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One child's decoration: attribute name → value. `null`/`undefined` removes the
|
|
75
|
+
* attribute; a boolean or number is stringified (so `false` writes
|
|
76
|
+
* `data-active="false"`, which CSS can select, rather than dropping the hook).
|
|
77
|
+
*/
|
|
78
|
+
export type ChildAttributes = Record<string, string | number | boolean | null | undefined>;
|
|
79
|
+
|
|
80
|
+
/** The `BlockChildren` per-child decorator (see {@link VueBlockRenderProps.BlockChildren}). */
|
|
81
|
+
export type ChildAttributesFn = (child: BlockAPI, index: number) => ChildAttributes;
|
|
82
|
+
|
|
83
|
+
/** What the previous decoration pass wrote for one child, so it can be undone. */
|
|
84
|
+
interface StampedChild {
|
|
85
|
+
holder: HTMLElement;
|
|
86
|
+
names: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply one pass of per-child holder attributes and clear the previous pass's
|
|
91
|
+
* leftovers — including on a child that has since left the container, which
|
|
92
|
+
* would otherwise keep a dead index forever.
|
|
93
|
+
* @param stamped - the slot's ledger of what the last pass wrote (mutated)
|
|
94
|
+
* @param children - the container's model children, in model order
|
|
95
|
+
* @param decorate - the authored per-child decorator, if any
|
|
96
|
+
*/
|
|
97
|
+
const applyChildAttributes = (
|
|
98
|
+
stamped: Map<string, StampedChild>,
|
|
99
|
+
children: BlockAPI[],
|
|
100
|
+
decorate: ChildAttributesFn | undefined
|
|
101
|
+
): void => {
|
|
102
|
+
const next = new Map<string, StampedChild>();
|
|
103
|
+
|
|
104
|
+
children.forEach((child, index) => {
|
|
105
|
+
const names: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const [name, value] of Object.entries(decorate?.(child, index) ?? {})) {
|
|
108
|
+
if (value === null || value === undefined) {
|
|
109
|
+
child.holder.removeAttribute(name);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
child.holder.setAttribute(name, String(value));
|
|
114
|
+
names.push(name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
next.set(child.id, { holder: child.holder, names });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (const [id, previous] of stamped) {
|
|
121
|
+
const current = next.get(id);
|
|
122
|
+
|
|
123
|
+
previous.names
|
|
124
|
+
.filter(name => current === undefined || !current.names.includes(name))
|
|
125
|
+
.forEach(name => previous.holder.removeAttribute(name));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
stamped.clear();
|
|
129
|
+
next.forEach((entry, id) => stamped.set(id, entry));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Announce that a container's child holders have settled in its slot — the
|
|
134
|
+
* signal a host waits on before putting the caret into a freshly inserted
|
|
135
|
+
* child, since the teleport commits a tick after core's `rendered()`.
|
|
136
|
+
*
|
|
137
|
+
* Optional-chained: the editor always supplies `events`, but a host unit test
|
|
138
|
+
* may hand a block a partial `api`, and an observability signal must never be
|
|
139
|
+
* able to break a render.
|
|
140
|
+
* @param api - the editor-level API
|
|
141
|
+
* @param blockId - the container block's id
|
|
142
|
+
* @param children - the container's model children, in model order
|
|
143
|
+
*/
|
|
144
|
+
const emitChildrenMounted = (api: API, blockId: string, children: BlockAPI[]): void => {
|
|
145
|
+
api?.events?.emit(BlockChildrenMounted, {
|
|
146
|
+
blockId,
|
|
147
|
+
childIds: children.map(child => child.id),
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
33
151
|
/** Context handed to a Vue block's `setup` (the only data write path is `commit`). */
|
|
34
152
|
export interface VueBlockRenderProps<Data> {
|
|
35
153
|
/** Reactive, FROZEN snapshot of the block data. Read `data.value`; never mutate. */
|
|
@@ -38,6 +156,17 @@ export interface VueBlockRenderProps<Data> {
|
|
|
38
156
|
commit: (patch: Partial<Data>) => void;
|
|
39
157
|
/** This block's per-block API (id, connection methods, dispatchChange…). */
|
|
40
158
|
block: BlockAPI;
|
|
159
|
+
/**
|
|
160
|
+
* The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`,
|
|
161
|
+
* `api.toolbar`…) — the same object a vanilla tool receives in its
|
|
162
|
+
* constructor. Reach for it when a block has to drive the document around it
|
|
163
|
+
* instead of routing everything through `block.call()` string dispatch.
|
|
164
|
+
*
|
|
165
|
+
* For the reactive, id/parentId-relative view of the tree (and to re-render
|
|
166
|
+
* when your own children change), pair `useBlocks` with `useBlokInstance()`
|
|
167
|
+
* instead — the api handle itself is not reactive.
|
|
168
|
+
*/
|
|
169
|
+
api: API;
|
|
41
170
|
/**
|
|
42
171
|
* Reactive read-only flag. Read `readOnly.value` in render to disable editing
|
|
43
172
|
* (e.g. drop `contenteditable`, hide controls). Toggled IN PLACE by core's
|
|
@@ -46,10 +175,46 @@ export interface VueBlockRenderProps<Data> {
|
|
|
46
175
|
* the editor is read-only (same contract as a vanilla tool's `setReadOnly`).
|
|
47
176
|
*/
|
|
48
177
|
readOnly: Readonly<ShallowRef<boolean>>;
|
|
49
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Engine-owned child slot — render `h(BlockChildren)` for a container block.
|
|
180
|
+
*
|
|
181
|
+
* Accepts one optional prop, `childAttributes: (child, index) => ChildAttributes`,
|
|
182
|
+
* applied to each child's HOLDER after the holders are mounted. Named hooks
|
|
183
|
+
* (`data-step-index`, `data-active`…) replace positional `:nth-child()` CSS
|
|
184
|
+
* over Blok's holders, which breaks the moment a child is inserted, removed or
|
|
185
|
+
* reordered. The holders stay DIRECT children of the slot — core requires that
|
|
186
|
+
* (hierarchy reparenting and caret navigation compare `holder.parentElement`
|
|
187
|
+
* by identity), so decoration is attributes, never wrapper elements. Attributes
|
|
188
|
+
* the callback stops producing are removed on the next pass.
|
|
189
|
+
*
|
|
190
|
+
* Writing on a child's holder is inert by design: core's mutation filter drops
|
|
191
|
+
* a holder-targeted attribute record for the child block and suppresses it for
|
|
192
|
+
* the container. The guarantee stops at the holder and its
|
|
193
|
+
* `[data-blok-element-content]` wrapper — writing AT or BELOW a child's tool
|
|
194
|
+
* root DOES score as that child's edit.
|
|
195
|
+
*/
|
|
50
196
|
BlockChildren: Component;
|
|
51
197
|
}
|
|
52
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Second argument of {@link CreateVueBlockSpec.onMounted} and
|
|
201
|
+
* {@link CreateVueBlockSpec.onCreated} — everything the block cannot read off
|
|
202
|
+
* its own `BlockAPI`.
|
|
203
|
+
*/
|
|
204
|
+
export interface VueBlockMountedContext {
|
|
205
|
+
/**
|
|
206
|
+
* Why this block instance was constructed: a CREATION origin (`user`, `api`,
|
|
207
|
+
* `convert`) means the author just made it, so seeding default children is
|
|
208
|
+
* correct; a RESTORE origin (`load`, `replay`, `paste`) means the document
|
|
209
|
+
* already says what the children are. `probe` is an off-tree instance built
|
|
210
|
+
* only to read a tool's default data — it must not touch the block tree at
|
|
211
|
+
* all. Defaults to `'api'` when the constructor was handed no origin.
|
|
212
|
+
*/
|
|
213
|
+
origin: BlockOrigin;
|
|
214
|
+
/** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */
|
|
215
|
+
api: API;
|
|
216
|
+
}
|
|
217
|
+
|
|
53
218
|
/** Spec for {@link createVueBlock}. Authored as a `.ts` render function (no SFC). */
|
|
54
219
|
export interface CreateVueBlockSpec<Data = BlockToolData> {
|
|
55
220
|
/** Tool type name (registered key). */
|
|
@@ -60,8 +225,89 @@ export interface CreateVueBlockSpec<Data = BlockToolData> {
|
|
|
60
225
|
propSchema: PropSchema;
|
|
61
226
|
/** Returns a render function (the `defineComponent` + `setup`-returns-`h` shape). */
|
|
62
227
|
setup: (props: VueBlockRenderProps<Data>) => () => VNodeChild;
|
|
228
|
+
/**
|
|
229
|
+
* Static members of core's tool contract, forwarded verbatim onto the
|
|
230
|
+
* generated tool class — the single channel for everything core reads off the
|
|
231
|
+
* CLASS rather than the instance (`ownsChildren`, `keepsChildrenOnEnter`,
|
|
232
|
+
* `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…).
|
|
233
|
+
* Without it the only way to declare one was to subclass the generated class.
|
|
234
|
+
*
|
|
235
|
+
* `keepsChildrenOnEnter` is the per-tool Enter POLICY: declare it and Enter on
|
|
236
|
+
* this container's empty LAST child creates the new line INSIDE the container
|
|
237
|
+
* instead of escaping to the container's parent (Blok's default, which is
|
|
238
|
+
* Notion's callout behaviour). Core cannot read that off the DOM — a callout
|
|
239
|
+
* renders the same `data-blok-nested-blocks` slot as a column yet wants the
|
|
240
|
+
* escape — so before it existed a layout container (a card, a `steps` block)
|
|
241
|
+
* had to hijack the editor-global `config.onEnter` and re-derive containment.
|
|
242
|
+
*
|
|
243
|
+
* `toolbox` and `isReadOnlySupported` are owned by the factory and cannot be
|
|
244
|
+
* overridden here (see {@link BlockToolStatics}).
|
|
245
|
+
*/
|
|
246
|
+
statics?: BlockToolStatics;
|
|
247
|
+
/**
|
|
248
|
+
* The element the +/drag toolbar should vertically center on — core's
|
|
249
|
+
* `getToolbarAnchorElement` hook, resolved against this block's host element
|
|
250
|
+
* on every call (never cached, so it tracks re-renders).
|
|
251
|
+
*
|
|
252
|
+
* A container block whose own chrome is not editable needs it: with no anchor,
|
|
253
|
+
* core centers the toolbar on the first `[contenteditable]` under the host,
|
|
254
|
+
* which for a container is its FIRST CHILD BLOCK. Return `null`/`undefined`
|
|
255
|
+
* (or omit the field) to keep core's default.
|
|
256
|
+
* @param host - this block's mutation-free host element
|
|
257
|
+
* @param block - this block's per-block API
|
|
258
|
+
*/
|
|
259
|
+
getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined;
|
|
63
260
|
/** Optional lifecycle callbacks mapped from Blok's block hooks. */
|
|
64
261
|
onRendered?: (block: BlockAPI) => void;
|
|
262
|
+
/**
|
|
263
|
+
* Fired ONCE per block instance, after the teleport's FIRST commit — the
|
|
264
|
+
* first moment this block's rendered DOM exists (and, for a container, the
|
|
265
|
+
* moment `BlockChildren` has adopted the child holders). `onRendered` cannot
|
|
266
|
+
* mean that: core calls `rendered()` in the same tick as `render()`, before
|
|
267
|
+
* Vue has drawn anything, which is why hosts ended up setting the caret twice
|
|
268
|
+
* around a `requestAnimationFrame`.
|
|
269
|
+
*
|
|
270
|
+
* It is also the create-vs-restore signal: `context.origin` says whether the
|
|
271
|
+
* author just made this block (`user`/`api`/`convert`) or the document is
|
|
272
|
+
* being re-materialised (`load`/`replay`/`paste`) — so a container can seed
|
|
273
|
+
* its default children here exactly once, without the "children are
|
|
274
|
+
* transiently empty during a replay" trap.
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* onMounted: (block, { origin, api }) => {
|
|
278
|
+
* if (origin === 'user' && block.getChildren().length === 0) {
|
|
279
|
+
* api.blocks.insertInsideParent(block.id);
|
|
280
|
+
* }
|
|
281
|
+
* }
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
onMounted?: (block: BlockAPI, context: VueBlockMountedContext) => void;
|
|
285
|
+
/**
|
|
286
|
+
* The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE
|
|
287
|
+
* per block instance, after the teleport's first commit, and only when this
|
|
288
|
+
* instance is the author making a new block (`origin` of `user`, `api` or
|
|
289
|
+
* `convert`) — never for a `load`/`replay`/`paste` restore, and never for the
|
|
290
|
+
* off-tree `probe` instance core builds to read a tool's default data.
|
|
291
|
+
*
|
|
292
|
+
* That predicate is why the hook exists rather than leaving every block to
|
|
293
|
+
* read `context.origin` in `onMounted`: the intuitive `origin === 'user'` test
|
|
294
|
+
* is wrong. It drops `api.blocks.insert('steps')` and turn-into, so a
|
|
295
|
+
* container seeded that way comes up empty for every path except a keystroke.
|
|
296
|
+
* Core refused to ship that axis into its own `column`/`column_list`; this
|
|
297
|
+
* encodes the correct one once, here.
|
|
298
|
+
*
|
|
299
|
+
* The `context` is the same object {@link CreateVueBlockSpec.onMounted}
|
|
300
|
+
* receives, so a block that only seeds can read `origin` for finer decisions.
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* onCreated: (block, { api }) => {
|
|
304
|
+
* if (block.getChildren().length === 0) {
|
|
305
|
+
* api.blocks.insertInsideParent(block.id);
|
|
306
|
+
* }
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
onCreated?: (block: BlockAPI, context: VueBlockMountedContext) => void;
|
|
65
311
|
onMoved?: (block: BlockAPI) => void;
|
|
66
312
|
onRemoved?: (block: BlockAPI) => void;
|
|
67
313
|
}
|
|
@@ -90,30 +336,53 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
90
336
|
save(): BlockToolData;
|
|
91
337
|
setData(newData: BlockToolData): Promise<boolean>;
|
|
92
338
|
setReadOnly(state: boolean): void;
|
|
339
|
+
getToolbarAnchorElement(): HTMLElement | undefined;
|
|
93
340
|
rendered(): void;
|
|
94
341
|
moved(): void;
|
|
95
342
|
removed(): void;
|
|
96
343
|
destroy(): void;
|
|
97
|
-
}) & {
|
|
344
|
+
}) & BlockToolStatics & {
|
|
98
345
|
readonly __isBlokVueBlock: true;
|
|
99
346
|
readonly toolbox: ToolboxConfig | undefined;
|
|
100
347
|
readonly isReadOnlySupported: boolean;
|
|
101
348
|
} {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
349
|
+
/**
|
|
350
|
+
* One wrapped component definition per BLOCK INSTANCE.
|
|
351
|
+
*
|
|
352
|
+
* Per-TYPE looks equivalent — each instance gets its own context through the
|
|
353
|
+
* `ctx` prop — but the portal host keys its `<Teleport>` by BLOCK ID, and core
|
|
354
|
+
* composes a replacement block (registering under that same id) BEFORE it
|
|
355
|
+
* destroys the block it replaces. With a shared type, Vue sees same key + same
|
|
356
|
+
* type and PATCHES the mounted instance instead of replacing it: `setup` has
|
|
357
|
+
* already run and its render closure captured the SUPERSEDED instance's `ctx`,
|
|
358
|
+
* so the live host renders the old block's data forever. A distinct type per
|
|
359
|
+
* instance makes Vue replace the instance, so the replacement's ctx renders.
|
|
360
|
+
* Within one instance the type is stable, so in-place prop updates still never
|
|
361
|
+
* remount.
|
|
362
|
+
*/
|
|
363
|
+
const defineWrappedComponent = (): Component =>
|
|
364
|
+
defineComponent({
|
|
365
|
+
name: `VueBlock(${spec.type})`,
|
|
366
|
+
props: {
|
|
367
|
+
ctx: { type: Object as PropType<VueBlockRenderProps<Data>>, required: true },
|
|
368
|
+
/**
|
|
369
|
+
* Adapter-internal: the tool instance's post-commit notifier. Declared as
|
|
370
|
+
* a prop (not part of `ctx`) so it never reaches the authored `setup`.
|
|
371
|
+
*/
|
|
372
|
+
notifyMounted: { type: Function as PropType<() => void>, default: undefined },
|
|
373
|
+
},
|
|
374
|
+
setup(props) {
|
|
375
|
+
// The block's DOM exists from here on — this is the first commit core's
|
|
376
|
+
// `rendered()` hook could not wait for.
|
|
377
|
+
onMounted(() => props.notifyMounted?.());
|
|
378
|
+
|
|
379
|
+
// props is shallow-reactive: `props.ctx` is the raw context object, so the
|
|
380
|
+
// `data` ref inside it is NOT unwrapped (no reactive ref-unwrap gotcha).
|
|
381
|
+
return spec.setup(props.ctx);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const VueBlockTool = class VueBlockTool {
|
|
117
386
|
/** Marker so `useBlok` can detect vue-block tools and inject the registry. */
|
|
118
387
|
public static readonly __isBlokVueBlock = true as const;
|
|
119
388
|
|
|
@@ -132,12 +401,20 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
132
401
|
}
|
|
133
402
|
|
|
134
403
|
private readonly blockApi: BlockAPI;
|
|
404
|
+
/** The editor-level API, handed to the block setup as `ctx.api`. */
|
|
405
|
+
private readonly api: API;
|
|
135
406
|
private readonly registry: BlockPortalRegistry | undefined;
|
|
136
407
|
private readonly pointerDrag: () => boolean;
|
|
137
408
|
private readonly dataRef: ShallowRef<Readonly<Data>>;
|
|
138
409
|
/** Reactive read-only flag handed to setup; flipped in place by setReadOnly. */
|
|
139
410
|
private readonly readOnlyRef: ShallowRef<boolean>;
|
|
140
411
|
private readonly childrenComponent: Component;
|
|
412
|
+
/** This instance's own wrapper type — see {@link defineWrappedComponent}. */
|
|
413
|
+
private readonly wrappedComponent: Component = defineWrappedComponent();
|
|
414
|
+
/** Why core built this instance — gates `onCreated`, handed to `onMounted`. */
|
|
415
|
+
private readonly origin: BlockOrigin;
|
|
416
|
+
/** True once the post-mount hooks fired; a re-mounted teleport must not re-fire them. */
|
|
417
|
+
private mountSignalled = false;
|
|
141
418
|
private mirror: Readonly<Data>;
|
|
142
419
|
/** Dedup baseline (Risk R3/R6): skip a redundant render of identical data. */
|
|
143
420
|
private lastRendered: Readonly<Data>;
|
|
@@ -152,29 +429,49 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
152
429
|
|
|
153
430
|
this.registry = config[BLOK_PORTAL_REGISTRY_CONFIG_KEY] as BlockPortalRegistry | undefined;
|
|
154
431
|
|
|
432
|
+
this.api = options.api;
|
|
433
|
+
|
|
155
434
|
// Read the LIVE pointer-drag flag so a mid-drag commit can be deferred
|
|
156
435
|
// (core silently drops a dispatchChange while a drag is active).
|
|
157
|
-
const
|
|
436
|
+
const drag = options.api as unknown as { blocks?: { isPointerDragActive?: boolean } } | undefined;
|
|
158
437
|
|
|
159
|
-
this.pointerDrag = (): boolean =>
|
|
438
|
+
this.pointerDrag = (): boolean => drag?.blocks?.isPointerDragActive === true;
|
|
160
439
|
|
|
161
440
|
this.mirror = fillDefaults<Data>(spec.propSchema, toRaw(options.data ?? {}) as Record<string, unknown>);
|
|
162
441
|
this.lastRendered = this.mirror;
|
|
163
442
|
this.dataRef = shallowRef(this.mirror);
|
|
164
443
|
this.readOnlyRef = shallowRef(options.readOnly);
|
|
444
|
+
// Absent origin means a caller that predates the signal; 'api' is core's
|
|
445
|
+
// own default, so it is never mistaken for a user gesture.
|
|
446
|
+
this.origin = options.origin ?? 'api';
|
|
165
447
|
|
|
166
448
|
const blockApi = this.blockApi;
|
|
449
|
+
const api = this.api;
|
|
167
450
|
|
|
168
451
|
// Per-instance child slot: a childless ref'd div Blok owns; mountChildBlocks
|
|
169
452
|
// appends the real child holders imperatively (Vue never reconciles them).
|
|
170
453
|
this.childrenComponent = defineComponent({
|
|
171
454
|
name: `VueBlockChildren(${spec.type})`,
|
|
172
|
-
|
|
455
|
+
props: {
|
|
456
|
+
childAttributes: {
|
|
457
|
+
type: Function as PropType<ChildAttributesFn>,
|
|
458
|
+
default: undefined,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
setup(props) {
|
|
173
462
|
const slot = ref<HTMLElement | null>(null);
|
|
463
|
+
// What the last decoration pass wrote, so a dropped key is cleaned up.
|
|
464
|
+
const stamped = new Map<string, StampedChild>();
|
|
174
465
|
const mountKids = (): void => {
|
|
175
|
-
if (slot.value
|
|
176
|
-
|
|
466
|
+
if (slot.value === null) {
|
|
467
|
+
return;
|
|
177
468
|
}
|
|
469
|
+
|
|
470
|
+
const children = blockApi.getChildren();
|
|
471
|
+
|
|
472
|
+
mountChildBlocks(slot.value, children);
|
|
473
|
+
applyChildAttributes(stamped, children, props.childAttributes);
|
|
474
|
+
emitChildrenMounted(api, blockApi.id, children);
|
|
178
475
|
};
|
|
179
476
|
|
|
180
477
|
onMounted(mountKids);
|
|
@@ -198,14 +495,15 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
198
495
|
data: this.dataRef,
|
|
199
496
|
commit: this.commit,
|
|
200
497
|
block: this.blockApi,
|
|
498
|
+
api: this.api,
|
|
201
499
|
readOnly: this.readOnlyRef,
|
|
202
500
|
BlockChildren: this.childrenComponent,
|
|
203
501
|
};
|
|
204
502
|
|
|
205
503
|
this.registry?.register(this.blockApi.id, {
|
|
206
504
|
hostEl: host,
|
|
207
|
-
component:
|
|
208
|
-
props: { ctx },
|
|
505
|
+
component: this.wrappedComponent,
|
|
506
|
+
props: { ctx, notifyMounted: this.notifyMounted },
|
|
209
507
|
});
|
|
210
508
|
|
|
211
509
|
// Returned synchronously; Vue flushes the teleport on the next tick.
|
|
@@ -216,6 +514,30 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
216
514
|
spec.onRendered?.(this.blockApi);
|
|
217
515
|
}
|
|
218
516
|
|
|
517
|
+
/**
|
|
518
|
+
* The wrapped component's post-commit callback: the first moment this
|
|
519
|
+
* block's DOM (and, for a container, its mounted child holders) exists.
|
|
520
|
+
* Fires the post-mount hooks once per block instance — a teleport that
|
|
521
|
+
* re-mounts must not repeat a creation signal.
|
|
522
|
+
*/
|
|
523
|
+
private readonly notifyMounted = (): void => {
|
|
524
|
+
if (this.mountSignalled) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this.mountSignalled = true;
|
|
529
|
+
|
|
530
|
+
const context = { origin: this.origin, api: this.api };
|
|
531
|
+
|
|
532
|
+
spec.onMounted?.(this.blockApi, context);
|
|
533
|
+
|
|
534
|
+
// Creation-only, and after onMounted: a seeding hook must see whatever
|
|
535
|
+
// the mount hook already put in place.
|
|
536
|
+
if (CREATION_ORIGINS.has(this.origin)) {
|
|
537
|
+
spec.onCreated?.(this.blockApi, context);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
219
541
|
public save(): BlockToolData {
|
|
220
542
|
// The complete, frozen, toRaw-clean mirror — never the DOM, never partial.
|
|
221
543
|
return this.mirror;
|
|
@@ -253,19 +575,40 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
253
575
|
this.readOnlyRef.value = state;
|
|
254
576
|
}
|
|
255
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Core's toolbar-anchor hook. Always defined so the delegation is one
|
|
580
|
+
* `typeof … === 'function'` probe away, and always resolved fresh against
|
|
581
|
+
* the LIVE host — the anchor element is Vue-rendered, so it does not exist
|
|
582
|
+
* yet when the tool is constructed and may be replaced on any re-render. A
|
|
583
|
+
* spec without a resolver returns undefined, which is exactly what core's
|
|
584
|
+
* default positioning already assumes.
|
|
585
|
+
*/
|
|
586
|
+
public getToolbarAnchorElement(): HTMLElement | undefined {
|
|
587
|
+
const host = this.hostEl;
|
|
588
|
+
|
|
589
|
+
if (host === null || spec.getToolbarAnchorElement === undefined) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return spec.getToolbarAnchorElement(host, this.blockApi) ?? undefined;
|
|
594
|
+
}
|
|
595
|
+
|
|
256
596
|
public moved(): void {
|
|
257
597
|
// No remount: Blok relocates the host element; the teleport follows it.
|
|
258
598
|
spec.onMoved?.(this.blockApi);
|
|
259
599
|
}
|
|
260
600
|
|
|
261
601
|
public removed(): void {
|
|
262
|
-
|
|
602
|
+
// Ownership-scoped: core composes a REPLACEMENT block (which registers
|
|
603
|
+
// under the SAME id) before it tears this one down, so an unqualified
|
|
604
|
+
// unregister here would delete the live entry and blank the block.
|
|
605
|
+
this.registry?.unregister(this.blockApi.id, this.hostEl ?? undefined);
|
|
263
606
|
spec.onRemoved?.(this.blockApi);
|
|
264
607
|
}
|
|
265
608
|
|
|
266
609
|
public destroy(): void {
|
|
267
610
|
// Idempotent with removed(); unregister is safe when already absent.
|
|
268
|
-
this.registry?.unregister(this.blockApi.id);
|
|
611
|
+
this.registry?.unregister(this.blockApi.id, this.hostEl ?? undefined);
|
|
269
612
|
}
|
|
270
613
|
|
|
271
614
|
/**
|
|
@@ -323,4 +666,23 @@ export function createVueBlock<Data = BlockToolData>(
|
|
|
323
666
|
requestAnimationFrame(retry);
|
|
324
667
|
}
|
|
325
668
|
};
|
|
669
|
+
|
|
670
|
+
// Forward the authored statics onto the generated class. `defineProperty`
|
|
671
|
+
// (not assignment) because `toolbox`/`isReadOnlySupported` are accessors and a
|
|
672
|
+
// plain write would throw in strict mode; the reserved list keeps a stray bag
|
|
673
|
+
// from taking those — or the adapter's own marker — over.
|
|
674
|
+
for (const [key, value] of Object.entries(spec.statics ?? {})) {
|
|
675
|
+
if (RESERVED_STATICS.includes(key)) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
Object.defineProperty(VueBlockTool, key, {
|
|
680
|
+
value,
|
|
681
|
+
writable: true,
|
|
682
|
+
enumerable: true,
|
|
683
|
+
configurable: true,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return VueBlockTool;
|
|
326
688
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { useBlokReady } from './useBlokReady';
|
|
|
4
4
|
export { BlokContent } from './BlokContent';
|
|
5
5
|
export { BlokEditor } from './BlokEditor';
|
|
6
6
|
export { provideBlok, useBlokDefaults, BLOK_DEFAULT_CONFIG } from './provide-blok';
|
|
7
|
+
export { useBlokInstance, BLOK_EDITOR_INSTANCE } from './blok-instance';
|
|
7
8
|
export { createVueBlock } from './createVueBlock';
|
|
8
9
|
export type { UseBlokConfig, BlokContentProps } from './types';
|
|
9
10
|
export type { UseBlokReadyOptions } from './useBlokReady';
|
|
@@ -17,8 +18,12 @@ export type {
|
|
|
17
18
|
MoveTarget,
|
|
18
19
|
} from './blocks-snapshot';
|
|
19
20
|
export type {
|
|
21
|
+
BlockToolStatics,
|
|
22
|
+
ChildAttributes,
|
|
23
|
+
ChildAttributesFn,
|
|
20
24
|
CreateVueBlockSpec,
|
|
21
25
|
PropSchema,
|
|
22
26
|
PropSchemaEntry,
|
|
27
|
+
VueBlockMountedContext,
|
|
23
28
|
VueBlockRenderProps,
|
|
24
29
|
} from './createVueBlock';
|