@jackuait/blok 0.23.3 → 0.23.5

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.
Files changed (34) hide show
  1. package/README.md +3 -3
  2. package/dist/blok.cjs +1 -1
  3. package/dist/blok.iife.js +14 -14
  4. package/dist/blok.mjs +2 -2
  5. package/dist/blok.umd.js +6 -6
  6. package/dist/chunks/{blok-J3Lfa_bK.cjs → blok-DixLFA0K.cjs} +5 -5
  7. package/dist/chunks/{blok-1H9roBGf.mjs → blok-TvOrZOjF.mjs} +2195 -2170
  8. package/dist/chunks/{constants-BZUc1kAY.cjs → constants-Bze_TXgc.cjs} +1 -1
  9. package/dist/chunks/{constants-BJ79cqMa.mjs → constants-DpIX_GOn.mjs} +2 -1
  10. package/dist/chunks/{tools-OHDv2C7w.cjs → tools-CRAPWuhj.cjs} +1 -1
  11. package/dist/chunks/{tools-ByOjWADT.mjs → tools-DBb88o-I.mjs} +1 -1
  12. package/dist/full.cjs +1 -1
  13. package/dist/full.mjs +3 -3
  14. package/dist/react.cjs +1 -1
  15. package/dist/react.mjs +42 -31
  16. package/dist/tools.cjs +1 -1
  17. package/dist/tools.mjs +2 -2
  18. package/package.json +1 -1
  19. package/src/components/block/index.ts +3 -2
  20. package/src/components/block/tunes-manager.ts +7 -2
  21. package/src/components/constants/data-attributes.ts +4 -0
  22. package/src/components/modules/modificationsObserver.ts +40 -2
  23. package/src/components/modules/paste/handlers/html-handler.ts +14 -1
  24. package/src/components/modules/paste/index.ts +1 -1
  25. package/src/components/modules/renderer.ts +32 -4
  26. package/src/components/modules/toolbar/blockSettings.ts +15 -2
  27. package/src/components/utils/apply-link-config.ts +40 -0
  28. package/src/react/BlokEditor.tsx +10 -2
  29. package/src/react/config-keys.ts +3 -0
  30. package/src/react/types.ts +4 -0
  31. package/src/react/useBlok.ts +45 -6
  32. package/types/block-tunes/block-tune.d.ts +21 -1
  33. package/types/configs/blok-config.d.ts +62 -5
  34. package/types/index.d.ts +1 -1
@@ -43,6 +43,17 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
43
43
  // eslint-disable-next-line react-hooks/exhaustive-deps
44
44
  const depsToken = useMemo(() => ({}), deps ?? []);
45
45
 
46
+ // Tracks the data the editor currently reflects — set when content is seeded
47
+ // or rendered (the reactive `data` effect below) AND when the editor emits its
48
+ // own serialized content via `onSave`. The latter is what makes a controlled
49
+ // `onSave -> setData -> data` round-trip a no-op: the echoed payload deep-equals
50
+ // this baseline, so the data effect skips the redundant render() (which would
51
+ // otherwise reset the caret). Declared here so the `onSave` wrapper below can
52
+ // update it before the consumer's setState re-runs the data effect.
53
+ const lastRenderedDataRef = useRef(config.data);
54
+ const seededEditorRef = useRef<Blok | null>(null);
55
+ const renderChainRef = useRef<Promise<void>>(Promise.resolve());
56
+
46
57
  // Main lifecycle effect
