@avocadostudio-ai/preview-adapter 0.5.0 → 0.6.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.
@@ -52,6 +52,29 @@ export declare function scrollBlockIntoView(el: HTMLElement, options?: {
52
52
  behavior?: ScrollBehavior;
53
53
  block?: ScrollLogicalPosition;
54
54
  }): void;
55
+ /**
56
+ * The full path a marked node stands for, composing any enclosing scopes.
57
+ *
58
+ * `data-editable-target` is scoped from the block down — `items[3].question` —
59
+ * which a renderer can only write if it knows where it sits. That is fine
60
+ * while one component draws the whole block, and false the moment a list row
61
+ * is rendered by a component of its own: the child knows it has a `question`
62
+ * and cannot know it is `items[3]`. So every renderer that can appear inside a
63
+ * list needed a prefix threaded in from its parent, and every parent needed to
64
+ * remember to pass it.
65
+ *
66
+ * Forgetting is silent and *wrong* rather than silent and absent. The child
67
+ * marks a bare `question`, the overlay resolves it against the enclosing
68
+ * block, and an edit to a headline inside a column patches a prop the section
69
+ * does not have.
70
+ *
71
+ * `data-editable-scope="items[3]"` on any ancestor removes the threading:
72
+ * children emit bare field names and composition happens here, at the one
73
+ * place that knows the whole ancestry. Scopes nest, outermost first.
74
+ */
75
+ export declare function editablePathOf(node: HTMLElement): string;
76
+ /** `items[3]` + `question` → `items[3].question`; `cards` + `[0].title` → `cards[0].title`. */
77
+ export declare function joinEditablePath(scope: string, path: string): string;
55
78
  export declare function findEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
