@jackuait/blok 0.23.0 → 0.23.1

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/full.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/blok-D4WXeg3t.cjs`),t=require(`./chunks/constants-BbCnmzAr.cjs`),n=require(`./chunks/objectSpread2-iNkE6aJh.cjs`),r=require(`./chunks/tools-tG5_UaWz.cjs`);var i={paragraph:{class:r.k,inlineToolbar:!0,config:{preserveBlank:!0}},header:{class:r.O,inlineToolbar:!0},list:{class:r.D,inlineToolbar:!0}},a=n.t(n.t({},i),{},{bold:{class:r.l},italic:{class:r.c},marker:{class:r.o},underline:{class:r.a},strikethrough:{class:r.i},link:{class:r.s}});exports.Blok=e.t,exports.Bold=r.l,exports.DATA_ATTR=t.ja,exports.Header=r.O,exports.Italic=r.c,exports.Link=r.s,exports.List=r.D,exports.Marker=r.o,exports.Paragraph=r.k,exports.Strikethrough=r.i,exports.Toggle=r.T,exports.Underline=r.a,exports.allTools=a,exports.defaultBlockTools=r.t,exports.defaultInlineTools=r.n,exports.defaultTools=i,exports.version=e.n;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/blok-Ci1zCOUg.cjs`),t=require(`./chunks/constants-Bkncy0y5.cjs`),n=require(`./chunks/objectSpread2-iNkE6aJh.cjs`),r=require(`./chunks/tools-DlOZpcVU.cjs`);var i={paragraph:{class:r.k,inlineToolbar:!0,config:{preserveBlank:!0}},header:{class:r.O,inlineToolbar:!0},list:{class:r.D,inlineToolbar:!0}},a=n.t(n.t({},i),{},{bold:{class:r.l},italic:{class:r.c},marker:{class:r.o},underline:{class:r.a},strikethrough:{class:r.i},link:{class:r.s}});exports.Blok=e.t,exports.Bold=r.l,exports.DATA_ATTR=t.ja,exports.Header=r.O,exports.Italic=r.c,exports.Link=r.s,exports.List=r.D,exports.Marker=r.o,exports.Paragraph=r.k,exports.Strikethrough=r.i,exports.Toggle=r.T,exports.Underline=r.a,exports.allTools=a,exports.defaultBlockTools=r.t,exports.defaultInlineTools=r.n,exports.defaultTools=i,exports.version=e.n;
package/dist/full.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { n as e, t } from "./chunks/blok-BmckXjmW.mjs";
2
- import { ja as n } from "./chunks/constants-tXKHkDAG.mjs";
1
+ import { n as e, t } from "./chunks/blok-BPeM6wKv.mjs";
2
+ import { ja as n } from "./chunks/constants-DR3sya6U.mjs";
3
3
  import { t as r } from "./chunks/objectSpread2-jlBcdYeb.mjs";
4
- import { D as i, O as a, T as o, a as s, c, i as l, k as u, l as d, n as f, o as p, s as m, t as h } from "./chunks/tools-oXMAa9zD.mjs";
4
+ import { D as i, O as a, T as o, a as s, c, i as l, k as u, l as d, n as f, o as p, s as m, t as h } from "./chunks/tools-B72rczVw.mjs";
5
5
  //#region src/full.ts