47
58
  useEffect(() => {
48
59
  if (typeof window === 'undefined') {
@@ -99,6 +110,35 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
99
110
  },
100
111
  };
101
112
 
113
+ // Only attach onSave when the consumer opted in: its mere presence makes the
114
+ // core serialize on every change batch, so an absent prop must stay absent.
115
+ // The wrapper reads through the ref so the latest callback is always used.
116
+ if (currentConfig.onSave) {
117
+ blokConfig.onSave = (...args: Parameters<NonNullable<UseBlokConfig['onSave']>>): void => {
118
+ // Record the editor's own serialized output as the rendered baseline so a
119
+ // controlled consumer echoing it straight back into `data` is a no-op —
120
+ // no redundant render(), no caret reset, no round-trip recursion.
121
+ lastRenderedDataRef.current = args[0];
122
+ configRef.current.onSave?.(...args);
123
+ };
124
+ }
125
+
126
+ // onBeforeRender / onAfterRender are opt-in (absent prop must stay absent so
127
+ // the core skips them). When present, route through the ref so the latest
128
+ // callback is always used without recreating the editor.
129
+ if (currentConfig.onBeforeRender) {
130
+ blokConfig.onBeforeRender = (
131
+ ...args: Parameters<NonNullable<UseBlokConfig['onBeforeRender']>>
132
+ ): ReturnType<NonNullable<UseBlokConfig['onBeforeRender']>> =>
133
+ configRef.current.onBeforeRender?.(...args) ?? args[0];
134
+ }
135
+
136
+ if (currentConfig.onAfterRender) {
137
+ blokConfig.onAfterRender = (...args: Parameters<NonNullable<UseBlokConfig['onAfterRender']>>): void => {
138
+ configRef.current.onAfterRender?.(...args);
139
+ };
140
+ }
141
+
102
142
  const blok = new BlokRuntime(blokConfig) as unknown as Blok;
103
143
  state.editor = blok;
104
144
  setHolder(blok, holder);
@@ -178,13 +218,12 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
178
218
  //
179
219
  // `data` seeds the editor at construction. Afterwards, changing the prop to
180
220
  // new *content* re-renders the editor via the public render() API — no
181
- // recreation. Updates are deep-equal–deduped (so a new reference with the
182
- // same content is a no-op and won't clobber the caret) and serialized (so
183
- // rapid changes can't trigger overlapping render passes).
221
+ // recreation. Updates are deep-equal–deduped against the editor's current
222
+ // content baseline (`lastRenderedDataRef`, declared above and also updated by
223
+ // the `onSave` wrapper) so a new reference with the same content — including
224
+ // the editor's own serialized output echoed back — is a no-op and won't
225
+ // clobber the caret. Renders are serialized so rapid changes can't overlap.
184
226
  const { data } = config;
185
- const lastRenderedDataRef = useRef(config.data);
186
- const seededEditorRef = useRef<Blok | null>(null);
187
- const renderChainRef = useRef<Promise<void>>(Promise.resolve());
188
227
  useEffect(() => {
189
228
  if (editor === null || data === undefined) {
190
229
  return;
@@ -2,6 +2,24 @@ import {API, BlockAPI, ToolConfig} from '../index';
2
2
  import { BlockTuneData } from './block-tune-data';
3
3
  import { BaseToolConstructable, MenuConfig } from '../tools';
4
4
 
5
+ /**
6
+ * Context passed to a Block Tune's `render()` method, giving the tune a handle
7
+ * on the popover it is rendered into — so custom tunes can anchor sub-menus or
8
+ * portals inside Blok's tune popover instead of reaching into the DOM via
9
+ * `closest('[data-blok-popover]')`.
10
+ */
11
+ export interface BlockTuneRenderContext {
12
+ /**
13
+ * Returns the popover element (`[data-blok-popover]`) the tune is rendered
14
+ * into, or `null` when accessed before the popover has mounted.
15
+ *
16
+ * `render()` itself runs *before* the popover exists, so calling this
17
+ * synchronously inside `render()` returns `null`. Capture the context and
18
+ * call this later (e.g. when opening a sub-menu) to get the live element.
19
+ */
20
+ getPopoverElement(): HTMLElement | null;
21
+ }
22
+
5
23
  /**
6
24
  * Describes BLockTune blueprint
7
25
  */
@@ -10,8 +28,10 @@ export interface BlockTune {
10
28
  * Returns BlockTune's UI.
11
29
  * Should return either MenuConfig (recommended)
12
30
  * or an HTMLElement (UI consistency is not guaranteed)
31
+ *
32
+ * @param context - render context exposing the host tune popover element
13
33
  */
14
- render(): HTMLElement | MenuConfig;
34
+ render(context?: BlockTuneRenderContext): HTMLElement | MenuConfig;
15
35
 
16
36
  /**
17
37
  * Method called on Tool render. Pass Tool content as an argument.
@@ -1,6 +1,6 @@
1
1
  import type { ThemeMode, ResolvedTheme } from '../api/theme';
2
2
  import {ToolConstructable, ToolSettings} from '../tools';
3
- import {API, LogLevels, OutputData} from '../index';
3
+ import {API, LogLevels, OutputBlockData, OutputData} from '../index';
4
4
  import {SanitizerConfig} from './sanitizer-config';
5
5
  import {I18nConfig} from './i18n-config';
6
6
  import { BlockMutationEvent } from '../events/block';
@@ -136,8 +136,17 @@ export interface BlokConfig {
136
136
  i18n?: I18nConfig;
137
137
 
138
138
  /**
139
- * Configures how the Link inline tool builds anchor elements, so consumers can
140
- * control the created `<a>` instead of post-processing the rendered DOM.
139
+ * Configures how Blok builds anchor (`<a>`) elements, so consumers can control
140
+ * the created links instead of post-processing the rendered DOM.
141
+ *
142
+ * Applies on every path that produces anchors:
143
+ * - the interactive **Link inline tool** (links the user creates by hand),
144
+ * - **render** — anchors coming from stored block HTML when `blocks.render()`
145
+ * runs (e.g. saved articles whose `<a>` never went through the inline tool),
146
+ * - **paste** — `<a>` arriving via the clipboard.
147
+ *
148
+ * On the render and paste paths `target`/`rel` are forced and `transformHref`
149
+ * rewrites the href, exactly as for hand-created links.
141
150
  */
142
151
  link?: {
143
152
  /**
@@ -154,8 +163,13 @@ export interface BlokConfig {
154
163
 
155
164
  /**
156
165
  * Transforms the href just before it is assigned to the anchor.
157
- * Runs after URL validation/normalization, on the final value.
158
- * @param href - the validated, protocol-normalized href
166
+ *
167
+ * On the inline-tool path it runs after URL validation/normalization, on the
168
+ * final value. On the render and paste paths it runs against each anchor's
169
+ * existing href, so it must be idempotent (re-rendering already-transformed
170
+ * content must not change the result again).
171
+ *
172
+ * @param href - the href to transform
159
173
  * @returns the href to set on the anchor
160
174
  */
161
175
  transformHref?: (href: string) => string;
@@ -187,6 +201,49 @@ export interface BlokConfig {
187
201
  */
188
202
  onChange?(api: API, event: BlockMutationEvent | BlockMutationEvent[]): void;
189
203
 
204
+ /**
205
+ * Fires with the full serialized {@link OutputData} whenever the content
206
+ * changes. This is the "output half" of a controlled editor: pair it with the
207
+ * `data` config (or the React `data` prop) to mirror the editor state into your
208
+ * own store with a single callback instead of calling `saver.save()` by hand.
209
+ *
210
+ * Serialization is debounced through the same change-batching window as
211
+ * `onChange`, so a burst of edits produces a single `onSave` call. Only
212
+ * user-driven content changes trigger it — programmatic `render()` does not
213
+ * (the change observer is disabled during render), so a controlled
214
+ * `data → render → onSave → setData` round-trip won't recurse.
215
+ *
216
+ * @param data - the full serialized output of the editor
217
+ * @param api - blok.js api
218
+ */
219
+ onSave?(data: OutputData, api: API): void;
220
+
221
+ /**
222
+ * Transforms the blocks array just before it is rendered, on every render
223
+ * (the initial render and every `blocks.render()` call). Use it to run
224
+ * app-specific data migrations inside Blok instead of pre-processing the
225
+ * data before handing it to the editor.
226
+ *
227
+ * Receives the raw saved blocks (the exact array passed to render, before any
228
+ * format analysis or hierarchical expansion) and must return the blocks to
229
+ * render. Returning an empty array renders an empty document; returning blocks
230
+ * for an empty input injects them.
231
+ *
232
+ * @param blocks - the blocks about to be rendered
233
+ * @returns the blocks to actually render
234
+ */
235
+ onBeforeRender?(blocks: OutputBlockData[]): OutputBlockData[];
236
+
237
+ /**
238
+ * Fires after a render completes and the blocks are in the DOM — on the
239
+ * initial render and on every `blocks.render()` call. Use it for post-render
240
+ * side effects (scroll restoration, attaching observers, …). Distinct from
241
+ * `onReady`, which fires once when the editor first becomes ready.
242
+ *
243
+ * @param api - blok.js api
244
+ */
245
+ onAfterRender?(api: API): void;
246
+
190
247
  /**
191
248
  * Defines default toolbar for all tools.
192
249
  */
package/types/index.d.ts CHANGED
@@ -72,7 +72,7 @@ export {
72
72
  FilePasteEvent,
73
73
  FilePasteEventDetail,
74
74
  } from './tools';
75
- export {BlockTune, BlockTuneConstructable} from './block-tunes';
75
+ export {BlockTune, BlockTuneConstructable, BlockTuneRenderContext} from './block-tunes';
76
76
  export {
77
77
  BlokConfig,
78
78
  SanitizerConfig,