@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.
- package/components/index.ts +14 -4
- package/components/ui/editable-array-item-controls.ts +61 -24
- package/helpers/checks.ts +8 -32
- package/helpers/cloudcannon.ts +19 -12
- package/helpers/hydrate-editable-regions.ts +18 -11
- package/helpers/loading.ts +7 -0
- package/integrations/astro/astro-integration.mjs +3 -7
- package/integrations/astro/index.mjs +2 -2
- package/nodes/editable-array-item.ts +105 -45
- package/nodes/editable-array.ts +115 -17
- package/nodes/editable-component.ts +108 -37
- package/nodes/editable-snippet.ts +0 -6
- package/nodes/editable-text.ts +2 -8
- package/nodes/editable.ts +212 -110
- package/package.json +3 -2
package/nodes/editable-array.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
116
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
}
|
package/nodes/editable-text.ts
CHANGED
|
@@ -117,13 +117,7 @@ export default class EditableText extends Editable {
|
|
|
117
117
|
return this.editor;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const
|
|
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:
|
|
129
|
+
editableType: this.contextBase?.isContent ? "content" : undefined,
|
|
136
130
|
inputConfig,
|
|
137
131
|
},
|
|
138
132
|
);
|