@cloudcannon/editable-regions 0.0.4 → 0.0.5

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/helpers/checks.ts CHANGED
@@ -100,35 +100,3 @@ export const isEditableArrayItem = (el?: Element | null): boolean => {
100
100
  (el instanceof HTMLElement && el.dataset.editable === "array-item")
101
101
  );
102
102
  };
103
-
104
- export const areEqualNodes = (a: ChildNode, b: ChildNode) => {
105
- if (a.nodeName !== b.nodeName) {
106
- return false;
107
- }
108
-
109
- if (a instanceof Element && b instanceof Element) {
110
- if (a.className !== b.className) {
111
- return false;
112
- }
113
-
114
- if (a.id !== b.id) {
115
- return false;
116
- }
117
-
118
- if (a.attributes.length !== b.attributes.length) {
119
- return false;
120
- }
121
-
122
- for (let i = 0; i < a.attributes.length; i++) {
123
- if (
124
- a.attributes[i].name !== b.attributes[i].name ||
125
- a.attributes[i].value !== b.attributes[i].value
126
- ) {
127
- return false;
128
- }
129
- }
130
-
131
- return true;
132
- }
133
- return a.isEqualNode(b);
134
- };
@@ -478,6 +478,10 @@ export default class EditableArrayItem extends EditableComponent {
478
478
 
479
479
  setupListeners(): void {
480
480
  super.setupListeners();
481
- this.parent?.registerListener({ editable: this });
481
+ if (!this.element.dataset.prop) {
482
+ this.parent?.registerListener({
483
+ editable: this,
484
+ });
485
+ }
482
486
  }
483
487
  }
@@ -7,7 +7,7 @@ import type EditableArrayItemComponent from "../components/editable-array-item-c
7
7
  import { hasEditableArrayItem, isEditableElement } from "../helpers/checks.js";
8
8
  import { CloudCannon } from "../helpers/cloudcannon.js";
9
9
  import type EditableArrayItem from "./editable-array-item.js";
10
- import Editable from "./editable.js";
10
+ import Editable, { type EditableListener } from "./editable.js";
11
11
 
