@cloudcannon/editable-regions 0.0.5 → 0.0.7
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-array-item-controls.ts +3 -7
- package/components/ui/editable-component-controls.ts +17 -2
- package/components/ui/editable-region-button.ts +39 -0
- package/components/ui/editable-region-error-card.ts +8 -0
- package/helpers/checks.ts +7 -0
- package/helpers/hydrate-editable-regions.ts +15 -2
- package/nodes/editable-array-item.ts +90 -81
- package/nodes/editable-array.ts +127 -22
- package/nodes/editable-component.ts +1 -1
- package/nodes/editable-image.ts +49 -31
- package/nodes/editable-snippet.ts +1 -0
- package/nodes/editable-source.ts +29 -6
- package/nodes/editable-text.ts +19 -3
- package/nodes/editable.ts +46 -35
- package/package.json +1 -1
- package/styles/editable-text.css +5 -0
- package/styles/ui/editable-region-button.css +36 -0
|
@@ -92,15 +92,11 @@ export default class EditableArrayItemControls extends EditableComponentControls
|
|
|
92
92
|
super.render(shadow);
|
|
93
93
|
|
|
94
94
|
this.dragHandle = document.createElement("button");
|
|
95
|
+
this.dragHandle.type = "button";
|
|
95
96
|
this.dragHandle.onclick = (e) => {
|
|
96
97
|
e.stopPropagation();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.removeAttribute("open");
|
|
100
|
-
} else if (this.contextMenu && this.contextMenu.childElementCount > 0) {
|
|
101
|
-
this.contextMenu?.classList.add("open");
|
|
102
|
-
this.setAttribute("open", "true");
|
|
103
|
-
}
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
this.toggleContextMenu();
|
|
104
100
|
};
|
|
105
101
|
this.dragHandle.ondragstart = () => {
|
|
106
102
|
this.contextMenu?.classList.remove("open");
|
|
@@ -7,6 +7,16 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
7
7
|
|
|
8
8
|
private editButton?: HTMLButtonElement;
|
|
9
9
|
|
|
10
|
+
toggleContextMenu() {
|
|
11
|
+
if (this.contextMenu?.classList.contains("open")) {
|
|
12
|
+
this.contextMenu?.classList.remove("open");
|
|
13
|
+
this.removeAttribute("open");
|
|
14
|
+
} else if (this.contextMenu && this.contextMenu.childElementCount > 0) {
|
|
15
|
+
this.contextMenu?.classList.add("open");
|
|
16
|
+
this.setAttribute("open", "true");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
render(shadow: ShadowRoot): void {
|
|
11
21
|
const style = document.createElement("style");
|
|
12
22
|
style.textContent = styleContent;
|
|
@@ -21,18 +31,23 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
21
31
|
shadow.appendChild(this.contextMenu);
|
|
22
32
|
|
|
23
33
|
this.editButton = document.createElement("button");
|
|
34
|
+
this.editButton.type = "button";
|
|
24
35
|
this.editButton.innerHTML = '<cc-icon name="edit"></cc-icon>';
|
|
25
36
|
this.editButton.onclick = () => {
|
|
26
37
|
this.dispatchEvent(new CustomEvent("edit", { detail: this }));
|
|
27
38
|
};
|
|
28
39
|
this.buttonRow.append(this.editButton);
|
|
29
40
|
|
|
30
|
-
this.onclick = () => {
|
|
41
|
+
this.onclick = (e) => {
|
|
42
|
+
e.preventDefault();
|
|
31
43
|
this.contextMenu?.classList.remove("open");
|
|
32
44
|
this.removeAttribute("open");
|
|
33
45
|
};
|
|
34
46
|
|
|
35
|
-
this.onblur = () => {
|
|
47
|
+
this.onblur = (e) => {
|
|
48
|
+
if ((e.relatedTarget as HTMLElement)?.tagName === "A") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
36
51
|
this.contextMenu?.classList.remove("open");
|
|
37
52
|
this.removeAttribute("open");
|
|
38
53
|
};
|
|
@@ -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
|
@@ -100,3 +100,10 @@ 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 isEditableArray = (el?: Element | null): boolean => {
|
|
105
|
+
return (
|
|
106
|
+
el?.tagName === "EDITABLE-ARRAY" ||
|
|
107
|
+
(el instanceof HTMLElement && el.dataset.editable === "array")
|
|
108
|
+
);
|
|
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;
|
|
@@ -49,20 +44,43 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
isValidDropzone(e: DragEvent): boolean {
|
|
52
|
-
|
|
53
|
-
if (!source || !e.dataTransfer) {
|
|
47
|
+
if (!e.dataTransfer) {
|
|
54
48
|
return false;
|
|
55
49
|
}
|
|
56
50
|
|
|
51
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
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
|
+
if (!e.dataTransfer || !this.element.dataset.prop) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
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
|
}
|
|
@@ -392,11 +423,7 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
392
423
|
return;
|
|
393
424
|
}
|
|
394
425
|
|
|
395
|
-
const source = this.parent?.contextBase?.
|
|
396
|
-
if (!source) {
|
|
397
|
-
throw new Error("Source not found");
|
|
398
|
-
}
|
|
399
|
-
|
|
426
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
400
427
|
const dragType = this.getDragType();
|
|
401
428
|
const sameArrayData = e.dataTransfer.getData(source);
|
|
402
429
|
const otherArrayData = dragType
|
|
@@ -428,25 +455,7 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
428
455
|
this.dispatchArrayMove(fromIndex, newIndex);
|
|
429
456
|
}
|
|
430
457
|
} 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
|
-
}
|
|
458
|
+
const { index, sourceId, value } = JSON.parse(otherArrayData);
|
|
450
459
|
|
|
451
460
|
const sourceElement = document.getElementById(sourceId);
|
|
452
461
|
if (sourceElement && hasEditableArrayItem(sourceElement)) {
|
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
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",
|
|
@@ -27,18 +29,21 @@ export default class EditableArray extends Editable {
|
|
|
27
29
|
value:
|
|
28
30
|
| CloudCannonJavaScriptV1APICollection
|
|
29
31
|
| CloudCannonJavaScriptV1APIDataset
|
|
32
|
+
| CloudCannonJavaScriptV1APIFile
|
|
30
33
|
| unknown[]
|
|
31
34
|
| null
|
|
32
35
|
| undefined = undefined;
|
|
33
36
|
|
|
34
37
|
private updatePromise: Promise<void> | undefined;
|
|
35
38
|
private needsReupdate = false;
|
|
39
|
+
private addButton?: EditableRegionButton;
|
|
36
40
|
|
|
37
41
|
async registerListener(listener: EditableListener): Promise<void> {
|
|
38
42
|
if (!this.value) {
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
const __base_context = { ...this.contextBase };
|
|
42
47
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
43
48
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
44
49
|
value = await this.value.items();
|
|
@@ -48,8 +53,13 @@ export default class EditableArray extends Editable {
|
|
|
48
53
|
value = items;
|
|
49
54
|
} else {
|
|
50
55
|
const data = await items.data.get();
|
|
56
|
+
__base_context.file = items;
|
|
51
57
|
value = Array.isArray(data) ? data : [];
|
|
52
58
|
}
|
|
59
|
+
} else if (CloudCannon.isAPIFile(this.value)) {
|
|
60
|
+
const data = await this.value.data.get();
|
|
61
|
+
__base_context.file = this.value;
|
|
62
|
+
value = Array.isArray(data) ? data : [];
|
|
53
63
|
} else if (Array.isArray(this.value)) {
|
|
54
64
|
value = this.value;
|
|
55
65
|
} else {
|
|
@@ -81,7 +91,7 @@ export default class EditableArray extends Editable {
|
|
|
81
91
|
|
|
82
92
|
if (listener.path) {
|
|
83
93
|
listener.editable.pushValue(value, listener, {
|
|
84
|
-
__base_context
|
|
94
|
+
__base_context,
|
|
85
95
|
});
|
|
86
96
|
}
|
|
87
97
|
}
|
|
@@ -94,7 +104,10 @@ export default class EditableArray extends Editable {
|
|
|
94
104
|
this.element.classList.add("errored");
|
|
95
105
|
const error = document.createElement("editable-region-error-card");
|
|
96
106
|
error.setAttribute("heading", "Failed to render array editable");
|
|
97
|
-
error.setAttribute(
|
|
107
|
+
error.setAttribute(
|
|
108
|
+
"message",
|
|
109
|
+
"Array editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
|
|
110
|
+
);
|
|
98
111
|
this.element.replaceChildren(error);
|
|
99
112
|
return false;
|
|
100
113
|
}
|
|
@@ -107,15 +120,27 @@ export default class EditableArray extends Editable {
|
|
|
107
120
|
!Array.isArray(value) &&
|
|
108
121
|
value !== null &&
|
|
109
122
|
!CloudCannon.isAPICollection(value) &&
|
|
110
|
-
!CloudCannon.isAPIDataset(value)
|
|
123
|
+
!CloudCannon.isAPIDataset(value) &&
|
|
124
|
+
!CloudCannon.isAPIFile(value)
|
|
111
125
|
) {
|
|
112
126
|
this.element.classList.add("errored");
|
|
113
127
|
const error = document.createElement("editable-region-error-card");
|
|
114
128
|
error.setAttribute("heading", "Failed to render array editable");
|
|
115
129
|
error.setAttribute(
|
|
116
130
|
"message",
|
|
117
|
-
`
|
|
131
|
+
`Array editable regions expect to receive a value of type "array" but instead received a value of type '${typeof value}'.`,
|
|
118
132
|
);
|
|
133
|
+
if (this.contextBase?.fullPath) {
|
|
134
|
+
error.setAttribute(
|
|
135
|
+
"hint",
|
|
136
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
error.setAttribute(
|
|
140
|
+
"hint",
|
|
141
|
+
`This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
119
144
|
this.element.replaceChildren(error);
|
|
120
145
|
return;
|
|
121
146
|
}
|
|
@@ -139,6 +164,7 @@ export default class EditableArray extends Editable {
|
|
|
139
164
|
|
|
140
165
|
private async _update(): Promise<void> {
|
|
141
166
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
167
|
+
const __base_context = { ...this.contextBase };
|
|
142
168
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
143
169
|
value = await this.value.items();
|
|
144
170
|
} else if (CloudCannon.isAPIDataset(this.value)) {
|
|
@@ -147,8 +173,13 @@ export default class EditableArray extends Editable {
|
|
|
147
173
|
value = items;
|
|
148
174
|
} else {
|
|
149
175
|
const data = await items.data.get();
|
|
176
|
+
__base_context.file = items;
|
|
150
177
|
value = Array.isArray(data) ? data : [];
|
|
151
178
|
}
|
|
179
|
+
} else if (CloudCannon.isAPIFile(this.value)) {
|
|
180
|
+
const data = await this.value.data.get();
|
|
181
|
+
__base_context.file = this.value;
|
|
182
|
+
value = Array.isArray(data) ? data : [];
|
|
152
183
|
} else if (Array.isArray(this.value)) {
|
|
153
184
|
value = this.value;
|
|
154
185
|
} else {
|
|
@@ -158,7 +189,7 @@ export default class EditableArray extends Editable {
|
|
|
158
189
|
const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
|
|
159
190
|
|
|
160
191
|
for (const child of this.element.querySelectorAll(
|
|
161
|
-
"editable-array-item,[data-editable='array-item']",
|
|
192
|
+
"editable-array-item,[data-editable='array-item'],array-placeholder",
|
|
162
193
|
)) {
|
|
163
194
|
let parent = child.parentElement;
|
|
164
195
|
while (parent instanceof HTMLElement && !isEditableElement(parent)) {
|
|
@@ -172,12 +203,33 @@ export default class EditableArray extends Editable {
|
|
|
172
203
|
children.push(child as any);
|
|
173
204
|
}
|
|
174
205
|
|
|
206
|
+
if (
|
|
207
|
+
children.length === 0 &&
|
|
208
|
+
!this.element.dataset.component &&
|
|
209
|
+
!this.element.dataset.componentKey
|
|
210
|
+
) {
|
|
211
|
+
const error = document.createElement("editable-region-error-card");
|
|
212
|
+
error.setAttribute("heading", "Failed to render array editable region");
|
|
213
|
+
error.setAttribute(
|
|
214
|
+
"message",
|
|
215
|
+
"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.",
|
|
216
|
+
);
|
|
217
|
+
this.element.replaceChildren(error);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
175
221
|
if (!this.element.dataset.idKey) {
|
|
176
222
|
while (children.length > value.length) {
|
|
177
223
|
children.pop()?.remove();
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
if (value.length === 0 && this.addButton) {
|
|
227
|
+
this.element.appendChild(this.addButton);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.addButton?.remove();
|
|
232
|
+
|
|
181
233
|
for (let i = 0; i < value.length; i++) {
|
|
182
234
|
let child = children[i];
|
|
183
235
|
if (!child) {
|
|
@@ -185,12 +237,11 @@ export default class EditableArray extends Editable {
|
|
|
185
237
|
child = document.createElement(
|
|
186
238
|
"editable-array-item",
|
|
187
239
|
) as EditableArrayItemComponent;
|
|
188
|
-
} else
|
|
240
|
+
} else {
|
|
241
|
+
// Empty arrays should be caught by the error case above so children[0] should always exist
|
|
189
242
|
child = children[0].cloneNode(true) as HTMLElement & {
|
|
190
243
|
editable?: EditableArrayItem;
|
|
191
244
|
};
|
|
192
|
-
} else {
|
|
193
|
-
child = document.createElement("array-placeholder");
|
|
194
245
|
}
|
|
195
246
|
this.element.appendChild(child);
|
|
196
247
|
}
|
|
@@ -203,7 +254,7 @@ export default class EditableArray extends Editable {
|
|
|
203
254
|
child.editable?.pushValue(
|
|
204
255
|
value,
|
|
205
256
|
{ path: `${i}`, editable: child.editable },
|
|
206
|
-
{ __base_context
|
|
257
|
+
{ __base_context },
|
|
207
258
|
);
|
|
208
259
|
}
|
|
209
260
|
return;
|
|
@@ -226,9 +277,12 @@ export default class EditableArray extends Editable {
|
|
|
226
277
|
}
|
|
227
278
|
|
|
228
279
|
if (data && typeof data === "object" && componentKey) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
280
|
+
const component = (data as any)[componentKey];
|
|
281
|
+
if (typeof component !== "undefined" && component !== null) {
|
|
282
|
+
componentKeys.push(String((data as any)[componentKey]));
|
|
283
|
+
} else {
|
|
284
|
+
componentKeys.push(null);
|
|
285
|
+
}
|
|
232
286
|
}
|
|
233
287
|
}
|
|
234
288
|
|
|
@@ -244,9 +298,15 @@ export default class EditableArray extends Editable {
|
|
|
244
298
|
child.editable?.pushValue(
|
|
245
299
|
value,
|
|
246
300
|
{ path: `${index}`, editable: child.editable },
|
|
247
|
-
{ __base_context
|
|
301
|
+
{ __base_context },
|
|
248
302
|
);
|
|
249
303
|
});
|
|
304
|
+
|
|
305
|
+
if (dataKeys.length === 0 && this.addButton) {
|
|
306
|
+
this.element.appendChild(this.addButton);
|
|
307
|
+
} else {
|
|
308
|
+
this.addButton?.remove();
|
|
309
|
+
}
|
|
250
310
|
return;
|
|
251
311
|
}
|
|
252
312
|
|
|
@@ -264,6 +324,8 @@ export default class EditableArray extends Editable {
|
|
|
264
324
|
}
|
|
265
325
|
const placeholder = placeholders[i];
|
|
266
326
|
const existingElement = children[i];
|
|
327
|
+
const componentKey = componentKeys[i] || this.element.dataset.component;
|
|
328
|
+
|
|
267
329
|
const matchingChildIndex = children.findIndex(
|
|
268
330
|
(child, i) => child.dataset.id === key && !moved[i],
|
|
269
331
|
);
|
|
@@ -273,17 +335,38 @@ export default class EditableArray extends Editable {
|
|
|
273
335
|
const clone = children.find((child) => child.dataset.id === key);
|
|
274
336
|
if (clone) {
|
|
275
337
|
matchingChild = clone.cloneNode(true) as any;
|
|
276
|
-
} else {
|
|
338
|
+
} else if (componentKey) {
|
|
277
339
|
matchingChild = document.createElement(
|
|
278
340
|
"editable-array-item",
|
|
279
341
|
) as EditableArrayItemComponent;
|
|
280
342
|
matchingChild.dataset.id = key;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
343
|
+
matchingChild.dataset.component = componentKey;
|
|
344
|
+
} else {
|
|
345
|
+
const error = document.createElement("editable-region-error-card");
|
|
346
|
+
error.setAttribute("heading", "Failed to render array item");
|
|
347
|
+
if (typeof this.element.dataset.componentKey === "string") {
|
|
348
|
+
error.setAttribute(
|
|
349
|
+
"message",
|
|
350
|
+
"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.",
|
|
351
|
+
);
|
|
352
|
+
error.setAttribute(
|
|
353
|
+
"hint",
|
|
354
|
+
`This may mean that the value for 'data-component-key' is incorrect or that your array data is incorrectly formatted.
|
|
355
|
+
The current value for 'data-component-key' is '${this.element.dataset.componentKey}' and the current value for 'data-id' is '${key}'.
|
|
356
|
+
`,
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
error.setAttribute(
|
|
360
|
+
"message",
|
|
361
|
+
"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.",
|
|
362
|
+
);
|
|
363
|
+
error.setAttribute(
|
|
364
|
+
"hint",
|
|
365
|
+
`The full value of "data-id" for this item is "${key}"`,
|
|
366
|
+
);
|
|
286
367
|
}
|
|
368
|
+
matchingChild = document.createElement("array-placeholder");
|
|
369
|
+
matchingChild.append(error);
|
|
287
370
|
}
|
|
288
371
|
} else {
|
|
289
372
|
moved[matchingChildIndex] = true;
|
|
@@ -305,7 +388,7 @@ export default class EditableArray extends Editable {
|
|
|
305
388
|
matchingChild.editable.pushValue(
|
|
306
389
|
value,
|
|
307
390
|
{ path: `${i}`, editable: matchingChild.editable },
|
|
308
|
-
{ __base_context
|
|
391
|
+
{ __base_context },
|
|
309
392
|
);
|
|
310
393
|
}
|
|
311
394
|
});
|
|
@@ -315,6 +398,12 @@ export default class EditableArray extends Editable {
|
|
|
315
398
|
child.remove();
|
|
316
399
|
}
|
|
317
400
|
});
|
|
401
|
+
|
|
402
|
+
if (dataKeys.length === 0 && this.addButton) {
|
|
403
|
+
this.element.appendChild(this.addButton);
|
|
404
|
+
} else {
|
|
405
|
+
this.addButton?.remove();
|
|
406
|
+
}
|
|
318
407
|
}
|
|
319
408
|
|
|
320
409
|
calculateArrayDirection(): ArrayDirection {
|
|
@@ -335,5 +424,21 @@ export default class EditableArray extends Editable {
|
|
|
335
424
|
|
|
336
425
|
mount(): void {
|
|
337
426
|
this.arrayDirection = this.calculateArrayDirection();
|
|
427
|
+
|
|
428
|
+
this.addButton = document.createElement("editable-region-button");
|
|
429
|
+
this.addButton.setAttribute("icon", "add");
|
|
430
|
+
this.addButton.setAttribute("text", "Add Item");
|
|
431
|
+
this.addButton.addEventListener("button-click", () => {
|
|
432
|
+
this.element.dispatchEvent(
|
|
433
|
+
new CustomEvent("cloudcannon-api", {
|
|
434
|
+
bubbles: true,
|
|
435
|
+
detail: {
|
|
436
|
+
source: this.element.dataset.prop,
|
|
437
|
+
action: "add-array-item",
|
|
438
|
+
newIndex: 0,
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
});
|
|
338
443
|
}
|
|
339
444
|
}
|
|
@@ -42,7 +42,7 @@ export default class EditableComponent extends Editable {
|
|
|
42
42
|
error.setAttribute("heading", "Failed to render component");
|
|
43
43
|
error.setAttribute(
|
|
44
44
|
"message",
|
|
45
|
-
"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.",
|
|
46
46
|
);
|
|
47
47
|
this.element.replaceChildren(error);
|
|
48
48
|
return false;
|
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
|
}
|
|
@@ -169,7 +185,9 @@ export default class EditableImage extends Editable {
|
|
|
169
185
|
!!this.element.dataset.propTitle || !!this.element.dataset.prop;
|
|
170
186
|
|
|
171
187
|
this.loadInputConfig().then(() => {
|
|
172
|
-
this.imageEl?.addEventListener("click", () => {
|
|
188
|
+
this.imageEl?.addEventListener("click", (e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
|
|
173
191
|
if (!this.value) {
|
|
174
192
|
throw new Error("Value is not defined");
|
|
175
193
|
}
|
|
@@ -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
|
}
|
|
@@ -66,6 +80,8 @@ export default class EditableText extends Editable {
|
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
mount(): void {
|
|
83
|
+
this.element.addEventListener("click", (e) => e.preventDefault());
|
|
84
|
+
|
|
69
85
|
this.element.addEventListener("blur", () => {
|
|
70
86
|
this.focused = false;
|
|
71
87
|
this.element.dispatchEvent(
|
package/nodes/editable.ts
CHANGED
|
@@ -88,14 +88,16 @@ export default class Editable {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
async lookupPathAndContext(
|
|
91
|
-
path
|
|
92
|
-
obj
|
|
91
|
+
path?: string,
|
|
92
|
+
obj?: unknown,
|
|
93
93
|
contexts: { [key: string]: EditableContext } = {},
|
|
94
94
|
): Promise<{ value: any; context: EditableContext }> {
|
|
95
95
|
if (!path) {
|
|
96
96
|
return {
|
|
97
97
|
value: obj,
|
|
98
|
-
context: {
|
|
98
|
+
context: {
|
|
99
|
+
...contexts.__base_context,
|
|
100
|
+
},
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -166,9 +168,9 @@ export default class Editable {
|
|
|
166
168
|
): Promise<unknown> {
|
|
167
169
|
const { key, path } = listener ?? {};
|
|
168
170
|
|
|
169
|
-
const { value: resolvedValue, context: newContext } =
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
const { value: resolvedValue, context: newContext } =
|
|
172
|
+
await this.lookupPathAndContext(path, value, contexts);
|
|
173
|
+
|
|
172
174
|
if (!key) {
|
|
173
175
|
this.propsBase = resolvedValue;
|
|
174
176
|
this.contextBase = newContext;
|
|
@@ -286,6 +288,10 @@ export default class Editable {
|
|
|
286
288
|
}
|
|
287
289
|
|
|
288
290
|
connect(): void {
|
|
291
|
+
if (!this.validateConfiguration()) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
289
295
|
if (this.disconnecting) {
|
|
290
296
|
this.needsReconnect = true;
|
|
291
297
|
return;
|
|
@@ -295,13 +301,11 @@ export default class Editable {
|
|
|
295
301
|
}
|
|
296
302
|
this.connectPromise = loadingPromise.then(() => {
|
|
297
303
|
this.setupListeners();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.update();
|
|
304
|
-
}
|
|
304
|
+
this.connected = true;
|
|
305
|
+
if (!this.mounted && this.shouldMount()) {
|
|
306
|
+
this.mounted = true;
|
|
307
|
+
this.mount();
|
|
308
|
+
this.update();
|
|
305
309
|
}
|
|
306
310
|
});
|
|
307
311
|
}
|
|
@@ -315,9 +319,12 @@ export default class Editable {
|
|
|
315
319
|
let parentEditable: Editable | undefined;
|
|
316
320
|
let parent = this.element.parentElement;
|
|
317
321
|
while (parent) {
|
|
318
|
-
if (hasEditable(parent)) {
|
|
322
|
+
if (hasEditable(parent) && !parentEditable) {
|
|
319
323
|
parentEditable = parent.editable;
|
|
320
|
-
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (parent.tagName === "A") {
|
|
327
|
+
parent.draggable = false;
|
|
321
328
|
}
|
|
322
329
|
parent = parent.parentElement;
|
|
323
330
|
}
|
|
@@ -329,7 +336,7 @@ export default class Editable {
|
|
|
329
336
|
return;
|
|
330
337
|
}
|
|
331
338
|
|
|
332
|
-
const { collection, file, dataset, source, absolute } =
|
|
339
|
+
const { collection, file, dataset, source, absolute, currentFile } =
|
|
333
340
|
this.parseSource(propPath);
|
|
334
341
|
|
|
335
342
|
const listener = {
|
|
@@ -347,8 +354,15 @@ export default class Editable {
|
|
|
347
354
|
// Any single data path should only be able to refer to a single absolute API object
|
|
348
355
|
const obj = collection || dataset || file;
|
|
349
356
|
if (obj) {
|
|
357
|
+
const fullPath = collection
|
|
358
|
+
? `@collections[${collection.collectionKey}]`
|
|
359
|
+
: dataset
|
|
360
|
+
? `@data[${dataset.datasetKey}]`
|
|
361
|
+
: currentFile
|
|
362
|
+
? undefined
|
|
363
|
+
: `@file[${file?.path}]`;
|
|
350
364
|
const handleAPIChange = () => {
|
|
351
|
-
this.pushValue(obj, listener);
|
|
365
|
+
this.pushValue(obj, listener, { __base_context: { fullPath } });
|
|
352
366
|
};
|
|
353
367
|
this.APIListeners.push({
|
|
354
368
|
obj,
|
|
@@ -423,22 +437,6 @@ export default class Editable {
|
|
|
423
437
|
}
|
|
424
438
|
|
|
425
439
|
filePromise.then((file) => {
|
|
426
|
-
if (typeof source !== "string") {
|
|
427
|
-
if (options.action === "get-input-config") {
|
|
428
|
-
options.callback({
|
|
429
|
-
options: {
|
|
430
|
-
disable_reorder: true,
|
|
431
|
-
disable_remove: true,
|
|
432
|
-
disable_add: true,
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
throw new Error(
|
|
438
|
-
`Failed to resolve source for API call: ${options.source}`,
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
440
|
switch (options.action) {
|
|
443
441
|
case "edit":
|
|
444
442
|
file?.data.edit({ slug: source, position: options.position });
|
|
@@ -472,7 +470,17 @@ export default class Editable {
|
|
|
472
470
|
});
|
|
473
471
|
break;
|
|
474
472
|
case "get-input-config":
|
|
475
|
-
|
|
473
|
+
if (!file) {
|
|
474
|
+
options.callback({
|
|
475
|
+
options: {
|
|
476
|
+
disable_reorder: true,
|
|
477
|
+
disable_remove: true,
|
|
478
|
+
disable_add: true,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
} else {
|
|
482
|
+
file?.getInputConfig({ slug: source }).then(options.callback);
|
|
483
|
+
}
|
|
476
484
|
break;
|
|
477
485
|
}
|
|
478
486
|
});
|
|
@@ -519,6 +527,7 @@ export default class Editable {
|
|
|
519
527
|
let file: CloudCannonJavaScriptV1APIFile | undefined;
|
|
520
528
|
let dataset: CloudCannonJavaScriptV1APIDataset | undefined;
|
|
521
529
|
let absolute = false;
|
|
530
|
+
let currentFile = false;
|
|
522
531
|
|
|
523
532
|
const collectionMatch = source?.match(
|
|
524
533
|
/^@collections\[(?<key>[^\]]+)\](\.(?<rest>.+))?$/,
|
|
@@ -548,6 +557,7 @@ export default class Editable {
|
|
|
548
557
|
absolute = true;
|
|
549
558
|
} else {
|
|
550
559
|
file = CloudCannon.currentFile();
|
|
560
|
+
currentFile = true;
|
|
551
561
|
}
|
|
552
562
|
}
|
|
553
563
|
}
|
|
@@ -564,10 +574,11 @@ export default class Editable {
|
|
|
564
574
|
return {
|
|
565
575
|
collection,
|
|
566
576
|
file,
|
|
567
|
-
source,
|
|
577
|
+
source: source ?? "",
|
|
568
578
|
absolute,
|
|
569
579
|
snippets,
|
|
570
580
|
dataset,
|
|
581
|
+
currentFile,
|
|
571
582
|
};
|
|
572
583
|
}
|
|
573
584
|
}
|
package/package.json
CHANGED
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
|
+
}
|