cms-renderer 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,156 +1,43 @@
1
- # Website
1
+ # cms-renderer
2
2
 
3
- **CMS-powered website built with Next.js 15 and App Router**
3
+ A library for rendering CMS-authored content in Next.js websites.
4
4
 
5
5
  ```
6
- CMS (authors content) --> tRPC API --> Website (renders blocks)
6
+ CMS (authors content) ──► tRPC API ──► Website (renders blocks)
7
7
  ```
8
8
 
9
- The website app fetches structured page data via tRPC and renders blocks using the ComponentMap pattern. Article content uses WASM-powered markdown rendering for performance.
9
+ ## Overview
10
+
11
+ | Export | Description |
12
+ |--------|-------------|
13
+ | `./lib/renderer` | `ParametricRoutePage` — drop-in catch-all page component |
14
+ | `./lib/block-renderer` | `BlockRenderer`, `walkReactNode` — core rendering primitives |
15
+ | `./lib/block-toolbar` | `BlockToolbar` — floating edit toolbar (portal to `document.body`) |
16
+ | `./lib/client-editable-block` | `ClientEditableBlock` — client-side span injector fallback |
17
+ | `./lib/cms-api` | `getCmsClient` — tRPC HTTP client for Server Components |
18
+ | `./lib/refresher` | `Refresher` — SSE-based live revalidation |
19
+ | `./lib/proxy` | `createCmsProxy` — Next.js middleware to proxy `/admin`, `/api`, `/auth` |
20
+ | `./lib/types` | `BlockData`, `BlockComponentRegistry`, `ResolvedRouteParams`, … |
21
+ | `./lib/schema` | Schema utilities |
22
+ | `./lib/custom-schemas` | Custom schema helpers |
23
+ | `./lib/markdown-utils` | Markdown helpers |
24
+ | `./lib/trpc` | Client-side tRPC hooks |
25
+ | `./lib/result` | `Result<T, E>` type utilities |
26
+ | `./lib/image/lazy-load` | Lazy image component |
10
27
 
11
28
  ---
12
29
 
13
30
  ## Quick Start
14
31
 
