@bloklabs/vue 1.7.0 → 1.9.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/useBlok.ts
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
type Ref,
|
|
11
11
|
} from 'vue';
|
|
12
12
|
import { Blok as BlokRuntime } from '@bloklabs/core';
|
|
13
|
+
import { setContentBaseline, removeContentBaseline } from './content-baseline-map';
|
|
13
14
|
import { setHolder, removeHolder } from './holder-map';
|
|
14
|
-
import { deepEqual , normalizeReadOnlyConfig, toRenderableData } from '@bloklabs/core/adapters';
|
|
15
|
+
import { deepEqual, equalsOutputData, normalizeReadOnlyConfig, toRenderableData } from '@bloklabs/core/adapters';
|
|
15
16
|
import { BLOK_DEFAULT_CONFIG, mergeBlokDefaults } from './provide-blok';
|
|
16
17
|
import {
|
|
17
18
|
createBlockPortalRegistry,
|
|
@@ -19,9 +20,29 @@ import {
|
|
|
19
20
|
type BlockPortalRegistry,
|
|
20
21
|
} from './block-portal-registry';
|
|
21
22
|
import { setRegistry, removeRegistry } from './registry-map';
|
|
22
|
-
import type { Blok, BlokConfig, LooseOutputData, OutputData } from '@/types';
|
|
23
|
+
import type { Blok, BlokConfig, LiveHandlers, LooseOutputData, OutputData } from '@/types';
|
|
23
24
|
import type { UseBlokConfig } from './types';
|
|
24
25
|
|
|
26
|
+
/** Presence map for the live callback config (see the handler watcher below). */
|
|
27
|
+
type HandlerPresence = Record<keyof LiveHandlers, boolean>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Which live callbacks the config currently carries. PRESENCE is load-bearing
|
|
31
|
+
* in core — an `onSubmit` turns Enter into serialize-and-submit, an `onSave`
|
|
32
|
+
* arms the change-observation pipeline — so it is tracked separately from the
|
|
33
|
+
* callbacks themselves (whose identity the adapter keeps stable).
|
|
34
|
+
* @param config - the merged per-render config
|
|
35
|
+
* @returns one boolean per live handler key
|
|
36
|
+
*/
|
|
37
|
+
const readHandlerPresence = (config: UseBlokConfig): HandlerPresence => ({
|
|
38
|
+
onChange: config.onChange !== undefined,
|
|
39
|
+
onSave: config.onSave !== undefined,
|
|
40
|
+
onEnter: config.onEnter !== undefined,
|
|
41
|
+
onSubmit: config.onSubmit !== undefined,
|
|
42
|
+
onBeforeRender: config.onBeforeRender !== undefined,
|
|
43
|
+
onAfterRender: config.onAfterRender !== undefined,
|
|
44
|
+
});
|
|
45
|
+
|
|
25
46
|
/**
|
|
26
47
|
* Inject the editor's portal registry into every `createVueBlock` tool's config
|
|
27
48
|
* (vanilla tools are left untouched), returning a NEW tools object so the
|
|
@@ -97,19 +118,60 @@ export function useBlok(
|
|
|
97
118
|
// - `lastRenderedData` is the content the editor currently reflects, updated
|
|
98
119
|
// when `data` is seeded/rendered AND when the editor emits its own serialized
|
|
99
120
|
// output via the `onSave` wrapper — so a controlled `update:data -> data`
|
|
100
|
-
// echo
|
|
121
|
+
// echo content-equals this baseline and is deduped to a no-op (no caret
|
|
122
|
+
// reset). `undefined` means "nothing recorded yet" — never an empty document.
|
|
101
123
|
const state: {
|
|
102
124
|
current: Blok | null;
|
|
103
125
|
holder: HTMLDivElement | null;
|
|
104
126
|
lastRenderedData: OutputData | LooseOutputData | null | undefined;
|
|
105
127
|
seededEditor: Blok | null;
|
|
106
128
|
renderChain: Promise<void>;
|
|
129
|
+
appliedHandlerPresence: HandlerPresence;
|
|
107
130
|
} = {
|
|
108
131
|
current: null,
|
|
109
132
|
holder: null,
|
|
110
133
|
lastRenderedData: mergedConfig().data,
|
|
111
134
|
seededEditor: null,
|
|
112
135
|
renderChain: Promise.resolve(),
|
|
136
|
+
appliedHandlerPresence: {
|
|
137
|
+
onChange: false,
|
|
138
|
+
onSave: false,
|
|
139
|
+
onEnter: false,
|
|
140
|
+
onSubmit: false,
|
|
141
|
+
onBeforeRender: false,
|
|
142
|
+
onAfterRender: false,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Stable wrappers for the live callback config. Each forwards to the LATEST
|
|
148
|
+
* config through `mergedConfig()`, so callback identity never goes stale and
|
|
149
|
+
* only PRESENCE has to be synced after mount (the watcher below).
|
|
150
|
+
*/
|
|
151
|
+
const handlerWrappers: Required<LiveHandlers> = {
|
|
152
|
+
onChange: (...args: Parameters<NonNullable<UseBlokConfig['onChange']>>): void => {
|
|
153
|
+
mergedConfig().onChange?.(...args);
|
|
154
|
+
},
|
|
155
|
+
onSave: (...args: Parameters<NonNullable<UseBlokConfig['onSave']>>): void => {
|
|
156
|
+
// Record the editor's own serialized output as the dedup baseline BEFORE
|
|
157
|
+
// notifying the consumer, so a controlled `update:data -> data` echo
|
|
158
|
+
// content-equals it and never re-renders.
|
|
159
|
+
state.lastRenderedData = args[0];
|
|
160
|
+
mergedConfig().onSave?.(...args);
|
|
161
|
+
},
|
|
162
|
+
// Forward the return value: it is the "handled" signal the core acts on.
|
|
163
|
+
onEnter: (...args: Parameters<NonNullable<UseBlokConfig['onEnter']>>): boolean | void =>
|
|
164
|
+
mergedConfig().onEnter?.(...args),
|
|
165
|
+
onSubmit: (...args: Parameters<NonNullable<UseBlokConfig['onSubmit']>>): void => {
|
|
166
|
+
mergedConfig().onSubmit?.(...args);
|
|
167
|
+
},
|
|
168
|
+
onBeforeRender: (
|
|
169
|
+
...args: Parameters<NonNullable<UseBlokConfig['onBeforeRender']>>
|
|
170
|
+
): ReturnType<NonNullable<UseBlokConfig['onBeforeRender']>> =>
|
|
171
|
+
mergedConfig().onBeforeRender?.(...args) ?? args[0],
|
|
172
|
+
onAfterRender: (...args: Parameters<NonNullable<UseBlokConfig['onAfterRender']>>): void => {
|
|
173
|
+
mergedConfig().onAfterRender?.(...args);
|
|
174
|
+
},
|
|
113
175
|
};
|
|
114
176
|
|
|
115
177
|
/**
|
|
@@ -137,6 +199,7 @@ export function useBlok(
|
|
|
137
199
|
if (state.current !== null) {
|
|
138
200
|
removeHolder(state.current);
|
|
139
201
|
removeRegistry(state.current);
|
|
202
|
+
removeContentBaseline(state.current);
|
|
140
203
|
try {
|
|
141
204
|
state.current.destroy();
|
|
142
205
|
} catch {
|
|
@@ -165,22 +228,37 @@ export function useBlok(
|
|
|
165
228
|
snapshot.tools = injectPortalRegistry(snapshot.tools, registry);
|
|
166
229
|
}
|
|
167
230
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
231
|
+
// Install the stable wrappers for the callbacks the consumer actually
|
|
232
|
+
// passed, and clear the rest: an absent handler must stay absent, because
|
|
233
|
+
// core reads PRESENCE as intent. What was installed is recorded so the
|
|
234
|
+
// watcher below can push later flips instead of recreating the editor.
|
|
235
|
+
const presence = readHandlerPresence(mergedConfig());
|
|
236
|
+
|
|
237
|
+
snapshot.onChange = presence.onChange ? handlerWrappers.onChange : undefined;
|
|
238
|
+
snapshot.onSave = presence.onSave ? handlerWrappers.onSave : undefined;
|
|
239
|
+
snapshot.onEnter = presence.onEnter ? handlerWrappers.onEnter : undefined;
|
|
240
|
+
snapshot.onSubmit = presence.onSubmit ? handlerWrappers.onSubmit : undefined;
|
|
241
|
+
snapshot.onBeforeRender = presence.onBeforeRender ? handlerWrappers.onBeforeRender : undefined;
|
|
242
|
+
snapshot.onAfterRender = presence.onAfterRender ? handlerWrappers.onAfterRender : undefined;
|
|
243
|
+
|
|
244
|
+
state.appliedHandlerPresence = presence;
|
|
178
245
|
|
|
179
246
|
const blok = new BlokRuntime({ ...snapshot, holder });
|
|
180
247
|
|
|
181
248
|
state.current = blok;
|
|
182
249
|
setHolder(blok, holder);
|
|
183
250
|
setRegistry(blok, registry);
|
|
251
|
+
// The out-of-band content channel: `<BlokEditor>`'s exposed `render()`
|
|
252
|
+
// changes the document without going through the reactive `data` watcher,
|
|
253
|
+
// so it reports the result here. Without it the watcher's baseline names a
|
|
254
|
+
// document the editor no longer shows, and a controlled `data` set back to
|
|
255
|
+
// that document is deduped away — leaving the editor on the imperative
|
|
256
|
+
// content while the host's state says otherwise.
|
|
257
|
+
setContentBaseline(blok, {
|
|
258
|
+
markRendered: (content): void => {
|
|
259
|
+
state.lastRenderedData = content;
|
|
260
|
+
},
|
|
261
|
+
});
|
|
184
262
|
|
|
185
263
|
void blok.isReady
|
|
186
264
|
.then(() => {
|
|
@@ -351,6 +429,63 @@ export function useBlok(
|
|
|
351
429
|
{ immediate: true }
|
|
352
430
|
);
|
|
353
431
|
|
|
432
|
+
// Live callback config. Callback presence was frozen at construction and IS
|
|
433
|
+
// the semantics in core (`onSubmit` makes Enter serialize-and-submit instead
|
|
434
|
+
// of splitting the block; `onSave` arms the change pipeline), so a `@save`
|
|
435
|
+
// listener or an "Enter sends" toggle added after mount used to need an
|
|
436
|
+
// editor recreate — losing caret and undo history. Diff presence against what
|
|
437
|
+
// is installed and push genuine flips through the runtime `handlers.set` API,
|
|
438
|
+
// writing `undefined` for a callback that disappeared so the change is
|
|
439
|
+
// reversible. Identity is never diffed: the wrappers forward to the latest
|
|
440
|
+
// config already.
|
|
441
|
+
watch(
|
|
442
|
+
[editor, () => JSON.stringify(readHandlerPresence(mergedConfig()))],
|
|
443
|
+
([ed]) => {
|
|
444
|
+
if (!ed) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const latest = readHandlerPresence(mergedConfig());
|
|
449
|
+
const applied = state.appliedHandlerPresence;
|
|
450
|
+
const diff: LiveHandlers = {};
|
|
451
|
+
const changed: { value: boolean } = { value: false };
|
|
452
|
+
|
|
453
|
+
const sync = <K extends keyof LiveHandlers>(key: K, write: (value: LiveHandlers[K]) => void): void => {
|
|
454
|
+
if (latest[key] === applied[key]) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
applied[key] = latest[key];
|
|
459
|
+
changed.value = true;
|
|
460
|
+
write(latest[key] ? handlerWrappers[key] : undefined);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
sync('onChange', (value) => {
|
|
464
|
+
diff.onChange = value;
|
|
465
|
+
});
|
|
466
|
+
sync('onSave', (value) => {
|
|
467
|
+
diff.onSave = value;
|
|
468
|
+
});
|
|
469
|
+
sync('onEnter', (value) => {
|
|
470
|
+
diff.onEnter = value;
|
|
471
|
+
});
|
|
472
|
+
sync('onSubmit', (value) => {
|
|
473
|
+
diff.onSubmit = value;
|
|
474
|
+
});
|
|
475
|
+
sync('onBeforeRender', (value) => {
|
|
476
|
+
diff.onBeforeRender = value;
|
|
477
|
+
});
|
|
478
|
+
sync('onAfterRender', (value) => {
|
|
479
|
+
diff.onAfterRender = value;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (changed.value) {
|
|
483
|
+
ed.handlers.set(diff);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
{ immediate: true }
|
|
487
|
+
);
|
|
488
|
+
|
|
354
489
|
// Reactive content. `data` seeds the editor at construction; afterwards a new
|
|
355
490
|
// *content* value re-renders via the public render() API — deep-equal-deduped
|
|
356
491
|
// against the baseline (so an unchanged reference, including the editor's own
|
|
@@ -371,7 +506,15 @@ export function useBlok(
|
|
|
371
506
|
return;
|
|
372
507
|
}
|
|
373
508
|
|
|
374
|
-
|
|
509
|
+
// Structural comparison (`equalsOutputData`, not a raw deep-equal): a host
|
|
510
|
+
// that persists the editor's own document and hands back a stripped copy —
|
|
511
|
+
// fresh `time`, dropped ids, no `lastEditedAt` stamp — is still echoing
|
|
512
|
+
// content the editor already shows, and re-rendering it would reset the
|
|
513
|
+
// caret for zero visual change. `undefined` means "nothing recorded yet",
|
|
514
|
+
// which is NOT an empty document, so it must never match.
|
|
515
|
+
const baseline = state.lastRenderedData;
|
|
516
|
+
|
|
517
|
+
if (baseline !== undefined && equalsOutputData(data, baseline)) {
|
|
375
518
|
return;
|
|
376
519
|
}
|
|
377
520
|
|
package/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
Blok,
|
|
5
5
|
BlockAPI,
|
|
6
6
|
BlockMutationEvent,
|
|
7
|
+
BlockOrigin,
|
|
7
8
|
BlockToolConstructable,
|
|
8
9
|
BlockToolData,
|
|
9
10
|
BlokConfig,
|
|
@@ -40,6 +41,13 @@ import type { MarkdownImportConfig } from '@bloklabs/core/markdown';
|
|
|
40
41
|
* forwarded — it only affects the initial locale resolution.
|
|
41
42
|
* - `data` — re-renders via `editor.render(value)` when content changes
|
|
42
43
|
* (deep-equal–deduped and serialized; seeds the initial content at creation)
|
|
44
|
+
* - `onChange`, `onSave`, `onEnter`, `onSubmit`, `onBeforeRender`,
|
|
45
|
+
* `onAfterRender` — calls `editor.handlers.set({ ... })` when a callback
|
|
46
|
+
* APPEARS or DISAPPEARS. Callback presence is load-bearing in core (an
|
|
47
|
+
* `onSubmit` turns Enter into serialize-and-submit; an `onSave` arms the
|
|
48
|
+
* change pipeline), so dropping the prop UNSETS the handler rather than
|
|
49
|
+
* leaving a stale one behind. Callback IDENTITY is always live — the adapter
|
|
50
|
+
* forwards to the latest render, so you never need to memoize them.
|
|
43
51
|
*
|
|
44
52
|
* All other config is consumed once at editor creation.
|
|
45
53
|
*/
|
|
@@ -117,7 +125,16 @@ export interface BlokEditorExposed {
|
|
|
117
125
|
save(): Promise<OutputData> | undefined;
|
|
118
126
|
/** Move the caret into the editor. */
|
|
119
127
|
focus(atEnd?: boolean): void;
|
|
120
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Replace the editor content (undefined until ready).
|
|
130
|
+
*
|
|
131
|
+
* Safe to mix with a controlled `data` prop: the rendered document becomes
|
|
132
|
+
* the adapter's content baseline once the call lands, so a later `data`
|
|
133
|
+
* change back to the previous document still re-renders instead of being
|
|
134
|
+
* deduped against a baseline the editor no longer reflects. Recorded only on
|
|
135
|
+
* the resolved path — a failed render leaves the editor on its previous
|
|
136
|
+
* content.
|
|
137
|
+
*/
|
|
121
138
|
render(data: OutputData): Promise<void> | undefined;
|
|
122
139
|
}
|
|
123
140
|
|
|
@@ -157,6 +174,21 @@ export declare function provideBlok(defaults: Partial<UseBlokConfig>): void;
|
|
|
157
174
|
*/
|
|
158
175
|
export declare function useBlokDefaults(): Partial<UseBlokConfig>;
|
|
159
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Injection key carrying the LIVE editor instance down to everything
|
|
179
|
+
* `BlokContent` renders — including every `createVueBlock` block, because
|
|
180
|
+
* `<Teleport>` preserves the component render context.
|
|
181
|
+
*/
|
|
182
|
+
export declare const BLOK_EDITOR_INSTANCE: InjectionKey<Readonly<ShallowRef<Blok | null>>>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The live Blok instance a component is rendered inside, as a ref (null before
|
|
186
|
+
* it exists). Inside a `createVueBlock` block this is the block's OWN editor, so
|
|
187
|
+
* `useBlocks(useBlokInstance())` works with no host plumbing — and makes a
|
|
188
|
+
* container block reactive to its own child tree. Call it inside `setup`.
|
|
189
|
+
*/
|
|
190
|
+
export declare function useBlokInstance(): Readonly<ShallowRef<Blok | null>>;
|
|
191
|
+
|
|
160
192
|
/**
|
|
161
193
|
* A plain, serializable view of one block in the tree. Snapshot-volatile: every
|
|
162
194
|
* read allocates a fresh node and `contentIds` is derived per read — depend on
|
|
@@ -502,6 +534,23 @@ export interface PropSchemaEntry {
|
|
|
502
534
|
*/
|
|
503
535
|
export type PropSchema = Record<string, PropSchemaEntry>;
|
|
504
536
|
|
|
537
|
+
/**
|
|
538
|
+
* One child's decoration: attribute name -> value. `null`/`undefined` removes
|
|
539
|
+
* the attribute; a boolean or number is stringified (so `false` writes
|
|
540
|
+
* `data-active="false"`, which CSS can select, rather than dropping the hook).
|
|
541
|
+
*/
|
|
542
|
+
export type ChildAttributes = Record<string, string | number | boolean | null | undefined>;
|
|
543
|
+
|
|
544
|
+
/** The `BlockChildren` per-child decorator (see {@link VueBlockRenderProps.BlockChildren}). */
|
|
545
|
+
export type ChildAttributesFn = (child: BlockAPI, index: number) => ChildAttributes;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Every STATIC member of core's block-tool contract a Vue block may declare for
|
|
549
|
+
* itself. `toolbox` and `isReadOnlySupported` are excluded — the factory owns
|
|
550
|
+
* them.
|
|
551
|
+
*/
|
|
552
|
+
export type BlockToolStatics = Omit<BlockToolConstructable, 'toolbox' | 'isReadOnlySupported'>;
|
|
553
|
+
|
|
505
554
|
/** Context handed to a Vue block's `setup` (the only data write path is `commit`). */
|
|
506
555
|
export interface VueBlockRenderProps<Data> {
|
|
507
556
|
/** Reactive, frozen snapshot of the block data. Read `data.value`; never mutate. */
|
|
@@ -510,6 +559,12 @@ export interface VueBlockRenderProps<Data> {
|
|
|
510
559
|
commit: (patch: Partial<Data>) => void;
|
|
511
560
|
/** This block's per-block API. */
|
|
512
561
|
block: BlockAPI;
|
|
562
|
+
/**
|
|
563
|
+
* The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`…) —
|
|
564
|
+
* the same object a vanilla tool receives. For a reactive view of the tree
|
|
565
|
+
* pair `useBlocks` with `useBlokInstance()` instead; the api is not reactive.
|
|
566
|
+
*/
|
|
567
|
+
api: API;
|
|
513
568
|
/**
|
|
514
569
|
* Reactive read-only flag. Read `readOnly.value` in render to disable editing.
|
|
515
570
|
* Toggled IN PLACE by core's read-only switch (no remount, ephemeral state
|
|
@@ -517,7 +572,13 @@ export interface VueBlockRenderProps<Data> {
|
|
|
517
572
|
* editor is read-only.
|
|
518
573
|
*/
|
|
519
574
|
readOnly: Readonly<ShallowRef<boolean>>;
|
|
520
|
-
/**
|
|
575
|
+
/**
|
|
576
|
+
* Engine-owned child slot — render `h(BlockChildren)` for a container block.
|
|
577
|
+
* Accepts one optional prop, `childAttributes: ChildAttributesFn`, applied to
|
|
578
|
+
* each child's HOLDER after mounting (named hooks instead of positional
|
|
579
|
+
* `:nth-child()` CSS). Holders stay DIRECT children of the slot; attributes
|
|
580
|
+
* the callback stops producing are removed on the next pass.
|
|
581
|
+
*/
|
|
521
582
|
BlockChildren: Component;
|
|
522
583
|
}
|
|
523
584
|
|
|
@@ -527,11 +588,87 @@ export interface CreateVueBlockSpec<Data extends BlockToolData = BlockToolData>
|
|
|
527
588
|
toolbox?: ToolboxConfig;
|
|
528
589
|
propSchema: PropSchema;
|
|
529
590
|
setup: (props: VueBlockRenderProps<Data>) => () => unknown;
|
|
591
|
+
/**
|
|
592
|
+
* Static members of core's tool contract, forwarded verbatim onto the
|
|
593
|
+
* generated tool class (`ownsChildren`, `keepsChildrenOnEnter`,
|
|
594
|
+
* `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…).
|
|
595
|
+
*
|
|
596
|
+
* `keepsChildrenOnEnter` is the per-tool Enter policy: declare it and Enter on
|
|
597
|
+
* this container's empty LAST child creates the new line INSIDE it instead of
|
|
598
|
+
* escaping to the container's parent (Blok's default, which is Notion's
|
|
599
|
+
* callout behaviour). It is policy the DOM cannot express — a callout renders
|
|
600
|
+
* the same nested-blocks slot as a column yet wants the escape — so a layout
|
|
601
|
+
* container (a card, a `steps` block) had to hijack the editor-global
|
|
602
|
+
* `config.onEnter` before it existed.
|
|
603
|
+
*/
|
|
604
|
+
statics?: BlockToolStatics;
|
|
605
|
+
/**
|
|
606
|
+
* The element the +/drag toolbar should vertically center on — core's
|
|
607
|
+
* `getToolbarAnchorElement` hook, resolved against the block's host on every
|
|
608
|
+
* call. Return `null`/`undefined` to keep core's default positioning.
|
|
609
|
+
*/
|
|
610
|
+
getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined;
|
|
530
611
|
onRendered?: (block: BlockAPI) => void;
|
|
612
|
+
/**
|
|
613
|
+
* Fired ONCE per block instance, after the teleport's FIRST commit — the
|
|
614
|
+
* first moment this block's rendered DOM exists (and, for a container, the
|
|
615
|
+
* moment `BlockChildren` has adopted the child holders). `onRendered` cannot
|
|
616
|
+
* mean that: core calls `rendered()` in the same tick as `render()`, before
|
|
617
|
+
* Vue has drawn anything.
|
|
618
|
+
*
|
|
619
|
+
* It is also the create-vs-restore signal: `context.origin` says whether the
|
|
620
|
+
* author just made this block (`user`/`api`/`convert`) or the document is
|
|
621
|
+
* being re-materialised (`load`/`replay`/`paste`), so a container can seed its
|
|
622
|
+
* default children here exactly once.
|
|
623
|
+
*
|
|
624
|
+
* To observe OTHER blocks settling — e.g. before placing the caret in a child
|
|
625
|
+
* you just inserted — listen for the `block:childrenMounted` editor event
|
|
626
|
+
* instead: `api.events.on('block:childrenMounted', ({ blockId, childIds }) => …)`.
|
|
627
|
+
*/
|
|
628
|
+
onMounted?: (block: BlockAPI, context: VueBlockMountedContext) => void;
|
|
629
|
+
/**
|
|
630
|
+
* The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE
|
|
631
|
+
* per block instance, after the teleport's first commit, and only when the
|
|
632
|
+
* author just made this block (`origin` of `user`, `api` or `convert`) —
|
|
633
|
+
* never for a `load`/`replay`/`paste` restore, and never for the off-tree
|
|
634
|
+
* `probe` instance core builds to read a tool's default data.
|
|
635
|
+
*
|
|
636
|
+
* Prefer it over hand-testing `context.origin` in `onMounted`: the intuitive
|
|
637
|
+
* `origin === 'user'` check is wrong — it drops `api.blocks.insert(...)` and
|
|
638
|
+
* turn-into, so the container comes up empty for every path except a
|
|
639
|
+
* keystroke.
|
|
640
|
+
* @example
|
|
641
|
+
* ```ts
|
|
642
|
+
* onCreated: (block, { api }) => {
|
|
643
|
+
* if (block.getChildren().length === 0) {
|
|
644
|
+
* api.blocks.insertInsideParent(block.id);
|
|
645
|
+
* }
|
|
646
|
+
* }
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
onCreated?: (block: BlockAPI, context: VueBlockMountedContext) => void;
|
|
531
650
|
onMoved?: (block: BlockAPI) => void;
|
|
532
651
|
onRemoved?: (block: BlockAPI) => void;
|
|
533
652
|
}
|
|
534
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Second argument of {@link CreateVueBlockSpec.onMounted} and
|
|
656
|
+
* {@link CreateVueBlockSpec.onCreated}.
|
|
657
|
+
*/
|
|
658
|
+
export interface VueBlockMountedContext {
|
|
659
|
+
/**
|
|
660
|
+
* Why this block instance was constructed. CREATION origins (`user`, `api`,
|
|
661
|
+
* `convert`) mean the author just made it, so seeding default children is
|
|
662
|
+
* correct; RESTORE origins (`load`, `replay`, `paste`) mean the document
|
|
663
|
+
* already says what the children are. `probe` is an off-tree instance built
|
|
664
|
+
* only to read a tool's default data and must not touch the block tree.
|
|
665
|
+
* Defaults to `'api'`.
|
|
666
|
+
*/
|
|
667
|
+
origin: BlockOrigin;
|
|
668
|
+
/** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */
|
|
669
|
+
api: API;
|
|
670
|
+
}
|
|
671
|
+
|
|
535
672
|
/**
|
|
536
673
|
* Author a first-party Vue block: returns a `BlockToolConstructable` registered
|
|
537
674
|
* exactly like a vanilla tool (`tools: { type: { class: createVueBlock(...) } }`).
|