6
6
  var g = {
7
7
  paragraph: {
package/dist/react.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`./chunks/chunk-BL1yB9zm.cjs`);const e=require(`./chunks/blok-D4WXeg3t.cjs`),t=require(`./chunks/objectSpread2-iNkE6aJh.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`);var i=new WeakMap;function a(e,t){i.set(e,t)}function o(e){return i.get(e)}function s(e){i.delete(e)}function c(r,i){let[o,c]=(0,n.useState)(null),u=(0,n.useRef)(r);u.current=r;let d=(0,n.useRef)({editor:null,holder:null,destroyTimeout:null,isDestroyed:!1,depsToken:null}),f=(0,n.useMemo)(()=>({}),i==null?[]:i);(0,n.useEffect)(()=>{if(typeof window>`u`)return;let n=d.current;if(n.destroyTimeout!==null&&(clearTimeout(n.destroyTimeout),n.destroyTimeout=null),n.editor!==null&&!n.isDestroyed&&n.depsToken===f)return c(n.editor),()=>{l(n,c)};if(n.editor!==null&&!n.isDestroyed){s(n.editor);try{n.editor.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}let r=document.createElement(`div`);n.holder=r,n.isDestroyed=!1,n.depsToken=f;let i=u.current,o=new e.t(t.t(t.t({},i),{},{holder:r,onReady:()=>{var e,t;(e=(t=u.current).onReady)==null||e.call(t)},onChange:(...e)=>{var t,n;(t=(n=u.current).onChange)==null||t.call(n,...e)}}));return n.editor=o,a(o,r),o.isReady.then(()=>{n.editor===o&&!n.isDestroyed&&c(o)}).catch(()=>{if(n.editor===o&&!n.isDestroyed){s(o);try{o.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}}),()=>{l(n,c)}},[f]);let{readOnly:p}=r;(0,n.useEffect)(()=>{o!==null&&o.readOnly.set(p==null?!1:p)},[o,p]);let{autofocus:m}=r;(0,n.useEffect)(()=>{o===null||!m||o.focus()},[o,m]);let{theme:h}=r;(0,n.useEffect)(()=>{o===null||h===void 0||o.theme.set(h)},[o,h]);let{width:g}=r;return(0,n.useEffect)(()=>{o===null||g===void 0||o.width.set(g)},[o,g]),o}function l(e,t){e.destroyTimeout=setTimeout(()=>{if(e.editor!==null){s(e.editor);try{e.editor.destroy()}catch(e){}e.editor=null,e.holder=null,e.isDestroyed=!0,e.destroyTimeout=null,t(null)}},0)}var u=[`editor`],d=(0,n.forwardRef)(function(i,a){let{editor:s}=i,c=e.i(i,u),l=(0,n.useRef)(null);return(0,n.useEffect)(()=>{if(s===null||l.current===null)return;let e=o(s);if(e!==void 0)return l.current.appendChild(e),()=>{e.remove()}},[s]),(0,r.jsx)(`div`,t.t({ref:e=>{if(l.current=e,typeof a==`function`)a(e);else if(a!=null){let t=a;t.current=e}}},c))}),f=[`deps`,`className`,`onReady`,`data-testid`],p=(0,n.forwardRef)(function(t,i){let{deps:a,className:o,onReady:s,"data-testid":l}=t,u=c(e.i(t,f),a);(0,n.useImperativeHandle)(i,()=>u,[u]);let p=(0,n.useRef)(s);return p.current=s,(0,n.useEffect)(()=>{if(u!==null){var e;(e=p.current)==null||e.call(p,u)}},[u]),(0,r.jsx)(d,{editor:u,className:o,"data-testid":l})});exports.BlokContent=d,exports.BlokEditor=p,exports.useBlok=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`./chunks/chunk-BL1yB9zm.cjs`);const e=require(`./chunks/blok-Ci1zCOUg.cjs`),t=require(`./chunks/objectSpread2-iNkE6aJh.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`);var i=new WeakMap;function a(e,t){i.set(e,t)}function o(e){return i.get(e)}function s(e){i.delete(e)}function c(r,i){let[o,c]=(0,n.useState)(null),u=(0,n.useRef)(r);u.current=r;let d=(0,n.useRef)({editor:null,holder:null,destroyTimeout:null,isDestroyed:!1,depsToken:null}),f=(0,n.useMemo)(()=>({}),i==null?[]:i);(0,n.useEffect)(()=>{if(typeof window>`u`)return;let n=d.current;if(n.destroyTimeout!==null&&(clearTimeout(n.destroyTimeout),n.destroyTimeout=null),n.editor!==null&&!n.isDestroyed&&n.depsToken===f)return c(n.editor),()=>{l(n,c)};if(n.editor!==null&&!n.isDestroyed){s(n.editor);try{n.editor.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}let r=document.createElement(`div`);n.holder=r,n.isDestroyed=!1,n.depsToken=f;let i=u.current,o=new e.t(t.t(t.t({},i),{},{holder:r,onReady:()=>{var e,t;(e=(t=u.current).onReady)==null||e.call(t)},onChange:(...e)=>{var t,n;(t=(n=u.current).onChange)==null||t.call(n,...e)}}));return n.editor=o,a(o,r),o.isReady.then(()=>{n.editor===o&&!n.isDestroyed&&c(o)}).catch(()=>{if(n.editor===o&&!n.isDestroyed){s(o);try{o.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}}),()=>{l(n,c)}},[f]);let{readOnly:p}=r;(0,n.useEffect)(()=>{o!==null&&o.readOnly.set(p==null?!1:p)},[o,p]);let{autofocus:m}=r;(0,n.useEffect)(()=>{o===null||!m||o.focus()},[o,m]);let{theme:h}=r;(0,n.useEffect)(()=>{o===null||h===void 0||o.theme.set(h)},[o,h]);let{width:g}=r;(0,n.useEffect)(()=>{o===null||g===void 0||o.width.set(g)},[o,g]);let{placeholder:_}=r;return(0,n.useEffect)(()=>{o===null||_===void 0||o.placeholder.set(_)},[o,_]),o}function l(e,t){e.destroyTimeout=setTimeout(()=>{if(e.editor!==null){s(e.editor);try{e.editor.destroy()}catch(e){}e.editor=null,e.holder=null,e.isDestroyed=!0,e.destroyTimeout=null,t(null)}},0)}var u=[`editor`],d=(0,n.forwardRef)(function(i,a){let{editor:s}=i,c=e.i(i,u),l=(0,n.useRef)(null);return(0,n.useEffect)(()=>{if(s===null||l.current===null)return;let e=o(s);if(e!==void 0)return l.current.appendChild(e),()=>{e.remove()}},[s]),(0,r.jsx)(`div`,t.t({ref:e=>{if(l.current=e,typeof a==`function`)a(e);else if(a!=null){let t=a;t.current=e}}},c))}),f=`autofocus.defaultBlock.dataModel.placeholder.sanitizer.hideToolbar.maxHistoryLength.historyDebounceTime.newGroupDelay.globalUndoRedo.tools.data.minHeight.logLevel.readOnly.i18n.linkPaste.onReady.onChange.inlineToolbar.tunes.style.theme.onThemeChange.scrollToBlock.user.resolveUser.notifierPosition.notifier.width`.split(`.`),p=[`deps`,`onReady`],m=new Set(f),h=(0,n.forwardRef)(function(i,a){let{deps:o,onReady:s}=i,l=e.i(i,p),u={},f={};for(let e of Object.keys(l)){let t=l[e];m.has(e)?u[e]=t:f[e]=t}let h=c(u,o);(0,n.useImperativeHandle)(a,()=>h,[h]);let g=(0,n.useRef)(s);return g.current=s,(0,n.useEffect)(()=>{if(h!==null){var e;(e=g.current)==null||e.call(g,h)}},[h]),(0,r.jsx)(d,t.t({editor:h},f))});exports.BlokContent=d,exports.BlokEditor=h,exports.useBlok=c;
package/dist/react.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as e, t } from "./chunks/blok-BmckXjmW.mjs";
1
+ import { i as e, t } from "./chunks/blok-BPeM6wKv.mjs";
2
2
  import { t as n } from "./chunks/objectSpread2-jlBcdYeb.mjs";
3
3
  import { forwardRef as r, useEffect as i, useImperativeHandle as a, useMemo as o, useRef as s, useState as c } from "react";
4
4
  import { jsx as l } from "react/jsx-runtime";
@@ -78,9 +78,13 @@ function m(e, r) {
78
78
  a === null || v === void 0 || a.theme.set(v);
79
79
  }, [a, v]);
80
80
  let { width: y } = e;
81
- return i(() => {
81
+ i(() => {
82
82
  a === null || y === void 0 || a.width.set(y);
83
- }, [a, y]), a;
83
+ }, [a, y]);
84
+ let { placeholder: b } = e;
85
+ return i(() => {
86
+ a === null || b === void 0 || a.placeholder.set(b);
87
+ }, [a, b]), a;
84
88
  }
85
89
  function h(e, t) {
86
90
  e.destroyTimeout = setTimeout(() => {
@@ -110,25 +114,21 @@ var g = ["editor"], _ = r(function(t, r) {
110
114
  t.current = e;
111
115
  }
112
116
  } }, o));
113
- }), v = [
114
- "deps",
115
- "className",
116
- "onReady",
117
- "data-testid"
118
- ], y = r(function(t, n) {
119
- let { deps: r, className: o, onReady: c, "data-testid": u } = t, d = m(e(t, v), r);
120
- a(n, () => d, [d]);
121
- let f = s(c);
122
- return f.current = c, i(() => {
123
- if (d !== null) {
117
+ }), v = /* @__PURE__ */ "autofocus.defaultBlock.dataModel.placeholder.sanitizer.hideToolbar.maxHistoryLength.historyDebounceTime.newGroupDelay.globalUndoRedo.tools.data.minHeight.logLevel.readOnly.i18n.linkPaste.onReady.onChange.inlineToolbar.tunes.style.theme.onThemeChange.scrollToBlock.user.resolveUser.notifierPosition.notifier.width".split("."), y = ["deps", "onReady"], b = new Set(v), x = r(function(t, r) {
118
+ let { deps: o, onReady: c } = t, u = e(t, y), d = {}, f = {};
119
+ for (let e of Object.keys(u)) {
120
+ let t = u[e];
121
+ b.has(e) ? d[e] = t : f[e] = t;
122
+ }
123
+ let p = m(d, o);
124
+ a(r, () => p, [p]);
125
+ let h = s(c);
126
+ return h.current = c, i(() => {
127
+ if (p !== null) {
124
128
  var e;
125
- (e = f.current) == null || e.call(f, d);
129
+ (e = h.current) == null || e.call(h, p);
126
130
  }
127
- }, [d]), /* @__PURE__ */ l(_, {
128
- editor: d,
129
- className: o,
130
- "data-testid": u
131
- });
131
+ }, [p]), /* @__PURE__ */ l(_, n({ editor: p }, f));
132
132
  });
133
133
  //#endregion
134
- export { _ as BlokContent, y as BlokEditor, m as useBlok };
134
+ export { _ as BlokContent, x as BlokEditor, m as useBlok };
package/dist/tools.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/constants-BbCnmzAr.cjs`),t=require(`./chunks/tools-tG5_UaWz.cjs`);exports.Audio=t.g,exports.Bold=t.l,exports.Bookmark=t.u,exports.Callout=t.w,exports.Code=t.y,exports.Column=t.p,exports.ColumnList=t.m,exports.Columns=t.f,exports.Convert=e.m,exports.Database=t.C,exports.DatabaseRow=t.S,exports.Divider=t.x,exports.Embed=t.d,exports.File=t._,exports.Header=t.O,exports.Image=t.v,exports.InlineCode=t.r,exports.Italic=t.c,exports.Link=t.s,exports.List=t.D,exports.Marker=t.o,exports.Paragraph=t.k,exports.Quote=t.b,exports.Strikethrough=t.i,exports.Table=t.E,exports.Toggle=t.T,exports.Underline=t.a,exports.Video=t.h,exports.defaultBlockTools=t.t,exports.defaultInlineTools=t.n;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/constants-Bkncy0y5.cjs`),t=require(`./chunks/tools-DlOZpcVU.cjs`);exports.Audio=t.g,exports.Bold=t.l,exports.Bookmark=t.u,exports.Callout=t.w,exports.Code=t.y,exports.Column=t.p,exports.ColumnList=t.m,exports.Columns=t.f,exports.Convert=e.m,exports.Database=t.C,exports.DatabaseRow=t.S,exports.Divider=t.x,exports.Embed=t.d,exports.File=t._,exports.Header=t.O,exports.Image=t.v,exports.InlineCode=t.r,exports.Italic=t.c,exports.Link=t.s,exports.List=t.D,exports.Marker=t.o,exports.Paragraph=t.k,exports.Quote=t.b,exports.Strikethrough=t.i,exports.Table=t.E,exports.Toggle=t.T,exports.Underline=t.a,exports.Video=t.h,exports.defaultBlockTools=t.t,exports.defaultInlineTools=t.n;
package/dist/tools.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { m as e } from "./chunks/constants-tXKHkDAG.mjs";
2
- import { C as t, D as n, E as r, O as i, S as a, T as o, _ as s, a as c, b as l, c as u, d, f, g as p, h as m, i as h, k as g, l as _, m as v, n as y, o as b, p as x, r as S, s as C, t as w, u as T, v as E, w as D, x as O, y as k } from "./chunks/tools-oXMAa9zD.mjs";
1
+ import { m as e } from "./chunks/constants-DR3sya6U.mjs";
2
+ import { C as t, D as n, E as r, O as i, S as a, T as o, _ as s, a as c, b as l, c as u, d, f, g as p, h as m, i as h, k as g, l as _, m as v, n as y, o as b, p as x, r as S, s as C, t as w, u as T, v as E, w as D, x as O, y as k } from "./chunks/tools-B72rczVw.mjs";
3
3
  export { p as Audio, _ as Bold, T as Bookmark, D as Callout, k as Code, x as Column, v as ColumnList, f as Columns, e as Convert, t as Database, a as DatabaseRow, O as Divider, d as Embed, s as File, i as Header, E as Image, S as InlineCode, u as Italic, C as Link, n as List, b as Marker, g as Paragraph, l as Quote, h as Strikethrough, r as Table, o as Toggle, c as Underline, m as Video, w as defaultBlockTools, y as defaultInlineTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackuait/blok",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
5
5
  "module": "dist/blok.mjs",
6
6
  "main": "./dist/blok.cjs",
package/src/blok.ts CHANGED
@@ -175,6 +175,39 @@ class Blok {
175
175
  toggle: (): void => applyWidthMode(readWidthMode() === 'full' ? 'narrow' : 'full'),
176
176
  };
177
177
 
178
+ /**
179
+ * Placeholder API — exposed immediately (mirrors theme/width) so host apps
180
+ * can call instance.placeholder.set(...) before isReady without a silent
181
+ * no-op. The value is buffered and replayed once BlockManager is ready.
182
+ */
183
+ const placeholderBuffer = { pending: null as string | false | null };
184
+
185
+ const getBlockManager = (): BlokModules['BlockManager'] | undefined =>
186
+ (blok.moduleInstances as Partial<BlokModules>).BlockManager;
187
+
188
+ const applyPlaceholder = (value: string | false): void => {
189
+ placeholderBuffer.pending = value;
190
+
191
+ const bm = getBlockManager();
192
+
193
+ if (bm !== undefined) {
194
+ bm.setPlaceholder(value);
195
+ }
196
+ };
197
+
198
+ (this as Record<string, unknown>).placeholder = {
199
+ get: (): string | false => {
200
+ if (placeholderBuffer.pending !== null) {
201
+ return placeholderBuffer.pending;
202
+ }
203
+
204
+ const cfg = this.initialConfiguration;
205
+
206
+ return isObject(cfg) ? ((cfg as BlokConfig).placeholder ?? false) : false;
207
+ },
208
+ set: (value: string | false): void => applyPlaceholder(value),
209
+ };
210
+
178
211
  /**
179
212
  * We need to export isReady promise in the constructor
180
213
  * as it can be used before other API methods are exported
@@ -247,6 +280,16 @@ class Blok {
247
280
  widthBuffer.pendingMode = null;
248
281
  }
249
282
 
283
+ // Apply any placeholder buffered before isReady resolved.
284
+ if (placeholderBuffer.pending !== null) {
285
+ const bm = (blok.moduleInstances as Partial<BlokModules>).BlockManager;
286
+
287
+ if (bm !== undefined) {
288
+ bm.setPlaceholder(placeholderBuffer.pending);
289
+ }
290
+ placeholderBuffer.pending = null;
291
+ }
292
+
250
293
  // Scroll to the block referenced by the URL hash, if present.
251
294
  // isReady resolves only after all blocks are in the DOM (requestIdleCallback fence in Renderer),
252
295
  // so no extra polling is needed even on slow connections.
@@ -610,6 +610,19 @@ export class Block extends EventsDispatcher<BlockEvents> {
610
610
  }
611
611
  }
612
612
 
613
+ /**
614
+ * Update the block's placeholder in place (reactive editor.placeholder API).
615
+ * Delegates to the tool instance if it implements `setPlaceholder`. No-op otherwise.
616
+ * @param value - new placeholder text, or false to clear it
617
+ */
618
+ public setPlaceholder(value: string | false): void {
619
+ const tool = this.toolInstance as unknown as { setPlaceholder?: (v: string | false) => void };
620
+
621
+ if (typeof tool.setPlaceholder === 'function') {
622
+ tool.setPlaceholder(value);
623
+ }
624
+ }
625
+
613
626
  /**
614
627
  * Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
615
628
  * This method returns the entry that is related to the Block (depended on the Block data)
@@ -153,6 +153,25 @@ export class BlockManager extends Module {
153
153
  return this.repository.blocks;
154
154
  }
155
155
 
156
+ /**
157
+ * Apply a new editor-level placeholder to existing blocks and future ones.
158
+ * Backs the reactive `editor.placeholder` API: mutates the shared config,
159
+ * updates the default tool adapter (so blocks created afterward use it), and
160
+ * sweeps existing blocks for an in-place update.
161
+ * @param value - new placeholder text, or false to clear it
162
+ */
163
+ public setPlaceholder(value: string | false): void {
164
+ this.config.placeholder = value;
165
+
166
+ const defaultToolName = this.config.defaultBlock ?? 'paragraph';
167
+
168
+ this.Blok.Tools.blockTools.get(defaultToolName)?.setDefaultPlaceholder(value);
169
+
170
+ for (const block of this.blocks) {
171
+ block.setPlaceholder(value);
172
+ }
173
+ }
174
+
156
175
  /**
157
176
  * Check if each Block is empty
158
177
  * @returns {boolean}
@@ -191,6 +191,15 @@ export abstract class BaseToolAdapter<Type extends ToolType = ToolType, ToolClas
191
191
  this.defaultPlaceholder = defaultPlaceholder;
192
192
  }
193
193
 
194
+ /**
195
+ * Update the editor-level default placeholder. Used by the reactive
196
+ * `editor.placeholder` API so blocks created AFTER the change use the new value.
197
+ * @param value - new default placeholder, or false to disable it
198
+ */
199
+ public setDefaultPlaceholder(value: string | false): void {
200
+ this.defaultPlaceholder = value;
201
+ }
202
+
194
203
  /**
195
204
  * Returns Tool user configuration.
196
205
  * Extracts tool-specific options from flat config and merges with nested config.
@@ -1,26 +1,35 @@
1
- import { forwardRef, useImperativeHandle, useEffect, useRef, type DependencyList } from 'react';
1
+ import {
2
+ forwardRef,
3
+ useImperativeHandle,
4
+ useEffect,
5
+ useRef,
6
+ type DependencyList,
7
+ type HTMLAttributes,
8
+ } from 'react';
2
9
  import { useBlok } from './useBlok';
3
10
  import { BlokContent } from './BlokContent';
11
+ import { USE_BLOK_CONFIG_KEYS } from './config-keys';
4
12
  import type { Blok } from '@/types';
5
13
  import type { UseBlokConfig } from './types';
6
14
 
7
15
  /**
8
16
  * Props for the all-in-one BlokEditor component.
9
17
  * Accepts every useBlok config prop (except `onReady`, re-typed below to receive
10
- * the ready instance), plus container `className`/`data-testid` and an optional
11
- * `deps` list. When any value in `deps` changes, the editor is destroyed and
12
- * recreated (use this when `tools` or other structural config changes).
18
+ * the ready instance) AND every standard HTML div attribute, which is forwarded
19
+ * to the editor container element (mirrors `BlokContent`). `style` and `onChange`
20
+ * keep their editor-config meaning (they collide with div attributes of the same
21
+ * name), so the container is styled via `className` rather than an inline `style`.
13
22
  *
14
23
  * `data` is uncontrolled (seed-only): it sets the INITIAL content. After mount the
15
24
  * editor owns the document — passing a new `data` reference does NOT reload it.
16
25
  * Read content via `onChange` or the ref (`ref.current.save()`); replace it
17
26
  * imperatively via `ref.current.render(newData)`.
18
27
  */
19
- export interface BlokEditorProps extends Omit<UseBlokConfig, 'onReady'> {
28
+ export interface BlokEditorProps
29
+ extends Omit<UseBlokConfig, 'onReady'>,
30
+ Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'onChange'> {
20
31
  /** When any value changes, the editor is destroyed and recreated. */
21
32
  deps?: DependencyList;
22
- /** Class name applied to the editor container element. */
23
- className?: string;
24
33
  /** Test id forwarded to the editor container element (via data-testid). */
25
34
  'data-testid'?: string;
26
35
  /**
@@ -30,6 +39,9 @@ export interface BlokEditorProps extends Omit<UseBlokConfig, 'onReady'> {
30
39
  onReady?: (editor: Blok) => void;
31
40
  }
32
41
 
42
+ /** Fast membership test for partitioning props into editor config vs. div attributes. */
43
+ const CONFIG_KEY_SET = new Set<string>(USE_BLOK_CONFIG_KEYS);
44
+
33
45
  /**
34
46
  * The recommended way to embed Blok in React. Internally wires `useBlok` and
35
47
  * `BlokContent`, and forwards a ref to the live `Blok` instance.
@@ -37,12 +49,25 @@ export interface BlokEditorProps extends Omit<UseBlokConfig, 'onReady'> {
37
49
  * @example
38
50
  * ```tsx
39
51
  * const ref = useRef<Blok | null>(null);
40
- * <BlokEditor ref={ref} tools={tools} data={data} theme={theme} onReady={(e) => e.focus()} />;
52
+ * <BlokEditor ref={ref} id="editor" tools={tools} data={data} theme={theme} onReady={(e) => e.focus()} />;
41
53
  * ```
42
54
  */
43
55
  export const BlokEditor = forwardRef<Blok | null, BlokEditorProps>(
44
- function BlokEditor({ deps, className, onReady, 'data-testid': dataTestId, ...config }, ref) {
45
- const editor = useBlok(config, deps);
56
+ function BlokEditor({ deps, onReady, ...rest }, ref) {
57
+ const config: Record<string, unknown> = {};
58
+ const divProps: Record<string, unknown> = {};
59
+
60
+ for (const key of Object.keys(rest)) {
61
+ const value = (rest as Record<string, unknown>)[key];
62
+
63
+ if (CONFIG_KEY_SET.has(key)) {
64
+ config[key] = value;
65
+ } else {
66
+ divProps[key] = value;
67
+ }
68
+ }
69
+
70
+ const editor = useBlok(config as UseBlokConfig, deps);
46
71
 
47
72
  useImperativeHandle<Blok | null, Blok | null>(ref, () => editor, [editor]);
48
73
 
@@ -58,6 +83,6 @@ export const BlokEditor = forwardRef<Blok | null, BlokEditorProps>(
58
83
  }
59
84
  }, [editor]);
60
85
 
61
- return <BlokContent editor={editor} className={className} data-testid={dataTestId} />;
86
+ return <BlokContent editor={editor} {...(divProps as HTMLAttributes<HTMLDivElement>)} />;
62
87
  }
63
88
  );
@@ -0,0 +1,51 @@
1
+ import type { UseBlokConfig } from './types';
2
+
3
+ /**
4
+ * Keys that belong to the editor config (consumed by `useBlok`). Any prop on
5
+ * `BlokEditor` whose key is NOT in this set is treated as a container div
6
+ * attribute and spread onto the underlying element (mirrors `BlokContent`).
7
+ *
8
+ * Keep in sync with `UseBlokConfig`. The two compile-time guards below fail the
9
+ * `yarn lint:types` typecheck if this list drifts.
10
+ */
11
+ export const USE_BLOK_CONFIG_KEYS = [
12
+ 'autofocus',
13
+ 'defaultBlock',
14
+ 'dataModel',
15
+ 'placeholder',
16
+ 'sanitizer',
17
+ 'hideToolbar',
18
+ 'maxHistoryLength',
19
+ 'historyDebounceTime',
20
+ 'newGroupDelay',
21
+ 'globalUndoRedo',
22
+ 'tools',
23
+ 'data',
24
+ 'minHeight',
25
+ 'logLevel',
26
+ 'readOnly',
27
+ 'i18n',
28
+ 'linkPaste',
29
+ 'onReady',
30
+ 'onChange',
31
+ 'inlineToolbar',
32
+ 'tunes',
33
+ 'style',
34
+ 'theme',
35
+ 'onThemeChange',
36
+ 'scrollToBlock',
37
+ 'user',
38
+ 'resolveUser',
39
+ 'notifierPosition',
40
+ 'notifier',
41
+ 'width',
42
+ ] as const satisfies readonly (keyof UseBlokConfig)[];
43
+
44
+ /**
45
+ * Compile error if a `UseBlokConfig` key is missing from the list above.
46
+ * (`satisfies` only proves each entry is a valid key; this proves coverage.)
47
+ */
48
+ type _MissingConfigKey = Exclude<keyof UseBlokConfig, (typeof USE_BLOK_CONFIG_KEYS)[number]>;
49
+ const _exhaustive: _MissingConfigKey extends never ? true : never = true;
50
+
51
+ void _exhaustive;
@@ -10,6 +10,7 @@ import type { BlokConfig, Blok, EditorWidth } from '@/types';
10
10
  * - `autofocus` — calls `editor.focus()` when changed to true
11
11
  * - `theme` — calls `editor.theme.set(value)`
12
12
  * - `width` — calls `editor.width.set(value)`
13
+ * - `placeholder` — calls `editor.placeholder.set(value)`
13
14
  */
14
15
  export interface UseBlokConfig extends Omit<BlokConfig, 'holder'> {
15
16
  /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
@@ -164,6 +164,15 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
164
164
  editor.width.set(width);
165
165
  }, [editor, width]);
166
166
 
167
+ // Reactive: placeholder
168
+ const { placeholder } = config;
169
+ useEffect(() => {
170
+ if (editor === null || placeholder === undefined) {
171
+ return;
172
+ }
173
+ editor.placeholder.set(placeholder);
174
+ }, [editor, placeholder]);
175
+
167
176
  return editor;
168
177
  }
