@cloudcannon/editable-regions 0.0.3 → 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.
@@ -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",
@@ -31,6 +31,63 @@ export default class EditableArray extends Editable {
31
31
  | null
32
32
  | undefined = undefined;
33
33
 
34
+ private updatePromise: Promise<void> | undefined;
35
+ private needsReupdate = false;
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
+
34
91
  validateConfiguration(): boolean {
35
92
  const prop = this.element.dataset.prop;
36
93
  if (typeof prop !== "string") {
@@ -65,7 +122,22 @@ export default class EditableArray extends Editable {
65
122
  return value;
66
123
  }
67
124
 
68
- async update(): Promise<void> {
125
+ update(): Promise<void> {
126
+ if (this.updatePromise) {
127
+ this.needsReupdate = true;
128
+ return this.updatePromise;
129
+ }
130
+ this.updatePromise = this._update().then(() => {
131
+ this.updatePromise = undefined;
132
+ if (this.needsReupdate) {
133
+ this.needsReupdate = false;
134
+ return this.update();
135
+ }
136
+ });
137
+ return this.updatePromise;
138
+ }
139
+
140
+ private async _update(): Promise<void> {
69
141
  let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
70
142
  if (CloudCannon.isAPICollection(this.value)) {
71
143
  value = await this.value.items();
@@ -83,21 +155,17 @@ export default class EditableArray extends Editable {
83
155
  value = [];
84
156
  }
85
157
 
86
- const children: (HTMLElement & { editable: EditableArrayItem })[] = [];
158
+ const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
87
159
 
88
160
  for (const child of this.element.querySelectorAll(
89
161
  "editable-array-item,[data-editable='array-item']",
90
162
  )) {
91
- if (!hasEditableArrayItem(child)) {
92
- continue;
93
- }
94
-
95
163
  let parent = child.parentElement;
96
164
  while (parent instanceof HTMLElement && !isEditableElement(parent)) {
97
165
  parent = parent.parentElement;
98
166
  }
99
167
 
100
- if (!parent || ("editable" in parent && parent.editable !== this)) {
168
+ if (parent !== this.element) {
101
169
  continue;
102
170
  }
103
171
 
@@ -109,17 +177,34 @@ export default class EditableArray extends Editable {
109
177
  children.pop()?.remove();
110
178
  }
111
179
 
180
+ const firstChild = children[0];
112
181
  for (let i = 0; i < value.length; i++) {
113
182
  let child = children[i];
114
183
  if (!child) {
115
- child = children[0].cloneNode(true) as HTMLElement & {
116
- editable: EditableArrayItem;
117
- };
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
+ }
118
195
  this.element.appendChild(child);
119
196
  }
197
+
198
+ if (this.element.dataset.component) {
199
+ child.dataset.component = this.element.dataset.component;
200
+ }
120
201
  child.dataset.prop = `${i}`;
121
202
  child.dataset.length = `${children.length}`;
122
- child.editable?.pushValue(value[i]);
203
+ child.editable?.pushValue(
204
+ value,
205
+ { path: `${i}`, editable: child.editable },
206
+ { __base_context: this.contextBase ?? {} },
207
+ );
123
208
  }
124
209
  return;
125
210
  }
@@ -155,7 +240,12 @@ export default class EditableArray extends Editable {
155
240
  if (equal && children.length === dataKeys?.length) {
156
241
  children.forEach((child, index) => {
157
242
  child.dataset.prop = `${index}`;
158
- child.editable.pushValue(value[index]);
243
+ child.dataset.length = `${children.length}`;
244
+ child.editable?.pushValue(
245
+ value,
246
+ { path: `${index}`, editable: child.editable },
247
+ { __base_context: this.contextBase ?? {} },
248
+ );
159
249
  });
160
250
  return;
161
251
  }
@@ -189,8 +279,10 @@ export default class EditableArray extends Editable {
189
279
  ) as EditableArrayItemComponent;
190
280
  matchingChild.dataset.id = key;
191
281
 
192
- if (componentKeys[i]) {
193
- matchingChild.dataset.component = componentKeys[i];
282
+ const componentKey =
283
+ componentKeys[i] || this.element.dataset.component;
284
+ if (componentKey) {
285
+ matchingChild.dataset.component = componentKey;
194
286
  }
195
287
  }
196
288
  } else {
@@ -208,8 +300,14 @@ export default class EditableArray extends Editable {
208
300
  this.element.appendChild(matchingChild);
209
301
  }
210
302
 
211
- matchingChild.editable.parent = this;
212
- matchingChild.editable.pushValue(value[i]);
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
+ }
213
311
  });
214
312
 
215
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,
@@ -11,35 +10,30 @@ import "../components/ui/editable-region-error-card.js";
11
10
  import "../components/ui/editable-component-controls.js";
12
11
  import type EditableComponentControls from "../components/ui/editable-component-controls.js";
13
12
  import {
14
- CloudCannon,
15
13
  getEditableComponentRenderers,
14
+ realizeAPIValue,
16
15
  } from "../helpers/cloudcannon.js";
17
16
 
18
- const realizeAPIValue = async (value: unknown): Promise<unknown> => {
19
- if (CloudCannon.isAPICollection(value)) {
20
- const items = await value.items();
21
- return Promise.all(items.map(realizeAPIValue));
22
- }
23
- if (CloudCannon.isAPIFile(value)) {
24
- return value.data.get();
25
- }
26
- if (CloudCannon.isAPIDataset(value)) {
27
- const items = await value.items();
28
- if (Array.isArray(items)) {
29
- return Promise.all(items.map(realizeAPIValue));
30
- }
31
- return items.data.get();
32
- }
33
- return value;
34
- };
35
-
36
17
  export default class EditableComponent extends Editable {
37
18
  protected controlsElement?: EditableComponentControls;
38
19
 
20
+ private updatePromise: Promise<void> | undefined;
21
+ private needsReupdate = false;
22
+
39
23
  getComponents() {
40
24
  return getEditableComponentRenderers();
41
25
  }
42
26
 
27
+ shouldMount(): boolean {
28
+ if (super.shouldMount()) {
29
+ return true;
30
+ }
31
+
32
+ return !Object.keys(this.element.dataset).some((key) =>
33
+ key.startsWith("prop"),
34
+ );
35
+ }
36
+
43
37
  validateConfiguration(): boolean {
44
38
  const key = this.element.dataset.component;
45
39
  if (!key) {
@@ -57,7 +51,22 @@ export default class EditableComponent extends Editable {
57
51
  return true;
58
52
  }
59
53
 
60
- async update(): Promise<void> {
54
+ update(): Promise<void> {
55
+ if (this.updatePromise) {
56
+ this.needsReupdate = true;
57
+ return this.updatePromise;
58
+ }
59
+ this.updatePromise = this._update().then(() => {
60
+ this.updatePromise = undefined;
61
+ if (this.needsReupdate) {
62
+ this.needsReupdate = false;
63
+ return this.update();
64
+ }
65
+ });
66
+ return this.updatePromise;
67
+ }
68
+
69
+ async _update(): Promise<void> {
61
70
  this.element.classList.remove("errored");
62
71
 
63
72
  const key = this.element.dataset.component;
@@ -168,33 +177,27 @@ export default class EditableComponent extends Editable {
168
177
  const listener = this.listeners[i];
169
178
  if (listener.editable.element === targetChild) {
170
179
  listener.editable.element = renderChild;
171
- renderChild.editable.pushValue(this.value, listener);
180
+ renderChild.editable.pushValue(this.value, listener, {
181
+ ...this.contexts,
182
+ __base_context: this.contextBase ?? {},
183
+ });
172
184
  }
173
185
  }
174
186
  } else if (hasEditable(targetChild)) {
175
- for (let i = 0; i < this.listeners.length; i++) {
176
- const listener = this.listeners[i];
177
- if (listener.editable.element === targetChild) {
178
- targetChild.editable.pushValue(this.value, listener);
179
- }
180
- }
187
+ this.updateEditable(renderChild, targetChild);
181
188
  }
182
189
  } else if (hasEditable(targetChild)) {
183
- for (let i = 0; i < this.listeners.length; i++) {
184
- const listener = this.listeners[i];
185
- if (listener.editable.element === targetChild) {
186
- targetChild.editable.pushValue(this.value, listener);
187
- }
188
- }
190
+ this.updateEditable(renderChild, targetChild);
189
191
  }
190
192
  } else if (renderChild && targetChild) {
191
193
  if (
192
- !areEqualNodes(targetChild, renderChild) ||
194
+ renderChild.nodeName !== targetChild.nodeName ||
193
195
  isEditableElement(renderChild) ||
194
196
  isEditableElement(targetChild)
195
197
  ) {
196
198
  targetChild.replaceWith(renderChild);
197
199
  } else {
200
+ this.updateNode(targetChild, renderChild);
198
201
  this.updateTree(targetChild, renderChild);
199
202
  }
200
203
  } else if (renderChild) {
@@ -208,11 +211,79 @@ export default class EditableComponent extends Editable {
208
211
  }
209
212
  }
210
213
 
211
- dispatchEdit(source?: string) {
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
+
227
+ updateEditable(
228
+ renderChild: HTMLElement,
229
+ targetChild: HTMLElement & { editable: Editable },
230
+ ) {
231
+ for (const attribute of renderChild.attributes) {
232
+ if (attribute.name !== "class") {
233
+ targetChild.setAttribute(attribute.name, attribute.value);
234
+ }
235
+ }
236
+ for (const attribute of targetChild.attributes) {
237
+ if (
238
+ !renderChild.hasAttribute(attribute.name) &&
239
+ attribute.name !== "class" &&
240
+ attribute.name !== "contenteditable"
241
+ ) {
242
+ targetChild.removeAttribute(attribute.name);
243
+ }
244
+ }
245
+
246
+ for (const className of renderChild.classList) {
247
+ targetChild.classList.add(className);
248
+ }
249
+
250
+ for (const className of targetChild.classList) {
251
+ if (
252
+ !renderChild.classList.contains(className) &&
253
+ !className.includes("ProseMirror")
254
+ ) {
255
+ targetChild.classList.remove(className);
256
+ }
257
+ }
258
+
259
+ for (let i = 0; i < this.listeners.length; i++) {
260
+ const listener = this.listeners[i];
261
+ if (listener.editable.element === targetChild) {
262
+ targetChild.editable.pushValue(this.value, listener, {
263
+ ...this.contexts,
264
+ __base_context: this.contextBase ?? {},
265
+ });
266
+ }
267
+ }
268
+ }
269
+
270
+ dispatchEdit(source?: string, originalEvent?: MouseEvent) {
271
+ const rect = this.element.getBoundingClientRect();
212
272
  this.element.dispatchEvent(
213
273
  new CustomEvent("cloudcannon-api", {
214
274
  bubbles: true,
215
- detail: { action: "edit", source },
275
+ detail: {
276
+ action: "edit",
277
+ source,
278
+ position: {
279
+ x: originalEvent?.clientX ?? 0,
280
+ y: originalEvent?.clientY ?? 0,
281
+ left: rect.left,
282
+ width: rect.width,
283
+ top: rect.top,
284
+ height: rect.height,
285
+ },
286
+ },
216
287
  }),
217
288
  );
218
289
  }
@@ -139,10 +139,4 @@ export default class EditableSnippet extends EditableComponent {
139
139
  validateConfiguration(): boolean {
140
140
  return true;
141
141
  }
142
-
143
- resolveSource(source?: string): string | undefined {
144
- return this.parent
145
- ? `${this.parent.resolveSource()}.@snippet[${this.element.getAttribute("data-cms-snippet-id")}].${source}`
146
- : `@snippet[${this.element.getAttribute("data-cms-snippet-id")}].${source}`;
147
- }
148
142
  }
@@ -117,13 +117,7 @@ export default class EditableText extends Editable {
117
117
  return this.editor;
118
118
  }
119
119
 
120
- const source = this.resolveSource();
121
-
122
- if (!source) {
123
- throw new Error("Source not found");
124
- }
125
-
126
- const inputConfig = source.endsWith("@content")
120
+ const inputConfig = this.contextBase?.isContent
127
121
  ? { type: "markdown" }
128
122
  : await this.dispatchGetInputConfig(this.element.dataset.prop);
129
123
 
@@ -132,7 +126,7 @@ export default class EditableText extends Editable {
132
126
  this.onChange.bind(this),
133
127
  {
134
128
  elementType: this.element.dataset.type,
135
- editableType: source.endsWith("@content") ? "content" : undefined,
129
+ editableType: this.contextBase?.isContent ? "content" : undefined,
136
130
  inputConfig,
137
131
  },
138
132
  );