15
- **Prerequisites**
16
-
17
- | Tool | Version | Check Command |
18
- |------|---------|---------------|
19
- | Bun | 1.2.8+ | `bun --version` |
20
- | Node.js | 18+ | `node --version` |
21
-
22
- **Installation**
23
-
24
32
  ```bash
25
- # From monorepo root
26
- bun install
27
-
28
- # Start the website dev server
29
- cd apps/website
30
- bun run dev
31
- ```
32
-
33
- **Verification**
34
-
35
- Open [http://localhost:3001](http://localhost:3001). You should see:
36
-
37
- ```
38
- Website
39
- CMS-powered website built with Next.js 15 and App Router.
33
+ bun install # from monorepo root
34
+ bun run build # or: bun run dev (watch mode)
40
35
  ```
41
36
 
42
- Visit [http://localhost:3001/demo](http://localhost:3001/demo) to see the complete demo page with navigation, header, and article blocks.
43
-
44
- ---
45
-
46
- ## Architecture
47
-
48
- ### How This App Fits in the Monorepo
49
-
50
- ```
51
- auteur/
52
- ├── apps/
53
- │ ├── cms/ # Content authoring (Lexical editor)
54
- │ └── website/ # This app - renders CMS content
55
- └── packages/
56
- ├── cms-schema/ # Shared Zod schemas
57
- └── markdown-wasm/ # WASM markdown renderer
58
- ```
59
-
60
- The website consumes schemas from `@repo/cms-schema` and uses `@repo/markdown-wasm` for article body rendering. It does not import from `apps/cms/` directly.
61
-
62
- ### Key Directories
63
-
64
- ```
65
- apps/website/
66
- ├── app/ # Next.js App Router pages
67
- │ ├── [slug]/ # Dynamic page routes
68
- │ │ ├── page.tsx # Server Component fetches via tRPC
69
- │ │ ├── loading.tsx # Skeleton loading state
70
- │ │ └── error.tsx # Error boundary
71
- │ ├── api/trpc/[trpc]/ # tRPC API handler
72
- │ ├── layout.tsx # Root layout with CSS imports
73
- │ ├── globals.css # CSS reset
74
- │ └── page.tsx # Homepage
75
- ├── components/
76
- │ ├── blocks/ # Block components + registry
77
- │ │ ├── navigation-block.tsx
78
- │ │ ├── header-block.tsx
79
- │ │ ├── article-block.tsx
80
- │ │ ├── block-renderer.tsx # Dispatcher component
81
- │ │ ├── types.ts # BlockData discriminated union
82
- │ │ └── index.ts # Exports + blockComponents registry
83
- │ ├── page-renderer.tsx # Renders array of blocks
84
- │ └── trpc-provider.tsx # Client-side tRPC/React Query
85
- ├── server/
86
- │ ├── trpc.ts # tRPC initialization
87
- │ └── routers/
88
- │ ├── blocks/ # Block data procedures
89
- │ │ ├── get-navigation.ts
90
- │ │ ├── get-header.ts
91
- │ │ └── get-article.ts
92
- │ └── pages/ # Page composition procedures
93
- │ └── get-page.ts
94
- ├── seed/ # Mock data for development
95
- │ └── index.ts
96
- ├── styles/
97
- │ └── blocks.css # Vanilla CSS for blocks
98
- └── lib/
99
- └── trpc.ts # Client-side tRPC hooks
100
- ```
101
-
102
- ### Data Flow
103
-
104
- ```
105
- ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
106
- │ Dynamic Route │────>│ tRPC Caller │────>│ Mock Data │
107
- │ [slug]/page │ │ pages.getPage │ │ seed/index.ts │
108
- └─────────────────┘ └─────────────────┘ └─────────────────┘
109
-
110
- v
111
- ┌─────────────────┐ ┌─────────────────┐
112
- │ PageRenderer │────>│ BlockRenderer │
113
- │ (iterates) │ │ (dispatches) │
114
- └─────────────────┘ └─────────────────┘
115
-
116
- ┌──────────────────────┼──────────────────────┐
117
- v v v
118
- ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
119
- │ NavigationBlock│ │ HeaderBlock │ │ ArticleBlock │
120
- │ │ │ │ │ (WASM markdown)│
121
- └───────────────┘ └───────────────┘ └───────────────┘
122
- ```
123
-
124
- ---
125
-
126
- ## Development Guide
127
-
128
- ### Available Scripts
129
-
130
- | Script | Command | Description |
131
- |--------|---------|-------------|
132
- | `dev` | `bun run dev` | Start dev server on port 3001 |
133
- | `build` | `bun run build` | Production build |
134
- | `start` | `bun run start` | Start production server |
135
- | `lint` | `bun run lint` | Run Biome linter |
136
- | `check-types` | `bun run check-types` | TypeScript type checking |
137
-
138
- ### Hot Reload Behavior
139
-
140
- - **Page changes**: Instant hot reload
141
- - **tRPC procedures**: Requires server restart
142
- - **CSS changes**: Instant hot reload
143
- - **Block components**: Instant hot reload
144
-
145
- ### Environment Variables
146
-
147
- No environment variables are required for development. The app uses mock data from `seed/index.ts`.
148
-
149
- For production with a real database, you would add:
37
+ **Required env vars** (in the consuming Next.js app):
150
38
 
151
39
  ```env
152
- # .env.local (not required for development)
153
- DATABASE_URL=your-database-url
40
+ NEXT_PUBLIC_WEBSITE_ID=<uuid> # or pass websiteId prop directly
154
41
  ```
155
42
 
156
43
  ---
@@ -159,204 +46,193 @@ DATABASE_URL=your-database-url
159
46
 
160
47
  ### ComponentMap Pattern
161
48
 
162
- The website uses a registry-based pattern to map block types to React components:
49
+ Map block type strings to React components via a registry:
50
+
51
+ ```ts
52
+ // app/registry.ts
53
+ import type { BlockComponentRegistry } from 'cms-renderer/lib/types';
54
+ import HeroBlock from '@/components/hero-block';
55
+ import ArticleBlock from '@/components/article-block';
163
56
 
164
- ```typescript
165
- // components/blocks/index.ts
166
- export const blockComponents: BlockComponentRegistry = {
167
- navigation: NavigationBlock,
168
- header: HeaderBlock,
169
- article: ArticleBlock,
170
- } as const;
57
+ export const registry: Partial<BlockComponentRegistry> = {
58
+ 'hero-block': HeroBlock,
59
+ 'article': ArticleBlock,
60
+ };
171
61
  ```
172
62
 
173
- The `BlockRenderer` uses this registry to dispatch blocks:
63
+ Each component receives `{ content, routeParams }`:
174
64
 
175
65
  ```tsx
176
- // Render any block by type
177
- <BlockRenderer block={{ type: 'header', content: {...} }} />
66
+ import type { BlockComponentProps, HeroBlockContent } from 'cms-renderer/lib/types';
178
67
 
179
- // Render a page of blocks
180
- {page.blocks.map((block, i) => (
181
- <BlockRenderer key={i} block={block} />
182
- ))}
68
+ export function HeroBlock({ content }: BlockComponentProps<HeroBlockContent>) {
69
+ return <h1>{content.headline}</h1>;
70
+ }
183
71
  ```
184
72
 
185
- ### Block Types
73
+ Custom blocks (defined via CMS schema builder) are registered by `schema_name`:
186
74
 
187
- | Block | Component | Content Fields |
188
- |-------|-----------|----------------|
189
- | `navigation` | `NavigationBlock` | logo, ariaLabel, links (3-level hierarchy) |
190
- | `header` | `HeaderBlock` | headline, subheadline, backgroundImage, ctaButton, alignment |
191
- | `article` | `ArticleBlock` | headline, author, publishedAt, body (markdown), tags |
75
+ ```ts
76
+ registry['my-custom-schema'] = MyCustomComponent;
77
+ ```
192
78
 
193
- ### tRPC Integration
79
+ ---
194
80
 
195
- **Server Component** (recommended):
81
+ ### Path-Namespaced Registry
196
82
 
197
- ```tsx
198
- // Direct procedure call in Server Components
199
- import { createCaller } from '@/server/routers';
200
- import { createContext } from '@/server/trpc';
83
+ When a page path is passed to `BlockRenderer`, registry keys of the form `"/{pattern} BlockType"` take priority over plain `"BlockType"` keys. Segments wrapped in `{…}` or `(…)` are wildcards.
201
84
 
202
- export default async function Page({ params }: PageProps) {
203
- const { slug } = await params;
204
- const caller = createCaller(createContext());
205
- const page = await caller.pages.getPage({ slug });
85
+ ```ts
86
+ registry = {
87
+ // Used only on /en/... paths
88
+ '/{lang}/products article': ArticleBlockLocalized,
89
+ // Fallback for all other paths
90
+ 'article': ArticleBlockDefault,
91
+ };
206
92
 
207
- return <PageRenderer title={page.title} blocks={page.blocks} />;
208
- }
93
+ <BlockRenderer block={block} registry={registry} path="/en/products" />
209
94
  ```
210
95
 
211
- **Client Component** (when needed):
212
-
213
- ```tsx
214
- 'use client';
215
- import { trpc } from '@/lib/trpc';
216
-
217
- function PageClient({ slug }: { slug: string }) {
218
- const { data, isLoading } = trpc.pages.getPage.useQuery({ slug });
96
+ This lets you mount path-specific variants without separate route files.
219
97
 
220
- if (isLoading) return <div>Loading...</div>;
221
- return <PageRenderer title={data.title} blocks={data.blocks} />;
222
- }
223
- ```
98
+ ---
224
99
 
225
- ### WASM Markdown Rendering
100
+ ### ParametricRoutePage
226
101
 
227
- The `ArticleBlock` uses `@repo/markdown-wasm` for fast markdown-to-React conversion:
102
+ A complete catch-all page handler. Drop it into `app/[...slug]/page.tsx`:
228
103
 
229
104
  ```tsx
230
- import { MarkdownRenderer } from '@repo/markdown-wasm';
231
-
232
- // Inside ArticleBlock
233
- <MarkdownRenderer content={body} className="article-block__content" />
105
+ // app/[...slug]/page.tsx
106
+ import ParametricRoutePage, { generateMetadata } from 'cms-renderer/lib/renderer';
107
+ import { registry } from '@/registry';
108
+
109
+ export { generateMetadata };
110
+
111
+ export default function Page(props: any) {
112
+ return (
113
+ <ParametricRoutePage
114
+ {...props}
115
+ cmsUrl={process.env.CMS_URL!}
116
+ apiKey={process.env.CMS_API_KEY}
117
+ registry={registry}
118
+ />
119
+ );
120
+ }
234
121
  ```
235
122
 
236
- md4w parses markdown 2.5x faster than JavaScript alternatives and outputs a traversable AST that maps directly to React components.
123
+ It handles:
124
+ - Route lookup via `client.route.getByPath`
125
+ - Block fetching (parallel, failures are skipped)
126
+ - Article publish-state filtering
127
+ - `?edit_mode=true` — enables editable wrappers (for iframe preview)
128
+ - `?ai_preview=1` — renders AI-generated content variant at index 1
237
129
 
238
130
  ---
239
131
 
240
- ## API Routes Reference
132
+ ### Edit Mode & BlockRenderer
241
133
 
242
- ### tRPC Endpoints
134
+ When `disableEditable` is `false` (i.e. `edit_mode=true`), `BlockRenderer` wraps each block with `data-cms-block` and injects `data-cms-editable` spans around every text node that corresponds to a content field.
243
135
 
244
- | Endpoint | Method | Input | Returns |
245
- |----------|--------|-------|---------|
246
- | `blocks.getNavigation` | Query | None | Navigation data |
247
- | `blocks.getHeader` | Query | None | Header data |
248
- | `blocks.getArticle` | Query | None | Article data |
249
- | `pages.getPage` | Query | `{ slug: string }` | Page with blocks array |
136
+ **How span injection works:**
250
137
 
251
- ### Testing Endpoints
138
+ 1. **Server-side (preferred):** `renderToWalkableTree` invokes sync function components to produce a host-element tree, then `walkReactNode` traverses it and wraps matching text values in `<span data-cms-editable … />`.
139
+ 2. **Client-side fallback:** When a component uses hooks and can't be invoked server-side, `ClientEditableBlock` takes over — it uses `MutationObserver` to re-inject spans after every React commit, keeping overlays alive through state-driven re-renders.
252
140
 
253
- ```bash
254
- # Start the dev server
255
- bun run dev
141
+ **`walkReactNode`** is also exported for custom transforms:
142
+
143
+ ```ts
144
+ import { walkReactNode } from 'cms-renderer/lib/block-renderer';
256
145
 
257
- # Test endpoints (in another terminal)
258
- curl "http://localhost:3001/api/trpc/blocks.getNavigation"
259
- curl "http://localhost:3001/api/trpc/blocks.getHeader"
260
- curl "http://localhost:3001/api/trpc/pages.getPage?input=%7B%22json%22%3A%7B%22slug%22%3A%22demo%22%7D%7D"
146
+ const highlighted = walkReactNode(tree, {
147
+ onText: ({ value }) => value.replace(/foo/g, '**foo**'),
148
+ onElement: ({ element }) => element,
149
+ });
261
150
  ```
262
151
 
263
152
  ---
264
153
 
265
- ## Testing
154
+ ### BlockToolbar
266
155
 
267
- The website app does not currently have unit tests. Integration testing is done via the tRPC endpoints.
156
+ Rendered inside every editable block. Portals to `document.body` so it never disrupts layout. Positioned by an inline script that tracks `mouseover` events on `[data-cms-block]` elements.
268
157
 
269
- ```bash
270
- # Type check
271
- bun run check-types
158
+ Actions are dispatched to the parent frame via `postMessage`:
272
159
 
273
- # Lint
274
- bun run lint
160
+ ```ts
161
+ // Messages sent to window.parent:
162
+ { type: 'cms-block-action', action: 'move-up' | 'move-down' | 'add-block' | 'delete', blockId }
163
+ { type: 'cms-editable-click', blockId, blockType, contentPath } // null contentPath = block-level click
275
164
  ```
276
165
 
277
- Test files would live at:
278
- - `components/blocks/*.test.tsx` for block components
279
- - `server/routers/**/*.test.ts` for tRPC procedures
166
+ The CMS admin panel listens for these messages to open the field editor or reorder blocks.
280
167
 
281
168
  ---
282
169
 
283
- ## Troubleshooting
284
-
285
- ### Port 3001 Already in Use
286
-
287
- ```bash
288
- # Find and kill the process
289
- lsof -i :3001
290
- kill -9 <PID>
291
-
292
- # Or use a different port
293
- bun run dev -- --port 3002
294
- ```
170
+ ### Refresher
295
171
 
296
- ### Cannot Find Module '@repo/cms-schema'
172
+ Subscribes to the CMS SSE stream and calls `router.refresh()` when content changes, keeping the page in sync during live editing:
297
173
 
298
- ```bash
299
- # Run bun install from monorepo root
300
- cd ../..
301
- bun install
174
+ ```tsx
175
+ // app/layout.tsx
176
+ import { Refresher } from 'cms-renderer/lib/refresher';
177
+
178
+ export default function Layout({ children }) {
179
+ return (
180
+ <html>
181
+ <body>
182
+ {children}
183
+ <Refresher
184
+ websiteId={process.env.NEXT_PUBLIC_WEBSITE_ID!}
185
+ cmsUrl={process.env.CMS_URL!}
186
+ apiKey={process.env.CMS_API_KEY}
187
+ />
188
+ </body>
189
+ </html>
190
+ );
191
+ }
302
192
  ```
303
193
 
304
- ### TypeScript Errors on First Run
305
-
306
- The `.next/types` directory is generated on first dev server run:
194
+ ---
307
195
 
308
- ```bash
309
- bun run dev # Run once to generate types
310
- bun run check-types # Now type checking works
311
- ```
196
+ ### Proxy Middleware
312
197
 
313
- ### WASM Module Not Found
198
+ Proxies `/admin`, `/api`, `/auth` (and optional extra paths) to the upstream CMS server. Add to `middleware.ts`:
314
199
 
315
- If you see errors about md4w:
200
+ ```ts
201
+ import { createCmsProxy } from 'cms-renderer/lib/proxy';
316
202
 
317
- ```bash
318
- # Rebuild the markdown-wasm package
319
- cd ../../packages/markdown-wasm
320
- bun install
321
- bun run build
203
+ export const middleware = createCmsProxy({
204
+ upstream: process.env.CMS_URL, // or ADMIN_UPSTREAM_ORIGIN env var
205
+ additionalPaths: ['/uploads'],
206
+ });
322
207
  ```
323
208
 
324
- ### tRPC 'NOT_FOUND' Error
325
-
326
- The mock data only includes pages with slugs: `demo`, `about`, `blog`. Requesting other slugs returns a 404.
327
-
328
209
  ---
329
210
 
330
- ## Related Documentation
211
+ ## File Structure
331
212
 
332
- - **Tutorial**: [`_docs/cms-website-integration/`](../../_docs/cms-website-integration/00-overview.md) - Full 11-chapter tutorial
333
- - **CMS App**: [`apps/cms/README.md`](../cms/README.md) - Content authoring application
334
- - **Schema Package**: [`packages/cms-schema/`](../../packages/cms-schema/) - Shared Zod schemas
335
- - **Markdown Package**: [`packages/markdown-wasm/`](../../packages/markdown-wasm/) - WASM renderer
213
+ The recommended structure for a consuming Next.js app:
336
214
 
337
- ---
338
-
339
- ## Contributing
340
-
341
- ### Code Style
342
-
343
- This project uses [Biome](https://biomejs.dev/) for linting and formatting:
344
-
345
- ```bash
346
- bun run lint # Check for issues
347
- bun run lint --fix # Auto-fix issues
215
+ ```
216
+ my-app/
217
+ app/
218
+ [...slug]/
219
+ page.tsx # ParametricRoutePage
220
+ layout.tsx # Refresher goes here
221
+ components/
222
+ hero-block.tsx # Block components
223
+ article-block.tsx
224
+ registry.ts # BlockComponentRegistry
225
+ middleware.ts # createCmsProxy
348
226
  ```
349
227
 
350
- ### Adding a New Block Type
351
-
352
- 1. Define the schema in `apps/cms/app/schemas/`
353
- 2. Add mock data to `seed/index.ts`
354
- 3. Create the component in `components/blocks/`
355
- 4. Add to `blockComponents` registry in `components/blocks/index.ts`
356
- 5. Add the type to `BlockData` union in `components/blocks/types.ts`
228
+ ---
357
229
 
358
- ### Pull Requests
230
+ ## Scripts
359
231
 
360
- - Run `bun run check-types` before submitting
361
- - Run `bun run lint` to ensure code style compliance
362
- - Test the demo page at `/demo` renders correctly
232
+ | Script | Description |
233
+ |--------|-------------|
234
+ | `bun run build` | Production build (tsup) |
235
+ | `bun run dev` | Watch mode |
236
+ | `bun run check-types` | TypeScript type check |
237
+ | `bun run lint` | Biome lint |
238
+ | `bun test` | Run tests |
@@ -186,28 +186,18 @@ function BlockRenderer({
186
186
  "data-cms-block": true,
187
187
  "data-block-id": block.id,
188
188
  "data-block-type": block.type,
189
- style: { position: "relative" },
189
+ style: { display: "contents" },
190
190
  children: [
191
191
  /* @__PURE__ */ jsx("style", { children: `
192
192
  [data-cms-block] {
193
- position: relative;
194
- }
195
- [data-cms-block]:hover {
196
- outline: 2px solid #3b82f6;
197
- outline-offset: 4px;
193
+ display: contents;
198
194
  }
199
195
  [data-cms-editable] {
196
+ display: contents;
200
197
  cursor: pointer;
201
- border-radius: 2px;
202
- }
203
- [data-cms-editable]:hover {
204
- outline: 2px solid #3b82f6;
205
- outline-offset: 2px;
206
198
  }
207
199
  .cms-block-toolbar {
208
- position: absolute;
209
- bottom: 8px;
210
- left: 50%;
200
+ position: fixed;
211
201
  transform: translateX(-50%);
212
202
  display: flex;
213
203
  gap: 4px;
@@ -218,11 +208,7 @@ function BlockRenderer({
218
208
  opacity: 0;
219
209
  pointer-events: none;
220
210
  transition: opacity 0.15s ease;
221
- z-index: 1000;
222
- }
223
- [data-cms-block]:hover .cms-block-toolbar {
224
- opacity: 1;
225
- pointer-events: auto;
211
+ z-index: 9999;
226
212
  }
227
213
  .cms-block-toolbar button {
228
214
  display: flex;
@@ -266,34 +252,70 @@ function BlockRenderer({
266
252
  (function() {
267
253
  if (!window.__cmsEditableInitialized) {
268
254
  window.__cmsEditableInitialized = true;
255
+ var _ab = null;
256
+
257
+ function _tb(bid) {
258
+ return document.querySelector('.cms-block-toolbar[data-block-id="' + bid + '"]');
259
+ }
260
+
261
+ function _show(bid, target) {
262
+ var tb = _tb(bid);
263
+ if (tb && target) {
264
+ var r = target.getBoundingClientRect();
265
+ tb.style.left = Math.round(r.left + r.width / 2) + 'px';
266
+ tb.style.top = Math.round(r.bottom + 8) + 'px';
267
+ tb.style.opacity = '1';
268
+ tb.style.pointerEvents = 'auto';
269
+ }
270
+ }
271
+
272
+ function _hide(bid) {
273
+ var tb = _tb(bid);
274
+ if (tb) { tb.style.opacity = '0'; tb.style.pointerEvents = 'none'; }
275
+ }
276
+
277
+ document.addEventListener('mouseover', function(e) {
278
+ if (e.target.closest('.cms-block-toolbar')) return;
279
+ var bel = e.target.closest('[data-cms-block]');
280
+ var bid = bel ? bel.getAttribute('data-block-id') : null;
281
+ if (bid === _ab) return;
282
+ if (_ab) _hide(_ab);
283
+ _ab = bid;
284
+ if (bid) _show(bid, e.target);
285
+ });
269
286
 
270
287
  document.addEventListener('click', function(e) {
271
288
  if (e.target.closest('.cms-block-toolbar')) return;
272
289
 
273
- var editableTarget = e.target.closest('[data-cms-editable]');
274
- if (editableTarget) {
275
- var message = {
276
- type: 'cms-editable-click',
277
- blockId: editableTarget.getAttribute('data-block-id'),
278
- blockType: editableTarget.getAttribute('data-block-type'),
279
- contentPath: editableTarget.getAttribute('data-content-path')
280
- };
290
+ var path = e.composedPath ? e.composedPath() : [e.target];
291
+ var et = null;
292
+ for (var i = 0; i < path.length; i++) {
293
+ var n = path[i];
294
+ if (n.nodeType === 1 && n.hasAttribute && n.hasAttribute('data-cms-editable')) { et = n; break; }
295
+ }
296
+ if (!et) et = e.target.closest && e.target.closest('[data-cms-editable]');
297
+
298
+ if (et) {
281
299
  if (window.parent && window.parent !== window) {
282
- window.parent.postMessage(message, '*');
300
+ window.parent.postMessage({
301
+ type: 'cms-editable-click',
302
+ blockId: et.getAttribute('data-block-id'),
303
+ blockType: et.getAttribute('data-block-type'),
304
+ contentPath: et.getAttribute('data-content-path')
305
+ }, '*');
283
306
  }
284
307
  return;
285
308
  }
286
309
 
287
- var blockTarget = e.target.closest('[data-cms-block]');
288
- if (blockTarget) {
289
- var message = {
290
- type: 'cms-editable-click',
291
- blockId: blockTarget.getAttribute('data-block-id'),
292
- blockType: blockTarget.getAttribute('data-block-type'),
293
- contentPath: null
294
- };
310
+ var bt = e.target.closest && e.target.closest('[data-cms-block]');
311
+ if (bt) {
295
312
  if (window.parent && window.parent !== window) {
296
- window.parent.postMessage(message, '*');
313
+ window.parent.postMessage({
314
+ type: 'cms-editable-click',
315
+ blockId: bt.getAttribute('data-block-id'),
316
+ blockType: bt.getAttribute('data-block-type'),
317
+ contentPath: null
318
+ }, '*');
297
319
  }
298
320
  }
299
321
  });