@cloudcannon/editable-regions 0.0.9 → 0.0.10
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/editable-snippet-component.ts +1 -1
- package/components/index.ts +0 -13
- package/helpers/checks.ts +9 -36
- package/helpers/cloudcannon.mjs +10 -0
- package/helpers/hydrate-editable-regions.ts +7 -1
- package/integrations/astro/astro-integration.mjs +10 -54
- package/integrations/astro/modules/content.js +7 -1
- package/integrations/astro/modules/secrets.js +4 -0
- package/nodes/editable-array-item.ts +9 -9
- package/nodes/editable-array.ts +74 -28
- package/nodes/editable-component.ts +78 -15
- package/nodes/editable-source.ts +8 -2
- package/nodes/editable.ts +197 -33
- package/package.json +2 -2
- package/helpers/loading.ts +0 -7
- package/integrations/astro/modules/actions.js +0 -74
- package/integrations/astro/modules/client-router.astro +0 -5
- package/integrations/astro/modules/i18n.js +0 -76
- package/integrations/astro/modules/middleware.js +0 -27
- package/integrations/astro/modules/transitions.js +0 -63
|
@@ -9,7 +9,7 @@ export default class EditableSnippetComponent extends HTMLElement {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
set snippetData(value: unknown) {
|
|
12
|
-
this.editable.pushValue(value);
|
|
12
|
+
this.editable.pushValue(value, {}, { path: "", editable: this.editable });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
connectedCallback(): void {
|
package/components/index.ts
CHANGED
|
@@ -6,12 +6,10 @@ export { default as EditableSnippetComponent } from "./editable-snippet-componen
|
|
|
6
6
|
export { default as EditableSourceComponent } from "./editable-source-component.js";
|
|
7
7
|
export { default as EditableTextComponent } from "./editable-text-component.js";
|
|
8
8
|
|
|
9
|
-
import { apiLoadedPromise } from "../helpers/cloudcannon.mjs";
|
|
10
9
|
import {
|
|
11
10
|
dehydrateDataEditableRegions,
|
|
12
11
|
hydrateDataEditableRegions,
|
|
13
12
|
} from "../helpers/hydrate-editable-regions";
|
|
14
|
-
import { completeLoading } from "../helpers/loading.js";
|
|
15
13
|
|
|
16
14
|
const observer = new MutationObserver((mutations) => {
|
|
17
15
|
mutations.forEach((mutation) => {
|
|
@@ -32,14 +30,3 @@ const observer = new MutationObserver((mutations) => {
|
|
|
32
30
|
hydrateDataEditableRegions(document.body);
|
|
33
31
|
|
|
34
32
|
observer.observe(document, { childList: true, subtree: true });
|
|
35
|
-
|
|
36
|
-
Promise.all([
|
|
37
|
-
apiLoadedPromise,
|
|
38
|
-
customElements.whenDefined("editable-array-item"),
|
|
39
|
-
customElements.whenDefined("editable-array"),
|
|
40
|
-
customElements.whenDefined("editable-text"),
|
|
41
|
-
customElements.whenDefined("editable-component"),
|
|
42
|
-
customElements.whenDefined("editable-image"),
|
|
43
|
-
customElements.whenDefined("editable-source"),
|
|
44
|
-
customElements.whenDefined("editable-snippet"),
|
|
45
|
-
]).then(() => completeLoading());
|
package/helpers/checks.ts
CHANGED
|
@@ -2,32 +2,11 @@ import Editable from "../nodes/editable.js";
|
|
|
2
2
|
import EditableArrayItem from "../nodes/editable-array-item.js";
|
|
3
3
|
import EditableText from "../nodes/editable-text.js";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
"EDITABLE-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"EDITABLE-IMAGE",
|
|
11
|
-
"EDITABLE-SOURCE",
|
|
12
|
-
"EDITABLE-SNIPPET",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const EDITABLE_REGION_TYPES = [
|
|
16
|
-
"text",
|
|
17
|
-
"component",
|
|
18
|
-
"array",
|
|
19
|
-
"array-item",
|
|
20
|
-
"image",
|
|
21
|
-
"source",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const CORRESPONDING_NAME: Record<string, string> = {
|
|
25
|
-
"EDITABLE-TEXT": "text",
|
|
26
|
-
"EDITABLE-COMPONENT": "component",
|
|
27
|
-
"EDITABLE-ARRAY-ITEM": "array-item",
|
|
28
|
-
"EDITABLE-ARRAY": "array",
|
|
29
|
-
"EDITABLE-IMAGE": "image",
|
|
30
|
-
"EDITABLE-SOURCE": "source",
|
|
5
|
+
const getEditableType = (el: HTMLElement): string | undefined => {
|
|
6
|
+
if (el.tagName.startsWith("EDITABLE-")) {
|
|
7
|
+
return el.tagName.slice(9).toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
return el.dataset.editable;
|
|
31
10
|
};
|
|
32
11
|
|
|
33
12
|
export const hasEditable = <T extends object>(
|
|
@@ -53,7 +32,7 @@ export const isEditableWebcomponent = (el: unknown): boolean => {
|
|
|
53
32
|
return false;
|
|
54
33
|
}
|
|
55
34
|
|
|
56
|
-
return
|
|
35
|
+
return el.tagName.startsWith("EDITABLE-");
|
|
57
36
|
};
|
|
58
37
|
|
|
59
38
|
export const isEditableElement = (el: unknown): boolean => {
|
|
@@ -62,19 +41,13 @@ export const isEditableElement = (el: unknown): boolean => {
|
|
|
62
41
|
}
|
|
63
42
|
|
|
64
43
|
return (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
EDITABLE_REGION_TYPES.includes(el.dataset.editable))
|
|
44
|
+
el.tagName.startsWith("EDITABLE-") ||
|
|
45
|
+
typeof el.dataset.editable === "string"
|
|
68
46
|
);
|
|
69
47
|
};
|
|
70
48
|
|
|
71
49
|
export const areEqualEditables = (a: HTMLElement, b: HTMLElement) => {
|
|
72
|
-
if (
|
|
73
|
-
a.tagName !== b.tagName &&
|
|
74
|
-
a.dataset.editable !== b.dataset.editable &&
|
|
75
|
-
CORRESPONDING_NAME[a.tagName] !== b.dataset.editable &&
|
|
76
|
-
CORRESPONDING_NAME[b.tagName] !== a.dataset.editable
|
|
77
|
-
) {
|
|
50
|
+
if (getEditableType(a) !== getEditableType(b)) {
|
|
78
51
|
return false;
|
|
79
52
|
}
|
|
80
53
|
|
package/helpers/cloudcannon.mjs
CHANGED
|
@@ -107,4 +107,14 @@ export const realizeAPIValue = async (value) => {
|
|
|
107
107
|
return value;
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
export const addCustomEditableRegion = (key, region) => {
|
|
111
|
+
extendedWindow.editableRegionMap ??= {};
|
|
112
|
+
extendedWindow.editableRegionMap[key] = region;
|
|
113
|
+
extendedWindow.hydrateDataEditableRegions(document.body);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const getCustomEditableRegions = () => {
|
|
117
|
+
return extendedWindow.editableRegionMap ?? {};
|
|
118
|
+
};
|
|
119
|
+
|
|
110
120
|
export { _cloudcannon as CloudCannon };
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "../nodes";
|
|
10
10
|
import { hasEditable, isEditableWebcomponent } from "./checks";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const baseEditableMap: Record<string, typeof Editable | undefined> = {
|
|
13
13
|
array: EditableArray,
|
|
14
14
|
"array-item": EditableArrayItem,
|
|
15
15
|
component: EditableComponent,
|
|
@@ -31,6 +31,10 @@ export const dehydrateDataEditableRegions = (root: Element) => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export const hydrateDataEditableRegions = (root: Element) => {
|
|
34
|
+
const editableMap: Record<string, typeof Editable | undefined> = {
|
|
35
|
+
...baseEditableMap,
|
|
36
|
+
...((window as any).editableRegionMap ?? {}),
|
|
37
|
+
};
|
|
34
38
|
if (
|
|
35
39
|
root instanceof HTMLElement &&
|
|
36
40
|
root.dataset.editable &&
|
|
@@ -89,3 +93,5 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
89
93
|
}
|
|
90
94
|
});
|
|
91
95
|
};
|
|
96
|
+
|
|
97
|
+
(window as any).hydrateDataEditableRegions = hydrateDataEditableRegions;
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"assets",
|
|
7
|
-
"content",
|
|
8
|
-
"i18n",
|
|
9
|
-
"middleware",
|
|
10
|
-
"transitions",
|
|
11
|
-
];
|
|
4
|
+
/** @type{string[]} */
|
|
5
|
+
const SUPPORTED_VIRTUAL_MODULES = ["assets", "content"];
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
8
|
* @return {import("astro").AstroIntegration}
|
|
15
9
|
*/
|
|
16
10
|
export default () => {
|
|
17
|
-
/** @type {import("astro").AstroConfig} */
|
|
18
|
-
let astroConfig;
|
|
19
|
-
|
|
20
11
|
return {
|
|
21
12
|
name: "editable-regions",
|
|
22
13
|
hooks: {
|
|
23
|
-
"astro:config:setup": ({
|
|
14
|
+
"astro:config:setup": ({ updateConfig }) => {
|
|
24
15
|
updateConfig({
|
|
25
16
|
vite: {
|
|
26
17
|
define: {
|
|
@@ -28,7 +19,6 @@ export default () => {
|
|
|
28
19
|
},
|
|
29
20
|
},
|
|
30
21
|
});
|
|
31
|
-
astroConfig = config;
|
|
32
22
|
},
|
|
33
23
|
"astro:build:setup": async ({ target, vite }) => {
|
|
34
24
|
if (target === "client") {
|
|
@@ -68,14 +58,6 @@ export default () => {
|
|
|
68
58
|
.replace("/client", "")
|
|
69
59
|
.replace("/server", "");
|
|
70
60
|
|
|
71
|
-
if (type === "env") {
|
|
72
|
-
return "\0editable-region:env";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
61
|
let dir = "";
|
|
80
62
|
if (typeof __dirname !== "undefined") {
|
|
81
63
|
dir = __dirname;
|
|
@@ -83,41 +65,15 @@ export default () => {
|
|
|
83
65
|
dir = dirname(fileURLToPath(import.meta.url));
|
|
84
66
|
}
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
if (type === "env" && id.endsWith("/server")) {
|
|
69
|
+
return join(dir, "modules", "secrets.js");
|
|
70
|
+
}
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Object.entries(astroConfig?.env?.schema ?? {}).forEach(
|
|
94
|
-
([key, schema]) => {
|
|
95
|
-
if (
|
|
96
|
-
schema.context !== "client" ||
|
|
97
|
-
schema.access !== "public"
|
|
98
|
-
) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
72
|
+
if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
101
75
|
|
|
102
|
-
|
|
103
|
-
switch (schema.type) {
|
|
104
|
-
case "boolean":
|
|
105
|
-
contents += `export const ${key} = ${!!process.env[key]};\n`;
|
|
106
|
-
break;
|
|
107
|
-
case "number":
|
|
108
|
-
contents += `export const ${key} = ${Number(process.env[key])};\n`;
|
|
109
|
-
break;
|
|
110
|
-
default:
|
|
111
|
-
contents += `export const ${key} = ${JSON.stringify(process.env[key] ?? "")};\n`;
|
|
112
|
-
}
|
|
113
|
-
} catch (_e) {
|
|
114
|
-
//Error intentionally ignored
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
);
|
|
118
|
-
contents +=
|
|
119
|
-
'export const getSecret = () => console.log("[CloudCannon] getSecret is not supported in an editable component. Please use an editing fallback instead.");';
|
|
120
|
-
return contents;
|
|
76
|
+
return join(dir, "modules", `${type}.js`);
|
|
121
77
|
}
|
|
122
78
|
},
|
|
123
79
|
});
|
|
@@ -26,12 +26,18 @@ export const getCollection = async (collectionKey, filter) => {
|
|
|
26
26
|
slug = data.slug;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const body = await file.get();
|
|
29
30
|
return {
|
|
30
31
|
collection: collectionKey,
|
|
31
32
|
id: id,
|
|
32
33
|
data: data,
|
|
33
34
|
slug: slug,
|
|
34
|
-
body:
|
|
35
|
+
body: body,
|
|
36
|
+
render: () => ({
|
|
37
|
+
Content: () => body ?? "Content is not available when live editing",
|
|
38
|
+
headings: [],
|
|
39
|
+
remarkPluginFrontmatter: {},
|
|
40
|
+
}),
|
|
35
41
|
};
|
|
36
42
|
});
|
|
37
43
|
|
|
@@ -249,8 +249,8 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
async update(): Promise<void> {
|
|
253
|
-
await super.update();
|
|
252
|
+
async update(partialSubtree?: ChildNode | null): Promise<void> {
|
|
253
|
+
await super.update(partialSubtree);
|
|
254
254
|
this.updateControls();
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -485,12 +485,12 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
488
|
+
getSpecialProps(
|
|
489
|
+
incomingSpecialProps: Record<string, unknown> = {},
|
|
490
|
+
): Record<string, unknown> {
|
|
491
|
+
return {
|
|
492
|
+
...super.getSpecialProps(incomingSpecialProps),
|
|
493
|
+
"@index": Number(this.element.dataset.prop),
|
|
494
|
+
};
|
|
495
495
|
}
|
|
496
496
|
}
|
package/nodes/editable-array.ts
CHANGED
|
@@ -37,13 +37,16 @@ export default class EditableArray extends Editable {
|
|
|
37
37
|
private updatePromise: Promise<void> | undefined;
|
|
38
38
|
private needsReupdate = false;
|
|
39
39
|
private addButton?: EditableRegionButton;
|
|
40
|
+
private pendingPartialSubtree?: ChildNode | null;
|
|
40
41
|
|
|
41
42
|
async registerListener(listener: EditableListener): Promise<void> {
|
|
42
43
|
if (!this.value) {
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const __base_context = {
|
|
47
|
+
const __base_context = {
|
|
48
|
+
...this.contextBase,
|
|
49
|
+
};
|
|
47
50
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
48
51
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
49
52
|
value = await this.value.items();
|
|
@@ -90,7 +93,7 @@ export default class EditableArray extends Editable {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
if (listener.path) {
|
|
93
|
-
listener.editable.pushValue(value, listener, {
|
|
96
|
+
listener.editable.pushValue(value, this.specialProps, listener, {
|
|
94
97
|
__base_context,
|
|
95
98
|
});
|
|
96
99
|
}
|
|
@@ -147,24 +150,29 @@ export default class EditableArray extends Editable {
|
|
|
147
150
|
return value;
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
update(): Promise<void> {
|
|
153
|
+
update(partialSubtree?: ChildNode | null): Promise<void> {
|
|
151
154
|
if (this.updatePromise) {
|
|
152
155
|
this.needsReupdate = true;
|
|
156
|
+
this.pendingPartialSubtree = partialSubtree;
|
|
153
157
|
return this.updatePromise;
|
|
154
158
|
}
|
|
155
|
-
this.updatePromise = this._update().then(() => {
|
|
159
|
+
this.updatePromise = this._update(partialSubtree).then(() => {
|
|
156
160
|
this.updatePromise = undefined;
|
|
157
161
|
if (this.needsReupdate) {
|
|
158
162
|
this.needsReupdate = false;
|
|
159
|
-
|
|
163
|
+
const savedPartialSubtree = this.pendingPartialSubtree;
|
|
164
|
+
this.pendingPartialSubtree = undefined;
|
|
165
|
+
return this.update(savedPartialSubtree);
|
|
160
166
|
}
|
|
161
167
|
});
|
|
162
168
|
return this.updatePromise;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
private async _update(): Promise<void> {
|
|
171
|
+
private async _update(partialSubtree?: ChildNode | null): Promise<void> {
|
|
166
172
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
167
|
-
const __base_context = {
|
|
173
|
+
const __base_context = {
|
|
174
|
+
...this.contextBase,
|
|
175
|
+
};
|
|
168
176
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
169
177
|
value = await this.value.items();
|
|
170
178
|
} else if (CloudCannon.isAPIDataset(this.value)) {
|
|
@@ -186,6 +194,24 @@ export default class EditableArray extends Editable {
|
|
|
186
194
|
value = [];
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
const partialChildren: (HTMLElement & { editable?: EditableArrayItem })[] =
|
|
198
|
+
[];
|
|
199
|
+
if (partialSubtree instanceof HTMLElement) {
|
|
200
|
+
for (const child of partialSubtree.querySelectorAll(
|
|
201
|
+
"editable-array-item,[data-editable='array-item']",
|
|
202
|
+
)) {
|
|
203
|
+
let parent = child.parentElement;
|
|
204
|
+
while (parent instanceof HTMLElement && !isEditableElement(parent)) {
|
|
205
|
+
parent = parent.parentElement;
|
|
206
|
+
}
|
|
207
|
+
if (parent !== partialSubtree) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
partialChildren.push(child as any);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
189
215
|
const templates: {
|
|
190
216
|
keyed: Record<
|
|
191
217
|
string,
|
|
@@ -241,23 +267,6 @@ export default class EditableArray extends Editable {
|
|
|
241
267
|
children.push(child as any);
|
|
242
268
|
}
|
|
243
269
|
|
|
244
|
-
if (
|
|
245
|
-
children.length === 0 &&
|
|
246
|
-
!this.element.dataset.component &&
|
|
247
|
-
!this.element.dataset.componentKey &&
|
|
248
|
-
!templates.unkeyed &&
|
|
249
|
-
Object.keys(templates.keyed).length === 0
|
|
250
|
-
) {
|
|
251
|
-
const error = document.createElement("editable-region-error-card");
|
|
252
|
-
error.setAttribute("heading", "Failed to render array editable region");
|
|
253
|
-
error.setAttribute(
|
|
254
|
-
"message",
|
|
255
|
-
"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.",
|
|
256
|
-
);
|
|
257
|
-
this.element.replaceChildren(error);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
270
|
const key = this.element.dataset.idKey ?? this.element.dataset.componentKey;
|
|
262
271
|
|
|
263
272
|
if (!key) {
|
|
@@ -274,8 +283,14 @@ export default class EditableArray extends Editable {
|
|
|
274
283
|
|
|
275
284
|
for (let i = 0; i < value.length; i++) {
|
|
276
285
|
let child = children[i];
|
|
286
|
+
const partialChild = partialChildren[i];
|
|
287
|
+
|
|
277
288
|
if (!child) {
|
|
278
|
-
if (
|
|
289
|
+
if (partialChild) {
|
|
290
|
+
child = partialChild.cloneNode(true) as HTMLElement & {
|
|
291
|
+
editable?: EditableArrayItem;
|
|
292
|
+
};
|
|
293
|
+
} else if (templates.unkeyed) {
|
|
279
294
|
child = templates.unkeyed.cloneNode(true) as HTMLElement & {
|
|
280
295
|
editable?: EditableArrayItem;
|
|
281
296
|
};
|
|
@@ -284,11 +299,22 @@ export default class EditableArray extends Editable {
|
|
|
284
299
|
child = document.createElement(
|
|
285
300
|
"editable-array-item",
|
|
286
301
|
) as EditableArrayItemComponent;
|
|
287
|
-
} else {
|
|
288
|
-
// Empty arrays should be caught by the error case above so children[0] should always exist
|
|
302
|
+
} else if (children[0]) {
|
|
289
303
|
child = children[0].cloneNode(true) as HTMLElement & {
|
|
290
304
|
editable?: EditableArrayItem;
|
|
291
305
|
};
|
|
306
|
+
} else {
|
|
307
|
+
const error = document.createElement("editable-region-error-card");
|
|
308
|
+
error.setAttribute(
|
|
309
|
+
"heading",
|
|
310
|
+
"Failed to render array editable region",
|
|
311
|
+
);
|
|
312
|
+
error.setAttribute(
|
|
313
|
+
"message",
|
|
314
|
+
"Array editable region contains an array item that cannot be rendered. Either a 'data-component' attribute or a 'data-component-key' attribute is required to render array items that weren't previously on the page. Please save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
|
|
315
|
+
);
|
|
316
|
+
this.element.replaceChildren(error);
|
|
317
|
+
return;
|
|
292
318
|
}
|
|
293
319
|
this.element.appendChild(child);
|
|
294
320
|
}
|
|
@@ -300,8 +326,10 @@ export default class EditableArray extends Editable {
|
|
|
300
326
|
child.dataset.length = `${children.length}`;
|
|
301
327
|
child.editable?.pushValue(
|
|
302
328
|
value,
|
|
329
|
+
this.specialProps,
|
|
303
330
|
{ path: `${i}`, editable: child.editable },
|
|
304
331
|
{ __base_context },
|
|
332
|
+
partialChild,
|
|
305
333
|
);
|
|
306
334
|
}
|
|
307
335
|
return;
|
|
@@ -351,8 +379,10 @@ export default class EditableArray extends Editable {
|
|
|
351
379
|
|
|
352
380
|
child.editable?.pushValue(
|
|
353
381
|
value,
|
|
382
|
+
this.specialProps,
|
|
354
383
|
{ path: `${index}`, editable: child.editable },
|
|
355
384
|
{ __base_context },
|
|
385
|
+
partialChildren[index],
|
|
356
386
|
);
|
|
357
387
|
});
|
|
358
388
|
|
|
@@ -380,6 +410,7 @@ export default class EditableArray extends Editable {
|
|
|
380
410
|
const existingElement = children[i];
|
|
381
411
|
const templateElement = templates.keyed[key] ?? templates.unkeyed;
|
|
382
412
|
const componentKey = componentKeys[i] || this.element.dataset.component;
|
|
413
|
+
const partialChild = partialChildren[i];
|
|
383
414
|
|
|
384
415
|
const matchingChildIndex = children.findIndex(
|
|
385
416
|
(child, i) => child.dataset.id === key && !moved[i],
|
|
@@ -388,7 +419,9 @@ export default class EditableArray extends Editable {
|
|
|
388
419
|
let matchingChild = children[matchingChildIndex];
|
|
389
420
|
if (!matchingChild) {
|
|
390
421
|
const clone = children.find((child) => child.dataset.id === key);
|
|
391
|
-
if (
|
|
422
|
+
if (partialChild) {
|
|
423
|
+
matchingChild = partialChild.cloneNode(true) as any;
|
|
424
|
+
} else if (templateElement) {
|
|
392
425
|
matchingChild = templateElement.cloneNode(true) as any;
|
|
393
426
|
matchingChild.dataset.editable = "array-item";
|
|
394
427
|
} else if (componentKey) {
|
|
@@ -448,8 +481,10 @@ export default class EditableArray extends Editable {
|
|
|
448
481
|
matchingChild.editable.parent = this;
|
|
449
482
|
matchingChild.editable.pushValue(
|
|
450
483
|
value,
|
|
484
|
+
this.specialProps,
|
|
451
485
|
{ path: `${i}`, editable: matchingChild.editable },
|
|
452
486
|
{ __base_context },
|
|
487
|
+
partialChild,
|
|
453
488
|
);
|
|
454
489
|
}
|
|
455
490
|
});
|
|
@@ -502,4 +537,15 @@ export default class EditableArray extends Editable {
|
|
|
502
537
|
);
|
|
503
538
|
});
|
|
504
539
|
}
|
|
540
|
+
|
|
541
|
+
getSpecialProps(
|
|
542
|
+
incomingSpecialProps: Record<string, unknown> = {},
|
|
543
|
+
): Record<string, unknown> {
|
|
544
|
+
return {
|
|
545
|
+
...super.getSpecialProps(incomingSpecialProps),
|
|
546
|
+
"@length": Array.isArray(this.propsBase)
|
|
547
|
+
? this.propsBase.length
|
|
548
|
+
: undefined,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
505
551
|
}
|
|
@@ -19,6 +19,7 @@ export default class EditableComponent extends Editable {
|
|
|
19
19
|
|
|
20
20
|
private updatePromise: Promise<void> | undefined;
|
|
21
21
|
private needsReupdate = false;
|
|
22
|
+
private pendingPartialSubtree?: ChildNode | null;
|
|
22
23
|
|
|
23
24
|
getComponents() {
|
|
24
25
|
return getEditableComponentRenderers();
|
|
@@ -51,31 +52,81 @@ export default class EditableComponent extends Editable {
|
|
|
51
52
|
return true;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
update(): Promise<void> {
|
|
55
|
+
update(partialSubtree?: ChildNode | null): Promise<void> {
|
|
55
56
|
if (this.updatePromise) {
|
|
56
57
|
this.needsReupdate = true;
|
|
58
|
+
this.pendingPartialSubtree = partialSubtree;
|
|
57
59
|
return this.updatePromise;
|
|
58
60
|
}
|
|
59
|
-
this.updatePromise = this._update().then(() => {
|
|
61
|
+
this.updatePromise = this._update(partialSubtree).then(() => {
|
|
60
62
|
this.updatePromise = undefined;
|
|
61
63
|
if (this.needsReupdate) {
|
|
62
64
|
this.needsReupdate = false;
|
|
63
|
-
|
|
65
|
+
const savedPartialSubtree = this.pendingPartialSubtree;
|
|
66
|
+
this.pendingPartialSubtree = undefined;
|
|
67
|
+
return this.update(savedPartialSubtree);
|
|
64
68
|
}
|
|
65
69
|
});
|
|
66
70
|
return this.updatePromise;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
santiseComponentOutput(el: HTMLElement): HTMLElement {
|
|
74
|
+
el.querySelectorAll("noscript").forEach((el) => el.remove());
|
|
75
|
+
return el;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async _update(partialSubtree?: ChildNode | null): Promise<void> {
|
|
79
|
+
if (partialSubtree) {
|
|
80
|
+
if (this.controlsElement) {
|
|
81
|
+
this.controlsElement.remove();
|
|
82
|
+
}
|
|
83
|
+
this.updateTree(this.element, partialSubtree);
|
|
84
|
+
if (this.controlsElement) {
|
|
85
|
+
this.element.appendChild(this.controlsElement);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
this.element.classList.remove("errored");
|
|
71
91
|
|
|
72
92
|
const key = this.element.dataset.component;
|
|
73
93
|
if (!key) {
|
|
74
94
|
return super.update();
|
|
75
95
|
}
|
|
76
|
-
|
|
96
|
+
|
|
97
|
+
let component = this.getComponents()?.[key];
|
|
98
|
+
for (let i = 0; !component && i < 20; i++) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
100
|
+
component = this.getComponents()?.[key];
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
if (!component) {
|
|
78
|
-
|
|
104
|
+
this.element.classList.add("errored");
|
|
105
|
+
const error = document.createElement("editable-region-error-card");
|
|
106
|
+
error.setAttribute("heading", "Failed to render component");
|
|
107
|
+
error.setAttribute(
|
|
108
|
+
"message",
|
|
109
|
+
`Failed to find a registered component with the key "${key}". This may mean that the provided "data-component" attribute is incorrect or that the component hasn't been registered.`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const caseMatch = Object.keys(this.getComponents()).find(
|
|
113
|
+
(k) => k.toLowerCase() === key.toLowerCase(),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (Object.keys(this.getComponents()).length === 0) {
|
|
117
|
+
error.setAttribute(
|
|
118
|
+
"hint",
|
|
119
|
+
"There are no registered components currently available. Please check that you've included your registration script and that it's running correctly.",
|
|
120
|
+
);
|
|
121
|
+
} else if (caseMatch) {
|
|
122
|
+
error.setAttribute(
|
|
123
|
+
"hint",
|
|
124
|
+
`The component key "${key}" is not case-sensitive. Did you mean "${caseMatch}"?`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.element.replaceChildren(error);
|
|
129
|
+
return;
|
|
79
130
|
}
|
|
80
131
|
|
|
81
132
|
const value = await realizeAPIValue(this.value);
|
|
@@ -87,7 +138,7 @@ export default class EditableComponent extends Editable {
|
|
|
87
138
|
|
|
88
139
|
let rootEl: HTMLElement;
|
|
89
140
|
try {
|
|
90
|
-
rootEl = await component(value);
|
|
141
|
+
rootEl = this.santiseComponentOutput(await component(value));
|
|
91
142
|
} catch (err: unknown) {
|
|
92
143
|
this.element.classList.add("errored");
|
|
93
144
|
const error = document.createElement("editable-region-error-card");
|
|
@@ -177,10 +228,16 @@ export default class EditableComponent extends Editable {
|
|
|
177
228
|
const listener = this.listeners[i];
|
|
178
229
|
if (listener.editable.element === targetChild) {
|
|
179
230
|
listener.editable.element = renderChild;
|
|
180
|
-
renderChild.editable.pushValue(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
231
|
+
renderChild.editable.pushValue(
|
|
232
|
+
this.value,
|
|
233
|
+
this.specialProps,
|
|
234
|
+
listener,
|
|
235
|
+
{
|
|
236
|
+
...this.contexts,
|
|
237
|
+
__base_context: this.contextBase ?? {},
|
|
238
|
+
},
|
|
239
|
+
renderChild,
|
|
240
|
+
);
|
|
184
241
|
}
|
|
185
242
|
}
|
|
186
243
|
} else if (hasEditable(targetChild)) {
|
|
@@ -259,10 +316,16 @@ export default class EditableComponent extends Editable {
|
|
|
259
316
|
for (let i = 0; i < this.listeners.length; i++) {
|
|
260
317
|
const listener = this.listeners[i];
|
|
261
318
|
if (listener.editable.element === targetChild) {
|
|
262
|
-
targetChild.editable.pushValue(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
319
|
+
targetChild.editable.pushValue(
|
|
320
|
+
this.value,
|
|
321
|
+
this.specialProps,
|
|
322
|
+
listener,
|
|
323
|
+
{
|
|
324
|
+
...this.contexts,
|
|
325
|
+
__base_context: this.contextBase ?? {},
|
|
326
|
+
},
|
|
327
|
+
renderChild,
|
|
328
|
+
);
|
|
266
329
|
}
|
|
267
330
|
}
|
|
268
331
|
}
|
package/nodes/editable-source.ts
CHANGED
|
@@ -45,9 +45,15 @@ export default class EditableSource extends EditableText {
|
|
|
45
45
|
|
|
46
46
|
this.file = CloudCannon.file(this.element.dataset.path);
|
|
47
47
|
this.file.addEventListener("change", () => {
|
|
48
|
-
this.file
|
|
48
|
+
this.file
|
|
49
|
+
?.get()
|
|
50
|
+
.then((source) =>
|
|
51
|
+
this.pushValue(source, {}, { path: "", editable: this }),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
this.file.get().then((source) => {
|
|
55
|
+
this.pushValue(source, {}, { path: "", editable: this });
|
|
49
56
|
});
|
|
50
|
-
this.file.get().then(this.pushValue.bind(this));
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
validateConfiguration(): boolean {
|
package/nodes/editable.ts
CHANGED
|
@@ -4,8 +4,13 @@ import type {
|
|
|
4
4
|
CloudCannonJavaScriptV1APIFile,
|
|
5
5
|
} from "@cloudcannon/javascript-api";
|
|
6
6
|
import { hasEditable } from "../helpers/checks";
|
|
7
|
-
import { CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
8
|
-
|
|
7
|
+
import { apiLoadedPromise, CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface HTMLElement {
|
|
11
|
+
__pendingEditableListeners?: EditableListener[];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
export interface EditableListener {
|
|
11
16
|
editable: Editable;
|
|
@@ -41,12 +46,15 @@ export default class Editable {
|
|
|
41
46
|
domListeners: DOMListener[] = [];
|
|
42
47
|
value: unknown = undefined;
|
|
43
48
|
parent: Editable | null = null;
|
|
49
|
+
pendingParentElement: HTMLElement | null = null;
|
|
44
50
|
element: HTMLElement;
|
|
45
51
|
mounted = false;
|
|
46
52
|
connected = false;
|
|
47
53
|
disconnecting = false;
|
|
48
54
|
needsReconnect = false;
|
|
49
55
|
|
|
56
|
+
specialPropListeners: EditableListener[] = [];
|
|
57
|
+
specialProps: Record<string, unknown> = {};
|
|
50
58
|
propsBase: unknown;
|
|
51
59
|
contextBase?: EditableContext;
|
|
52
60
|
props: Record<string, unknown> = {};
|
|
@@ -150,7 +158,10 @@ export default class Editable {
|
|
|
150
158
|
: key;
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
value,
|
|
163
|
+
context: context ?? {},
|
|
164
|
+
};
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
shouldUpdate(_value: unknown) {
|
|
@@ -161,45 +172,126 @@ export default class Editable {
|
|
|
161
172
|
return this.value !== undefined;
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
getLiteralProps() {
|
|
176
|
+
let literalPropsBase: unknown;
|
|
177
|
+
const literalProps: Record<string, unknown> = {};
|
|
178
|
+
Object.entries(this.element.dataset).forEach(([propName, propPath]) => {
|
|
179
|
+
if (!propName.startsWith("literal") || typeof propPath !== "string") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const key =
|
|
184
|
+
propName === "prop" ? undefined : propName.substring(7).toLowerCase();
|
|
185
|
+
let value = propPath;
|
|
186
|
+
try {
|
|
187
|
+
value = JSON.parse(value);
|
|
188
|
+
} catch (_error) {
|
|
189
|
+
// Error intentionally ignored
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (key) {
|
|
193
|
+
literalProps[key] = value;
|
|
194
|
+
} else {
|
|
195
|
+
literalPropsBase = value;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return { literalPropsBase, literalProps };
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
async getNewValue(
|
|
165
202
|
value: unknown,
|
|
203
|
+
specialProps: Record<string, unknown>,
|
|
166
204
|
listener?: EditableListener,
|
|
167
205
|
contexts?: { [key: string]: EditableContext },
|
|
168
206
|
): Promise<unknown> {
|
|
169
207
|
const { key, path } = listener ?? {};
|
|
170
208
|
|
|
171
|
-
|
|
172
|
-
|
|
209
|
+
if (typeof path === "string") {
|
|
210
|
+
const { value: resolvedValue, context: newContext } =
|
|
211
|
+
await this.lookupPathAndContext(path, value, contexts);
|
|
173
212
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
if (!key) {
|
|
214
|
+
this.propsBase = resolvedValue;
|
|
215
|
+
this.contextBase = newContext;
|
|
216
|
+
} else {
|
|
217
|
+
this.props[key] = resolvedValue;
|
|
218
|
+
this.contexts[key] = newContext;
|
|
219
|
+
}
|
|
180
220
|
}
|
|
181
221
|
|
|
182
|
-
|
|
183
|
-
|
|
222
|
+
this.specialProps = this.getSpecialProps(specialProps);
|
|
223
|
+
const { literalPropsBase, literalProps } = this.getLiteralProps();
|
|
224
|
+
|
|
225
|
+
let newValue: unknown;
|
|
226
|
+
const specialPropsBase = this.specialPropListeners.find(({ key }) => !key);
|
|
227
|
+
|
|
228
|
+
if (this.propsBase !== undefined) {
|
|
229
|
+
newValue = this.propsBase;
|
|
230
|
+
} else if (specialPropsBase?.path) {
|
|
231
|
+
newValue = structuredClone(this.specialProps[specialPropsBase.path]);
|
|
232
|
+
} else if (literalPropsBase !== undefined) {
|
|
233
|
+
newValue = literalPropsBase;
|
|
184
234
|
}
|
|
185
235
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(acc
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
236
|
+
if (Object.entries(this.props).length > 0) {
|
|
237
|
+
newValue = Object.entries(this.props).reduce(
|
|
238
|
+
(acc, [key, val]) => {
|
|
239
|
+
(acc as any)[key] = structuredClone(val);
|
|
240
|
+
return acc;
|
|
241
|
+
},
|
|
242
|
+
newValue && typeof newValue === "object"
|
|
243
|
+
? structuredClone(newValue)
|
|
244
|
+
: {},
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const filteredSpecialPropsListener = this.specialPropListeners.filter(
|
|
249
|
+
({ key }) => !!key,
|
|
192
250
|
);
|
|
251
|
+
if (filteredSpecialPropsListener.length > 0) {
|
|
252
|
+
newValue = filteredSpecialPropsListener.reduce(
|
|
253
|
+
(acc, { key, path }) => {
|
|
254
|
+
if (key && path) {
|
|
255
|
+
(acc as any)[key] = structuredClone(this.specialProps[path]);
|
|
256
|
+
}
|
|
257
|
+
return acc;
|
|
258
|
+
},
|
|
259
|
+
structuredClone(
|
|
260
|
+
newValue && typeof newValue === "object"
|
|
261
|
+
? structuredClone(newValue)
|
|
262
|
+
: {},
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Object.entries(literalProps).length > 0) {
|
|
268
|
+
newValue = Object.entries(literalProps).reduce(
|
|
269
|
+
(acc, [key, val]) => {
|
|
270
|
+
(acc as any)[key] = structuredClone(val);
|
|
271
|
+
return acc;
|
|
272
|
+
},
|
|
273
|
+
newValue && typeof newValue === "object"
|
|
274
|
+
? structuredClone(newValue)
|
|
275
|
+
: {},
|
|
276
|
+
);
|
|
277
|
+
}
|
|
193
278
|
|
|
194
279
|
return this.validateValue(newValue);
|
|
195
280
|
}
|
|
196
281
|
|
|
197
282
|
async pushValue(
|
|
198
283
|
value: unknown,
|
|
284
|
+
specialProps: Record<string, unknown>,
|
|
199
285
|
listener?: EditableListener,
|
|
200
286
|
contexts?: { [key: string]: EditableContext },
|
|
287
|
+
partialSubtree?: ChildNode | null,
|
|
201
288
|
): Promise<void> {
|
|
202
|
-
const newValue = await this.getNewValue(
|
|
289
|
+
const newValue = await this.getNewValue(
|
|
290
|
+
value,
|
|
291
|
+
specialProps,
|
|
292
|
+
listener,
|
|
293
|
+
contexts,
|
|
294
|
+
);
|
|
203
295
|
|
|
204
296
|
if (typeof newValue === "undefined" || !this.shouldUpdate(newValue)) {
|
|
205
297
|
return;
|
|
@@ -209,17 +301,17 @@ export default class Editable {
|
|
|
209
301
|
if (this.connected && !this.mounted) {
|
|
210
302
|
this.mounted = true;
|
|
211
303
|
this.mount();
|
|
212
|
-
return this.update();
|
|
304
|
+
return this.update(partialSubtree);
|
|
213
305
|
}
|
|
214
306
|
|
|
215
307
|
if (this.mounted) {
|
|
216
|
-
return this.update();
|
|
308
|
+
return this.update(partialSubtree);
|
|
217
309
|
}
|
|
218
310
|
}
|
|
219
311
|
|
|
220
|
-
update(): void {
|
|
312
|
+
update(_partialSubtree?: ChildNode | null): void {
|
|
221
313
|
this.listeners.forEach((listener) =>
|
|
222
|
-
listener.editable.pushValue(this.value, listener, {
|
|
314
|
+
listener.editable.pushValue(this.value, this.specialProps, listener, {
|
|
223
315
|
...this.contexts,
|
|
224
316
|
__base_context: this.contextBase ?? {},
|
|
225
317
|
}),
|
|
@@ -232,7 +324,7 @@ export default class Editable {
|
|
|
232
324
|
|
|
233
325
|
registerListener(listener: EditableListener): void {
|
|
234
326
|
if (this.value !== undefined) {
|
|
235
|
-
listener.editable.pushValue(this.value, listener, {
|
|
327
|
+
listener.editable.pushValue(this.value, this.specialProps, listener, {
|
|
236
328
|
...this.contexts,
|
|
237
329
|
__base_context: this.contextBase ?? {},
|
|
238
330
|
});
|
|
@@ -256,6 +348,29 @@ export default class Editable {
|
|
|
256
348
|
);
|
|
257
349
|
}
|
|
258
350
|
|
|
351
|
+
private queueListenerOnParent(
|
|
352
|
+
parentElement: HTMLElement,
|
|
353
|
+
listener: EditableListener,
|
|
354
|
+
): void {
|
|
355
|
+
if (!parentElement.__pendingEditableListeners) {
|
|
356
|
+
parentElement.__pendingEditableListeners = [];
|
|
357
|
+
}
|
|
358
|
+
parentElement.__pendingEditableListeners.push(listener);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private replayPendingListeners(): void {
|
|
362
|
+
const pending = this.element.__pendingEditableListeners;
|
|
363
|
+
if (!pending || pending.length === 0) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.element.__pendingEditableListeners = [];
|
|
367
|
+
for (const listener of pending) {
|
|
368
|
+
listener.editable.parent = this;
|
|
369
|
+
listener.editable.pendingParentElement = null;
|
|
370
|
+
this.registerListener(listener);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
259
374
|
async disconnect(): Promise<void> {
|
|
260
375
|
if (this.disconnecting) {
|
|
261
376
|
return;
|
|
@@ -268,6 +383,15 @@ export default class Editable {
|
|
|
268
383
|
|
|
269
384
|
this.parent?.deregisterListener(this);
|
|
270
385
|
this.parent = null;
|
|
386
|
+
if (this.pendingParentElement) {
|
|
387
|
+
const pending = this.pendingParentElement.__pendingEditableListeners;
|
|
388
|
+
if (pending) {
|
|
389
|
+
this.pendingParentElement.__pendingEditableListeners = pending.filter(
|
|
390
|
+
(listener) => listener.editable !== this,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
this.pendingParentElement = null;
|
|
394
|
+
}
|
|
271
395
|
this.APIListeners.forEach(({ obj, fn }) => {
|
|
272
396
|
obj.removeEventListener("change", fn);
|
|
273
397
|
obj.removeEventListener("delete", fn);
|
|
@@ -277,6 +401,7 @@ export default class Editable {
|
|
|
277
401
|
this.element.removeEventListener(event, fn);
|
|
278
402
|
});
|
|
279
403
|
this.domListeners = [];
|
|
404
|
+
this.specialPropListeners = [];
|
|
280
405
|
this.connected = false;
|
|
281
406
|
this.connectPromise = undefined;
|
|
282
407
|
this.disconnecting = false;
|
|
@@ -299,7 +424,7 @@ export default class Editable {
|
|
|
299
424
|
if (this.connectPromise) {
|
|
300
425
|
return;
|
|
301
426
|
}
|
|
302
|
-
this.connectPromise =
|
|
427
|
+
this.connectPromise = apiLoadedPromise.then(() => {
|
|
303
428
|
this.setupListeners();
|
|
304
429
|
this.connected = true;
|
|
305
430
|
if (!this.mounted && this.shouldMount()) {
|
|
@@ -316,11 +441,14 @@ export default class Editable {
|
|
|
316
441
|
}
|
|
317
442
|
|
|
318
443
|
setupListeners(): void {
|
|
319
|
-
let
|
|
444
|
+
let parentElement: HTMLElement | null = null;
|
|
320
445
|
let parent = this.element.parentElement;
|
|
321
446
|
while (parent) {
|
|
322
|
-
if (
|
|
323
|
-
|
|
447
|
+
if (
|
|
448
|
+
parent.tagName.startsWith("EDITABLE-") ||
|
|
449
|
+
"editable" in parent.dataset
|
|
450
|
+
) {
|
|
451
|
+
parentElement ??= parent;
|
|
324
452
|
}
|
|
325
453
|
|
|
326
454
|
if (parent.tagName === "A") {
|
|
@@ -329,7 +457,13 @@ export default class Editable {
|
|
|
329
457
|
parent = parent.parentElement;
|
|
330
458
|
}
|
|
331
459
|
|
|
332
|
-
|
|
460
|
+
let hasParentListener = false;
|
|
461
|
+
|
|
462
|
+
if (parentElement && hasEditable(parentElement)) {
|
|
463
|
+
this.parent = parentElement.editable;
|
|
464
|
+
} else if (parentElement) {
|
|
465
|
+
this.pendingParentElement = parentElement;
|
|
466
|
+
}
|
|
333
467
|
|
|
334
468
|
Object.entries(this.element.dataset).forEach(([propName, propPath]) => {
|
|
335
469
|
if (!propName.startsWith("prop") || typeof propPath !== "string") {
|
|
@@ -346,8 +480,20 @@ export default class Editable {
|
|
|
346
480
|
path: source,
|
|
347
481
|
};
|
|
348
482
|
|
|
349
|
-
if (!absolute &&
|
|
350
|
-
|
|
483
|
+
if (!absolute && source.startsWith("@") && source !== "@content") {
|
|
484
|
+
this.specialPropListeners.push(listener);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!absolute && this.parent) {
|
|
489
|
+
hasParentListener = true;
|
|
490
|
+
this.parent.registerListener(listener);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!absolute && this.pendingParentElement) {
|
|
495
|
+
hasParentListener = true;
|
|
496
|
+
this.queueListenerOnParent(this.pendingParentElement, listener);
|
|
351
497
|
return;
|
|
352
498
|
}
|
|
353
499
|
|
|
@@ -362,7 +508,9 @@ export default class Editable {
|
|
|
362
508
|
? undefined
|
|
363
509
|
: `@file[${file?.path}]`;
|
|
364
510
|
const handleAPIChange = () => {
|
|
365
|
-
this.pushValue(obj, listener, {
|
|
511
|
+
this.pushValue(obj, {}, listener, {
|
|
512
|
+
__base_context: { fullPath },
|
|
513
|
+
});
|
|
366
514
|
};
|
|
367
515
|
this.APIListeners.push({
|
|
368
516
|
obj,
|
|
@@ -374,7 +522,14 @@ export default class Editable {
|
|
|
374
522
|
}
|
|
375
523
|
});
|
|
376
524
|
|
|
525
|
+
if (this.parent && !hasParentListener) {
|
|
526
|
+
this.parent.registerListener({ editable: this });
|
|
527
|
+
} else if (this.pendingParentElement && !hasParentListener) {
|
|
528
|
+
this.queueListenerOnParent(this.pendingParentElement, { editable: this });
|
|
529
|
+
}
|
|
530
|
+
|
|
377
531
|
this.addEventListener("cloudcannon-api", this.handleApiEvent.bind(this));
|
|
532
|
+
this.replayPendingListeners();
|
|
378
533
|
}
|
|
379
534
|
|
|
380
535
|
handleApiEvent(e: any): void {
|
|
@@ -597,4 +752,13 @@ export default class Editable {
|
|
|
597
752
|
currentFile,
|
|
598
753
|
};
|
|
599
754
|
}
|
|
755
|
+
|
|
756
|
+
getSpecialProps(
|
|
757
|
+
incomingSpecialProps: Record<string, unknown> = {},
|
|
758
|
+
): Record<string, unknown> {
|
|
759
|
+
return {
|
|
760
|
+
...this.specialProps,
|
|
761
|
+
...incomingSpecialProps,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
600
764
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcannon/editable-regions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Visual Editing for the CloudCannon CMS.",
|
|
6
6
|
"keywords": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@biomejs/biome": "2.3.6",
|
|
57
57
|
"@cloudcannon/javascript-api": "0.0.10",
|
|
58
58
|
"@types/js-beautify": "1.14.3",
|
|
59
|
-
"@types/react": "19.2.
|
|
59
|
+
"@types/react": "19.2.8",
|
|
60
60
|
"@types/react-dom": "18.3.1",
|
|
61
61
|
"astro": "^5.14.1",
|
|
62
62
|
"js-beautify": "^1.15.4",
|
package/helpers/loading.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
export const actions = new Proxy(
|
|
2
|
-
{},
|
|
3
|
-
{
|
|
4
|
-
get() {
|
|
5
|
-
console.warn(
|
|
6
|
-
"[CloudCannon] actions is not supported in an editable component. Please use an editing fallback instead.",
|
|
7
|
-
);
|
|
8
|
-
return () => {};
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
export const defineAction = () => {
|
|
14
|
-
console.warn(
|
|
15
|
-
"[CloudCannon] defineAction is not supported in an editable component. Please use an editing fallback instead.",
|
|
16
|
-
);
|
|
17
|
-
return {
|
|
18
|
-
handler: () => {},
|
|
19
|
-
input: null,
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const isInputError = () => {
|
|
24
|
-
console.warn(
|
|
25
|
-
"[CloudCannon] isInputError is not supported in an editable component. Please use an editing fallback instead.",
|
|
26
|
-
);
|
|
27
|
-
return false;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const isActionError = () => {
|
|
31
|
-
console.warn(
|
|
32
|
-
"[CloudCannon] isActionError is not supported in an editable component. Please use an editing fallback instead.",
|
|
33
|
-
);
|
|
34
|
-
return false;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export class ActionError extends Error {
|
|
38
|
-
/**
|
|
39
|
-
* @param {any} code
|
|
40
|
-
* @param {any} message
|
|
41
|
-
*/
|
|
42
|
-
constructor(code, message) {
|
|
43
|
-
super(message);
|
|
44
|
-
console.warn(
|
|
45
|
-
"[CloudCannon] ActionError is not supported in an editable component. Please use an editing fallback instead.",
|
|
46
|
-
);
|
|
47
|
-
this.code = code;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const getActionContext = () => {
|
|
52
|
-
console.warn(
|
|
53
|
-
"[CloudCannon] getActionContext is not supported in an editable component. Please use an editing fallback instead.",
|
|
54
|
-
);
|
|
55
|
-
return {
|
|
56
|
-
action: undefined,
|
|
57
|
-
setActionResult: () => {},
|
|
58
|
-
serializeActionResult: () => ({}),
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const deserializeActionResult = () => {
|
|
63
|
-
console.warn(
|
|
64
|
-
"[CloudCannon] deserializeActionResult is not supported in an editable component. Please use an editing fallback instead.",
|
|
65
|
-
);
|
|
66
|
-
return {};
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const getActionPath = () => {
|
|
70
|
-
console.warn(
|
|
71
|
-
"[CloudCannon] getActionPath is not supported in an editable component. Please use an editing fallback instead.",
|
|
72
|
-
);
|
|
73
|
-
return "";
|
|
74
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export const getRelativeLocaleUrl = () => {
|
|
2
|
-
console.warn(
|
|
3
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
4
|
-
);
|
|
5
|
-
return "";
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const getAbsoluteLocaleUrl = () => {
|
|
9
|
-
console.warn(
|
|
10
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
11
|
-
);
|
|
12
|
-
return "";
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const getRelativeLocaleUrlList = () => {
|
|
16
|
-
console.warn(
|
|
17
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
18
|
-
);
|
|
19
|
-
return [];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const getAbsoluteLocaleUrlList = () => {
|
|
23
|
-
console.warn(
|
|
24
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
25
|
-
);
|
|
26
|
-
return [];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const getPathByLocale = () => {
|
|
30
|
-
console.warn(
|
|
31
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
32
|
-
);
|
|
33
|
-
return "";
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const getLocaleByPath = () => {
|
|
37
|
-
console.warn(
|
|
38
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
39
|
-
);
|
|
40
|
-
return "";
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const redirectToDefaultLocale = () => {
|
|
44
|
-
console.warn(
|
|
45
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
46
|
-
);
|
|
47
|
-
return Promise.resolve(new Response());
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const redirectToFallback = () => {
|
|
51
|
-
console.warn(
|
|
52
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
53
|
-
);
|
|
54
|
-
return Promise.resolve(new Response());
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const notFound = () => {
|
|
58
|
-
console.warn(
|
|
59
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
60
|
-
);
|
|
61
|
-
return Promise.resolve(new Response());
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const middleware = () => {
|
|
65
|
-
console.warn(
|
|
66
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
67
|
-
);
|
|
68
|
-
return () => {};
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const requestHasLocale = () => {
|
|
72
|
-
console.warn(
|
|
73
|
-
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
74
|
-
);
|
|
75
|
-
return false;
|
|
76
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export const sequence = () => {
|
|
2
|
-
console.warn(
|
|
3
|
-
"[CloudCannon] middleware is not supported in an editable component. Please use an editing fallback instead.",
|
|
4
|
-
);
|
|
5
|
-
return () => {};
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const defineMiddleware = () => {
|
|
9
|
-
console.warn(
|
|
10
|
-
"[CloudCannon] middleware is not supported in an editable component. Please use an editing fallback instead.",
|
|
11
|
-
);
|
|
12
|
-
return () => {};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const createContext = () => {
|
|
16
|
-
console.warn(
|
|
17
|
-
"[CloudCannon] middleware is not supported in an editable component. Please use an editing fallback instead.",
|
|
18
|
-
);
|
|
19
|
-
return {};
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const trySerializeLocals = () => {
|
|
23
|
-
console.warn(
|
|
24
|
-
"[CloudCannon] middleware is not supported in an editable component. Please use an editing fallback instead.",
|
|
25
|
-
);
|
|
26
|
-
return "";
|
|
27
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import ClientRouterInternal from "./client-router.astro";
|
|
2
|
-
|
|
3
|
-
export const ClientRouter = ClientRouterInternal;
|
|
4
|
-
|
|
5
|
-
export const fade = () => {
|
|
6
|
-
console.warn(
|
|
7
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
8
|
-
);
|
|
9
|
-
return {};
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const slide = () => {
|
|
13
|
-
console.warn(
|
|
14
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
15
|
-
);
|
|
16
|
-
return {};
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const navigate = () => {
|
|
20
|
-
console.warn(
|
|
21
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const supportsViewTransitions = false;
|
|
26
|
-
|
|
27
|
-
export const transitionEnabledOnThisPage = false;
|
|
28
|
-
|
|
29
|
-
export const getFallback = () => {
|
|
30
|
-
console.warn(
|
|
31
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
32
|
-
);
|
|
33
|
-
return "none";
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const swapFunctions = {
|
|
37
|
-
deselectScripts: () => {
|
|
38
|
-
console.warn(
|
|
39
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
40
|
-
);
|
|
41
|
-
},
|
|
42
|
-
swapRootAttributes: () => {
|
|
43
|
-
console.warn(
|
|
44
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
45
|
-
);
|
|
46
|
-
},
|
|
47
|
-
swapHeadElements: () => {
|
|
48
|
-
console.warn(
|
|
49
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
50
|
-
);
|
|
51
|
-
},
|
|
52
|
-
saveFocus: () => {
|
|
53
|
-
console.warn(
|
|
54
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
55
|
-
);
|
|
56
|
-
return () => {};
|
|
57
|
-
},
|
|
58
|
-
swapBodyElement: () => {
|
|
59
|
-
console.warn(
|
|
60
|
-
"[CloudCannon] view transitions are not supported in an editable component. Please use an editing fallback instead.",
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
};
|