@storyblok/live-preview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Storyblok GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ <div align="center">
2
+
3
+ ![Storyblok ImagoType](https://raw.githubusercontent.com/storyblok/.github/refs/heads/main/profile/public/github-banner.png)
4
+
5
+ <h1 align="center">@storyblok/live-preview</h1>
6
+ <p>
7
+ Lightweight helpers to enable Storyblok Live Preview and Visual Editor
8
+ updates across client-side and SSR frameworks.
9
+ </p>
10
+ <br />
11
+ </div>
12
+
13
+ <p align="center">
14
+ <a href="https://npmjs.com/package/@storyblok/live-preview">
15
+ <img src="https://img.shields.io/npm/v/@storyblok/live-preview/latest.svg?style=flat-square&color=8d60ff" alt="Storyblok Live Preview" />
16
+ </a>
17
+ <a href="https://npmjs.com/package/@storyblok/live-preview" rel="nofollow">
18
+ <img src="https://img.shields.io/npm/dt/@storyblok/live-preview.svg?style=appveyor&color=8d60ff" alt="npm">
19
+ </a>
20
+ <a href="https://storyblok.com/join-discord">
21
+ <img src="https://img.shields.io/discord/700316478792138842?label=Join%20Our%20Discord%20Community&style=appveyor&logo=discord&color=8d60ff">
22
+ </a>
23
+ <a href="https://twitter.com/intent/follow?screen_name=storyblok">
24
+ <img src="https://img.shields.io/badge/Follow-%40storyblok-8d60ff?style=appveyor&logo=twitter" alt="Follow @Storyblok" />
25
+ </a>
26
+ </p>
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @storyblok/live-preview
32
+ # or
33
+ yarn add @storyblok/live-preview
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Live preview
39
+
40
+ Covers the most common live preview use cases with minimal setup.
41
+
42
+ ```ts
43
+ import { onStoryblokEditorEvent } from '@storyblok/live-preview';
44
+
45
+ onStoryblokEditorEvent((story) => {
46
+ // update local state
47
+ });
48
+ ```
49
+ ### Low-level Preview Bridge access
50
+ For advanced use cases where full control is needed, the Preview Bridge can be accessed directly.
51
+ ```ts
52
+ import { loadStoryblokBridge } from '@storyblok/live-preview';
53
+
54
+ const bridge = await loadStoryblokBridge(bridgeOptions);
55
+
56
+ bridge.on(['input', 'change', 'published'], (event) => {
57
+ // custom preview handling
58
+ });
59
+ ```
60
+ ## Documentation
61
+
62
+ This package is intentionally minimal.
63
+ More helpers and examples will be added over time as usage expands.
64
+
65
+ ## Community
66
+
67
+ For help, best practices, or discussions:
68
+
69
+ * [Storyblok GitHub Discussions](https://github.com/storyblok/monoblok/discussions)
70
+ * [Join the Storyblok Discord](https://storyblok.com/join-discord)
71
+
72
+ ## Support
73
+
74
+ For bugs or feature requests, please
75
+ [submit an issue](https://github.com/storyblok/monoblok/issues/new/choose).
76
+
77
+ ## License
78
+
79
+ [MIT](/LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,201 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/editable.ts
4
+ function storyblokEditable(blok) {
5
+ const editable = blok?._editable;
6
+ if (!editable) return {};
7
+ if (!editable.startsWith("<!--#storyblok#") || !editable.endsWith("-->")) return {};
8
+ try {
9
+ const json = editable.slice(15, -3);
10
+ const options = JSON.parse(json);
11
+ return {
12
+ "data-blok-c": JSON.stringify(options),
13
+ "data-blok-uid": `${options.id}-${options.uid}`
14
+ };
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/loadStoryblokBridge.ts
22
+ let bridgePromise;
23
+ let storedConfig;
24
+ function configsAreEqual(config1, config2) {
25
+ return JSON.stringify(config1) === JSON.stringify(config2);
26
+ }
27
+ /**
28
+ * Get or create a StoryblokBridge instance.
29
+ *⚠️ The bridge is a singleton. Configuration is applied only on first load.
30
+ * @param config Optional configuration for the StoryblokBridge.
31
+ * @returns A promise that resolves to a StoryblokBridge instance.
32
+ */
33
+ function loadStoryblokBridge(config) {
34
+ if (bridgePromise) {
35
+ if (config && !configsAreEqual(config, storedConfig)) throw new Error("[Storyblok] Preview Bridge already initialized with a different configuration. The bridge can only be created once per page and does not support runtime reconfiguration.");
36
+ return bridgePromise;
37
+ }
38
+ storedConfig = config;
39
+ bridgePromise = import("@storyblok/preview-bridge").then(({ default: StoryblokBridge }) => new StoryblokBridge(config)).catch((error) => {
40
+ bridgePromise = void 0;
41
+ storedConfig = void 0;
42
+ throw error;
43
+ });
44
+ return bridgePromise;
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/utils/isBrowser.ts
49
+ function isBrowser() {
50
+ return typeof window !== "undefined";
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/utils/isInEditor.ts
55
+ /**
56
+ * Required query parameters that must be present in all Storyblok Visual Editor requests.
57
+ */
58
+ const REQUIRED_STORYBLOK_PARAMS = [
59
+ "_storyblok",
60
+ "_storyblok_c",
61
+ "_storyblok_tk[space_id]"
62
+ ];
63
+ /**
64
+ * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.
65
+ *
66
+ * This function performs a multi-layered validation to ensure the request originates from
67
+ * the Storyblok Visual Editor by checking for required query parameters and optionally
68
+ * validating the space ID.
69
+ *
70
+ * @param url - The URL object to validate.
71
+ * @param options - Optional validation configuration.
72
+ * @param options.spaceId - If provided, validates that the request's space ID matches this value.
73
+ *
74
+ * @returns `true` if the URL contains all required Storyblok Visual Editor parameters
75
+ * and passes optional space ID validation; `false` otherwise.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');
80
+ *
81
+ * // Basic validation
82
+ * if (isInEditor(url)) {
83
+ * console.log('Valid Storyblok editor request');
84
+ * }
85
+ *
86
+ * // Validation with space ID check
87
+ * if (isInEditor(url, { spaceId: '789' })) {
88
+ * console.log('Valid request for specific space');
89
+ * }
90
+ * ```
91
+ */
92
+ function isInEditor(url, options = {}) {
93
+ const params = url.searchParams;
94
+ if (!REQUIRED_STORYBLOK_PARAMS.every((param) => params.has(param))) return false;
95
+ if (options.spaceId && params.get("_storyblok_tk[space_id]") !== options.spaceId) return false;
96
+ return true;
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/utils/canUseStoryblokBridge.ts
101
+ function canUseStoryblokBridge() {
102
+ if (!isBrowser()) return false;
103
+ return isInEditor(new URL(window.location.href));
104
+ }
105
+
106
+ //#endregion
107
+ //#region src/onStoryblokEditorEvent.ts
108
+ /**
109
+ * Internal listener registry for Storyblok `input` events.
110
+ * Each listener receives the updated story data from the Visual Editor.
111
+ */
112
+ const inputListeners = /* @__PURE__ */ new Set();
113
+ /**
114
+ * Tracks whether the Storyblok Preview Bridge event listeners
115
+ * have already been registered.
116
+ */
117
+ let bridgeInitPromise;
118
+ /**
119
+ * Initializes the Storyblok Preview Bridge and attaches event listeners.
120
+ *
121
+ * This function ensures that the bridge is only initialized once per page.
122
+ *
123
+ * Registered events:
124
+ * - `input` → Dispatches updated story data to all registered listeners.
125
+ * - `change` → Forces a full page reload.
126
+ * - `published` → Forces a full page reload.
127
+ *
128
+ * @param bridgeOptions Optional configuration for the Preview Bridge.
129
+ */
130
+ async function initializeBridge(bridgeOptions) {
131
+ if (!canUseStoryblokBridge()) return;
132
+ if (bridgeInitPromise) return bridgeInitPromise;
133
+ bridgeInitPromise = (async () => {
134
+ (await loadStoryblokBridge(bridgeOptions)).on([
135
+ "input",
136
+ "change",
137
+ "published"
138
+ ], (event) => {
139
+ if (!event) return;
140
+ if (event.action === "input" && event.story) {
141
+ for (const listener of inputListeners) listener(event.story);
142
+ return;
143
+ }
144
+ if (event.action === "change" || event.action === "published") window.location.reload();
145
+ });
146
+ })();
147
+ return bridgeInitPromise;
148
+ }
149
+ /**
150
+ * Registers a callback for Storyblok Visual Editor live preview updates.
151
+ *
152
+ * This utility connects to the Storyblok Preview Bridge and listens
153
+ * for Visual Editor events.
154
+ *
155
+ * Behavior:
156
+ * - **input** → Calls the provided callback with the updated story data.
157
+ * - **change** → Reloads the page.
158
+ * - **published** → Reloads the page.
159
+ *
160
+ * Multiple listeners can be registered simultaneously. Each call returns
161
+ * a cleanup function that removes the registered listener.
162
+ *
163
+ * @typeParam T - The Storyblok component schema type.
164
+ *
165
+ * @param callback
166
+ * Callback executed when the Visual Editor sends an `input` event.
167
+ *
168
+ * @param bridgeOptions
169
+ * Optional configuration for the Storyblok Preview Bridge.
170
+ * This configuration is applied **only during the first initialization**.
171
+ *
172
+ * @returns
173
+ * A cleanup function that removes the registered listener.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * const cleanup = await onStoryblokEditorEvent((story) => {
178
+ * console.log('Live updated story:', story)
179
+ * })
180
+ *
181
+ * // later
182
+ * cleanup()
183
+ * ```
184
+ */
185
+ async function onStoryblokEditorEvent(callback, bridgeOptions) {
186
+ await initializeBridge(bridgeOptions);
187
+ const listener = (story) => {
188
+ callback(story);
189
+ };
190
+ inputListeners.add(listener);
191
+ return () => {
192
+ inputListeners.delete(listener);
193
+ };
194
+ }
195
+
196
+ //#endregion
197
+ exports.isInEditor = isInEditor;
198
+ exports.loadStoryblokBridge = loadStoryblokBridge;
199
+ exports.onStoryblokEditorEvent = onStoryblokEditorEvent;
200
+ exports.storyblokEditable = storyblokEditable;
201
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/editable.ts","../src/loadStoryblokBridge.ts","../src/utils/isBrowser.ts","../src/utils/isInEditor.ts","../src/utils/canUseStoryblokBridge.ts","../src/onStoryblokEditorEvent.ts"],"sourcesContent":["interface Blok {\n _editable?: string;\n}\n\ninterface EditableOptions {\n id: string;\n uid: string;\n}\n\nexport default function storyblokEditable(blok?: Blok) {\n const editable = blok?._editable;\n if (!editable) {\n return {};\n }\n\n const prefix = '<!--#storyblok#';\n const suffix = '-->';\n\n if (!editable.startsWith(prefix) || !editable.endsWith(suffix)) {\n return {};\n }\n\n try {\n const json = editable.slice(prefix.length, -suffix.length);\n const options = JSON.parse(json) as EditableOptions;\n\n return {\n 'data-blok-c': JSON.stringify(options),\n 'data-blok-uid': `${options.id}-${options.uid}`,\n };\n }\n catch {\n return {};\n }\n}\n","import type StoryblokBridge from '@storyblok/preview-bridge';\nimport type { BridgeParams } from '@storyblok/preview-bridge';\n\nlet bridgePromise: Promise<StoryblokBridge> | undefined;\nlet storedConfig: BridgeParams | undefined;\nfunction configsAreEqual(config1: BridgeParams | undefined, config2: BridgeParams | undefined): boolean {\n return JSON.stringify(config1) === JSON.stringify(config2);\n}\n\n/**\n * Get or create a StoryblokBridge instance.\n *⚠️ The bridge is a singleton. Configuration is applied only on first load.\n * @param config Optional configuration for the StoryblokBridge.\n * @returns A promise that resolves to a StoryblokBridge instance.\n */\nexport function loadStoryblokBridge(config?: BridgeParams) {\n if (bridgePromise) {\n if (config && !configsAreEqual(config, storedConfig)) {\n throw new Error(\n '[Storyblok] Preview Bridge already initialized with a different configuration. '\n + 'The bridge can only be created once per page and does not support runtime reconfiguration.',\n );\n }\n return bridgePromise;\n }\n\n storedConfig = config;\n\n bridgePromise = import('@storyblok/preview-bridge')\n .then(({ default: StoryblokBridge }) => new StoryblokBridge(config))\n .catch((error) => {\n bridgePromise = undefined;\n storedConfig = undefined;\n throw error;\n });\n\n return bridgePromise;\n}\n","export function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n","/**\n * Options for validating Storyblok Visual Editor requests.\n */\ninterface StoryblokValidationOptions {\n /**\n * Optional space ID to validate against the request.\n * If provided, the request must match this space ID to be considered valid.\n */\n spaceId?: string;\n}\n\n/**\n * Required query parameters that must be present in all Storyblok Visual Editor requests.\n */\nconst REQUIRED_STORYBLOK_PARAMS = ['_storyblok', '_storyblok_c', '_storyblok_tk[space_id]'] as const;\n\n/**\n * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.\n *\n * This function performs a multi-layered validation to ensure the request originates from\n * the Storyblok Visual Editor by checking for required query parameters and optionally\n * validating the space ID.\n *\n * @param url - The URL object to validate.\n * @param options - Optional validation configuration.\n * @param options.spaceId - If provided, validates that the request's space ID matches this value.\n *\n * @returns `true` if the URL contains all required Storyblok Visual Editor parameters\n * and passes optional space ID validation; `false` otherwise.\n *\n * @example\n * ```typescript\n * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');\n *\n * // Basic validation\n * if (isInEditor(url)) {\n * console.log('Valid Storyblok editor request');\n * }\n *\n * // Validation with space ID check\n * if (isInEditor(url, { spaceId: '789' })) {\n * console.log('Valid request for specific space');\n * }\n * ```\n */\nexport function isInEditor(\n url: URL,\n options: StoryblokValidationOptions = {},\n): boolean {\n const params = url.searchParams;\n\n // Early return: Check all required parameters exist\n const hasRequiredParams = REQUIRED_STORYBLOK_PARAMS.every(param => params.has(param));\n\n if (!hasRequiredParams) {\n return false;\n }\n\n // Optional space ID validation\n if (options.spaceId && params.get('_storyblok_tk[space_id]') !== options.spaceId) {\n return false;\n }\n\n return true;\n}\n","import { isBrowser } from './isBrowser';\nimport { isInEditor } from './isInEditor';\n\nexport function canUseStoryblokBridge(): boolean {\n if (!isBrowser()) {\n return false;\n }\n return isInEditor(new URL(window.location.href));\n}\n","import type { BridgeParams } from '@storyblok/preview-bridge';\nimport type { ISbComponentType, ISbStoryData } from 'storyblok-js-client';\n\nimport { loadStoryblokBridge } from './loadStoryblokBridge';\nimport { canUseStoryblokBridge } from './utils/canUseStoryblokBridge';\n\n/**\n * Internal listener registry for Storyblok `input` events.\n * Each listener receives the updated story data from the Visual Editor.\n */\nconst inputListeners = new Set<(story: ISbStoryData) => void>();\n\n/**\n * Tracks whether the Storyblok Preview Bridge event listeners\n * have already been registered.\n */\nlet bridgeInitPromise: Promise<void> | undefined;\n\n/**\n * Initializes the Storyblok Preview Bridge and attaches event listeners.\n *\n * This function ensures that the bridge is only initialized once per page.\n *\n * Registered events:\n * - `input` → Dispatches updated story data to all registered listeners.\n * - `change` → Forces a full page reload.\n * - `published` → Forces a full page reload.\n *\n * @param bridgeOptions Optional configuration for the Preview Bridge.\n */\nasync function initializeBridge(bridgeOptions?: BridgeParams): Promise<void> {\n if (!canUseStoryblokBridge()) {\n return;\n }\n\n // If initialization already started, reuse it\n if (bridgeInitPromise) {\n return bridgeInitPromise;\n }\n\n bridgeInitPromise = (async () => {\n const bridge = await loadStoryblokBridge(bridgeOptions);\n\n bridge.on(['input', 'change', 'published'], (event) => {\n if (!event) {\n return;\n }\n\n if (event.action === 'input' && event.story) {\n for (const listener of inputListeners) {\n listener(event.story as ISbStoryData);\n }\n return;\n }\n\n if (event.action === 'change' || event.action === 'published') {\n window.location.reload();\n }\n });\n })();\n\n return bridgeInitPromise;\n}\n\n/**\n * Registers a callback for Storyblok Visual Editor live preview updates.\n *\n * This utility connects to the Storyblok Preview Bridge and listens\n * for Visual Editor events.\n *\n * Behavior:\n * - **input** → Calls the provided callback with the updated story data.\n * - **change** → Reloads the page.\n * - **published** → Reloads the page.\n *\n * Multiple listeners can be registered simultaneously. Each call returns\n * a cleanup function that removes the registered listener.\n *\n * @typeParam T - The Storyblok component schema type.\n *\n * @param callback\n * Callback executed when the Visual Editor sends an `input` event.\n *\n * @param bridgeOptions\n * Optional configuration for the Storyblok Preview Bridge.\n * This configuration is applied **only during the first initialization**.\n *\n * @returns\n * A cleanup function that removes the registered listener.\n *\n * @example\n * ```ts\n * const cleanup = await onStoryblokEditorEvent((story) => {\n * console.log('Live updated story:', story)\n * })\n *\n * // later\n * cleanup()\n * ```\n */\nexport async function onStoryblokEditorEvent<\n T extends ISbComponentType<string> = ISbComponentType<string>,\n>(\n callback: (story: ISbStoryData<T>) => void,\n bridgeOptions?: BridgeParams,\n): Promise<() => void> {\n await initializeBridge(bridgeOptions);\n\n const listener = (story: ISbStoryData) => {\n callback(story as ISbStoryData<T>);\n };\n\n inputListeners.add(listener);\n\n return () => {\n inputListeners.delete(listener);\n };\n}\n"],"mappings":";;;AASA,SAAwB,kBAAkB,MAAa;CACrD,MAAM,WAAW,MAAM;AACvB,KAAI,CAAC,SACH,QAAO,EAAE;AAMX,KAAI,CAAC,SAAS,WAHC,kBAGiB,IAAI,CAAC,SAAS,SAF/B,MAE+C,CAC5D,QAAO,EAAE;AAGX,KAAI;EACF,MAAM,OAAO,SAAS,MAAM,IAAe,GAAe;EAC1D,MAAM,UAAU,KAAK,MAAM,KAAK;AAEhC,SAAO;GACL,eAAe,KAAK,UAAU,QAAQ;GACtC,iBAAiB,GAAG,QAAQ,GAAG,GAAG,QAAQ;GAC3C;SAEG;AACJ,SAAO,EAAE;;;;;;AC7Bb,IAAI;AACJ,IAAI;AACJ,SAAS,gBAAgB,SAAmC,SAA4C;AACtG,QAAO,KAAK,UAAU,QAAQ,KAAK,KAAK,UAAU,QAAQ;;;;;;;;AAS5D,SAAgB,oBAAoB,QAAuB;AACzD,KAAI,eAAe;AACjB,MAAI,UAAU,CAAC,gBAAgB,QAAQ,aAAa,CAClD,OAAM,IAAI,MACR,4KAED;AAEH,SAAO;;AAGT,gBAAe;AAEf,iBAAgB,OAAO,6BACpB,MAAM,EAAE,SAAS,sBAAsB,IAAI,gBAAgB,OAAO,CAAC,CACnE,OAAO,UAAU;AAChB,kBAAgB;AAChB,iBAAe;AACf,QAAM;GACN;AAEJ,QAAO;;;;;ACpCT,SAAgB,YAAqB;AACnC,QAAO,OAAO,WAAW;;;;;;;;ACa3B,MAAM,4BAA4B;CAAC;CAAc;CAAgB;CAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B3F,SAAgB,WACd,KACA,UAAsC,EAAE,EAC/B;CACT,MAAM,SAAS,IAAI;AAKnB,KAAI,CAFsB,0BAA0B,OAAM,UAAS,OAAO,IAAI,MAAM,CAAC,CAGnF,QAAO;AAIT,KAAI,QAAQ,WAAW,OAAO,IAAI,0BAA0B,KAAK,QAAQ,QACvE,QAAO;AAGT,QAAO;;;;;AC5DT,SAAgB,wBAAiC;AAC/C,KAAI,CAAC,WAAW,CACd,QAAO;AAET,QAAO,WAAW,IAAI,IAAI,OAAO,SAAS,KAAK,CAAC;;;;;;;;;ACGlD,MAAM,iCAAiB,IAAI,KAAoC;;;;;AAM/D,IAAI;;;;;;;;;;;;;AAcJ,eAAe,iBAAiB,eAA6C;AAC3E,KAAI,CAAC,uBAAuB,CAC1B;AAIF,KAAI,kBACF,QAAO;AAGT,sBAAqB,YAAY;AAG/B,GAFe,MAAM,oBAAoB,cAAc,EAEhD,GAAG;GAAC;GAAS;GAAU;GAAY,GAAG,UAAU;AACrD,OAAI,CAAC,MACH;AAGF,OAAI,MAAM,WAAW,WAAW,MAAM,OAAO;AAC3C,SAAK,MAAM,YAAY,eACrB,UAAS,MAAM,MAAsB;AAEvC;;AAGF,OAAI,MAAM,WAAW,YAAY,MAAM,WAAW,YAChD,QAAO,SAAS,QAAQ;IAE1B;KACA;AAEJ,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCT,eAAsB,uBAGpB,UACA,eACqB;AACrB,OAAM,iBAAiB,cAAc;CAErC,MAAM,YAAY,UAAwB;AACxC,WAAS,MAAyB;;AAGpC,gBAAe,IAAI,SAAS;AAE5B,cAAa;AACX,iBAAe,OAAO,SAAS"}
@@ -0,0 +1,214 @@
1
+ import StoryblokBridge, { BridgeParams, BridgeParams as BridgeParams$1 } from "@storyblok/preview-bridge";
2
+
3
+ //#region src/editable.d.ts
4
+ interface Blok {
5
+ _editable?: string;
6
+ }
7
+ declare function storyblokEditable(blok?: Blok): {
8
+ 'data-blok-c'?: undefined;
9
+ 'data-blok-uid'?: undefined;
10
+ } | {
11
+ 'data-blok-c': string;
12
+ 'data-blok-uid': string;
13
+ };
14
+ //#endregion
15
+ //#region src/loadStoryblokBridge.d.ts
16
+ /**
17
+ * Get or create a StoryblokBridge instance.
18
+ *⚠️ The bridge is a singleton. Configuration is applied only on first load.
19
+ * @param config Optional configuration for the StoryblokBridge.
20
+ * @returns A promise that resolves to a StoryblokBridge instance.
21
+ */
22
+ declare function loadStoryblokBridge(config?: BridgeParams$1): Promise<StoryblokBridge>;
23
+ //#endregion
24
+ //#region ../js-client/dist/index.d.mts
25
+ interface ISbComponentType<T extends string> {
26
+ _uid?: string;
27
+ component?: T;
28
+ _editable?: string;
29
+ }
30
+ interface PreviewToken {
31
+ token: string;
32
+ timestamp: string;
33
+ }
34
+ interface LocalizedPath {
35
+ path: string;
36
+ name: string | null;
37
+ lang: string;
38
+ published: boolean;
39
+ }
40
+ interface ISbStoryData<Content = ISbComponentType<string> & {
41
+ [index: string]: any;
42
+ }> extends ISbMultipleStoriesData {
43
+ alternates: ISbAlternateObject[];
44
+ breadcrumbs?: ISbLinkURLObject[];
45
+ content: Content;
46
+ created_at: string;
47
+ deleted_at?: string;
48
+ default_full_slug?: string | null;
49
+ default_root?: string;
50
+ disable_fe_editor?: boolean;
51
+ favourite_for_user_ids?: number[] | null;
52
+ first_published_at?: string | null;
53
+ full_slug: string;
54
+ group_id: string;
55
+ id: number;
56
+ imported_at?: string;
57
+ is_folder?: boolean;
58
+ is_startpage?: boolean;
59
+ lang: string;
60
+ last_author?: {
61
+ id: number;
62
+ userid: string;
63
+ };
64
+ last_author_id?: number;
65
+ localized_paths?: LocalizedPath[] | null;
66
+ meta_data: any;
67
+ name: string;
68
+ parent?: ISbStoryData;
69
+ parent_id: number | null;
70
+ path?: string;
71
+ pinned?: '1' | boolean;
72
+ position: number;
73
+ preview_token?: PreviewToken;
74
+ published?: boolean;
75
+ published_at: string | null;
76
+ release_id?: number | null;
77
+ scheduled_date?: string | null;
78
+ slug: string;
79
+ sort_by_date: string | null;
80
+ tag_list: string[];
81
+ translated_slugs?: {
82
+ path: string;
83
+ name: string | null;
84
+ lang: ISbStoryData['lang'];
85
+ published: boolean;
86
+ }[] | null;
87
+ unpublished_changes?: boolean;
88
+ updated_at?: string;
89
+ uuid: string;
90
+ }
91
+ interface ISbMultipleStoriesData {
92
+ by_ids?: string;
93
+ by_uuids?: string;
94
+ contain_component?: string;
95
+ excluding_ids?: string;
96
+ filter_query?: any;
97
+ folder_only?: boolean;
98
+ full_slug?: string;
99
+ in_release?: string;
100
+ in_trash?: boolean;
101
+ is_published?: boolean;
102
+ in_workflow_stages?: string;
103
+ page?: number;
104
+ pinned?: '1' | boolean;
105
+ search?: string;
106
+ sort_by?: string;
107
+ starts_with?: string;
108
+ story_only?: boolean;
109
+ text_search?: string;
110
+ with_parent?: number;
111
+ with_slug?: string;
112
+ with_tag?: string;
113
+ }
114
+ interface ISbAlternateObject {
115
+ id: number;
116
+ name: string;
117
+ slug: string;
118
+ published: boolean;
119
+ full_slug: string;
120
+ is_folder: boolean;
121
+ parent_id: number;
122
+ }
123
+ interface ISbLinkURLObject {
124
+ id: number;
125
+ name: string;
126
+ slug: string;
127
+ full_slug: string;
128
+ url: string;
129
+ uuid: string;
130
+ }
131
+ //#endregion
132
+ //#region src/onStoryblokEditorEvent.d.ts
133
+ /**
134
+ * Registers a callback for Storyblok Visual Editor live preview updates.
135
+ *
136
+ * This utility connects to the Storyblok Preview Bridge and listens
137
+ * for Visual Editor events.
138
+ *
139
+ * Behavior:
140
+ * - **input** → Calls the provided callback with the updated story data.
141
+ * - **change** → Reloads the page.
142
+ * - **published** → Reloads the page.
143
+ *
144
+ * Multiple listeners can be registered simultaneously. Each call returns
145
+ * a cleanup function that removes the registered listener.
146
+ *
147
+ * @typeParam T - The Storyblok component schema type.
148
+ *
149
+ * @param callback
150
+ * Callback executed when the Visual Editor sends an `input` event.
151
+ *
152
+ * @param bridgeOptions
153
+ * Optional configuration for the Storyblok Preview Bridge.
154
+ * This configuration is applied **only during the first initialization**.
155
+ *
156
+ * @returns
157
+ * A cleanup function that removes the registered listener.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * const cleanup = await onStoryblokEditorEvent((story) => {
162
+ * console.log('Live updated story:', story)
163
+ * })
164
+ *
165
+ * // later
166
+ * cleanup()
167
+ * ```
168
+ */
169
+ declare function onStoryblokEditorEvent<T extends ISbComponentType<string> = ISbComponentType<string>>(callback: (story: ISbStoryData<T>) => void, bridgeOptions?: BridgeParams$1): Promise<() => void>;
170
+ //#endregion
171
+ //#region src/utils/isInEditor.d.ts
172
+ /**
173
+ * Options for validating Storyblok Visual Editor requests.
174
+ */
175
+ interface StoryblokValidationOptions {
176
+ /**
177
+ * Optional space ID to validate against the request.
178
+ * If provided, the request must match this space ID to be considered valid.
179
+ */
180
+ spaceId?: string;
181
+ }
182
+ /**
183
+ * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.
184
+ *
185
+ * This function performs a multi-layered validation to ensure the request originates from
186
+ * the Storyblok Visual Editor by checking for required query parameters and optionally
187
+ * validating the space ID.
188
+ *
189
+ * @param url - The URL object to validate.
190
+ * @param options - Optional validation configuration.
191
+ * @param options.spaceId - If provided, validates that the request's space ID matches this value.
192
+ *
193
+ * @returns `true` if the URL contains all required Storyblok Visual Editor parameters
194
+ * and passes optional space ID validation; `false` otherwise.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');
199
+ *
200
+ * // Basic validation
201
+ * if (isInEditor(url)) {
202
+ * console.log('Valid Storyblok editor request');
203
+ * }
204
+ *
205
+ * // Validation with space ID check
206
+ * if (isInEditor(url, { spaceId: '789' })) {
207
+ * console.log('Valid request for specific space');
208
+ * }
209
+ * ```
210
+ */
211
+ declare function isInEditor(url: URL, options?: StoryblokValidationOptions): boolean;
212
+ //#endregion
213
+ export { type BridgeParams, type ISbComponentType, type ISbStoryData, isInEditor, loadStoryblokBridge, onStoryblokEditorEvent, storyblokEditable };
214
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,214 @@
1
+ import StoryblokBridge, { BridgeParams, BridgeParams as BridgeParams$1 } from "@storyblok/preview-bridge";
2
+
3
+ //#region src/editable.d.ts
4
+ interface Blok {
5
+ _editable?: string;
6
+ }
7
+ declare function storyblokEditable(blok?: Blok): {
8
+ 'data-blok-c'?: undefined;
9
+ 'data-blok-uid'?: undefined;
10
+ } | {
11
+ 'data-blok-c': string;
12
+ 'data-blok-uid': string;
13
+ };
14
+ //#endregion
15
+ //#region src/loadStoryblokBridge.d.ts
16
+ /**
17
+ * Get or create a StoryblokBridge instance.
18
+ *⚠️ The bridge is a singleton. Configuration is applied only on first load.
19
+ * @param config Optional configuration for the StoryblokBridge.
20
+ * @returns A promise that resolves to a StoryblokBridge instance.
21
+ */
22
+ declare function loadStoryblokBridge(config?: BridgeParams$1): Promise<StoryblokBridge>;
23
+ //#endregion
24
+ //#region ../js-client/dist/index.d.mts
25
+ interface ISbComponentType<T extends string> {
26
+ _uid?: string;
27
+ component?: T;
28
+ _editable?: string;
29
+ }
30
+ interface PreviewToken {
31
+ token: string;
32
+ timestamp: string;
33
+ }
34
+ interface LocalizedPath {
35
+ path: string;
36
+ name: string | null;
37
+ lang: string;
38
+ published: boolean;
39
+ }
40
+ interface ISbStoryData<Content = ISbComponentType<string> & {
41
+ [index: string]: any;
42
+ }> extends ISbMultipleStoriesData {
43
+ alternates: ISbAlternateObject[];
44
+ breadcrumbs?: ISbLinkURLObject[];
45
+ content: Content;
46
+ created_at: string;
47
+ deleted_at?: string;
48
+ default_full_slug?: string | null;
49
+ default_root?: string;
50
+ disable_fe_editor?: boolean;
51
+ favourite_for_user_ids?: number[] | null;
52
+ first_published_at?: string | null;
53
+ full_slug: string;
54
+ group_id: string;
55
+ id: number;
56
+ imported_at?: string;
57
+ is_folder?: boolean;
58
+ is_startpage?: boolean;
59
+ lang: string;
60
+ last_author?: {
61
+ id: number;
62
+ userid: string;
63
+ };
64
+ last_author_id?: number;
65
+ localized_paths?: LocalizedPath[] | null;
66
+ meta_data: any;
67
+ name: string;
68
+ parent?: ISbStoryData;
69
+ parent_id: number | null;
70
+ path?: string;
71
+ pinned?: '1' | boolean;
72
+ position: number;
73
+ preview_token?: PreviewToken;
74
+ published?: boolean;
75
+ published_at: string | null;
76
+ release_id?: number | null;
77
+ scheduled_date?: string | null;
78
+ slug: string;
79
+ sort_by_date: string | null;
80
+ tag_list: string[];
81
+ translated_slugs?: {
82
+ path: string;
83
+ name: string | null;
84
+ lang: ISbStoryData['lang'];
85
+ published: boolean;
86
+ }[] | null;
87
+ unpublished_changes?: boolean;
88
+ updated_at?: string;
89
+ uuid: string;
90
+ }
91
+ interface ISbMultipleStoriesData {
92
+ by_ids?: string;
93
+ by_uuids?: string;
94
+ contain_component?: string;
95
+ excluding_ids?: string;
96
+ filter_query?: any;
97
+ folder_only?: boolean;
98
+ full_slug?: string;
99
+ in_release?: string;
100
+ in_trash?: boolean;
101
+ is_published?: boolean;
102
+ in_workflow_stages?: string;
103
+ page?: number;
104
+ pinned?: '1' | boolean;
105
+ search?: string;
106
+ sort_by?: string;
107
+ starts_with?: string;
108
+ story_only?: boolean;
109
+ text_search?: string;
110
+ with_parent?: number;
111
+ with_slug?: string;
112
+ with_tag?: string;
113
+ }
114
+ interface ISbAlternateObject {
115
+ id: number;
116
+ name: string;
117
+ slug: string;
118
+ published: boolean;
119
+ full_slug: string;
120
+ is_folder: boolean;
121
+ parent_id: number;
122
+ }
123
+ interface ISbLinkURLObject {
124
+ id: number;
125
+ name: string;
126
+ slug: string;
127
+ full_slug: string;
128
+ url: string;
129
+ uuid: string;
130
+ }
131
+ //#endregion
132
+ //#region src/onStoryblokEditorEvent.d.ts
133
+ /**
134
+ * Registers a callback for Storyblok Visual Editor live preview updates.
135
+ *
136
+ * This utility connects to the Storyblok Preview Bridge and listens
137
+ * for Visual Editor events.
138
+ *
139
+ * Behavior:
140
+ * - **input** → Calls the provided callback with the updated story data.
141
+ * - **change** → Reloads the page.
142
+ * - **published** → Reloads the page.
143
+ *
144
+ * Multiple listeners can be registered simultaneously. Each call returns
145
+ * a cleanup function that removes the registered listener.
146
+ *
147
+ * @typeParam T - The Storyblok component schema type.
148
+ *
149
+ * @param callback
150
+ * Callback executed when the Visual Editor sends an `input` event.
151
+ *
152
+ * @param bridgeOptions
153
+ * Optional configuration for the Storyblok Preview Bridge.
154
+ * This configuration is applied **only during the first initialization**.
155
+ *
156
+ * @returns
157
+ * A cleanup function that removes the registered listener.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * const cleanup = await onStoryblokEditorEvent((story) => {
162
+ * console.log('Live updated story:', story)
163
+ * })
164
+ *
165
+ * // later
166
+ * cleanup()
167
+ * ```
168
+ */
169
+ declare function onStoryblokEditorEvent<T extends ISbComponentType<string> = ISbComponentType<string>>(callback: (story: ISbStoryData<T>) => void, bridgeOptions?: BridgeParams$1): Promise<() => void>;
170
+ //#endregion
171
+ //#region src/utils/isInEditor.d.ts
172
+ /**
173
+ * Options for validating Storyblok Visual Editor requests.
174
+ */
175
+ interface StoryblokValidationOptions {
176
+ /**
177
+ * Optional space ID to validate against the request.
178
+ * If provided, the request must match this space ID to be considered valid.
179
+ */
180
+ spaceId?: string;
181
+ }
182
+ /**
183
+ * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.
184
+ *
185
+ * This function performs a multi-layered validation to ensure the request originates from
186
+ * the Storyblok Visual Editor by checking for required query parameters and optionally
187
+ * validating the space ID.
188
+ *
189
+ * @param url - The URL object to validate.
190
+ * @param options - Optional validation configuration.
191
+ * @param options.spaceId - If provided, validates that the request's space ID matches this value.
192
+ *
193
+ * @returns `true` if the URL contains all required Storyblok Visual Editor parameters
194
+ * and passes optional space ID validation; `false` otherwise.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');
199
+ *
200
+ * // Basic validation
201
+ * if (isInEditor(url)) {
202
+ * console.log('Valid Storyblok editor request');
203
+ * }
204
+ *
205
+ * // Validation with space ID check
206
+ * if (isInEditor(url, { spaceId: '789' })) {
207
+ * console.log('Valid request for specific space');
208
+ * }
209
+ * ```
210
+ */
211
+ declare function isInEditor(url: URL, options?: StoryblokValidationOptions): boolean;
212
+ //#endregion
213
+ export { type BridgeParams, type ISbComponentType, type ISbStoryData, isInEditor, loadStoryblokBridge, onStoryblokEditorEvent, storyblokEditable };
214
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,196 @@
1
+ //#region src/editable.ts
2
+ function storyblokEditable(blok) {
3
+ const editable = blok?._editable;
4
+ if (!editable) return {};
5
+ if (!editable.startsWith("<!--#storyblok#") || !editable.endsWith("-->")) return {};
6
+ try {
7
+ const json = editable.slice(15, -3);
8
+ const options = JSON.parse(json);
9
+ return {
10
+ "data-blok-c": JSON.stringify(options),
11
+ "data-blok-uid": `${options.id}-${options.uid}`
12
+ };
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ //#endregion
19
+ //#region src/loadStoryblokBridge.ts
20
+ let bridgePromise;
21
+ let storedConfig;
22
+ function configsAreEqual(config1, config2) {
23
+ return JSON.stringify(config1) === JSON.stringify(config2);
24
+ }
25
+ /**
26
+ * Get or create a StoryblokBridge instance.
27
+ *⚠️ The bridge is a singleton. Configuration is applied only on first load.
28
+ * @param config Optional configuration for the StoryblokBridge.
29
+ * @returns A promise that resolves to a StoryblokBridge instance.
30
+ */
31
+ function loadStoryblokBridge(config) {
32
+ if (bridgePromise) {
33
+ if (config && !configsAreEqual(config, storedConfig)) throw new Error("[Storyblok] Preview Bridge already initialized with a different configuration. The bridge can only be created once per page and does not support runtime reconfiguration.");
34
+ return bridgePromise;
35
+ }
36
+ storedConfig = config;
37
+ bridgePromise = import("@storyblok/preview-bridge").then(({ default: StoryblokBridge }) => new StoryblokBridge(config)).catch((error) => {
38
+ bridgePromise = void 0;
39
+ storedConfig = void 0;
40
+ throw error;
41
+ });
42
+ return bridgePromise;
43
+ }
44
+
45
+ //#endregion
46
+ //#region src/utils/isBrowser.ts
47
+ function isBrowser() {
48
+ return typeof window !== "undefined";
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/utils/isInEditor.ts
53
+ /**
54
+ * Required query parameters that must be present in all Storyblok Visual Editor requests.
55
+ */
56
+ const REQUIRED_STORYBLOK_PARAMS = [
57
+ "_storyblok",
58
+ "_storyblok_c",
59
+ "_storyblok_tk[space_id]"
60
+ ];
61
+ /**
62
+ * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.
63
+ *
64
+ * This function performs a multi-layered validation to ensure the request originates from
65
+ * the Storyblok Visual Editor by checking for required query parameters and optionally
66
+ * validating the space ID.
67
+ *
68
+ * @param url - The URL object to validate.
69
+ * @param options - Optional validation configuration.
70
+ * @param options.spaceId - If provided, validates that the request's space ID matches this value.
71
+ *
72
+ * @returns `true` if the URL contains all required Storyblok Visual Editor parameters
73
+ * and passes optional space ID validation; `false` otherwise.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');
78
+ *
79
+ * // Basic validation
80
+ * if (isInEditor(url)) {
81
+ * console.log('Valid Storyblok editor request');
82
+ * }
83
+ *
84
+ * // Validation with space ID check
85
+ * if (isInEditor(url, { spaceId: '789' })) {
86
+ * console.log('Valid request for specific space');
87
+ * }
88
+ * ```
89
+ */
90
+ function isInEditor(url, options = {}) {
91
+ const params = url.searchParams;
92
+ if (!REQUIRED_STORYBLOK_PARAMS.every((param) => params.has(param))) return false;
93
+ if (options.spaceId && params.get("_storyblok_tk[space_id]") !== options.spaceId) return false;
94
+ return true;
95
+ }
96
+
97
+ //#endregion
98
+ //#region src/utils/canUseStoryblokBridge.ts
99
+ function canUseStoryblokBridge() {
100
+ if (!isBrowser()) return false;
101
+ return isInEditor(new URL(window.location.href));
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/onStoryblokEditorEvent.ts
106
+ /**
107
+ * Internal listener registry for Storyblok `input` events.
108
+ * Each listener receives the updated story data from the Visual Editor.
109
+ */
110
+ const inputListeners = /* @__PURE__ */ new Set();
111
+ /**
112
+ * Tracks whether the Storyblok Preview Bridge event listeners
113
+ * have already been registered.
114
+ */
115
+ let bridgeInitPromise;
116
+ /**
117
+ * Initializes the Storyblok Preview Bridge and attaches event listeners.
118
+ *
119
+ * This function ensures that the bridge is only initialized once per page.
120
+ *
121
+ * Registered events:
122
+ * - `input` → Dispatches updated story data to all registered listeners.
123
+ * - `change` → Forces a full page reload.
124
+ * - `published` → Forces a full page reload.
125
+ *
126
+ * @param bridgeOptions Optional configuration for the Preview Bridge.
127
+ */
128
+ async function initializeBridge(bridgeOptions) {
129
+ if (!canUseStoryblokBridge()) return;
130
+ if (bridgeInitPromise) return bridgeInitPromise;
131
+ bridgeInitPromise = (async () => {
132
+ (await loadStoryblokBridge(bridgeOptions)).on([
133
+ "input",
134
+ "change",
135
+ "published"
136
+ ], (event) => {
137
+ if (!event) return;
138
+ if (event.action === "input" && event.story) {
139
+ for (const listener of inputListeners) listener(event.story);
140
+ return;
141
+ }
142
+ if (event.action === "change" || event.action === "published") window.location.reload();
143
+ });
144
+ })();
145
+ return bridgeInitPromise;
146
+ }
147
+ /**
148
+ * Registers a callback for Storyblok Visual Editor live preview updates.
149
+ *
150
+ * This utility connects to the Storyblok Preview Bridge and listens
151
+ * for Visual Editor events.
152
+ *
153
+ * Behavior:
154
+ * - **input** → Calls the provided callback with the updated story data.
155
+ * - **change** → Reloads the page.
156
+ * - **published** → Reloads the page.
157
+ *
158
+ * Multiple listeners can be registered simultaneously. Each call returns
159
+ * a cleanup function that removes the registered listener.
160
+ *
161
+ * @typeParam T - The Storyblok component schema type.
162
+ *
163
+ * @param callback
164
+ * Callback executed when the Visual Editor sends an `input` event.
165
+ *
166
+ * @param bridgeOptions
167
+ * Optional configuration for the Storyblok Preview Bridge.
168
+ * This configuration is applied **only during the first initialization**.
169
+ *
170
+ * @returns
171
+ * A cleanup function that removes the registered listener.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const cleanup = await onStoryblokEditorEvent((story) => {
176
+ * console.log('Live updated story:', story)
177
+ * })
178
+ *
179
+ * // later
180
+ * cleanup()
181
+ * ```
182
+ */
183
+ async function onStoryblokEditorEvent(callback, bridgeOptions) {
184
+ await initializeBridge(bridgeOptions);
185
+ const listener = (story) => {
186
+ callback(story);
187
+ };
188
+ inputListeners.add(listener);
189
+ return () => {
190
+ inputListeners.delete(listener);
191
+ };
192
+ }
193
+
194
+ //#endregion
195
+ export { isInEditor, loadStoryblokBridge, onStoryblokEditorEvent, storyblokEditable };
196
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/editable.ts","../src/loadStoryblokBridge.ts","../src/utils/isBrowser.ts","../src/utils/isInEditor.ts","../src/utils/canUseStoryblokBridge.ts","../src/onStoryblokEditorEvent.ts"],"sourcesContent":["interface Blok {\n _editable?: string;\n}\n\ninterface EditableOptions {\n id: string;\n uid: string;\n}\n\nexport default function storyblokEditable(blok?: Blok) {\n const editable = blok?._editable;\n if (!editable) {\n return {};\n }\n\n const prefix = '<!--#storyblok#';\n const suffix = '-->';\n\n if (!editable.startsWith(prefix) || !editable.endsWith(suffix)) {\n return {};\n }\n\n try {\n const json = editable.slice(prefix.length, -suffix.length);\n const options = JSON.parse(json) as EditableOptions;\n\n return {\n 'data-blok-c': JSON.stringify(options),\n 'data-blok-uid': `${options.id}-${options.uid}`,\n };\n }\n catch {\n return {};\n }\n}\n","import type StoryblokBridge from '@storyblok/preview-bridge';\nimport type { BridgeParams } from '@storyblok/preview-bridge';\n\nlet bridgePromise: Promise<StoryblokBridge> | undefined;\nlet storedConfig: BridgeParams | undefined;\nfunction configsAreEqual(config1: BridgeParams | undefined, config2: BridgeParams | undefined): boolean {\n return JSON.stringify(config1) === JSON.stringify(config2);\n}\n\n/**\n * Get or create a StoryblokBridge instance.\n *⚠️ The bridge is a singleton. Configuration is applied only on first load.\n * @param config Optional configuration for the StoryblokBridge.\n * @returns A promise that resolves to a StoryblokBridge instance.\n */\nexport function loadStoryblokBridge(config?: BridgeParams) {\n if (bridgePromise) {\n if (config && !configsAreEqual(config, storedConfig)) {\n throw new Error(\n '[Storyblok] Preview Bridge already initialized with a different configuration. '\n + 'The bridge can only be created once per page and does not support runtime reconfiguration.',\n );\n }\n return bridgePromise;\n }\n\n storedConfig = config;\n\n bridgePromise = import('@storyblok/preview-bridge')\n .then(({ default: StoryblokBridge }) => new StoryblokBridge(config))\n .catch((error) => {\n bridgePromise = undefined;\n storedConfig = undefined;\n throw error;\n });\n\n return bridgePromise;\n}\n","export function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n","/**\n * Options for validating Storyblok Visual Editor requests.\n */\ninterface StoryblokValidationOptions {\n /**\n * Optional space ID to validate against the request.\n * If provided, the request must match this space ID to be considered valid.\n */\n spaceId?: string;\n}\n\n/**\n * Required query parameters that must be present in all Storyblok Visual Editor requests.\n */\nconst REQUIRED_STORYBLOK_PARAMS = ['_storyblok', '_storyblok_c', '_storyblok_tk[space_id]'] as const;\n\n/**\n * Validates whether a given URL is a legitimate request from the Storyblok Visual Editor.\n *\n * This function performs a multi-layered validation to ensure the request originates from\n * the Storyblok Visual Editor by checking for required query parameters and optionally\n * validating the space ID.\n *\n * @param url - The URL object to validate.\n * @param options - Optional validation configuration.\n * @param options.spaceId - If provided, validates that the request's space ID matches this value.\n *\n * @returns `true` if the URL contains all required Storyblok Visual Editor parameters\n * and passes optional space ID validation; `false` otherwise.\n *\n * @example\n * ```typescript\n * const url = new URL('https://example.com/?_storyblok=123&_storyblok_c=456&_storyblok_tk[space_id]=789');\n *\n * // Basic validation\n * if (isInEditor(url)) {\n * console.log('Valid Storyblok editor request');\n * }\n *\n * // Validation with space ID check\n * if (isInEditor(url, { spaceId: '789' })) {\n * console.log('Valid request for specific space');\n * }\n * ```\n */\nexport function isInEditor(\n url: URL,\n options: StoryblokValidationOptions = {},\n): boolean {\n const params = url.searchParams;\n\n // Early return: Check all required parameters exist\n const hasRequiredParams = REQUIRED_STORYBLOK_PARAMS.every(param => params.has(param));\n\n if (!hasRequiredParams) {\n return false;\n }\n\n // Optional space ID validation\n if (options.spaceId && params.get('_storyblok_tk[space_id]') !== options.spaceId) {\n return false;\n }\n\n return true;\n}\n","import { isBrowser } from './isBrowser';\nimport { isInEditor } from './isInEditor';\n\nexport function canUseStoryblokBridge(): boolean {\n if (!isBrowser()) {\n return false;\n }\n return isInEditor(new URL(window.location.href));\n}\n","import type { BridgeParams } from '@storyblok/preview-bridge';\nimport type { ISbComponentType, ISbStoryData } from 'storyblok-js-client';\n\nimport { loadStoryblokBridge } from './loadStoryblokBridge';\nimport { canUseStoryblokBridge } from './utils/canUseStoryblokBridge';\n\n/**\n * Internal listener registry for Storyblok `input` events.\n * Each listener receives the updated story data from the Visual Editor.\n */\nconst inputListeners = new Set<(story: ISbStoryData) => void>();\n\n/**\n * Tracks whether the Storyblok Preview Bridge event listeners\n * have already been registered.\n */\nlet bridgeInitPromise: Promise<void> | undefined;\n\n/**\n * Initializes the Storyblok Preview Bridge and attaches event listeners.\n *\n * This function ensures that the bridge is only initialized once per page.\n *\n * Registered events:\n * - `input` → Dispatches updated story data to all registered listeners.\n * - `change` → Forces a full page reload.\n * - `published` → Forces a full page reload.\n *\n * @param bridgeOptions Optional configuration for the Preview Bridge.\n */\nasync function initializeBridge(bridgeOptions?: BridgeParams): Promise<void> {\n if (!canUseStoryblokBridge()) {\n return;\n }\n\n // If initialization already started, reuse it\n if (bridgeInitPromise) {\n return bridgeInitPromise;\n }\n\n bridgeInitPromise = (async () => {\n const bridge = await loadStoryblokBridge(bridgeOptions);\n\n bridge.on(['input', 'change', 'published'], (event) => {\n if (!event) {\n return;\n }\n\n if (event.action === 'input' && event.story) {\n for (const listener of inputListeners) {\n listener(event.story as ISbStoryData);\n }\n return;\n }\n\n if (event.action === 'change' || event.action === 'published') {\n window.location.reload();\n }\n });\n })();\n\n return bridgeInitPromise;\n}\n\n/**\n * Registers a callback for Storyblok Visual Editor live preview updates.\n *\n * This utility connects to the Storyblok Preview Bridge and listens\n * for Visual Editor events.\n *\n * Behavior:\n * - **input** → Calls the provided callback with the updated story data.\n * - **change** → Reloads the page.\n * - **published** → Reloads the page.\n *\n * Multiple listeners can be registered simultaneously. Each call returns\n * a cleanup function that removes the registered listener.\n *\n * @typeParam T - The Storyblok component schema type.\n *\n * @param callback\n * Callback executed when the Visual Editor sends an `input` event.\n *\n * @param bridgeOptions\n * Optional configuration for the Storyblok Preview Bridge.\n * This configuration is applied **only during the first initialization**.\n *\n * @returns\n * A cleanup function that removes the registered listener.\n *\n * @example\n * ```ts\n * const cleanup = await onStoryblokEditorEvent((story) => {\n * console.log('Live updated story:', story)\n * })\n *\n * // later\n * cleanup()\n * ```\n */\nexport async function onStoryblokEditorEvent<\n T extends ISbComponentType<string> = ISbComponentType<string>,\n>(\n callback: (story: ISbStoryData<T>) => void,\n bridgeOptions?: BridgeParams,\n): Promise<() => void> {\n await initializeBridge(bridgeOptions);\n\n const listener = (story: ISbStoryData) => {\n callback(story as ISbStoryData<T>);\n };\n\n inputListeners.add(listener);\n\n return () => {\n inputListeners.delete(listener);\n };\n}\n"],"mappings":";AASA,SAAwB,kBAAkB,MAAa;CACrD,MAAM,WAAW,MAAM;AACvB,KAAI,CAAC,SACH,QAAO,EAAE;AAMX,KAAI,CAAC,SAAS,WAHC,kBAGiB,IAAI,CAAC,SAAS,SAF/B,MAE+C,CAC5D,QAAO,EAAE;AAGX,KAAI;EACF,MAAM,OAAO,SAAS,MAAM,IAAe,GAAe;EAC1D,MAAM,UAAU,KAAK,MAAM,KAAK;AAEhC,SAAO;GACL,eAAe,KAAK,UAAU,QAAQ;GACtC,iBAAiB,GAAG,QAAQ,GAAG,GAAG,QAAQ;GAC3C;SAEG;AACJ,SAAO,EAAE;;;;;;AC7Bb,IAAI;AACJ,IAAI;AACJ,SAAS,gBAAgB,SAAmC,SAA4C;AACtG,QAAO,KAAK,UAAU,QAAQ,KAAK,KAAK,UAAU,QAAQ;;;;;;;;AAS5D,SAAgB,oBAAoB,QAAuB;AACzD,KAAI,eAAe;AACjB,MAAI,UAAU,CAAC,gBAAgB,QAAQ,aAAa,CAClD,OAAM,IAAI,MACR,4KAED;AAEH,SAAO;;AAGT,gBAAe;AAEf,iBAAgB,OAAO,6BACpB,MAAM,EAAE,SAAS,sBAAsB,IAAI,gBAAgB,OAAO,CAAC,CACnE,OAAO,UAAU;AAChB,kBAAgB;AAChB,iBAAe;AACf,QAAM;GACN;AAEJ,QAAO;;;;;ACpCT,SAAgB,YAAqB;AACnC,QAAO,OAAO,WAAW;;;;;;;;ACa3B,MAAM,4BAA4B;CAAC;CAAc;CAAgB;CAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B3F,SAAgB,WACd,KACA,UAAsC,EAAE,EAC/B;CACT,MAAM,SAAS,IAAI;AAKnB,KAAI,CAFsB,0BAA0B,OAAM,UAAS,OAAO,IAAI,MAAM,CAAC,CAGnF,QAAO;AAIT,KAAI,QAAQ,WAAW,OAAO,IAAI,0BAA0B,KAAK,QAAQ,QACvE,QAAO;AAGT,QAAO;;;;;AC5DT,SAAgB,wBAAiC;AAC/C,KAAI,CAAC,WAAW,CACd,QAAO;AAET,QAAO,WAAW,IAAI,IAAI,OAAO,SAAS,KAAK,CAAC;;;;;;;;;ACGlD,MAAM,iCAAiB,IAAI,KAAoC;;;;;AAM/D,IAAI;;;;;;;;;;;;;AAcJ,eAAe,iBAAiB,eAA6C;AAC3E,KAAI,CAAC,uBAAuB,CAC1B;AAIF,KAAI,kBACF,QAAO;AAGT,sBAAqB,YAAY;AAG/B,GAFe,MAAM,oBAAoB,cAAc,EAEhD,GAAG;GAAC;GAAS;GAAU;GAAY,GAAG,UAAU;AACrD,OAAI,CAAC,MACH;AAGF,OAAI,MAAM,WAAW,WAAW,MAAM,OAAO;AAC3C,SAAK,MAAM,YAAY,eACrB,UAAS,MAAM,MAAsB;AAEvC;;AAGF,OAAI,MAAM,WAAW,YAAY,MAAM,WAAW,YAChD,QAAO,SAAS,QAAQ;IAE1B;KACA;AAEJ,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCT,eAAsB,uBAGpB,UACA,eACqB;AACrB,OAAM,iBAAiB,cAAc;CAErC,MAAM,YAAY,UAAwB;AACxC,WAAS,MAAyB;;AAGpC,gBAAe,IAAI,SAAS;AAE5B,cAAa;AACX,iBAAe,OAAO,SAAS"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@storyblok/live-preview",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "private": false,
6
+ "description": "Official Live Preview integration for the Storyblok Headless CMS",
7
+ "author": "Storyblok",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/storyblok/monoblok/tree/main/packages/live-preview#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/storyblok/monoblok.git",
13
+ "directory": "packages/live-preview"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/storyblok/monoblok/issues"
17
+ },
18
+ "keywords": [
19
+ "live-preview",
20
+ "storyblok"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.mjs",
31
+ "types": "./dist/index.d.cts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@storyblok/preview-bridge": "^2.1.6"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.11.0",
43
+ "bumpp": "^10.4.1",
44
+ "eslint": "^9.39.2",
45
+ "tsdown": "^0.20.3",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.0.18",
48
+ "@storyblok/eslint-config": "0.4.2",
49
+ "storyblok-js-client": "7.2.4"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown",
53
+ "dev": "tsdown --watch",
54
+ "test": "vitest",
55
+ "lint": "eslint .",
56
+ "lint:fix": "eslint . --fix",
57
+ "typecheck": "tsc --noEmit"
58
+ }
59
+ }