56
79
  /**
57
80
  * The node to point at for a field, which is not always the field's own node.
@@ -243,10 +243,59 @@ export function scrollBlockIntoView(el, options) {
243
243
  lastY = window.scrollY;
244
244
  }, SCROLL_SETTLE_INTERVAL_MS);
245
245
  }
246
+ /**
247
+ * The full path a marked node stands for, composing any enclosing scopes.
248
+ *
249
+ * `data-editable-target` is scoped from the block down — `items[3].question` —
250
+ * which a renderer can only write if it knows where it sits. That is fine
251
+ * while one component draws the whole block, and false the moment a list row
252
+ * is rendered by a component of its own: the child knows it has a `question`
253
+ * and cannot know it is `items[3]`. So every renderer that can appear inside a
254
+ * list needed a prefix threaded in from its parent, and every parent needed to
255
+ * remember to pass it.
256
+ *
257
+ * Forgetting is silent and *wrong* rather than silent and absent. The child
258
+ * marks a bare `question`, the overlay resolves it against the enclosing
259
+ * block, and an edit to a headline inside a column patches a prop the section
260
+ * does not have.
261
+ *
262
+ * `data-editable-scope="items[3]"` on any ancestor removes the threading:
263
+ * children emit bare field names and composition happens here, at the one
264
+ * place that knows the whole ancestry. Scopes nest, outermost first.
265
+ */
266
+ export function editablePathOf(node) {
267
+ const own = node.getAttribute("data-editable-target") ?? "";
268
+ const scopes = [];
269
+ // The node's own scope does not apply to itself — a component that both
270
+ // scopes its children and marks a field of its own would otherwise prefix
271
+ // its own path with the scope it sets for them.
272
+ let current = node.parentElement;
273
+ while (current) {
274
+ const scope = current.getAttribute("data-editable-scope");
275
+ if (scope)
276
+ scopes.unshift(scope);
277
+ // A block boundary ends the search: a path is scoped from the block down,
278
+ // and a stray scope outside one must not reach into it.
279
+ if (current.hasAttribute("data-block-id"))
280
+ break;
281
+ current = current.parentElement;
282
+ }
283
+ if (scopes.length === 0)
284
+ return own;
285
+ return joinEditablePath(scopes.join("."), own);
286
+ }
287
+ /** `items[3]` + `question` → `items[3].question`; `cards` + `[0].title` → `cards[0].title`. */
288
+ export function joinEditablePath(scope, path) {
289
+ if (scope === "")
290
+ return path;
291
+ if (path === "")
292
+ return scope;
293
+ return path.startsWith("[") ? `${scope}${path}` : `${scope}.${path}`;
294
+ }
246
295
  export function findEditableNode(parent, editablePath) {
247
296
  const nodes = parent.querySelectorAll("[data-editable-target]");
248
297
  for (const node of nodes) {
249
- if ((node.getAttribute("data-editable-target") ?? "") === editablePath)
298
+ if (editablePathOf(node) === editablePath)
250
299
  return node;
251
300
  }
252
301
  return null;
@@ -475,7 +524,7 @@ export function computeInsertBefore(blockId, preferredNode) {
475
524
  export function groupListItemNodes(block) {
476
525
  const groups = new Map();
477
526
  block.querySelectorAll("[data-editable-target]").forEach((node) => {
478
- const path = String(node.getAttribute("data-editable-target") ?? "");
527
+ const path = editablePathOf(node);
479
528
  const match = /^([A-Za-z_][A-Za-z0-9_]*)\[([0-9]+)\](?:\.|$)/.exec(path);
480
529
  if (!match)
481
530
  return;
@@ -839,7 +888,7 @@ export function createBridgeFunctions(state, callbacks, config) {
839
888
  const applyLiveDraftFields = (block, fields) => {
840
889
  const nextPaths = new Set(Object.keys(fields));
841
890
  for (const [node, html] of [...state.liveDraftOriginals.entries()]) {
842
- const path = node.getAttribute("data-editable-target") ?? "";
891
+ const path = editablePathOf(node);
843
892
  if (!nextPaths.has(path)) {
844
893
  node.innerHTML = html;
845
894
  node.classList.remove("editor-live-typing");
@@ -1350,7 +1399,7 @@ export function createBridgeFunctions(state, callbacks, config) {
1350
1399
  return;
1351
1400
  }
1352
1401
  document.querySelectorAll(".editor-selectable [data-editable-target]").forEach((el) => {
1353
- const path = el.getAttribute("data-editable-target") ?? "";
1402
+ const path = editablePathOf(el);
1354
1403
  if (!isImageEditable(el, path))
1355
1404
  return;
1356
1405
  const block = el.closest("[data-block-id]");
@@ -1741,12 +1790,12 @@ export function createBridgeFunctions(state, callbacks, config) {
1741
1790
  const blockType = node.getAttribute("data-block-type");
1742
1791
  if (!blockId || !blockType)
1743
1792
  return;
1744
- let editablePath = childNode && node.contains(childNode) ? String(childNode.getAttribute("data-editable-target") ?? "") || undefined : undefined;
1793
+ let editablePath = childNode && node.contains(childNode) ? editablePathOf(childNode) || undefined : undefined;
1745
1794
  if (!editablePath) {
1746
1795
  const itemRoot = target?.closest(".editor-item-has-delete");
1747
1796
  if (itemRoot && node.contains(itemRoot)) {
1748
1797
  const firstEditable = itemRoot.querySelector("[data-editable-target]");
1749
- editablePath = String(firstEditable?.getAttribute("data-editable-target") ?? "") || undefined;
1798
+ editablePath = (firstEditable ? editablePathOf(firstEditable) : "") || undefined;
1750
1799
  }
1751
1800
  }
1752
1801
  if (state.selectedBlockId === blockId) {
@@ -1806,7 +1855,7 @@ export function createBridgeFunctions(state, callbacks, config) {
1806
1855
  return;
1807
1856
  const blockId = node.getAttribute("data-block-id");
1808
1857
  const blockType = node.getAttribute("data-block-type");
1809
- const editablePath = String(childNode.getAttribute("data-editable-target") ?? "");
1858
+ const editablePath = editablePathOf(childNode);
1810
1859
  if (!blockId || !blockType || !editablePath)
1811
1860
  return;
1812
1861
  if (!supportsInlineEditablePath(editablePath))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/preview-adapter",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  "src/styles.css"
33
33
  ],
34
34
  "dependencies": {
35
- "@avocadostudio-ai/shared": "^0.5.0"
35
+ "@avocadostudio-ai/shared": "^0.6.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "next": ">=15.0.0",