cms-renderer 0.6.2 → 0.6.4
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/lib/block-renderer.js +2 -1
- package/dist/lib/block-renderer.js.map +1 -1
- package/dist/lib/block-toolbar.d.ts +4 -4
- package/dist/lib/block-toolbar.js +62 -102
- package/dist/lib/block-toolbar.js.map +1 -1
- package/dist/lib/client-editable-block.js +157 -118
- package/dist/lib/client-editable-block.js.map +1 -1
- package/dist/lib/custom-schemas.js.map +1 -1
- package/dist/lib/renderer.js +6 -3
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/schema.d.ts +8 -0
- package/dist/lib/schema.js +8 -2
- package/dist/lib/schema.js.map +1 -1
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -216,7 +216,8 @@ function BlockRenderer({
|
|
|
216
216
|
}
|
|
217
217
|
return null;
|
|
218
218
|
}
|
|
219
|
-
const
|
|
219
|
+
const language = routeParams ? Object.values(routeParams).find((p) => p.schemaName === "language")?.value : void 0;
|
|
220
|
+
const component = /* @__PURE__ */ jsx(Component, { content: block.content, routeParams, language });
|
|
220
221
|
if (disableEditable) {
|
|
221
222
|
return component;
|
|
222
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: fixed;\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n transition: opacity 0.15s ease;\n z-index: 99999;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: fixed;\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n transition: opacity 0.15s ease;\n z-index: 99999;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // Extract language code from route params if any param is bound to the language schema.\n const language = routeParams\n ? Object.values(routeParams).find((p) => p.schemaName === 'language')?.value\n : undefined;\n\n const component = (\n <Component content={block.content} routeParams={routeParams} language={language} />\n );\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,11 +4,11 @@ import * as react from 'react';
|
|
|
4
4
|
* Block Toolbar Component
|
|
5
5
|
*
|
|
6
6
|
* Provides move up/down and delete controls for blocks in edit mode.
|
|
7
|
-
*
|
|
7
|
+
* Position is managed imperatively by the parent via a forwarded ref —
|
|
8
|
+
* the toolbar follows the mouse cursor and has no internal layout logic.
|
|
8
9
|
*/
|
|
9
|
-
declare
|
|
10
|
+
declare const BlockToolbar: react.ForwardRefExoticComponent<{
|
|
10
11
|
blockId: string;
|
|
11
|
-
|
|
12
|
-
}): react.JSX.Element;
|
|
12
|
+
} & react.RefAttributes<HTMLDivElement>>;
|
|
13
13
|
|
|
14
14
|
export { BlockToolbar };
|
|
@@ -3,114 +3,74 @@
|
|
|
3
3
|
|
|
4
4
|
// lib/block-toolbar.tsx
|
|
5
5
|
import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react";
|
|
6
|
-
import {
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
|
|
9
|
-
const toolbarRef = useRef(null);
|
|
10
|
-
useLayoutEffect(() => {
|
|
11
|
-
const el = toolbarRef.current;
|
|
12
|
-
if (!el) return;
|
|
13
|
-
const vw = window.innerWidth;
|
|
14
|
-
const vh = window.innerHeight;
|
|
15
|
-
const { width: tw, height: th } = el.getBoundingClientRect();
|
|
16
|
-
const b = blockRect;
|
|
17
|
-
const candidates = [
|
|
18
|
-
{ top: b.bottom + 8, left: b.left + b.width / 2 - tw / 2 },
|
|
19
|
-
// below, centered
|
|
20
|
-
{ top: b.top - th - 8, left: b.left + b.width / 2 - tw / 2 },
|
|
21
|
-
// above, centered
|
|
22
|
-
{ top: b.bottom + 8, left: b.right - tw },
|
|
23
|
-
// below, right-aligned
|
|
24
|
-
{ top: b.bottom + 8, left: b.left },
|
|
25
|
-
// below, left-aligned
|
|
26
|
-
{ top: b.top - th - 8, left: b.right - tw }
|
|
27
|
-
// above, right-aligned
|
|
28
|
-
];
|
|
29
|
-
let chosen = candidates[0] ?? { top: b.bottom + 8, left: b.left + b.width / 2 - tw / 2 };
|
|
30
|
-
for (const c of candidates) {
|
|
31
|
-
if (c.top >= 0 && c.left >= 0 && c.top + th <= vh && c.left + tw <= vw) {
|
|
32
|
-
chosen = c;
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
el.style.top = `${Math.min(Math.max(0, chosen.top), vh - th)}px`;
|
|
37
|
-
el.style.left = `${Math.min(Math.max(0, chosen.left), vw - tw)}px`;
|
|
38
|
-
el.style.visibility = "visible";
|
|
39
|
-
}, [blockRect]);
|
|
8
|
+
var BlockToolbar = forwardRef(function BlockToolbar2({ blockId }, ref) {
|
|
40
9
|
const handleAction = (action) => {
|
|
41
10
|
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
|
42
11
|
window.parent.postMessage({ type: "cms-block-action", action, blockId }, "*");
|
|
43
12
|
}
|
|
44
13
|
};
|
|
45
|
-
return /* @__PURE__ */ jsxs(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
handleAction("delete");
|
|
106
|
-
},
|
|
107
|
-
children: /* @__PURE__ */ jsx(Trash2, {})
|
|
108
|
-
}
|
|
109
|
-
)
|
|
110
|
-
]
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
}
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "cms-block-toolbar", "data-cms-toolbar": "true", children: [
|
|
15
|
+
/* @__PURE__ */ jsx(
|
|
16
|
+
"button",
|
|
17
|
+
{
|
|
18
|
+
type: "button",
|
|
19
|
+
title: "Move up",
|
|
20
|
+
"aria-label": "Move up",
|
|
21
|
+
"data-action": "move-up",
|
|
22
|
+
onClick: (e) => {
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
handleAction("move-up");
|
|
25
|
+
},
|
|
26
|
+
children: /* @__PURE__ */ jsx(ChevronUp, {})
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
/* @__PURE__ */ jsx(
|
|
30
|
+
"button",
|
|
31
|
+
{
|
|
32
|
+
type: "button",
|
|
33
|
+
title: "Move down",
|
|
34
|
+
"aria-label": "Move down",
|
|
35
|
+
"data-action": "move-down",
|
|
36
|
+
onClick: (e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
handleAction("move-down");
|
|
39
|
+
},
|
|
40
|
+
children: /* @__PURE__ */ jsx(ChevronDown, {})
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
type: "button",
|
|
47
|
+
title: "Add block",
|
|
48
|
+
"aria-label": "Add block",
|
|
49
|
+
"data-action": "add-block",
|
|
50
|
+
onClick: (e) => {
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
handleAction("add-block");
|
|
53
|
+
},
|
|
54
|
+
children: /* @__PURE__ */ jsx(Plus, {})
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"button",
|
|
59
|
+
{
|
|
60
|
+
type: "button",
|
|
61
|
+
className: "delete",
|
|
62
|
+
title: "Delete block",
|
|
63
|
+
"aria-label": "Delete block",
|
|
64
|
+
"data-action": "delete",
|
|
65
|
+
onClick: (e) => {
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
handleAction("delete");
|
|
68
|
+
},
|
|
69
|
+
children: /* @__PURE__ */ jsx(Trash2, {})
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] });
|
|
73
|
+
});
|
|
114
74
|
export {
|
|
115
75
|
BlockToolbar
|
|
116
76
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport { forwardRef } from 'react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * Position is managed imperatively by the parent via a forwarded ref —\n * the toolbar follows the mouse cursor and has no internal layout logic.\n */\n\nexport const BlockToolbar = forwardRef<HTMLDivElement, { blockId: string }>(function BlockToolbar(\n { blockId },\n ref\n) {\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div ref={ref} className=\"cms-block-toolbar\" data-cms-toolbar=\"true\">\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n});\n"],"mappings":";;;;AAEA,SAAS,aAAa,WAAW,MAAM,cAAc;AACrD,SAAS,kBAAkB;AAqBvB,SAWI,KAXJ;AAXG,IAAM,eAAe,WAAgD,SAASA,cACnF,EAAE,QAAQ,GACV,KACA;AACA,QAAM,eAAe,CAAC,WAAmB;AACvC,QAAI,OAAO,WAAW,eAAe,OAAO,UAAU,OAAO,WAAW,QAAQ;AAC9E,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,KAAU,WAAU,qBAAoB,oBAAiB,QAC5D;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,SAAS;AAAA,QACxB;AAAA,QAEA,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,WAAW;AAAA,QAC1B;AAAA,QAEA,8BAAC,eAAY;AAAA;AAAA,IACf;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,WAAW;AAAA,QAC1B;AAAA,QAEA,8BAAC,QAAK;AAAA;AAAA,IACR;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,QAAQ;AAAA,QACvB;AAAA,QAEA,8BAAC,UAAO;AAAA;AAAA,IACV;AAAA,KACF;AAEJ,CAAC;","names":["BlockToolbar"]}
|