12
12
  const arrayDirectionValues = [
13
13
  "row",
@@ -34,6 +34,60 @@ export default class EditableArray extends Editable {
34
34
  private updatePromise: Promise<void> | undefined;
35
35
  private needsReupdate = false;
36
36
 
37
+ async registerListener(listener: EditableListener): Promise<void> {
38
+ if (!this.value) {
39
+ return;
40
+ }
41
+
42
+ let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
43
+ if (CloudCannon.isAPICollection(this.value)) {
44
+ value = await this.value.items();
45
+ } else if (CloudCannon.isAPIDataset(this.value)) {
46
+ const items = await this.value.items();
47
+ if (Array.isArray(items)) {
48
+ value = items;
49
+ } else {
50
+ const data = await items.data.get();
51
+ value = Array.isArray(data) ? data : [];
52
+ }
53
+ } else if (Array.isArray(this.value)) {
54
+ value = this.value;
55
+ } else {
56
+ value = [];
57
+ }
58
+
59
+ if (!listener.path) {
60
+ let index = 0;
61
+ for (const child of this.element.querySelectorAll(
62
+ "editable-array-item,[data-editable='array-item']",
63
+ )) {
64
+ let parent = child.parentElement;
65
+ while (parent instanceof HTMLElement && !isEditableElement(parent)) {
66
+ parent = parent.parentElement;
67
+ }
68
+
69
+ if (parent !== this.element) {
70
+ continue;
71
+ }
72
+
73
+ if (child === listener.editable.element) {
74
+ listener.path = `${index}`;
75
+ break;
76
+ }
77
+
78
+ index += 1;
79
+ }
80
+ }
81
+
82
+ if (listener.path) {
83
+ listener.editable.pushValue(value, listener, {
84
+ __base_context: this.contextBase ?? {},
85
+ });
86
+ }
87
+ }
88
+
89
+ deregisterListener(_target: Editable): void {}
90
+
37
91
  validateConfiguration(): boolean {
38
92
  const prop = this.element.dataset.prop;
39
93
  if (typeof prop !== "string") {
@@ -101,21 +155,17 @@ export default class EditableArray extends Editable {
101
155
  value = [];
102
156
  }
103
157
 
104
- const children: (HTMLElement & { editable: EditableArrayItem })[] = [];
158
+ const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
105
159
 
106
160
  for (const child of this.element.querySelectorAll(
107
161
  "editable-array-item,[data-editable='array-item']",
108
162
  )) {
109
- if (!hasEditableArrayItem(child)) {
110
- continue;
111
- }
112
-
113
163
  let parent = child.parentElement;
114
164
  while (parent instanceof HTMLElement && !isEditableElement(parent)) {
115
165
  parent = parent.parentElement;
116
166
  }
117
167
 
118
- if (!parent || ("editable" in parent && parent.editable !== this)) {
168
+ if (parent !== this.element) {
119
169
  continue;
120
170
  }
121
171
 
@@ -127,14 +177,27 @@ export default class EditableArray extends Editable {
127
177
  children.pop()?.remove();
128
178
  }
129
179
 
180
+ const firstChild = children[0];
130
181
  for (let i = 0; i < value.length; i++) {
131
182
  let child = children[i];
132
183
  if (!child) {
133
- child = children[0].cloneNode(true) as HTMLElement & {
134
- editable: EditableArrayItem;
135
- };
184
+ if (this.element.dataset.component) {
185
+ child = document.createElement(
186
+ "editable-array-item",
187
+ ) as EditableArrayItemComponent;
188
+ } else if (firstChild) {
189
+ child = children[0].cloneNode(true) as HTMLElement & {
190
+ editable?: EditableArrayItem;
191
+ };
192
+ } else {
193
+ child = document.createElement("array-placeholder");
194
+ }
136
195
  this.element.appendChild(child);
137
196
  }
197
+
198
+ if (this.element.dataset.component) {
199
+ child.dataset.component = this.element.dataset.component;
200
+ }
138
201
  child.dataset.prop = `${i}`;
139
202
  child.dataset.length = `${children.length}`;
140
203
  child.editable?.pushValue(
@@ -178,7 +241,7 @@ export default class EditableArray extends Editable {
178
241
  children.forEach((child, index) => {
179
242
  child.dataset.prop = `${index}`;
180
243
  child.dataset.length = `${children.length}`;
181
- child.editable.pushValue(
244
+ child.editable?.pushValue(
182
245
  value,
183
246
  { path: `${index}`, editable: child.editable },
184
247
  { __base_context: this.contextBase ?? {} },
@@ -216,8 +279,10 @@ export default class EditableArray extends Editable {
216
279
  ) as EditableArrayItemComponent;
217
280
  matchingChild.dataset.id = key;
218
281
 
219
- if (componentKeys[i]) {
220
- matchingChild.dataset.component = componentKeys[i];
282
+ const componentKey =
283
+ componentKeys[i] || this.element.dataset.component;
284
+ if (componentKey) {
285
+ matchingChild.dataset.component = componentKey;
221
286
  }
222
287
  }
223
288
  } else {
@@ -235,12 +300,14 @@ export default class EditableArray extends Editable {
235
300
  this.element.appendChild(matchingChild);
236
301
  }
237
302
 
238
- matchingChild.editable.parent = this;
239
- matchingChild.editable.pushValue(
240
- value,
241
- { path: `${i}`, editable: matchingChild.editable },
242
- { __base_context: this.contextBase ?? {} },
243
- );
303
+ if (matchingChild.editable) {
304
+ matchingChild.editable.parent = this;
305
+ matchingChild.editable.pushValue(
306
+ value,
307
+ { path: `${i}`, editable: matchingChild.editable },
308
+ { __base_context: this.contextBase ?? {} },
309
+ );
310
+ }
244
311
  });
245
312
 
246
313
  children.forEach((child, i) => {
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  areEqualEditables,
3
- areEqualNodes,
4
3
  hasEditable,
5
4
  hasEditableText,
6
5
  isEditableElement,
@@ -192,12 +191,13 @@ export default class EditableComponent extends Editable {
192
191
  }
193
192
  } else if (renderChild && targetChild) {
194
193
  if (
195
- !areEqualNodes(targetChild, renderChild) ||
194
+ renderChild.nodeName !== targetChild.nodeName ||
196
195
  isEditableElement(renderChild) ||
197
196
  isEditableElement(targetChild)
198
197
  ) {
199
198
  targetChild.replaceWith(renderChild);
200
199
  } else {
200
+ this.updateNode(targetChild, renderChild);
201
201
  this.updateTree(targetChild, renderChild);
202
202
  }
203
203
  } else if (renderChild) {
@@ -211,6 +211,19 @@ export default class EditableComponent extends Editable {
211
211
  }
212
212
  }
213
213
 
214
+ updateNode(targetChild: ChildNode, renderChild: ChildNode) {
215
+ if (targetChild instanceof Element && renderChild instanceof Element) {
216
+ for (const attribute of renderChild.attributes) {
217
+ targetChild.setAttribute(attribute.name, attribute.value);
218
+ }
219
+ for (const attribute of targetChild.attributes) {
220
+ if (!renderChild.hasAttribute(attribute.name)) {
221
+ targetChild.removeAttribute(attribute.name);
222
+ }
223
+ }
224
+ }
225
+ }
226
+
214
227
  updateEditable(
215
228
  renderChild: HTMLElement,
216
229
  targetChild: HTMLElement & { editable: Editable },
package/nodes/editable.ts CHANGED
@@ -27,8 +27,8 @@ export interface DOMListener {
27
27
  event: string;
28
28
  }
29
29
 
30
- export interface APIListener extends DOMListener {
31
- event: "change" | "delete";
30
+ export interface APIListener {
31
+ fn: (e: any) => void;
32
32
  obj:
33
33
  | CloudCannonJavaScriptV1APIFile
34
34
  | CloudCannonJavaScriptV1APICollection
@@ -118,10 +118,7 @@ export default class Editable {
118
118
  value = await value.items();
119
119
  } else if (CloudCannon.isAPIFile(value)) {
120
120
  context.file = value;
121
- if (key === "@content") {
122
- context.isContent = true;
123
- value = await value.content.get();
124
- } else {
121
+ if (key !== "@content") {
125
122
  value = await value.data.get();
126
123
  }
127
124
  } else if (CloudCannon.isAPIDataset(value)) {
@@ -135,8 +132,13 @@ export default class Editable {
135
132
  }
136
133
  }
137
134
 
138
- if (value && typeof value === "object" && key in value) {
135
+ if (key === "@content" && CloudCannon.isAPIFile(value)) {
136
+ context.isContent = true;
137
+ value = await value.content.get();
138
+ } else if (value && typeof value === "object" && key in value) {
139
139
  value = (value as any)[key];
140
+ } else {
141
+ value = undefined;
140
142
  }
141
143
 
142
144
  context.fullPath = context.fullPath ? `${context.fullPath}.${key}` : key;
@@ -227,6 +229,13 @@ export default class Editable {
227
229
  }
228
230
 
229
231
  registerListener(listener: EditableListener): void {
232
+ if (this.value !== undefined) {
233
+ listener.editable.pushValue(this.value, listener, {
234
+ ...this.contexts,
235
+ __base_context: this.contextBase ?? {},
236
+ });
237
+ }
238
+
230
239
  if (
231
240
  this.listeners.find(
232
241
  ({ editable: other, key }) =>
@@ -236,13 +245,6 @@ export default class Editable {
236
245
  return;
237
246
  }
238
247
 
239
- if (this.value !== undefined) {
240
- listener.editable.pushValue(this.value, listener, {
241
- ...this.contexts,
242
- __base_context: this.contextBase ?? {},
243
- });
244
- }
245
-
246
248
  this.listeners.push(listener);
247
249
  }
248
250
 
@@ -253,6 +255,9 @@ export default class Editable {
253
255
  }
254
256
 
255
257
  async disconnect(): Promise<void> {
258
+ if (this.disconnecting) {
259
+ return;
260
+ }
256
261
  this.disconnecting = true;
257
262
 
258
263
  if (this.connectPromise) {
@@ -261,9 +266,10 @@ export default class Editable {
261
266
 
262
267
  this.parent?.deregisterListener(this);
263
268
  this.parent = null;
264
- this.APIListeners.forEach(({ obj, fn, event }) =>
265
- obj.removeEventListener(event, fn),
266
- );
269
+ this.APIListeners.forEach(({ obj, fn }) => {
270
+ obj.removeEventListener("change", fn);
271
+ obj.removeEventListener("delete", fn);
272
+ });
267
273
  this.APIListeners = [];
268
274
  this.domListeners.forEach(({ event, fn }) => {
269
275
  this.element.removeEventListener(event, fn);
@@ -347,9 +353,9 @@ export default class Editable {
347
353
  this.APIListeners.push({
348
354
  obj,
349
355
  fn: handleAPIChange,
350
- event: "change",
351
356
  });
352
357
  obj.addEventListener("change", handleAPIChange);
358
+ obj.addEventListener("delete", handleAPIChange);
353
359
  handleAPIChange();
354
360
  }
355
361
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -59,6 +59,7 @@
59
59
  "@types/react": "18.3.12",
60
60
  "@types/react-dom": "18.3.1",
61
61
  "astro": "^5.14.1",
62
+ "js-beautify": "^1.15.4",
62
63
  "typescript": "5.9.3"
63
64
  }
64
65
  }