@freelygive/canvas-utils 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/dist/canvas-slots.d.ts +21 -0
- package/dist/canvas-slots.js +19 -0
- package/dist/canvas-slots.js.map +1 -0
- package/dist/editor-mode.d.ts +12 -0
- package/dist/editor-mode.js +18 -0
- package/dist/editor-mode.js.map +1 -0
- package/package.json +44 -0
- package/src/components/utils_editor_note/component.yml +7 -0
- package/src/components/utils_editor_note/dist/index.js +32 -0
- package/src/components/utils_editor_note/index.jsx +37 -0
- package/src/components/utils_entity/component.yml +7 -0
- package/src/components/utils_entity/dist/index.js +74 -0
- package/src/components/utils_entity/index.jsx +53 -0
- package/src/components/utils_slots/component.yml +7 -0
- package/src/components/utils_slots/dist/index.js +79 -0
- package/src/components/utils_slots/index.jsx +91 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper to create canvas island props in the ["raw", value] format
|
|
5
|
+
* Canvas stores prop values as tuples: ["raw", actualValue]
|
|
6
|
+
*/
|
|
7
|
+
declare const rawProp: (value: unknown) => [string, unknown];
|
|
8
|
+
/**
|
|
9
|
+
* Helper to create a canvas-island element HTML string
|
|
10
|
+
* Canvas uses these custom elements to represent components in slot content
|
|
11
|
+
*/
|
|
12
|
+
declare const createCanvasIsland: (props: Record<string, unknown>) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Helper to create a Canvas slot as a React element.
|
|
15
|
+
* The returned element has props.value containing the HTML string,
|
|
16
|
+
* which parseCanvasSlot reads to extract canvas-island data.
|
|
17
|
+
* When rendered directly (editor mode), it outputs the raw HTML.
|
|
18
|
+
*/
|
|
19
|
+
declare const createCanvasSlot: (html: string) => React.ReactElement;
|
|
20
|
+
|
|
21
|
+
export { createCanvasIsland, createCanvasSlot, rawProp };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/testing/canvas-slots.ts
|
|
2
|
+
import React from "react";
|
|
3
|
+
var rawProp = (value) => ["raw", value];
|
|
4
|
+
var createCanvasIsland = (props) => {
|
|
5
|
+
const rawProps = {};
|
|
6
|
+
for (const [key, value] of Object.entries(props)) {
|
|
7
|
+
rawProps[key] = rawProp(value);
|
|
8
|
+
}
|
|
9
|
+
const propsJson = JSON.stringify(rawProps).replace(/"/g, """);
|
|
10
|
+
return `<canvas-island props="${propsJson}"></canvas-island>`;
|
|
11
|
+
};
|
|
12
|
+
var CanvasSlot = ({ value }) => React.createElement("div", { dangerouslySetInnerHTML: { __html: value } });
|
|
13
|
+
var createCanvasSlot = (html) => React.createElement(CanvasSlot, { value: html });
|
|
14
|
+
export {
|
|
15
|
+
createCanvasIsland,
|
|
16
|
+
createCanvasSlot,
|
|
17
|
+
rawProp
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=canvas-slots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing/canvas-slots.ts"],"sourcesContent":["import React from 'react';\n\n/**\n * Helper to create canvas island props in the [\"raw\", value] format\n * Canvas stores prop values as tuples: [\"raw\", actualValue]\n */\nexport const rawProp = (value: unknown): [string, unknown] => ['raw', value];\n\n/**\n * Helper to create a canvas-island element HTML string\n * Canvas uses these custom elements to represent components in slot content\n */\nexport const createCanvasIsland = (props: Record<string, unknown>): string => {\n const rawProps: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(props)) {\n rawProps[key] = rawProp(value);\n }\n const propsJson = JSON.stringify(rawProps).replace(/\"/g, '"');\n return `<canvas-island props=\"${propsJson}\"></canvas-island>`;\n};\n\n/**\n * React component that simulates a Canvas slot element.\n * Renders the slot HTML via dangerouslySetInnerHTML so canvas-island\n * custom elements appear in the DOM when rendered directly (editor mode).\n * Also exposes `value` as a prop so parseCanvasSlot can read it.\n */\nconst CanvasSlot = ({ value }: { value: string }) =>\n React.createElement('div', { dangerouslySetInnerHTML: { __html: value } });\n\n/**\n * Helper to create a Canvas slot as a React element.\n * The returned element has props.value containing the HTML string,\n * which parseCanvasSlot reads to extract canvas-island data.\n * When rendered directly (editor mode), it outputs the raw HTML.\n */\nexport const createCanvasSlot = (html: string): React.ReactElement =>\n React.createElement(CanvasSlot, { value: html });\n"],"mappings":";AAAA,OAAO,WAAW;AAMX,IAAM,UAAU,CAAC,UAAsC,CAAC,OAAO,KAAK;AAMpE,IAAM,qBAAqB,CAAC,UAA2C;AAC5E,QAAM,WAAoC,CAAC;AAC3C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,aAAS,GAAG,IAAI,QAAQ,KAAK;AAAA,EAC/B;AACA,QAAM,YAAY,KAAK,UAAU,QAAQ,EAAE,QAAQ,MAAM,QAAQ;AACjE,SAAO,yBAAyB,SAAS;AAC3C;AAQA,IAAM,aAAa,CAAC,EAAE,MAAM,MAC1B,MAAM,cAAc,OAAO,EAAE,yBAAyB,EAAE,QAAQ,MAAM,EAAE,CAAC;AAQpE,IAAM,mBAAmB,CAAC,SAC/B,MAAM,cAAc,YAAY,EAAE,OAAO,KAAK,CAAC;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enable Canvas editor mode for a story
|
|
3
|
+
* Sets drupalSettings.canvas and cleans up on unmount
|
|
4
|
+
*/
|
|
5
|
+
declare const enableEditorMode: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Disable Canvas editor mode
|
|
8
|
+
* Removes drupalSettings.canvas
|
|
9
|
+
*/
|
|
10
|
+
declare const disableEditorMode: () => void;
|
|
11
|
+
|
|
12
|
+
export { disableEditorMode, enableEditorMode };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/testing/editor-mode.ts
|
|
2
|
+
var enableEditorMode = () => {
|
|
3
|
+
window.drupalSettings = {
|
|
4
|
+
...window.drupalSettings,
|
|
5
|
+
canvas: {}
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
var disableEditorMode = () => {
|
|
9
|
+
const win = window;
|
|
10
|
+
if (win.drupalSettings?.canvas !== void 0) {
|
|
11
|
+
delete win.drupalSettings.canvas;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
disableEditorMode,
|
|
16
|
+
enableEditorMode
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=editor-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing/editor-mode.ts"],"sourcesContent":["/**\n * Enable Canvas editor mode for a story\n * Sets drupalSettings.canvas and cleans up on unmount\n */\nexport const enableEditorMode = () => {\n (\n window as Window & { drupalSettings?: Record<string, unknown> }\n ).drupalSettings = {\n ...(window as Window & { drupalSettings?: Record<string, unknown> })\n .drupalSettings,\n canvas: {},\n };\n};\n\n/**\n * Disable Canvas editor mode\n * Removes drupalSettings.canvas\n */\nexport const disableEditorMode = () => {\n const win = window as Window & { drupalSettings?: { canvas?: unknown } };\n if (win.drupalSettings?.canvas !== undefined) {\n delete win.drupalSettings.canvas;\n }\n};\n"],"mappings":";AAIO,IAAM,mBAAmB,MAAM;AACpC,EACE,OACA,iBAAiB;AAAA,IACjB,GAAI,OACD;AAAA,IACH,QAAQ,CAAC;AAAA,EACX;AACF;AAMO,IAAM,oBAAoB,MAAM;AACrC,QAAM,MAAM;AACZ,MAAI,IAAI,gBAAgB,WAAW,QAAW;AAC5C,WAAO,IAAI,eAAe;AAAA,EAC5B;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@freelygive/canvas-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Canvas utility components and test helpers for Drupal Canvas projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/components",
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
"./testing/canvas-slots": {
|
|
12
|
+
"types": "./dist/canvas-slots.d.ts",
|
|
13
|
+
"default": "./dist/canvas-slots.js"
|
|
14
|
+
},
|
|
15
|
+
"./testing/editor-mode": {
|
|
16
|
+
"types": "./dist/editor-mode.d.ts",
|
|
17
|
+
"default": "./dist/editor-mode.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"npmScaffold": {
|
|
21
|
+
"file-mapping": {
|
|
22
|
+
"components/utils_editor_note": "src/components/utils_editor_note",
|
|
23
|
+
"components/utils_slots": "src/components/utils_slots",
|
|
24
|
+
"components/utils_entity": "src/components/utils_entity"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"test": "echo \"No tests yet\""
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@freelygive/npm-scaffold": "^0.3.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^19.0.0",
|
|
39
|
+
"react": "^19.2.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
},
|
|
43
|
+
"license": "GPL-2.0-or-later"
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Detect if we're in Canvas page editor mode
|
|
4
|
+
* Returns true when the component is being edited in the Canvas UI
|
|
5
|
+
*/ export const isCanvasEditorMode = ()=>{
|
|
6
|
+
var _window_drupalSettings;
|
|
7
|
+
if (typeof window === 'undefined') return false;
|
|
8
|
+
// Check for drupalSettings.canvas (set in editor mode)
|
|
9
|
+
if (((_window_drupalSettings = window.drupalSettings) === null || _window_drupalSettings === void 0 ? void 0 : _window_drupalSettings.canvas) !== undefined) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
var _window_top_location, _window_top;
|
|
14
|
+
const pathname = ((_window_top = window.top) === null || _window_top === void 0 ? void 0 : (_window_top_location = _window_top.location) === null || _window_top_location === void 0 ? void 0 : _window_top_location.pathname) || '';
|
|
15
|
+
// Check if in Canvas editor (but not in component code editor)
|
|
16
|
+
if (pathname.startsWith('/canvas/')) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
} catch (unused) {
|
|
20
|
+
// Cross-origin access blocked - likely in an iframe
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Editor note component for displaying messages only visible in Canvas editor mode
|
|
27
|
+
* Used to explain runtime behavior that differs from editor preview
|
|
28
|
+
*/ const EditorNote = ({ children })=>/*#__PURE__*/ _jsx("div", {
|
|
29
|
+
className: "border-2 border-dashed border-black/30 bg-cream/50 px-6 py-4 text-sm text-black/70",
|
|
30
|
+
children: children
|
|
31
|
+
});
|
|
32
|
+
export default EditorNote;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect if we're in Canvas page editor mode
|
|
3
|
+
* Returns true when the component is being edited in the Canvas UI
|
|
4
|
+
*/
|
|
5
|
+
export const isCanvasEditorMode = () => {
|
|
6
|
+
if (typeof window === 'undefined') return false;
|
|
7
|
+
|
|
8
|
+
// Check for drupalSettings.canvas (set in editor mode)
|
|
9
|
+
if (window.drupalSettings?.canvas !== undefined) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const pathname = window.top?.location?.pathname || '';
|
|
15
|
+
// Check if in Canvas editor (but not in component code editor)
|
|
16
|
+
if (pathname.startsWith('/canvas/')) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Cross-origin access blocked - likely in an iframe
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Editor note component for displaying messages only visible in Canvas editor mode
|
|
29
|
+
* Used to explain runtime behavior that differs from editor preview
|
|
30
|
+
*/
|
|
31
|
+
const EditorNote = ({ children }) => (
|
|
32
|
+
<div className="border-2 border-dashed border-black/30 bg-cream/50 px-6 py-4 text-sm text-black/70">
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export default EditorNote;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
|
|
2
|
+
try {
|
|
3
|
+
var info = gen[key](arg);
|
|
4
|
+
var value = info.value;
|
|
5
|
+
} catch (error) {
|
|
6
|
+
reject(error);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (info.done) {
|
|
10
|
+
resolve(value);
|
|
11
|
+
} else {
|
|
12
|
+
Promise.resolve(value).then(_next, _throw);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function _async_to_generator(fn) {
|
|
16
|
+
return function() {
|
|
17
|
+
var self = this, args = arguments;
|
|
18
|
+
return new Promise(function(resolve, reject) {
|
|
19
|
+
var gen = fn.apply(self, args);
|
|
20
|
+
function _next(value) {
|
|
21
|
+
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
|
|
22
|
+
}
|
|
23
|
+
function _throw(err) {
|
|
24
|
+
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
|
|
25
|
+
}
|
|
26
|
+
_next(undefined);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
import { getPageData, JsonApiClient } from 'drupal-canvas';
|
|
31
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
32
|
+
import useSWR from 'swr';
|
|
33
|
+
/**
|
|
34
|
+
* Fetch the main entity for the current page using SWR.
|
|
35
|
+
* Uses Canvas mainEntity (when available) with fallback to title-based lookup.
|
|
36
|
+
* @param {string} entityType - The JSON:API entity type (e.g., 'node--news_article')
|
|
37
|
+
* @param {object} options - Fetch options
|
|
38
|
+
* @param {string[]} options.includes - Relationships to include
|
|
39
|
+
* @param {object} options.fields - Sparse fieldset map (e.g., { 'node--news_article': ['title'] })
|
|
40
|
+
* @returns {{ data: object|undefined, error: Error|undefined, isLoading: boolean }}
|
|
41
|
+
*/ export const useMainEntity = (entityType, { includes = [], fields = {} } = {})=>{
|
|
42
|
+
const pageData = getPageData();
|
|
43
|
+
const { pageTitle } = pageData;
|
|
44
|
+
// mainEntity will be available in future Canvas versions
|
|
45
|
+
const mainEntity = pageData.mainEntity;
|
|
46
|
+
const entityId = (mainEntity === null || mainEntity === void 0 ? void 0 : mainEntity.uuid) || pageTitle || null;
|
|
47
|
+
return useSWR(entityId ? [
|
|
48
|
+
'mainEntity',
|
|
49
|
+
entityType,
|
|
50
|
+
entityId
|
|
51
|
+
] : null, ()=>_async_to_generator(function*() {
|
|
52
|
+
const params = new DrupalJsonApiParams();
|
|
53
|
+
if (mainEntity === null || mainEntity === void 0 ? void 0 : mainEntity.uuid) {
|
|
54
|
+
params.addFilter('id', mainEntity.uuid);
|
|
55
|
+
} else {
|
|
56
|
+
params.addFilter('title', pageTitle);
|
|
57
|
+
}
|
|
58
|
+
if (includes.length > 0) {
|
|
59
|
+
params.addInclude(includes);
|
|
60
|
+
}
|
|
61
|
+
for (const [type, fieldList] of Object.entries(fields)){
|
|
62
|
+
params.addFields(type, fieldList);
|
|
63
|
+
}
|
|
64
|
+
const client = new JsonApiClient();
|
|
65
|
+
const queryString = params.getQueryString();
|
|
66
|
+
const data = yield client.getCollection(entityType, {
|
|
67
|
+
queryString
|
|
68
|
+
});
|
|
69
|
+
return (data === null || data === void 0 ? void 0 : data[0]) || null;
|
|
70
|
+
})());
|
|
71
|
+
};
|
|
72
|
+
// Empty default export for component compatibility
|
|
73
|
+
const UtilsEntity = ()=>null;
|
|
74
|
+
export default UtilsEntity;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getPageData, JsonApiClient } from 'drupal-canvas';
|
|
2
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch the main entity for the current page using SWR.
|
|
7
|
+
* Uses Canvas mainEntity (when available) with fallback to title-based lookup.
|
|
8
|
+
* @param {string} entityType - The JSON:API entity type (e.g., 'node--news_article')
|
|
9
|
+
* @param {object} options - Fetch options
|
|
10
|
+
* @param {string[]} options.includes - Relationships to include
|
|
11
|
+
* @param {object} options.fields - Sparse fieldset map (e.g., { 'node--news_article': ['title'] })
|
|
12
|
+
* @returns {{ data: object|undefined, error: Error|undefined, isLoading: boolean }}
|
|
13
|
+
*/
|
|
14
|
+
export const useMainEntity = (
|
|
15
|
+
entityType,
|
|
16
|
+
{ includes = [], fields = {} } = {},
|
|
17
|
+
) => {
|
|
18
|
+
const pageData = getPageData();
|
|
19
|
+
const { pageTitle } = pageData;
|
|
20
|
+
// mainEntity will be available in future Canvas versions
|
|
21
|
+
const mainEntity = pageData.mainEntity;
|
|
22
|
+
const entityId = mainEntity?.uuid || pageTitle || null;
|
|
23
|
+
|
|
24
|
+
return useSWR(
|
|
25
|
+
entityId ? ['mainEntity', entityType, entityId] : null,
|
|
26
|
+
async () => {
|
|
27
|
+
const params = new DrupalJsonApiParams();
|
|
28
|
+
|
|
29
|
+
if (mainEntity?.uuid) {
|
|
30
|
+
params.addFilter('id', mainEntity.uuid);
|
|
31
|
+
} else {
|
|
32
|
+
params.addFilter('title', pageTitle);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (includes.length > 0) {
|
|
36
|
+
params.addInclude(includes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const [type, fieldList] of Object.entries(fields)) {
|
|
40
|
+
params.addFields(type, fieldList);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const client = new JsonApiClient();
|
|
44
|
+
const queryString = params.getQueryString();
|
|
45
|
+
const data = await client.getCollection(entityType, { queryString });
|
|
46
|
+
return data?.[0] || null;
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Empty default export for component compatibility
|
|
52
|
+
const UtilsEntity = () => null;
|
|
53
|
+
export default UtilsEntity;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Flatten React children, handling nested fragments
|
|
4
|
+
* Useful for processing slot content that may contain fragments
|
|
5
|
+
* @param {React.ReactNode} children - React children to flatten
|
|
6
|
+
* @returns {Array} - Flattened array of children
|
|
7
|
+
*/ const flattenChildren = (children)=>{
|
|
8
|
+
const result = [];
|
|
9
|
+
React.Children.forEach(children, (child)=>{
|
|
10
|
+
if (!child) return;
|
|
11
|
+
// Check if it's a Fragment (type is Symbol(react.fragment))
|
|
12
|
+
if ((child === null || child === void 0 ? void 0 : child.type) === React.Fragment) {
|
|
13
|
+
result.push(...flattenChildren(child.props.children));
|
|
14
|
+
} else {
|
|
15
|
+
result.push(child);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Parse Canvas slot format to extract component props
|
|
22
|
+
* Canvas slots contain HTML with canvas-island elements that hold component data
|
|
23
|
+
* @param {object} slot - The Canvas slot object
|
|
24
|
+
* @returns {Array|null} - Array of parsed component objects with props, or null
|
|
25
|
+
*/ const parseCanvasSlot = (slot)=>{
|
|
26
|
+
var _slot_props;
|
|
27
|
+
// Check if this is Canvas slot format (object with props.value containing HTML)
|
|
28
|
+
if (typeof slot !== 'object' || !(slot === null || slot === void 0 ? void 0 : (_slot_props = slot.props) === null || _slot_props === void 0 ? void 0 : _slot_props.value)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const html = slot.props.value;
|
|
32
|
+
if (typeof html !== 'string' || typeof window === 'undefined') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Use DOMParser for reliable HTML parsing
|
|
37
|
+
const parser = new DOMParser();
|
|
38
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
39
|
+
const islands = doc.querySelectorAll('canvas-island[props]');
|
|
40
|
+
if (islands.length === 0) return null;
|
|
41
|
+
const results = [];
|
|
42
|
+
islands.forEach((island)=>{
|
|
43
|
+
try {
|
|
44
|
+
const propsAttr = island.getAttribute('props');
|
|
45
|
+
if (!propsAttr) return;
|
|
46
|
+
const rawProps = JSON.parse(propsAttr);
|
|
47
|
+
// Convert from ["raw", value] format to just value
|
|
48
|
+
const props = {};
|
|
49
|
+
for (const [key, val] of Object.entries(rawProps)){
|
|
50
|
+
if (Array.isArray(val) && val[0] === 'raw') {
|
|
51
|
+
props[key] = val[1];
|
|
52
|
+
} else {
|
|
53
|
+
props[key] = val;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
results.push({
|
|
57
|
+
props
|
|
58
|
+
});
|
|
59
|
+
} catch (unused) {
|
|
60
|
+
// Skip malformed entries
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return results.length > 0 ? results : null;
|
|
64
|
+
} catch (unused) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Get children from a slot - handles both Canvas slot format and React children
|
|
70
|
+
* @param {object|React.ReactNode} slot - The slot content
|
|
71
|
+
* @returns {Array} - Array of children (parsed objects or React elements)
|
|
72
|
+
*/ export const getSlotChildren = (slot)=>{
|
|
73
|
+
if (!slot) return [];
|
|
74
|
+
const parsed = parseCanvasSlot(slot);
|
|
75
|
+
return parsed || flattenChildren(slot);
|
|
76
|
+
};
|
|
77
|
+
// Empty default export for component compatibility
|
|
78
|
+
const UtilsSlots = ()=>null;
|
|
79
|
+
export default UtilsSlots;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flatten React children, handling nested fragments
|
|
5
|
+
* Useful for processing slot content that may contain fragments
|
|
6
|
+
* @param {React.ReactNode} children - React children to flatten
|
|
7
|
+
* @returns {Array} - Flattened array of children
|
|
8
|
+
*/
|
|
9
|
+
const flattenChildren = (children) => {
|
|
10
|
+
const result = [];
|
|
11
|
+
React.Children.forEach(children, (child) => {
|
|
12
|
+
if (!child) return;
|
|
13
|
+
// Check if it's a Fragment (type is Symbol(react.fragment))
|
|
14
|
+
if (child?.type === React.Fragment) {
|
|
15
|
+
result.push(...flattenChildren(child.props.children));
|
|
16
|
+
} else {
|
|
17
|
+
result.push(child);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse Canvas slot format to extract component props
|
|
25
|
+
* Canvas slots contain HTML with canvas-island elements that hold component data
|
|
26
|
+
* @param {object} slot - The Canvas slot object
|
|
27
|
+
* @returns {Array|null} - Array of parsed component objects with props, or null
|
|
28
|
+
*/
|
|
29
|
+
const parseCanvasSlot = (slot) => {
|
|
30
|
+
// Check if this is Canvas slot format (object with props.value containing HTML)
|
|
31
|
+
if (typeof slot !== 'object' || !slot?.props?.value) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const html = slot.props.value;
|
|
36
|
+
if (typeof html !== 'string' || typeof window === 'undefined') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Use DOMParser for reliable HTML parsing
|
|
42
|
+
const parser = new DOMParser();
|
|
43
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
44
|
+
const islands = doc.querySelectorAll('canvas-island[props]');
|
|
45
|
+
|
|
46
|
+
if (islands.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const results = [];
|
|
49
|
+
islands.forEach((island) => {
|
|
50
|
+
try {
|
|
51
|
+
const propsAttr = island.getAttribute('props');
|
|
52
|
+
if (!propsAttr) return;
|
|
53
|
+
|
|
54
|
+
const rawProps = JSON.parse(propsAttr);
|
|
55
|
+
|
|
56
|
+
// Convert from ["raw", value] format to just value
|
|
57
|
+
const props = {};
|
|
58
|
+
for (const [key, val] of Object.entries(rawProps)) {
|
|
59
|
+
if (Array.isArray(val) && val[0] === 'raw') {
|
|
60
|
+
props[key] = val[1];
|
|
61
|
+
} else {
|
|
62
|
+
props[key] = val;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
results.push({ props });
|
|
67
|
+
} catch {
|
|
68
|
+
// Skip malformed entries
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return results.length > 0 ? results : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get children from a slot - handles both Canvas slot format and React children
|
|
80
|
+
* @param {object|React.ReactNode} slot - The slot content
|
|
81
|
+
* @returns {Array} - Array of children (parsed objects or React elements)
|
|
82
|
+
*/
|
|
83
|
+
export const getSlotChildren = (slot) => {
|
|
84
|
+
if (!slot) return [];
|
|
85
|
+
const parsed = parseCanvasSlot(slot);
|
|
86
|
+
return parsed || flattenChildren(slot);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Empty default export for component compatibility
|
|
90
|
+
const UtilsSlots = () => null;
|
|
91
|
+
export default UtilsSlots;
|