169
178
 
@@ -321,6 +321,26 @@ export class Paragraph implements BlockTool {
321
321
  }
322
322
  }
323
323
 
324
+ /**
325
+ * Update the block's placeholder text in place (reactive editor.placeholder API).
326
+ * Updates internal state always; writes the live attribute only when editable
327
+ * and rendered. A read-only block picks up the new text on the next edit toggle.
328
+ *
329
+ * @param value - new placeholder text, or false to clear it
330
+ */
331
+ public setPlaceholder(value: string | false): void {
332
+ this._placeholder = value === false ? '' : value;
333
+
334
+ if (!this._element || this.readOnly) {
335
+ return;
336
+ }
337
+
338
+ this._element.setAttribute(
339
+ 'data-blok-placeholder-active',
340
+ value === false ? '' : this.api.i18n.t(this._placeholder)
341
+ );
342
+ }
343
+
324
344
  /**
325
345
  * Method that specified how to merge two Text blocks.
326
346
  * Called by Editor by backspace at the beginning of the Block
@@ -19,3 +19,4 @@ export * from './tools';
19
19
 
20
20
  export * from './theme';
21
21
  export * from './width';
22
+ export * from './placeholder';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Describes Blok's placeholder API for controlling the editor-level placeholder
3
+ * (the hint shown on the empty default block) at runtime.
4
+ */
5
+ export interface Placeholder {
6
+ /**
7
+ * Returns the current editor placeholder.
8
+ * @returns the placeholder text, or `false` when disabled.
9
+ */
10
+ get(): string | false;
11
+
12
+ /**
13
+ * Sets the editor placeholder. Updates existing blocks in place and applies to
14
+ * blocks created afterward. Pass `false` to clear it.
15
+ * @param value - new placeholder text, or false to disable it
16
+ */
17
+ set(value: string | false): void;
18
+ }
package/types/index.d.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  ThemeMode,
34
34
  ResolvedTheme,
