@cloudcannon/editable-regions 0.0.4 → 0.0.6
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/ui/editable-region-button.ts +39 -0
- package/components/ui/editable-region-error-card.ts +8 -0
- package/helpers/checks.ts +5 -30
- package/helpers/hydrate-editable-regions.ts +15 -2
- package/nodes/editable-array-item.ts +92 -75
- package/nodes/editable-array.ts +186 -28
- package/nodes/editable-component.ts +16 -3
- package/nodes/editable-image.ts +46 -30
- package/nodes/editable-snippet.ts +1 -0
- package/nodes/editable-source.ts +29 -6
- package/nodes/editable-text.ts +17 -3
- package/nodes/editable.ts +33 -25
- package/package.json +2 -1
- package/styles/editable-text.css +5 -0
- package/styles/ui/editable-region-button.css +36 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import styleContent from "../../styles/ui/editable-region-button.css?inline";
|
|
2
|
+
|
|
3
|
+
export default class EditableRegionButton extends HTMLElement {
|
|
4
|
+
private shadow?: ShadowRoot;
|
|
5
|
+
|
|
6
|
+
connectedCallback() {
|
|
7
|
+
if (this.shadow) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
12
|
+
|
|
13
|
+
this.render(this.shadow);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render(shadow: ShadowRoot) {
|
|
17
|
+
const style = document.createElement("style");
|
|
18
|
+
style.textContent = styleContent;
|
|
19
|
+
shadow.appendChild(style);
|
|
20
|
+
|
|
21
|
+
const button = document.createElement("button");
|
|
22
|
+
button.innerHTML = `<cc-icon name='${this.getAttribute("icon")}'></cc-icon>${this.getAttribute("text")}`;
|
|
23
|
+
shadow.appendChild(button);
|
|
24
|
+
|
|
25
|
+
button.addEventListener("click", (e) => {
|
|
26
|
+
this.dispatchEvent(
|
|
27
|
+
new CustomEvent("button-click", { detail: { originalEvent: e } }),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
customElements.define("editable-region-button", EditableRegionButton);
|
|
34
|
+
|
|
35
|
+
declare global {
|
|
36
|
+
interface HTMLElementTagNameMap {
|
|
37
|
+
"editable-region-button": EditableRegionButton;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -65,6 +65,14 @@ export default class EditableRegionErrorCard extends HTMLElement {
|
|
|
65
65
|
body.appendChild(stack);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
if (this.hasAttribute("hint")) {
|
|
70
|
+
const hint = document.createElement("p");
|
|
71
|
+
hint.className = "hint";
|
|
72
|
+
hint.innerHTML = this.getAttribute("hint") ?? "";
|
|
73
|
+
|
|
74
|
+
body.appendChild(hint);
|
|
75
|
+
}
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
|
package/helpers/checks.ts
CHANGED
|
@@ -101,34 +101,9 @@ export const isEditableArrayItem = (el?: Element | null): boolean => {
|
|
|
101
101
|
);
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
export const
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
104
|
+
export const isEditableArray = (el?: Element | null): boolean => {
|
|
105
|
+
return (
|
|
106
|
+
el?.tagName === "EDITABLE-ARRAY" ||
|
|
107
|
+
(el instanceof HTMLElement && el.dataset.editable === "array")
|
|
108
|
+
);
|
|
134
109
|
};
|
|
@@ -52,7 +52,10 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
typeof element.dataset.editable !== "string" ||
|
|
57
|
+
typeof element.dataset.cloudcannonIgnore === "string"
|
|
58
|
+
) {
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -62,7 +65,17 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
62
65
|
error.setAttribute("heading", "Failed to render editable region");
|
|
63
66
|
error.setAttribute(
|
|
64
67
|
"message",
|
|
65
|
-
`
|
|
68
|
+
`This element has an invalid editable type for the 'data-editable' HTML attribute. The supported editable types are ${Object.keys(
|
|
69
|
+
editableMap,
|
|
70
|
+
)
|
|
71
|
+
.map((type) => `"${type}"`)
|
|
72
|
+
.join(
|
|
73
|
+
", ",
|
|
74
|
+
)} but instead received the type "${element.dataset.editable}". Please make sure that this element has a valid "data-editable" attribute.`,
|
|
75
|
+
);
|
|
76
|
+
error.setAttribute(
|
|
77
|
+
"hint",
|
|
78
|
+
"If this is intentional you can use the 'data-cloudcannon-ignore' attribute to exclude this element from editable regions.",
|
|
66
79
|
);
|
|
67
80
|
element.replaceWith(error);
|
|
68
81
|
return;
|
|
@@ -2,10 +2,12 @@ import "../components/ui/editable-array-item-controls.js";
|
|
|
2
2
|
import type EditableArrayItemControls from "../components/ui/editable-array-item-controls.js";
|
|
3
3
|
import {
|
|
4
4
|
hasEditableArrayItem,
|
|
5
|
+
isEditableArray,
|
|
5
6
|
isEditableArrayItem,
|
|
7
|
+
isEditableElement,
|
|
6
8
|
} from "../helpers/checks.js";
|
|
7
9
|
import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.js";
|
|
8
|
-
import EditableArray from "./editable-array.js";
|
|
10
|
+
import type EditableArray from "./editable-array.js";
|
|
9
11
|
import EditableComponent from "./editable-component.js";
|
|
10
12
|
|
|
11
13
|
export default class EditableArrayItem extends EditableComponent {
|
|
@@ -14,32 +16,25 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
14
16
|
protected controlsElement?: EditableArrayItemControls;
|
|
15
17
|
|
|
16
18
|
private inputConfig?: any;
|
|
19
|
+
private structureStrings: string[] = [];
|
|
17
20
|
|
|
18
21
|
shouldMount(): boolean {
|
|
19
22
|
return this.value !== undefined;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
validateConfiguration(): boolean {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!component) {
|
|
27
|
-
this.element.classList.add("errored");
|
|
28
|
-
const error = document.createElement("editable-region-error-card");
|
|
29
|
-
error.setAttribute("heading", "Failed to render component");
|
|
30
|
-
error.setAttribute("message", `Couldn't find component '${key}'`);
|
|
31
|
-
this.element.replaceChildren(error);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
26
|
+
let parentElement = this.element.parentElement;
|
|
27
|
+
while (parentElement && !isEditableElement(parentElement)) {
|
|
28
|
+
parentElement = parentElement.parentElement;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
if (!
|
|
31
|
+
if (!parentElement || !isEditableArray(parentElement)) {
|
|
37
32
|
this.element.classList.add("errored");
|
|
38
33
|
const error = document.createElement("editable-region-error-card");
|
|
39
34
|
error.setAttribute("heading", "Failed to render array item");
|
|
40
35
|
error.setAttribute(
|
|
41
36
|
"message",
|
|
42
|
-
"
|
|
37
|
+
"Array item editable regions must be nested inside an array editable region but this element has no parent editable region. Please check that this element is inside an array editable region.",
|
|
43
38
|
);
|
|
44
39
|
this.element.replaceChildren(error);
|
|
45
40
|
return false;
|
|
@@ -54,15 +49,38 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
54
49
|
return false;
|
|
55
50
|
}
|
|
56
51
|
|
|
52
|
+
if (e.dataTransfer?.types.includes(source.toLowerCase())) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
57
56
|
const dragType = this.getDragType();
|
|
58
57
|
|
|
59
|
-
if (
|
|
60
|
-
!e.dataTransfer?.types.includes(source.toLowerCase()) &&
|
|
61
|
-
(!dragType || !e.dataTransfer.types.includes(dragType))
|
|
62
|
-
) {
|
|
58
|
+
if (!dragType || !e.dataTransfer.types.includes(dragType)) {
|
|
63
59
|
return false;
|
|
64
60
|
}
|
|
65
61
|
|
|
62
|
+
if (dragType === "cc:structure") {
|
|
63
|
+
if (this.structureStrings.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const structureString = e.dataTransfer.types
|
|
68
|
+
.find((type) => type.startsWith("structure:"))
|
|
69
|
+
?.replace(/^structure:/, "");
|
|
70
|
+
|
|
71
|
+
if (!structureString) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const targetStructure = this.structureStrings.find(
|
|
76
|
+
(targetValue: unknown) => structureString === targetValue,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!targetStructure) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
return true;
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -77,6 +95,47 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
77
95
|
this.element.style.boxShadow = this.getDraggingBoxShadow(e);
|
|
78
96
|
}
|
|
79
97
|
|
|
98
|
+
onDragStart(e: DragEvent): void {
|
|
99
|
+
const source = this.parent?.contextBase?.filePath;
|
|
100
|
+
if (!source || !e.dataTransfer || !this.element.dataset.prop) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const clientRect = this.element.getBoundingClientRect();
|
|
105
|
+
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
this.element.classList.add("dragging");
|
|
108
|
+
|
|
109
|
+
e.dataTransfer.setDragImage(this.element, clientRect.width - 35, 35);
|
|
110
|
+
e.dataTransfer.effectAllowed = "move";
|
|
111
|
+
|
|
112
|
+
const id = Math.random().toString(36).slice(2);
|
|
113
|
+
this.element.id = id;
|
|
114
|
+
|
|
115
|
+
const data = {
|
|
116
|
+
index: this.element.dataset.prop,
|
|
117
|
+
sourceId: id,
|
|
118
|
+
value: this.value,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (this.inputConfig?.options?.structures?.values?.length > 0) {
|
|
122
|
+
const structure = CloudCannon.findStructure(
|
|
123
|
+
this.inputConfig?.options?.structures,
|
|
124
|
+
this.value,
|
|
125
|
+
);
|
|
126
|
+
if (structure) {
|
|
127
|
+
e.dataTransfer.setData(`structure:${JSON.stringify(structure)}`, "");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const payload = JSON.stringify(data);
|
|
132
|
+
e.dataTransfer?.setData(source, payload);
|
|
133
|
+
const dragType = this.getDragType();
|
|
134
|
+
if (dragType) {
|
|
135
|
+
e.dataTransfer?.setData(dragType, payload);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
80
139
|
getDragType(): string | undefined {
|
|
81
140
|
if (this.inputConfig?.options?.structures?.values?.length) {
|
|
82
141
|
return "cc:structure";
|
|
@@ -300,43 +359,9 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
300
359
|
this.element.remove();
|
|
301
360
|
});
|
|
302
361
|
|
|
303
|
-
this.controlsElement.addEventListener("dragstart", (e: DragEvent) =>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const clientRect = this.element.getBoundingClientRect();
|
|
310
|
-
|
|
311
|
-
e.stopPropagation();
|
|
312
|
-
this.element.classList.add("dragging");
|
|
313
|
-
|
|
314
|
-
e.dataTransfer.setDragImage(this.element, clientRect.width - 35, 35);
|
|
315
|
-
e.dataTransfer.effectAllowed = "move";
|
|
316
|
-
|
|
317
|
-
const id = Math.random().toString(36).slice(2);
|
|
318
|
-
this.element.id = id;
|
|
319
|
-
|
|
320
|
-
const data: Record<string, any> = {
|
|
321
|
-
index: this.element.dataset.prop,
|
|
322
|
-
sourceId: id,
|
|
323
|
-
value: this.value,
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
if (this.inputConfig?.options?.structures?.values?.length > 0) {
|
|
327
|
-
data.structure = CloudCannon.findStructure(
|
|
328
|
-
this.inputConfig?.options?.structures,
|
|
329
|
-
this.value,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const payload = JSON.stringify(data);
|
|
334
|
-
e.dataTransfer?.setData(source, payload);
|
|
335
|
-
const dragType = this.getDragType();
|
|
336
|
-
if (dragType) {
|
|
337
|
-
e.dataTransfer?.setData(dragType, payload);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
362
|
+
this.controlsElement.addEventListener("dragstart", (e: DragEvent) =>
|
|
363
|
+
this.onDragStart(e),
|
|
364
|
+
);
|
|
340
365
|
|
|
341
366
|
this.updateControls();
|
|
342
367
|
|
|
@@ -358,6 +383,12 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
358
383
|
(inputConfig as any)?.options?.disable_add ?? false;
|
|
359
384
|
|
|
360
385
|
this.inputConfig = inputConfig;
|
|
386
|
+
if (this.inputConfig?.options?.structures?.values?.length > 0) {
|
|
387
|
+
this.structureStrings =
|
|
388
|
+
this.inputConfig.options.structures.values.map((value: unknown) =>
|
|
389
|
+
JSON.stringify(value).toLowerCase(),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
361
392
|
this.element.append(this.controlsElement);
|
|
362
393
|
});
|
|
363
394
|
}
|
|
@@ -428,25 +459,7 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
428
459
|
this.dispatchArrayMove(fromIndex, newIndex);
|
|
429
460
|
}
|
|
430
461
|
} else if (otherArrayData) {
|
|
431
|
-
const { index, sourceId, value
|
|
432
|
-
JSON.parse(otherArrayData);
|
|
433
|
-
if (dragType === "cc:structure") {
|
|
434
|
-
if (!this.inputConfig?.options?.structures?.values) {
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const targetStructure = CloudCannon.findStructure(
|
|
439
|
-
this.inputConfig.options.structures,
|
|
440
|
-
this.value,
|
|
441
|
-
);
|
|
442
|
-
if (!targetStructure) {
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (JSON.stringify(structure) !== JSON.stringify(targetStructure)) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
462
|
+
const { index, sourceId, value } = JSON.parse(otherArrayData);
|
|
450
463
|
|
|
451
464
|
const sourceElement = document.getElementById(sourceId);
|
|
452
465
|
if (sourceElement && hasEditableArrayItem(sourceElement)) {
|
|
@@ -478,6 +491,10 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
478
491
|
|
|
479
492
|
setupListeners(): void {
|
|
480
493
|
super.setupListeners();
|
|
481
|
-
this.
|
|
494
|
+
if (!this.element.dataset.prop) {
|
|
495
|
+
this.parent?.registerListener({
|
|
496
|
+
editable: this,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
482
499
|
}
|
|
483
500
|
}
|
package/nodes/editable-array.ts
CHANGED
|
@@ -4,10 +4,12 @@ import type {
|
|
|
4
4
|
CloudCannonJavaScriptV1APIFile,
|
|
5
5
|
} from "@cloudcannon/javascript-api";
|
|
6
6
|
import type EditableArrayItemComponent from "../components/editable-array-item-component.js";
|
|
7
|
-
import
|
|
7
|
+
import type EditableRegionButton from "../components/ui/editable-region-button.js";
|
|
8
|
+
import { isEditableElement } from "../helpers/checks.js";
|
|
8
9
|
import { CloudCannon } from "../helpers/cloudcannon.js";
|
|
9
10
|
import type EditableArrayItem from "./editable-array-item.js";
|
|
10
|
-
import Editable from "./editable.js";
|
|
11
|
+
import Editable, { type EditableListener } from "./editable.js";
|
|
12
|
+
import "../components/ui/editable-region-button.js";
|
|
11
13
|
|
|
12
14
|
const arrayDirectionValues = [
|
|
13
15
|
"row",
|
|
@@ -33,6 +35,61 @@ export default class EditableArray extends Editable {
|
|
|
33
35
|
|
|
34
36
|
private updatePromise: Promise<void> | undefined;
|
|
35
37
|
private needsReupdate = false;
|
|
38
|
+
private addButton?: EditableRegionButton;
|
|
39
|
+
|
|
40
|
+
async registerListener(listener: EditableListener): Promise<void> {
|
|
41
|
+
if (!this.value) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
46
|
+
if (CloudCannon.isAPICollection(this.value)) {
|
|
47
|
+
value = await this.value.items();
|
|
48
|
+
} else if (CloudCannon.isAPIDataset(this.value)) {
|
|
49
|
+
const items = await this.value.items();
|
|
50
|
+
if (Array.isArray(items)) {
|
|
51
|
+
value = items;
|
|
52
|
+
} else {
|
|
53
|
+
const data = await items.data.get();
|
|
54
|
+
value = Array.isArray(data) ? data : [];
|
|
55
|
+
}
|
|
56
|
+
} else if (Array.isArray(this.value)) {
|
|
57
|
+
value = this.value;
|
|
58
|
+
} else {
|
|
59
|
+
value = [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!listener.path) {
|
|
63
|
+
let index = 0;
|
|
64
|
+
for (const child of this.element.querySelectorAll(
|
|
65
|
+
"editable-array-item,[data-editable='array-item']",
|
|
66
|
+
)) {
|
|
67
|
+
let parent = child.parentElement;
|
|
68
|
+
while (parent instanceof HTMLElement && !isEditableElement(parent)) {
|
|
69
|
+
parent = parent.parentElement;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parent !== this.element) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (child === listener.editable.element) {
|
|
77
|
+
listener.path = `${index}`;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (listener.path) {
|
|
86
|
+
listener.editable.pushValue(value, listener, {
|
|
87
|
+
__base_context: this.contextBase ?? {},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deregisterListener(_target: Editable): void {}
|
|
36
93
|
|
|
37
94
|
validateConfiguration(): boolean {
|
|
38
95
|
const prop = this.element.dataset.prop;
|
|
@@ -40,7 +97,10 @@ export default class EditableArray extends Editable {
|
|
|
40
97
|
this.element.classList.add("errored");
|
|
41
98
|
const error = document.createElement("editable-region-error-card");
|
|
42
99
|
error.setAttribute("heading", "Failed to render array editable");
|
|
43
|
-
error.setAttribute(
|
|
100
|
+
error.setAttribute(
|
|
101
|
+
"message",
|
|
102
|
+
"Array editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
|
|
103
|
+
);
|
|
44
104
|
this.element.replaceChildren(error);
|
|
45
105
|
return false;
|
|
46
106
|
}
|
|
@@ -60,8 +120,19 @@ export default class EditableArray extends Editable {
|
|
|
60
120
|
error.setAttribute("heading", "Failed to render array editable");
|
|
61
121
|
error.setAttribute(
|
|
62
122
|
"message",
|
|
63
|
-
`
|
|
123
|
+
`Array editable regions expect to receive a value of type "array" but instead received a value of type '${typeof value}'.`,
|
|
64
124
|
);
|
|
125
|
+
if (this.contextBase?.fullPath) {
|
|
126
|
+
error.setAttribute(
|
|
127
|
+
"hint",
|
|
128
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
error.setAttribute(
|
|
132
|
+
"hint",
|
|
133
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
65
136
|
this.element.replaceChildren(error);
|
|
66
137
|
return;
|
|
67
138
|
}
|
|
@@ -101,40 +172,69 @@ export default class EditableArray extends Editable {
|
|
|
101
172
|
value = [];
|
|
102
173
|
}
|
|
103
174
|
|
|
104
|
-
const children: (HTMLElement & { editable
|
|
175
|
+
const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
|
|
105
176
|
|
|
106
177
|
for (const child of this.element.querySelectorAll(
|
|
107
|
-
"editable-array-item,[data-editable='array-item']",
|
|
178
|
+
"editable-array-item,[data-editable='array-item'],array-placeholder",
|
|
108
179
|
)) {
|
|
109
|
-
if (!hasEditableArrayItem(child)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
180
|
let parent = child.parentElement;
|
|
114
181
|
while (parent instanceof HTMLElement && !isEditableElement(parent)) {
|
|
115
182
|
parent = parent.parentElement;
|
|
116
183
|
}
|
|
117
184
|
|
|
118
|
-
if (
|
|
185
|
+
if (parent !== this.element) {
|
|
119
186
|
continue;
|
|
120
187
|
}
|
|
121
188
|
|
|
122
189
|
children.push(child as any);
|
|
123
190
|
}
|
|
124
191
|
|
|
192
|
+
if (
|
|
193
|
+
children.length === 0 &&
|
|
194
|
+
!this.element.dataset.component &&
|
|
195
|
+
!this.element.dataset.componentKey
|
|
196
|
+
) {
|
|
197
|
+
const error = document.createElement("editable-region-error-card");
|
|
198
|
+
error.setAttribute("heading", "Failed to render array editable region");
|
|
199
|
+
error.setAttribute(
|
|
200
|
+
"message",
|
|
201
|
+
"Array editable regions with no child array items must have either a 'data-component' attribute or a 'data-component-key' attribute. Please add an item to this array then save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
|
|
202
|
+
);
|
|
203
|
+
this.element.replaceChildren(error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
125
207
|
if (!this.element.dataset.idKey) {
|
|
126
208
|
while (children.length > value.length) {
|
|
127
209
|
children.pop()?.remove();
|
|
128
210
|
}
|
|
129
211
|
|
|
212
|
+
if (value.length === 0 && this.addButton) {
|
|
213
|
+
this.element.appendChild(this.addButton);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.addButton?.remove();
|
|
218
|
+
|
|
130
219
|
for (let i = 0; i < value.length; i++) {
|
|
131
220
|
let child = children[i];
|
|
132
221
|
if (!child) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
222
|
+
if (this.element.dataset.component) {
|
|
223
|
+
child = document.createElement(
|
|
224
|
+
"editable-array-item",
|
|
225
|
+
) as EditableArrayItemComponent;
|
|
226
|
+
} else {
|
|
227
|
+
// Empty arrays should be caught by the error case above so children[0] should always exist
|
|
228
|
+
child = children[0].cloneNode(true) as HTMLElement & {
|
|
229
|
+
editable?: EditableArrayItem;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
136
232
|
this.element.appendChild(child);
|
|
137
233
|
}
|
|
234
|
+
|
|
235
|
+
if (this.element.dataset.component) {
|
|
236
|
+
child.dataset.component = this.element.dataset.component;
|
|
237
|
+
}
|
|
138
238
|
child.dataset.prop = `${i}`;
|
|
139
239
|
child.dataset.length = `${children.length}`;
|
|
140
240
|
child.editable?.pushValue(
|
|
@@ -163,9 +263,12 @@ export default class EditableArray extends Editable {
|
|
|
163
263
|
}
|
|
164
264
|
|
|
165
265
|
if (data && typeof data === "object" && componentKey) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
266
|
+
const component = (data as any)[componentKey];
|
|
267
|
+
if (typeof component !== "undefined" && component !== null) {
|
|
268
|
+
componentKeys.push(String((data as any)[componentKey]));
|
|
269
|
+
} else {
|
|
270
|
+
componentKeys.push(null);
|
|
271
|
+
}
|
|
169
272
|
}
|
|
170
273
|
}
|
|
171
274
|
|
|
@@ -178,12 +281,18 @@ export default class EditableArray extends Editable {
|
|
|
178
281
|
children.forEach((child, index) => {
|
|
179
282
|
child.dataset.prop = `${index}`;
|
|
180
283
|
child.dataset.length = `${children.length}`;
|
|
181
|
-
child.editable
|
|
284
|
+
child.editable?.pushValue(
|
|
182
285
|
value,
|
|
183
286
|
{ path: `${index}`, editable: child.editable },
|
|
184
287
|
{ __base_context: this.contextBase ?? {} },
|
|
185
288
|
);
|
|
186
289
|
});
|
|
290
|
+
|
|
291
|
+
if (dataKeys.length === 0 && this.addButton) {
|
|
292
|
+
this.element.appendChild(this.addButton);
|
|
293
|
+
} else {
|
|
294
|
+
this.addButton?.remove();
|
|
295
|
+
}
|
|
187
296
|
return;
|
|
188
297
|
}
|
|
189
298
|
|
|
@@ -201,6 +310,8 @@ export default class EditableArray extends Editable {
|
|
|
201
310
|
}
|
|
202
311
|
const placeholder = placeholders[i];
|
|
203
312
|
const existingElement = children[i];
|
|
313
|
+
const componentKey = componentKeys[i] || this.element.dataset.component;
|
|
314
|
+
|
|
204
315
|
const matchingChildIndex = children.findIndex(
|
|
205
316
|
(child, i) => child.dataset.id === key && !moved[i],
|
|
206
317
|
);
|
|
@@ -210,15 +321,38 @@ export default class EditableArray extends Editable {
|
|
|
210
321
|
const clone = children.find((child) => child.dataset.id === key);
|
|
211
322
|
if (clone) {
|
|
212
323
|
matchingChild = clone.cloneNode(true) as any;
|
|
213
|
-
} else {
|
|
324
|
+
} else if (componentKey) {
|
|
214
325
|
matchingChild = document.createElement(
|
|
215
326
|
"editable-array-item",
|
|
216
327
|
) as EditableArrayItemComponent;
|
|
217
328
|
matchingChild.dataset.id = key;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
329
|
+
matchingChild.dataset.component = componentKey;
|
|
330
|
+
} else {
|
|
331
|
+
const error = document.createElement("editable-region-error-card");
|
|
332
|
+
error.setAttribute("heading", "Failed to render array item");
|
|
333
|
+
if (typeof this.element.dataset.componentKey === "string") {
|
|
334
|
+
error.setAttribute(
|
|
335
|
+
"message",
|
|
336
|
+
"Array editable region has no child with a matching 'data-id' value for this element and the value has no key matching the 'data-component-key' attribute. Please check that the 'data-component-key' attribute for this element is correct and that each element has an entry for that key, or provide a fallback 'data-component' attribute.",
|
|
337
|
+
);
|
|
338
|
+
error.setAttribute(
|
|
339
|
+
"hint",
|
|
340
|
+
`This may mean that the value for 'data-component-key' is incorrect or that your array data is incorrectly formatted.
|
|
341
|
+
The current value for 'data-component-key' is '${this.element.dataset.componentKey}' and the current value for 'data-id' is '${key}'.
|
|
342
|
+
`,
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
error.setAttribute(
|
|
346
|
+
"message",
|
|
347
|
+
"Array editable region has no child with a matching 'data-id' value for this element and no 'data-component' or 'data-component-key' attribute. Please save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
|
|
348
|
+
);
|
|
349
|
+
error.setAttribute(
|
|
350
|
+
"hint",
|
|
351
|
+
`The full value of "data-id" for this item is "${key}"`,
|
|
352
|
+
);
|
|
221
353
|
}
|
|
354
|
+
matchingChild = document.createElement("array-placeholder");
|
|
355
|
+
matchingChild.append(error);
|
|
222
356
|
}
|
|
223
357
|
} else {
|
|
224
358
|
moved[matchingChildIndex] = true;
|
|
@@ -235,12 +369,14 @@ export default class EditableArray extends Editable {
|
|
|
235
369
|
this.element.appendChild(matchingChild);
|
|
236
370
|
}
|
|
237
371
|
|
|
238
|
-
matchingChild.editable
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
372
|
+
if (matchingChild.editable) {
|
|
373
|
+
matchingChild.editable.parent = this;
|
|
374
|
+
matchingChild.editable.pushValue(
|
|
375
|
+
value,
|
|
376
|
+
{ path: `${i}`, editable: matchingChild.editable },
|
|
377
|
+
{ __base_context: this.contextBase ?? {} },
|
|
378
|
+
);
|
|
379
|
+
}
|
|
244
380
|
});
|
|
245
381
|
|
|
246
382
|
children.forEach((child, i) => {
|
|
@@ -248,6 +384,12 @@ export default class EditableArray extends Editable {
|
|
|
248
384
|
child.remove();
|
|
249
385
|
}
|
|
250
386
|
});
|
|
387
|
+
|
|
388
|
+
if (dataKeys.length === 0 && this.addButton) {
|
|
389
|
+
this.element.appendChild(this.addButton);
|
|
390
|
+
} else {
|
|
391
|
+
this.addButton?.remove();
|
|
392
|
+
}
|
|
251
393
|
}
|
|
252
394
|
|
|
253
395
|
calculateArrayDirection(): ArrayDirection {
|
|
@@ -268,5 +410,21 @@ export default class EditableArray extends Editable {
|
|
|
268
410
|
|
|
269
411
|
mount(): void {
|
|
270
412
|
this.arrayDirection = this.calculateArrayDirection();
|
|
413
|
+
|
|
414
|
+
this.addButton = document.createElement("editable-region-button");
|
|
415
|
+
this.addButton.setAttribute("icon", "add");
|
|
416
|
+
this.addButton.setAttribute("text", "Add Item");
|
|
417
|
+
this.addButton.addEventListener("button-click", () => {
|
|
418
|
+
this.element.dispatchEvent(
|
|
419
|
+
new CustomEvent("cloudcannon-api", {
|
|
420
|
+
bubbles: true,
|
|
421
|
+
detail: {
|
|
422
|
+
source: this.element.dataset.prop,
|
|
423
|
+
action: "add-array-item",
|
|
424
|
+
newIndex: 0,
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
});
|
|
271
429
|
}
|
|
272
430
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
areEqualEditables,
|
|
3
|
-
areEqualNodes,
|
|
4
3
|
hasEditable,
|
|
5
4
|
hasEditableText,
|
|
6
5
|
isEditableElement,
|
|
@@ -43,7 +42,7 @@ export default class EditableComponent extends Editable {
|
|
|
43
42
|
error.setAttribute("heading", "Failed to render component");
|
|
44
43
|
error.setAttribute(
|
|
45
44
|
"message",
|
|
46
|
-
"Component
|
|
45
|
+
"Component editable regions require a 'data-component' HTML attribute but none was provided. Please check that this element has a valid 'data-component' attribute.",
|
|
47
46
|
);
|
|
48
47
|
this.element.replaceChildren(error);
|
|
49
48
|
return false;
|
|
@@ -192,12 +191,13 @@ export default class EditableComponent extends Editable {
|
|
|
192
191
|
}
|
|
193
192
|
} else if (renderChild && targetChild) {
|
|
194
193
|
if (
|
|
195
|
-
|
|
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-image.ts
CHANGED
|
@@ -11,11 +11,14 @@ export default class EditableImage extends Editable {
|
|
|
11
11
|
configuredAlt = false;
|
|
12
12
|
configuredTitle = false;
|
|
13
13
|
|
|
14
|
-
displayError(heading: string, message: string) {
|
|
14
|
+
displayError(heading: string, message: string, hint?: string) {
|
|
15
15
|
this.element.classList.add("errored");
|
|
16
16
|
const error = document.createElement("editable-region-error-card");
|
|
17
17
|
error.setAttribute("heading", heading);
|
|
18
18
|
error.setAttribute("message", message);
|
|
19
|
+
if (hint) {
|
|
20
|
+
error.setAttribute("hint", hint);
|
|
21
|
+
}
|
|
19
22
|
if (this.imageEl) {
|
|
20
23
|
this.imageEl?.replaceWith(error);
|
|
21
24
|
} else {
|
|
@@ -32,7 +35,7 @@ export default class EditableImage extends Editable {
|
|
|
32
35
|
if (!(child instanceof HTMLImageElement)) {
|
|
33
36
|
this.displayError(
|
|
34
37
|
"Failed to render image editable region",
|
|
35
|
-
"Image editable
|
|
38
|
+
"Image editable regions must contain a child HTML element of type 'img'. Please check that this element has a child 'img' element.",
|
|
36
39
|
);
|
|
37
40
|
return false;
|
|
38
41
|
}
|
|
@@ -47,7 +50,7 @@ export default class EditableImage extends Editable {
|
|
|
47
50
|
) {
|
|
48
51
|
this.displayError(
|
|
49
52
|
"Failed to render image editable region",
|
|
50
|
-
"
|
|
53
|
+
"Image editable regions require atleast one valid 'data-prop-*' HTML attribute. The valid attributes are 'data-prop', 'data-prop-src', 'data-prop-alt', and 'data-prop-title'. Please check that this element has atleast one of these attributes.",
|
|
51
54
|
);
|
|
52
55
|
return false;
|
|
53
56
|
}
|
|
@@ -58,7 +61,10 @@ export default class EditableImage extends Editable {
|
|
|
58
61
|
if (typeof value !== "object") {
|
|
59
62
|
this.displayError(
|
|
60
63
|
"Failed to render image editable region",
|
|
61
|
-
`
|
|
64
|
+
`Image editable regions expect to receive a value of type "object" but instead received a value of type '${typeof value}'.`,
|
|
65
|
+
this.contextBase?.fullPath
|
|
66
|
+
? `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`
|
|
67
|
+
: `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
|
|
62
68
|
);
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
@@ -67,32 +73,30 @@ export default class EditableImage extends Editable {
|
|
|
67
73
|
return value;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
for (const key of ["src", "alt", "title"]) {
|
|
77
|
+
if (
|
|
78
|
+
key in value &&
|
|
79
|
+
typeof value[key as keyof typeof value] !== "string" &&
|
|
80
|
+
value[key as keyof typeof value] !== null
|
|
81
|
+
) {
|
|
82
|
+
let hint: string;
|
|
83
|
+
if (this.contexts[key]?.fullPath) {
|
|
84
|
+
hint = `This may mean that the 'data-prop-${key}' attribute is incorrectly set for this element, the full 'data-prop-${key}' path was '${this.contexts[key]?.fullPath}'.`;
|
|
85
|
+
} else if (typeof this.element.dataset[key] === "string") {
|
|
86
|
+
hint = `This may mean that the 'data-prop-${key}' attribute is incorrectly set for this element.`;
|
|
87
|
+
} else if (this.contextBase?.fullPath) {
|
|
88
|
+
hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`;
|
|
89
|
+
} else {
|
|
90
|
+
hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element.`;
|
|
91
|
+
}
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`Illegal value type for "title": ${typeof value.title}. Supported types are string.`,
|
|
94
|
-
);
|
|
95
|
-
return;
|
|
93
|
+
this.displayError(
|
|
94
|
+
"Failed to render image editable region",
|
|
95
|
+
`Image editable regions expect the "${key}" key to have a value of type "string" but instead it was a value of type '${typeof value[key as keyof typeof value]}'.`,
|
|
96
|
+
hint,
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
const unexpectedKey = Object.keys(value).find(
|
|
@@ -100,9 +104,21 @@ export default class EditableImage extends Editable {
|
|
|
100
104
|
);
|
|
101
105
|
|
|
102
106
|
if (unexpectedKey) {
|
|
107
|
+
let hint: string | undefined;
|
|
108
|
+
const capitalizedUnexpectedKey =
|
|
109
|
+
unexpectedKey.charAt(0).toUpperCase() + unexpectedKey.slice(1);
|
|
110
|
+
|
|
111
|
+
if (this.element.dataset[`prop${capitalizedUnexpectedKey}`]) {
|
|
112
|
+
hint = `Try removing the 'data-prop-${unexpectedKey}' HTML attribute from this element.`;
|
|
113
|
+
} else if (this.contextBase?.fullPath) {
|
|
114
|
+
hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`;
|
|
115
|
+
} else {
|
|
116
|
+
hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element.`;
|
|
117
|
+
}
|
|
103
118
|
this.displayError(
|
|
104
119
|
"Failed to render image editable region",
|
|
105
|
-
`
|
|
120
|
+
`Image editable region received an unexpected value key "${unexpectedKey}". The supported values are "src", "alt", and "title". Please check that your data is correctly formatted.`,
|
|
121
|
+
hint,
|
|
106
122
|
);
|
|
107
123
|
return;
|
|
108
124
|
}
|
|
@@ -51,6 +51,7 @@ export default class EditableSnippet extends EditableComponent {
|
|
|
51
51
|
const { snippets, source } = this.parseSource(options.source);
|
|
52
52
|
|
|
53
53
|
if (options.action === "get-input-config") {
|
|
54
|
+
// TODO: This should actually load the input config, including the snippet in the cascade
|
|
54
55
|
return false;
|
|
55
56
|
}
|
|
56
57
|
|
package/nodes/editable-source.ts
CHANGED
|
@@ -5,7 +5,7 @@ import EditableText from "./editable-text.js";
|
|
|
5
5
|
|
|
6
6
|
const INDENTATION_REGEX = /^([ \t]+)[^\s]/gm;
|
|
7
7
|
const TAG_REGEX =
|
|
8
|
-
/<\s*(?<closing>\/?)\s*(?<tagname>[-a-
|
|
8
|
+
/<\s*(?<closing>\/?)\s*(?<tagname>[-a-z0-9]+)(\s+[^>]+)*?\s*(?<selfclosing>\/?)\s*>/gi;
|
|
9
9
|
|
|
10
10
|
const HTML_VOID_ELEMENT: Record<string, boolean> = {
|
|
11
11
|
area: true,
|
|
@@ -38,6 +38,11 @@ export default class EditableSource extends EditableText {
|
|
|
38
38
|
if (!this.element.dataset.path) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
if (!this.element.dataset.path.startsWith("/")) {
|
|
43
|
+
this.element.dataset.path = `/${this.element.dataset.path}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
this.file = CloudCannon.file(this.element.dataset.path);
|
|
42
47
|
this.file.addEventListener("change", () => {
|
|
43
48
|
this.file?.get().then(this.pushValue.bind(this));
|
|
@@ -51,7 +56,10 @@ export default class EditableSource extends EditableText {
|
|
|
51
56
|
this.element.classList.add("errored");
|
|
52
57
|
const error = document.createElement("editable-region-error-card");
|
|
53
58
|
error.setAttribute("heading", "Failed to render source editable region");
|
|
54
|
-
error.setAttribute(
|
|
59
|
+
error.setAttribute(
|
|
60
|
+
"message",
|
|
61
|
+
"Source editable regions require a 'data-path' HTML attribute but none was provided. Please check that this element has a valid 'data-path' attribute.",
|
|
62
|
+
);
|
|
55
63
|
this.element.replaceChildren(error);
|
|
56
64
|
return false;
|
|
57
65
|
}
|
|
@@ -61,7 +69,10 @@ export default class EditableSource extends EditableText {
|
|
|
61
69
|
this.element.classList.add("errored");
|
|
62
70
|
const error = document.createElement("editable-region-error-card");
|
|
63
71
|
error.setAttribute("heading", "Failed to render source editable region");
|
|
64
|
-
error.setAttribute(
|
|
72
|
+
error.setAttribute(
|
|
73
|
+
"message",
|
|
74
|
+
"Source editable regions require a 'data-key' HTML attribute but none was provided. Please check that this element has a valid 'data-key' attribute.",
|
|
75
|
+
);
|
|
65
76
|
this.element.replaceChildren(error);
|
|
66
77
|
return false;
|
|
67
78
|
}
|
|
@@ -75,7 +86,11 @@ export default class EditableSource extends EditableText {
|
|
|
75
86
|
error.setAttribute("heading", "Failed to render source editable region");
|
|
76
87
|
error.setAttribute(
|
|
77
88
|
"message",
|
|
78
|
-
|
|
89
|
+
"The provided 'data-path' HTML attribute references a file that does not exist. Please check that the file exists and that the 'data-path' attribute on this element is correct.",
|
|
90
|
+
);
|
|
91
|
+
error.setAttribute(
|
|
92
|
+
"hint",
|
|
93
|
+
`The current value of the "data-path" attribute is "${this.element.dataset.path}"`,
|
|
79
94
|
);
|
|
80
95
|
this.element.replaceChildren(error);
|
|
81
96
|
return;
|
|
@@ -92,7 +107,11 @@ export default class EditableSource extends EditableText {
|
|
|
92
107
|
);
|
|
93
108
|
error.setAttribute(
|
|
94
109
|
"message",
|
|
95
|
-
"Failed to find element
|
|
110
|
+
"Failed to find an element matching the provided 'data-key' attribute.",
|
|
111
|
+
);
|
|
112
|
+
error.setAttribute(
|
|
113
|
+
"hint",
|
|
114
|
+
`This might mean that your 'data-path' attribute is incorrect. The current value of the "data-path" attribute is "${this.element.dataset.path}" and the current value of the "data-key" attribute is "${this.element.dataset.key}".`,
|
|
96
115
|
);
|
|
97
116
|
this.element.replaceChildren(error);
|
|
98
117
|
return;
|
|
@@ -111,7 +130,11 @@ export default class EditableSource extends EditableText {
|
|
|
111
130
|
);
|
|
112
131
|
error.setAttribute(
|
|
113
132
|
"message",
|
|
114
|
-
|
|
133
|
+
`Source editable regions require that all 'data-key' attributes in the same file have unique values but the current file contains multiple instances of the key '${this.element.dataset.key}'. Please make sure all 'data-key' attributes are unique within the same file.`,
|
|
134
|
+
);
|
|
135
|
+
error.setAttribute(
|
|
136
|
+
"hint",
|
|
137
|
+
`The current value of the 'data-path' attribute is '${this.element.dataset.path}'"'.`,
|
|
115
138
|
);
|
|
116
139
|
this.element.replaceChildren(error);
|
|
117
140
|
return;
|
package/nodes/editable-text.ts
CHANGED
|
@@ -15,7 +15,10 @@ export default class EditableText extends Editable {
|
|
|
15
15
|
this.element.classList.add("errored");
|
|
16
16
|
const error = document.createElement("editable-region-error-card");
|
|
17
17
|
error.setAttribute("heading", "Failed to render text editable region");
|
|
18
|
-
error.setAttribute(
|
|
18
|
+
error.setAttribute(
|
|
19
|
+
"message",
|
|
20
|
+
"Text editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
|
|
21
|
+
);
|
|
19
22
|
this.element.replaceChildren(error);
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
@@ -30,7 +33,7 @@ export default class EditableText extends Editable {
|
|
|
30
33
|
error.setAttribute("heading", "Failed to render text editable region");
|
|
31
34
|
error.setAttribute(
|
|
32
35
|
"message",
|
|
33
|
-
`
|
|
36
|
+
`Text editable region received an invalid type for the 'data-type' HTML attribute. The provided element type was '${elementType}' but the supported element types are 'span', 'text', and 'block'. Please set the 'data-type' attribute to one of the supported types.`,
|
|
34
37
|
);
|
|
35
38
|
this.element.replaceChildren(error);
|
|
36
39
|
return false;
|
|
@@ -45,8 +48,19 @@ export default class EditableText extends Editable {
|
|
|
45
48
|
error.setAttribute("heading", "Failed to render text editable region");
|
|
46
49
|
error.setAttribute(
|
|
47
50
|
"message",
|
|
48
|
-
`
|
|
51
|
+
`Text editable regions expect to receive a value of type "string" but instead received a value of type '${typeof value}'.`,
|
|
49
52
|
);
|
|
53
|
+
if (this.contextBase?.fullPath) {
|
|
54
|
+
error.setAttribute(
|
|
55
|
+
"hint",
|
|
56
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
error.setAttribute(
|
|
60
|
+
"hint",
|
|
61
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
50
64
|
this.element.replaceChildren(error);
|
|
51
65
|
return;
|
|
52
66
|
}
|
package/nodes/editable.ts
CHANGED
|
@@ -27,8 +27,8 @@ export interface DOMListener {
|
|
|
27
27
|
event: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export interface APIListener
|
|
31
|
-
|
|
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
|
|
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 (
|
|
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
|
|
265
|
-
obj.removeEventListener(
|
|
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);
|
|
@@ -280,6 +286,10 @@ export default class Editable {
|
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
connect(): void {
|
|
289
|
+
if (!this.validateConfiguration()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
283
293
|
if (this.disconnecting) {
|
|
284
294
|
this.needsReconnect = true;
|
|
285
295
|
return;
|
|
@@ -289,13 +299,11 @@ export default class Editable {
|
|
|
289
299
|
}
|
|
290
300
|
this.connectPromise = loadingPromise.then(() => {
|
|
291
301
|
this.setupListeners();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.update();
|
|
298
|
-
}
|
|
302
|
+
this.connected = true;
|
|
303
|
+
if (!this.mounted && this.shouldMount()) {
|
|
304
|
+
this.mounted = true;
|
|
305
|
+
this.mount();
|
|
306
|
+
this.update();
|
|
299
307
|
}
|
|
300
308
|
});
|
|
301
309
|
}
|
|
@@ -347,9 +355,9 @@ export default class Editable {
|
|
|
347
355
|
this.APIListeners.push({
|
|
348
356
|
obj,
|
|
349
357
|
fn: handleAPIChange,
|
|
350
|
-
event: "change",
|
|
351
358
|
});
|
|
352
359
|
obj.addEventListener("change", handleAPIChange);
|
|
360
|
+
obj.addEventListener("delete", handleAPIChange);
|
|
353
361
|
handleAPIChange();
|
|
354
362
|
}
|
|
355
363
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcannon/editable-regions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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
|
}
|
package/styles/editable-text.css
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
button {
|
|
2
|
+
all: initial;
|
|
3
|
+
-webkit-appearance: none;
|
|
4
|
+
-moz-appearance: none;
|
|
5
|
+
appearance: none;
|
|
6
|
+
text-decoration: none;
|
|
7
|
+
text-align: center;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
|
|
10
|
+
font-size: 16px;
|
|
11
|
+
font-weight: bold;
|
|
12
|
+
font-family: var(--ccrt-font-family);
|
|
13
|
+
line-height: 1.4;
|
|
14
|
+
color: var(--ccrt-color-cc-blue);
|
|
15
|
+
--cc-icon-fill: var(--ccrt-color-cc-blue);
|
|
16
|
+
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
|
|
21
|
+
border: var(--ccrt-border-width) solid var(--ccrt-color-alto);
|
|
22
|
+
border-radius: var(--ccrt-border-radius);
|
|
23
|
+
|
|
24
|
+
padding: 0 var(--ccrt-gap);
|
|
25
|
+
height: 48px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
button:hover {
|
|
29
|
+
background-color: rgba(226, 234, 250);
|
|
30
|
+
border-color: color-mix(
|
|
31
|
+
in srgb,
|
|
32
|
+
var(--ccrt-color-cc-blue) 80%,
|
|
33
|
+
var(--ccrt-color-carbon)
|
|
34
|
+
);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|