35
35
  Width,
36
+ Placeholder,
36
37
  } from './api';
37
38
 
38
39
  import { OutputData } from './data-formats';
@@ -129,6 +130,7 @@ export {
129
130
  ResolvedTheme,
130
131
  Width,
131
132
  EditorWidth,
133
+ Placeholder,
132
134
  } from './api';
133
135
  export {
134
136
  BlockMutationType,
@@ -228,6 +230,8 @@ export interface PendingBlok {
228
230
  theme: Theme;
229
231
  /** Width API, exposed immediately after construction. */
230
232
  width: Width;
233
+ /** Placeholder API, exposed immediately after construction. */
234
+ placeholder: Placeholder;
231
235
  }
232
236
 
233
237
  /**
@@ -249,6 +253,7 @@ export class Blok {
249
253
  public readOnly: ReadOnly;
250
254
  public theme: Theme;
251
255
  public width: Width;
256
+ public placeholder: Placeholder;
252
257
  constructor(configuration?: BlokConfig|string);
253
258
 
254
259
  /**
package/types/react.d.ts CHANGED
@@ -11,6 +11,7 @@ import type React from 'react';
11
11
  * - `autofocus` — calls `editor.focus()` when changed to true
12
12
  * - `theme` — calls `editor.theme.set(value)`
13
13
  * - `width` — calls `editor.width.set(value)`
14
+ * - `placeholder` — calls `editor.placeholder.set(value)`
14
15
  *
15
16
  * All other config is consumed once at editor creation.
16
17
  */
@@ -64,14 +65,15 @@ export declare const BlokContent: React.ForwardRefExoticComponent<BlokContentPro
64
65
 
65
66
  /**
66
67
  * Props for the BlokEditor component — all useBlok config (except `onReady`,
67
- * re-typed to receive the ready instance) plus container `className`/`data-testid`
68
- * and an optional `deps` list that forces recreation when changed.
68
+ * re-typed to receive the ready instance) plus every standard HTML div attribute
69
+ * (forwarded to the container, like `BlokContent`) and an optional `deps` list
70
+ * that forces recreation when changed. `style`/`onChange` keep editor-config meaning.
69
71
  */
70
- export interface BlokEditorProps extends Omit<UseBlokConfig, 'onReady'> {
72
+ export interface BlokEditorProps
73
+ extends Omit<UseBlokConfig, 'onReady'>,
74
+ Omit<React.HTMLAttributes<HTMLDivElement>, 'style' | 'onChange'> {
71
75
  /** When any value changes, the editor is destroyed and recreated. */
72
76
  deps?: React.DependencyList;
73
- /** Class name applied to the editor container element. */
74
- className?: string;
75
77
  /** Test id forwarded to the editor container element (via data-testid). */
76
78
  'data-testid'?: string;
77
79
  /** Called once the editor is ready, with the live Blok instance (ref is also committed). */