@cloudcannon/editable-regions 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/index.ts +3 -3
- package/components/ui/editable-region-button.ts +7 -5
- package/helpers/checks.ts +1 -1
- package/helpers/cloudcannon.mjs +110 -0
- package/integrations/astro/astro-integration.mjs +24 -2
- package/integrations/astro/index.mjs +2 -2
- package/integrations/astro/modules/content.js +1 -1
- package/integrations/astro/react-renderer.mjs +1 -1
- package/integrations/react.mjs +1 -1
- package/nodes/editable-array-item.ts +3 -3
- package/nodes/editable-array.ts +74 -13
- package/nodes/editable-component.ts +3 -3
- package/nodes/editable-image.ts +1 -1
- package/nodes/editable-snippet.ts +2 -2
- package/nodes/editable-source.ts +1 -1
- package/nodes/editable-text.ts +3 -2
- package/nodes/editable.ts +33 -17
- package/nodes/index.ts +2 -2
- package/package.json +3 -3
- package/styles/editable-array-item.css +7 -0
- package/styles/editable-component.css +7 -0
- package/styles/ui/editable-component-controls.css +24 -25
- package/types/react.d.ts +46 -2
- package/helpers/cloudcannon.ts +0 -74
package/components/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { default as EditableArrayComponent } from "./editable-array-component.js";
|
|
2
2
|
export { default as EditableArrayItemComponent } from "./editable-array-item-component.js";
|
|
3
|
-
export { default as EditableTextComponent } from "./editable-text-component.js";
|
|
4
3
|
export { default as EditableComponentComponent } from "./editable-component-component.js";
|
|
5
4
|
export { default as EditableImageComponent } from "./editable-image-component.js";
|
|
6
|
-
export { default as EditableSourceComponent } from "./editable-source-component.js";
|
|
7
5
|
export { default as EditableSnippetComponent } from "./editable-snippet-component.js";
|
|
6
|
+
export { default as EditableSourceComponent } from "./editable-source-component.js";
|
|
7
|
+
export { default as EditableTextComponent } from "./editable-text-component.js";
|
|
8
8
|
|
|
9
|
-
import { apiLoadedPromise } from "../helpers/cloudcannon.
|
|
9
|
+
import { apiLoadedPromise } from "../helpers/cloudcannon.mjs";
|
|
10
10
|
import {
|
|
11
11
|
dehydrateDataEditableRegions,
|
|
12
12
|
hydrateDataEditableRegions,
|
|
@@ -22,11 +22,13 @@ export default class EditableRegionButton extends HTMLElement {
|
|
|
22
22
|
button.innerHTML = `<cc-icon name='${this.getAttribute("icon")}'></cc-icon>${this.getAttribute("text")}`;
|
|
23
23
|
shadow.appendChild(button);
|
|
24
24
|
|
|
25
|
-
button.addEventListener("click", (e) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
button.addEventListener("click", (e) => this.triggerClick(e));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
triggerClick(e?: MouseEvent) {
|
|
29
|
+
this.dispatchEvent(
|
|
30
|
+
new CustomEvent("button-click", { detail: { originalEvent: e } }),
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
package/helpers/checks.ts
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("@cloudcannon/javascript-api").CloudCannonEditorWindow} CloudCannonEditorWindow
|
|
3
|
+
* @typedef {import("@cloudcannon/javascript-api").CloudCannonJavaScriptV1API} CloudCannonJavaScriptV1API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {(props: any) => HTMLElement | Promise<HTMLElement>} ComponentRenderer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {CloudCannonEditorWindow & {
|
|
12
|
+
* cc_components?: Record<string, ComponentRenderer>;
|
|
13
|
+
* cc_snippets?: Record<string, ComponentRenderer>;
|
|
14
|
+
* }} ExtendedWindow
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {ExtendedWindow} */
|
|
18
|
+
const extendedWindow = /** @type {any} */ (window);
|
|
19
|
+
|
|
20
|
+
/** @type {CloudCannonJavaScriptV1API} */
|
|
21
|
+
let _cloudcannon;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Promise that resolves when the CloudCannon API is loaded
|
|
25
|
+
* @type {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
export const apiLoadedPromise = new Promise((resolve) => {
|
|
28
|
+
if (extendedWindow.CloudCannonAPI) {
|
|
29
|
+
_cloudcannon = /** @type {any} */ (
|
|
30
|
+
extendedWindow.CloudCannonAPI.useVersion("v1", true)
|
|
31
|
+
);
|
|
32
|
+
resolve();
|
|
33
|
+
} else {
|
|
34
|
+
document.addEventListener(
|
|
35
|
+
"cloudcannon:load",
|
|
36
|
+
() => {
|
|
37
|
+
if (extendedWindow.CloudCannonAPI) {
|
|
38
|
+
_cloudcannon = /** @type {any} */ (
|
|
39
|
+
extendedWindow.CloudCannonAPI.useVersion("v1", true)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return resolve();
|
|
43
|
+
},
|
|
44
|
+
{ once: true },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add a renderer for editable components
|
|
51
|
+
* @param {string} key - The component key
|
|
52
|
+
* @param {ComponentRenderer} renderer - The component renderer function
|
|
53
|
+
* @returns {void}
|
|
54
|
+
*/
|
|
55
|
+
export const addEditableComponentRenderer = (key, renderer) => {
|
|
56
|
+
extendedWindow.cc_components = extendedWindow.cc_components || {};
|
|
57
|
+
extendedWindow.cc_components[key] = renderer;
|
|
58
|
+
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add a renderer for editable snippets
|
|
63
|
+
* @param {string} key - The snippet key
|
|
64
|
+
* @param {ComponentRenderer} renderer - The snippet renderer function
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
export const addEditableSnippetRenderer = (key, renderer) => {
|
|
68
|
+
extendedWindow.cc_snippets = extendedWindow.cc_snippets || {};
|
|
69
|
+
extendedWindow.cc_snippets[key] = renderer;
|
|
70
|
+
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get all registered editable component renderers
|
|
75
|
+
* @returns {Record<string, ComponentRenderer>}
|
|
76
|
+
*/
|
|
77
|
+
export const getEditableComponentRenderers = () =>
|
|
78
|
+
extendedWindow.cc_components ?? {};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all registered editable snippet renderers
|
|
82
|
+
* @returns {Record<string, ComponentRenderer>}
|
|
83
|
+
*/
|
|
84
|
+
export const getEditableSnippetRenderers = () =>
|
|
85
|
+
extendedWindow.cc_snippets ?? {};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Realize API values by converting CloudCannon API objects to their data representations
|
|
89
|
+
* @param {unknown} value - The value to realize
|
|
90
|
+
* @returns {Promise<unknown>}
|
|
91
|
+
*/
|
|
92
|
+
export const realizeAPIValue = async (value) => {
|
|
93
|
+
if (_cloudcannon.isAPICollection(value)) {
|
|
94
|
+
const items = await value.items();
|
|
95
|
+
return Promise.all(items.map(realizeAPIValue));
|
|
96
|
+
}
|
|
97
|
+
if (_cloudcannon.isAPIFile(value)) {
|
|
98
|
+
return value.data.get();
|
|
99
|
+
}
|
|
100
|
+
if (_cloudcannon.isAPIDataset(value)) {
|
|
101
|
+
const items = await value.items();
|
|
102
|
+
if (Array.isArray(items)) {
|
|
103
|
+
return Promise.all(items.map(realizeAPIValue));
|
|
104
|
+
}
|
|
105
|
+
return items.data.get();
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export { _cloudcannon as CloudCannon };
|
|
@@ -32,10 +32,32 @@ export default () => {
|
|
|
32
32
|
},
|
|
33
33
|
"astro:build:setup": async ({ target, vite }) => {
|
|
34
34
|
if (target === "client") {
|
|
35
|
+
vite.plugins ??= [];
|
|
35
36
|
vite.define ??= {};
|
|
36
37
|
vite.define.ENV_CLIENT = true;
|
|
37
38
|
|
|
38
|
-
vite.plugins?.
|
|
39
|
+
const flatPlugins = vite.plugins?.flat(10);
|
|
40
|
+
const astroBuildPlugin = flatPlugins?.find((obj) => {
|
|
41
|
+
return (
|
|
42
|
+
obj &&
|
|
43
|
+
typeof obj === "object" &&
|
|
44
|
+
"name" in obj &&
|
|
45
|
+
obj.name === "astro:build"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
astroBuildPlugin &&
|
|
51
|
+
"transform" in astroBuildPlugin &&
|
|
52
|
+
typeof astroBuildPlugin.transform === "function"
|
|
53
|
+
) {
|
|
54
|
+
const original = astroBuildPlugin.transform;
|
|
55
|
+
astroBuildPlugin.transform = function (source, id, options) {
|
|
56
|
+
return original.bind(this)(source, id, { ...options, ssr: true });
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
vite.plugins.unshift({
|
|
39
61
|
name: "vite-plugin-editable-regions",
|
|
40
62
|
enforce: "pre",
|
|
41
63
|
|
|
@@ -88,7 +110,7 @@ export default () => {
|
|
|
88
110
|
default:
|
|
89
111
|
contents += `export const ${key} = ${JSON.stringify(process.env[key] ?? "")};\n`;
|
|
90
112
|
}
|
|
91
|
-
} catch (
|
|
113
|
+
} catch (_e) {
|
|
92
114
|
//Error intentionally ignored
|
|
93
115
|
}
|
|
94
116
|
},
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
renderSlotToString,
|
|
3
3
|
renderToString,
|
|
4
4
|
} from "astro/runtime/server/index.js";
|
|
5
|
-
import { addEditableComponentRenderer } from "../../helpers/cloudcannon";
|
|
5
|
+
import { addEditableComponentRenderer } from "../../helpers/cloudcannon.mjs";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Queue of React components waiting to be rendered
|
|
@@ -86,7 +86,7 @@ export const registerAstroComponent = (key, component) => {
|
|
|
86
86
|
true,
|
|
87
87
|
["encrypt", "decrypt"],
|
|
88
88
|
);
|
|
89
|
-
} catch (
|
|
89
|
+
} catch (_err) {
|
|
90
90
|
console.warn(
|
|
91
91
|
"[CloudCannon] Could not generate a key for Astro component. This may cause issues with Astro components that use server-islands",
|
|
92
92
|
);
|
|
@@ -24,7 +24,7 @@ addFrameworkRenderer({
|
|
|
24
24
|
try {
|
|
25
25
|
const reactNode = Component(props);
|
|
26
26
|
return { html: renderToStaticMarkup(reactNode) };
|
|
27
|
-
} catch (
|
|
27
|
+
} catch (_err) {
|
|
28
28
|
const id = queueForClientSideRender((node) => {
|
|
29
29
|
const reactNode = createElement(Component, props, null);
|
|
30
30
|
const root = createRoot(node);
|
package/integrations/react.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
2
|
import { flushSync } from "react-dom";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
|
-
import { addEditableComponentRenderer } from "../helpers/cloudcannon";
|
|
4
|
+
import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Registers a React component with the CloudCannon component system.
|
|
@@ -6,14 +6,14 @@ import {
|
|
|
6
6
|
isEditableArrayItem,
|
|
7
7
|
isEditableElement,
|
|
8
8
|
} from "../helpers/checks.js";
|
|
9
|
-
import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.
|
|
9
|
+
import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.mjs";
|
|
10
10
|
import type EditableArray from "./editable-array.js";
|
|
11
11
|
import EditableComponent from "./editable-component.js";
|
|
12
12
|
|
|
13
13
|
export default class EditableArrayItem extends EditableComponent {
|
|
14
14
|
parent: EditableArray | null = null;
|
|
15
15
|
|
|
16
|
-
protected controlsElement?: EditableArrayItemControls;
|
|
16
|
+
protected declare controlsElement?: EditableArrayItemControls;
|
|
17
17
|
|
|
18
18
|
private inputConfig?: any;
|
|
19
19
|
private structureStrings: string[] = [];
|
|
@@ -292,7 +292,7 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
292
292
|
this.controlsElement = document.createElement(
|
|
293
293
|
"editable-array-item-controls",
|
|
294
294
|
);
|
|
295
|
-
this.controlsElement.addEventListener("edit", (
|
|
295
|
+
this.controlsElement.addEventListener("edit", (_e: any) => {
|
|
296
296
|
this.dispatchEdit(this.element.dataset.prop);
|
|
297
297
|
});
|
|
298
298
|
|
package/nodes/editable-array.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type {
|
|
|
6
6
|
import type EditableArrayItemComponent from "../components/editable-array-item-component.js";
|
|
7
7
|
import type EditableRegionButton from "../components/ui/editable-region-button.js";
|
|
8
8
|
import { isEditableElement } from "../helpers/checks.js";
|
|
9
|
-
import { CloudCannon } from "../helpers/cloudcannon.
|
|
10
|
-
import type EditableArrayItem from "./editable-array-item.js";
|
|
9
|
+
import { CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
11
10
|
import Editable, { type EditableListener } from "./editable.js";
|
|
11
|
+
import type EditableArrayItem from "./editable-array-item.js";
|
|
12
12
|
import "../components/ui/editable-region-button.js";
|
|
13
13
|
|
|
14
14
|
const arrayDirectionValues = [
|
|
@@ -186,6 +186,44 @@ export default class EditableArray extends Editable {
|
|
|
186
186
|
value = [];
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
const templates: {
|
|
190
|
+
keyed: Record<
|
|
191
|
+
string,
|
|
192
|
+
HTMLElement & {
|
|
193
|
+
editable?: EditableArrayItem;
|
|
194
|
+
}
|
|
195
|
+
>;
|
|
196
|
+
unkeyed?: HTMLElement & {
|
|
197
|
+
editable?: EditableArrayItem;
|
|
198
|
+
};
|
|
199
|
+
} = { keyed: {} };
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < this.element.children.length; i += 1) {
|
|
202
|
+
const childEl = this.element.children[i];
|
|
203
|
+
if (
|
|
204
|
+
childEl instanceof HTMLTemplateElement &&
|
|
205
|
+
typeof childEl.dataset.cloudcannonIgnore !== "string"
|
|
206
|
+
) {
|
|
207
|
+
const key = childEl.dataset.id;
|
|
208
|
+
const content = childEl.content;
|
|
209
|
+
let templateEl: HTMLElement;
|
|
210
|
+
if (content.childElementCount === 1) {
|
|
211
|
+
templateEl = content.children[0] as HTMLElement;
|
|
212
|
+
} else {
|
|
213
|
+
templateEl = document.createElement(
|
|
214
|
+
"editable-array-item",
|
|
215
|
+
) as HTMLElement;
|
|
216
|
+
templateEl.append(content.cloneNode(true));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof key === "string") {
|
|
220
|
+
templates.keyed[key] = templateEl;
|
|
221
|
+
} else {
|
|
222
|
+
templates.unkeyed = templateEl;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
189
227
|
const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
|
|
190
228
|
|
|
191
229
|
for (const child of this.element.querySelectorAll(
|
|
@@ -206,7 +244,9 @@ export default class EditableArray extends Editable {
|
|
|
206
244
|
if (
|
|
207
245
|
children.length === 0 &&
|
|
208
246
|
!this.element.dataset.component &&
|
|
209
|
-
!this.element.dataset.componentKey
|
|
247
|
+
!this.element.dataset.componentKey &&
|
|
248
|
+
!templates.unkeyed &&
|
|
249
|
+
Object.keys(templates.keyed).length === 0
|
|
210
250
|
) {
|
|
211
251
|
const error = document.createElement("editable-region-error-card");
|
|
212
252
|
error.setAttribute("heading", "Failed to render array editable region");
|
|
@@ -218,7 +258,9 @@ export default class EditableArray extends Editable {
|
|
|
218
258
|
return;
|
|
219
259
|
}
|
|
220
260
|
|
|
221
|
-
|
|
261
|
+
const key = this.element.dataset.idKey ?? this.element.dataset.componentKey;
|
|
262
|
+
|
|
263
|
+
if (!key) {
|
|
222
264
|
while (children.length > value.length) {
|
|
223
265
|
children.pop()?.remove();
|
|
224
266
|
}
|
|
@@ -233,7 +275,12 @@ export default class EditableArray extends Editable {
|
|
|
233
275
|
for (let i = 0; i < value.length; i++) {
|
|
234
276
|
let child = children[i];
|
|
235
277
|
if (!child) {
|
|
236
|
-
if (
|
|
278
|
+
if (templates.unkeyed) {
|
|
279
|
+
child = templates.unkeyed.cloneNode(true) as HTMLElement & {
|
|
280
|
+
editable?: EditableArrayItem;
|
|
281
|
+
};
|
|
282
|
+
child.dataset.editable = "array-item";
|
|
283
|
+
} else if (this.element.dataset.component) {
|
|
237
284
|
child = document.createElement(
|
|
238
285
|
"editable-array-item",
|
|
239
286
|
) as EditableArrayItemComponent;
|
|
@@ -260,7 +307,6 @@ export default class EditableArray extends Editable {
|
|
|
260
307
|
return;
|
|
261
308
|
}
|
|
262
309
|
|
|
263
|
-
const key = this.element.dataset.idKey;
|
|
264
310
|
const componentKey = this.element.dataset.componentKey;
|
|
265
311
|
const dataKeys: (string | null)[] = [];
|
|
266
312
|
const componentKeys: (string | null)[] = [];
|
|
@@ -287,14 +333,22 @@ export default class EditableArray extends Editable {
|
|
|
287
333
|
}
|
|
288
334
|
|
|
289
335
|
const childKeys = children.map((element) => {
|
|
290
|
-
return element.dataset.id;
|
|
336
|
+
return element.dataset.id ?? element.dataset.component;
|
|
291
337
|
});
|
|
292
338
|
|
|
293
339
|
const equal = dataKeys?.every((key, i) => key === childKeys[i]);
|
|
294
340
|
if (equal && children.length === dataKeys?.length) {
|
|
295
341
|
children.forEach((child, index) => {
|
|
342
|
+
const componentKey =
|
|
343
|
+
componentKeys[index] || this.element.dataset.component;
|
|
344
|
+
|
|
345
|
+
child.dataset.id = String(childKeys[index]);
|
|
296
346
|
child.dataset.prop = `${index}`;
|
|
297
347
|
child.dataset.length = `${children.length}`;
|
|
348
|
+
if (componentKey) {
|
|
349
|
+
child.dataset.component = componentKey;
|
|
350
|
+
}
|
|
351
|
+
|
|
298
352
|
child.editable?.pushValue(
|
|
299
353
|
value,
|
|
300
354
|
{ path: `${index}`, editable: child.editable },
|
|
@@ -324,6 +378,7 @@ export default class EditableArray extends Editable {
|
|
|
324
378
|
}
|
|
325
379
|
const placeholder = placeholders[i];
|
|
326
380
|
const existingElement = children[i];
|
|
381
|
+
const templateElement = templates.keyed[key] ?? templates.unkeyed;
|
|
327
382
|
const componentKey = componentKeys[i] || this.element.dataset.component;
|
|
328
383
|
|
|
329
384
|
const matchingChildIndex = children.findIndex(
|
|
@@ -333,18 +388,19 @@ export default class EditableArray extends Editable {
|
|
|
333
388
|
let matchingChild = children[matchingChildIndex];
|
|
334
389
|
if (!matchingChild) {
|
|
335
390
|
const clone = children.find((child) => child.dataset.id === key);
|
|
336
|
-
if (
|
|
337
|
-
matchingChild =
|
|
391
|
+
if (templateElement) {
|
|
392
|
+
matchingChild = templateElement.cloneNode(true) as any;
|
|
393
|
+
matchingChild.dataset.editable = "array-item";
|
|
338
394
|
} else if (componentKey) {
|
|
339
395
|
matchingChild = document.createElement(
|
|
340
396
|
"editable-array-item",
|
|
341
397
|
) as EditableArrayItemComponent;
|
|
342
|
-
|
|
343
|
-
matchingChild
|
|
398
|
+
} else if (clone) {
|
|
399
|
+
matchingChild = clone.cloneNode(true) as any;
|
|
344
400
|
} else {
|
|
345
401
|
const error = document.createElement("editable-region-error-card");
|
|
346
402
|
error.setAttribute("heading", "Failed to render array item");
|
|
347
|
-
if (typeof
|
|
403
|
+
if (typeof componentKey === "string") {
|
|
348
404
|
error.setAttribute(
|
|
349
405
|
"message",
|
|
350
406
|
"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.",
|
|
@@ -352,7 +408,7 @@ export default class EditableArray extends Editable {
|
|
|
352
408
|
error.setAttribute(
|
|
353
409
|
"hint",
|
|
354
410
|
`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 '${
|
|
411
|
+
The current value for 'data-component-key' is '${componentKey}' and the current value for 'data-id' is '${key}'.
|
|
356
412
|
`,
|
|
357
413
|
);
|
|
358
414
|
} else {
|
|
@@ -374,6 +430,11 @@ export default class EditableArray extends Editable {
|
|
|
374
430
|
|
|
375
431
|
matchingChild.dataset.id = String(key);
|
|
376
432
|
matchingChild.dataset.prop = `${i}`;
|
|
433
|
+
matchingChild.dataset.length = `${dataKeys.length}`;
|
|
434
|
+
|
|
435
|
+
if (componentKey) {
|
|
436
|
+
matchingChild.dataset.component = componentKey;
|
|
437
|
+
}
|
|
377
438
|
|
|
378
439
|
if (existingElement === matchingChild) {
|
|
379
440
|
placeholder.remove();
|
|
@@ -12,7 +12,7 @@ import type EditableComponentControls from "../components/ui/editable-component-
|
|
|
12
12
|
import {
|
|
13
13
|
getEditableComponentRenderers,
|
|
14
14
|
realizeAPIValue,
|
|
15
|
-
} from "../helpers/cloudcannon.
|
|
15
|
+
} from "../helpers/cloudcannon.mjs";
|
|
16
16
|
|
|
17
17
|
export default class EditableComponent extends Editable {
|
|
18
18
|
protected controlsElement?: EditableComponentControls;
|
|
@@ -87,7 +87,7 @@ export default class EditableComponent extends Editable {
|
|
|
87
87
|
|
|
88
88
|
let rootEl: HTMLElement;
|
|
89
89
|
try {
|
|
90
|
-
rootEl = await component(
|
|
90
|
+
rootEl = await component(value);
|
|
91
91
|
} catch (err: unknown) {
|
|
92
92
|
this.element.classList.add("errored");
|
|
93
93
|
const error = document.createElement("editable-region-error-card");
|
|
@@ -332,7 +332,7 @@ export default class EditableComponent extends Editable {
|
|
|
332
332
|
this.controlsElement = document.createElement(
|
|
333
333
|
"editable-component-controls",
|
|
334
334
|
);
|
|
335
|
-
this.controlsElement.addEventListener("edit", (
|
|
335
|
+
this.controlsElement.addEventListener("edit", (_e: any) => {
|
|
336
336
|
this.dispatchEdit(editPath);
|
|
337
337
|
});
|
|
338
338
|
this.element.append(this.controlsElement);
|
package/nodes/editable-image.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getEditableSnippetRenderers } from "../helpers/cloudcannon.
|
|
2
|
-
import EditableComponent from "./editable-component.js";
|
|
1
|
+
import { getEditableSnippetRenderers } from "../helpers/cloudcannon.mjs";
|
|
3
2
|
import Editable from "./editable.js";
|
|
3
|
+
import EditableComponent from "./editable-component.js";
|
|
4
4
|
|
|
5
5
|
export default class EditableSnippet extends EditableComponent {
|
|
6
6
|
getComponents() {
|
package/nodes/editable-source.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CloudCannonJavaScriptV1APIFile } from "@cloudcannon/javascript-api";
|
|
2
2
|
import { html as beautifyHtml } from "js-beautify";
|
|
3
|
-
import { CloudCannon } from "../helpers/cloudcannon.
|
|
3
|
+
import { CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
4
4
|
import EditableText from "./editable-text.js";
|
|
5
5
|
|
|
6
6
|
const INDENTATION_REGEX = /^([ \t]+)[^\s]/gm;
|
package/nodes/editable-text.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CloudCannon } from "../helpers/cloudcannon.
|
|
1
|
+
import { CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
2
2
|
import Editable from "./editable.js";
|
|
3
3
|
|
|
4
4
|
type EditableFocusEvent = CustomEvent<number>;
|
|
@@ -7,7 +7,8 @@ export default class EditableText extends Editable {
|
|
|
7
7
|
editor?: any;
|
|
8
8
|
focused = false;
|
|
9
9
|
focusIndex = 0;
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
declare value: string | null | undefined;
|
|
11
12
|
|
|
12
13
|
validateConfiguration(): boolean {
|
|
13
14
|
const prop = this.element.dataset.prop;
|
package/nodes/editable.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
CloudCannonJavaScriptV1APIFile,
|
|
5
5
|
} from "@cloudcannon/javascript-api";
|
|
6
6
|
import { hasEditable } from "../helpers/checks";
|
|
7
|
-
import { CloudCannon } from "../helpers/cloudcannon";
|
|
7
|
+
import { CloudCannon } from "../helpers/cloudcannon.mjs";
|
|
8
8
|
import { loadingPromise } from "../helpers/loading";
|
|
9
9
|
|
|
10
10
|
export interface EditableListener {
|
|
@@ -522,6 +522,28 @@ export default class Editable {
|
|
|
522
522
|
});
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
matchSourcePart(type: "collections" | "data" | "file", source?: string) {
|
|
526
|
+
if (!source) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!source.startsWith(`@${type}`)) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const argsPart = source.slice(type.length + 1);
|
|
535
|
+
const brackets = argsPart.match(/^\[+/);
|
|
536
|
+
if (!brackets) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const bracketsLength = brackets[0].length;
|
|
540
|
+
return argsPart.match(
|
|
541
|
+
new RegExp(
|
|
542
|
+
`^\\[{${bracketsLength}}(?<key>.+?)\\]{${bracketsLength}}(\\.(?<rest>.+))?$`,
|
|
543
|
+
),
|
|
544
|
+
)?.groups;
|
|
545
|
+
}
|
|
546
|
+
|
|
525
547
|
parseSource(source?: string) {
|
|
526
548
|
let collection: CloudCannonJavaScriptV1APICollection | undefined;
|
|
527
549
|
let file: CloudCannonJavaScriptV1APIFile | undefined;
|
|
@@ -529,29 +551,23 @@ export default class Editable {
|
|
|
529
551
|
let absolute = false;
|
|
530
552
|
let currentFile = false;
|
|
531
553
|
|
|
532
|
-
const collectionMatch = source
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (collectionMatch?.groups) {
|
|
536
|
-
const { key, rest } = collectionMatch.groups;
|
|
554
|
+
const collectionMatch = this.matchSourcePart("collections", source);
|
|
555
|
+
if (collectionMatch) {
|
|
556
|
+
const { key, rest } = collectionMatch;
|
|
537
557
|
collection = CloudCannon.collection(key);
|
|
538
558
|
source = rest;
|
|
539
559
|
absolute = true;
|
|
540
560
|
} else {
|
|
541
|
-
const fileMatch = source
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const { path, rest } = fileMatch.groups;
|
|
546
|
-
file = CloudCannon.file(path);
|
|
561
|
+
const fileMatch = this.matchSourcePart("file", source);
|
|
562
|
+
if (fileMatch) {
|
|
563
|
+
const { key, rest } = fileMatch;
|
|
564
|
+
file = CloudCannon.file(key);
|
|
547
565
|
source = rest;
|
|
548
566
|
absolute = true;
|
|
549
567
|
} else {
|
|
550
|
-
const dataMatch = source
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (dataMatch?.groups) {
|
|
554
|
-
const { key, rest } = dataMatch.groups;
|
|
568
|
+
const dataMatch = this.matchSourcePart("data", source);
|
|
569
|
+
if (dataMatch) {
|
|
570
|
+
const { key, rest } = dataMatch;
|
|
555
571
|
dataset = CloudCannon.dataset(key);
|
|
556
572
|
source = rest;
|
|
557
573
|
absolute = true;
|
package/nodes/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { default as Editable } from "./editable.js";
|
|
2
2
|
export { default as EditableArray } from "./editable-array.js";
|
|
3
3
|
export { default as EditableArrayItem } from "./editable-array-item.js";
|
|
4
|
-
export { default as EditableText } from "./editable-text.js";
|
|
5
4
|
export { default as EditableComponent } from "./editable-component.js";
|
|
6
5
|
export { default as EditableImage } from "./editable-image.js";
|
|
7
|
-
export { default as EditableSource } from "./editable-source.js";
|
|
8
6
|
export { default as EditableSnippet } from "./editable-snippet.js";
|
|
7
|
+
export { default as EditableSource } from "./editable-source.js";
|
|
8
|
+
export { default as EditableText } from "./editable-text.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcannon/editable-regions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Visual Editing for the CloudCannon CMS.",
|
|
6
6
|
"keywords": [
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"./internal/styles": "./styles/index.js"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@biomejs/biome": "
|
|
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": "
|
|
59
|
+
"@types/react": "19.2.6",
|
|
60
60
|
"@types/react-dom": "18.3.1",
|
|
61
61
|
"astro": "^5.14.1",
|
|
62
62
|
"js-beautify": "^1.15.4",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
editable-array-item {
|
|
2
2
|
display: block;
|
|
3
|
+
/*
|
|
4
|
+
In Chrome 143.0.7499.41 setting display: block sometimes causes text elements to not be rendered.
|
|
5
|
+
But in Firefox, not setting display: block causes the positioning of the controls element to be wrong.
|
|
6
|
+
If you want to remove this line (either because Chrome has fixed it or because you're fixing the positioning logic)
|
|
7
|
+
make sure you test against [this ticket](https://www.notion.so/cloudcannon/Text-elements-in-editable-regions-in-Chrome-sometimes-don-t-render-2beb2cf63b238067bdbdfc5efb2b2ead).
|
|
8
|
+
*/
|
|
9
|
+
will-change: contents;
|
|
3
10
|
}
|
|
4
11
|
|
|
5
12
|
editable-array-item,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
editable-component {
|
|
2
2
|
display: block;
|
|
3
|
+
/*
|
|
4
|
+
In Chrome 143.0.7499.41 setting display: block sometimes causes text elements to not be rendered.
|
|
5
|
+
But in Firefox, not setting display: block causes the positioning of the controls element to be wrong.
|
|
6
|
+
If you want to remove this line (either because Chrome has fixed it or because you're fixing the positioning logic)
|
|
7
|
+
make sure you test against [this ticket](https://www.notion.so/cloudcannon/Text-elements-in-editable-regions-in-Chrome-sometimes-don-t-render-2beb2cf63b238067bdbdfc5efb2b2ead).
|
|
8
|
+
*/
|
|
9
|
+
will-change: contents;
|
|
3
10
|
}
|
|
4
11
|
|
|
5
12
|
editable-component,
|
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
align-items: end;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
button {
|
|
14
|
+
background-color: transparent;
|
|
15
|
+
border: 0;
|
|
16
|
+
padding: 0;
|
|
17
|
+
|
|
18
|
+
border-radius: var(--ccve-border-radius);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
|
|
21
|
+
&[draggable="true"] {
|
|
22
|
+
cursor: grab;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&:disabled {
|
|
26
|
+
color: #979797;
|
|
27
|
+
--cc-icon-fill: #979797;
|
|
28
|
+
cursor: not-allowed;
|
|
29
|
+
|
|
30
|
+
&:hover {
|
|
31
|
+
background-color: transparent;
|
|
32
|
+
--cc-icon-fill: #979797;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
.button-row {
|
|
14
38
|
display: flex;
|
|
15
39
|
background: #fff;
|
|
@@ -40,7 +64,6 @@
|
|
|
40
64
|
padding: 10px 6px;
|
|
41
65
|
margin: 4px;
|
|
42
66
|
width: 154px;
|
|
43
|
-
border: 1px solid #eee;
|
|
44
67
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.1);
|
|
45
68
|
border: 2px solid #cfcfcf;
|
|
46
69
|
display: none;
|
|
@@ -67,30 +90,6 @@
|
|
|
67
90
|
}
|
|
68
91
|
}
|
|
69
92
|
|
|
70
|
-
button {
|
|
71
|
-
background-color: transparent;
|
|
72
|
-
border: 0;
|
|
73
|
-
padding: 0;
|
|
74
|
-
|
|
75
|
-
border-radius: var(--ccve-border-radius);
|
|
76
|
-
cursor: pointer;
|
|
77
|
-
|
|
78
|
-
&[draggable="true"] {
|
|
79
|
-
cursor: grab;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
&:disabled {
|
|
83
|
-
color: #979797;
|
|
84
|
-
--cc-icon-fill: #979797;
|
|
85
|
-
cursor: not-allowed;
|
|
86
|
-
|
|
87
|
-
&:hover {
|
|
88
|
-
background-color: transparent;
|
|
89
|
-
--cc-icon-fill: #979797;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
93
|
@keyframes fade-in-up {
|
|
95
94
|
from {
|
|
96
95
|
opacity: 0.5;
|
package/types/react.d.ts
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
/// <reference path="./cloudcannon.d.ts" />
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { HTMLAttributes, RefAttributes } from "react";
|
|
4
|
+
|
|
5
|
+
export function registerReactComponent(key: string, component: unknown): void;
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
namespace React.JSX {
|
|
9
|
+
interface IntrinsicElements {
|
|
10
|
+
"editable-component": RefAttributes<HTMLElement> &
|
|
11
|
+
Omit<HTMLAttributes<HTMLElement>, "className"> & {
|
|
12
|
+
class?: string;
|
|
13
|
+
"data-prop": string;
|
|
14
|
+
"data-component": string;
|
|
15
|
+
};
|
|
16
|
+
"editable-text": RefAttributes<HTMLElement> &
|
|
17
|
+
Omit<HTMLAttributes<HTMLElement>, "className"> & {
|
|
18
|
+
class?: string;
|
|
19
|
+
"data-prop": string;
|
|
20
|
+
"data-type"?: "block" | "text" | "span";
|
|
21
|
+
};
|
|
22
|
+
"editable-source": RefAttributes<HTMLElement> &
|
|
23
|
+
Omit<HTMLAttributes<HTMLElement>, "className"> & {
|
|
24
|
+
class?: string;
|
|
25
|
+
"data-path": string;
|
|
26
|
+
"data-key": string;
|
|
27
|
+
};
|
|
28
|
+
"editable-array": RefAttributes<HTMLElement> &
|
|
29
|
+
Omit<HTMLAttributes<HTMLElement>, "className"> & {
|
|
30
|
+
class?: string;
|
|
31
|
+
"data-prop": string;
|
|
32
|
+
"data-id-key"?: string;
|
|
33
|
+
"data-component-key"?: string;
|
|
34
|
+
"data-component"?: string;
|
|
35
|
+
"data-direction"?:
|
|
36
|
+
| "column"
|
|
37
|
+
| "row"
|
|
38
|
+
| "column-reverse"
|
|
39
|
+
| "row-reverse";
|
|
40
|
+
};
|
|
41
|
+
"editable-array-item": RefAttributes<HTMLElement> &
|
|
42
|
+
Omit<HTMLAttributes<HTMLElement>, "className"> & {
|
|
43
|
+
class?: string;
|
|
44
|
+
"data-id"?: string;
|
|
45
|
+
"data-component"?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
5
49
|
}
|
package/helpers/cloudcannon.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CloudCannonEditorWindow,
|
|
3
|
-
CloudCannonJavaScriptV1API,
|
|
4
|
-
} from "@cloudcannon/javascript-api";
|
|
5
|
-
|
|
6
|
-
export type ComponentRenderer = (
|
|
7
|
-
props: any,
|
|
8
|
-
) => HTMLElement | Promise<HTMLElement>;
|
|
9
|
-
|
|
10
|
-
declare const window: CloudCannonEditorWindow & {
|
|
11
|
-
cc_components?: Record<string, ComponentRenderer>;
|
|
12
|
-
cc_snippets?: Record<string, ComponentRenderer>;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
let _cloudcannon: CloudCannonJavaScriptV1API;
|
|
16
|
-
|
|
17
|
-
export const apiLoadedPromise = new Promise<void>((resolve) => {
|
|
18
|
-
if (window.CloudCannonAPI) {
|
|
19
|
-
_cloudcannon = window.CloudCannonAPI.useVersion("v1", true) as any;
|
|
20
|
-
resolve();
|
|
21
|
-
} else {
|
|
22
|
-
document.addEventListener(
|
|
23
|
-
"cloudcannon:load",
|
|
24
|
-
() => {
|
|
25
|
-
if (window.CloudCannonAPI) {
|
|
26
|
-
_cloudcannon = window.CloudCannonAPI.useVersion("v1", true) as any;
|
|
27
|
-
}
|
|
28
|
-
return resolve();
|
|
29
|
-
},
|
|
30
|
-
{ once: true },
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export const addEditableComponentRenderer = (
|
|
36
|
-
key: string,
|
|
37
|
-
renderer: ComponentRenderer,
|
|
38
|
-
) => {
|
|
39
|
-
window.cc_components = window.cc_components || {};
|
|
40
|
-
window.cc_components[key] = renderer;
|
|
41
|
-
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const addEditableSnippetRenderer = (
|
|
45
|
-
key: string,
|
|
46
|
-
renderer: ComponentRenderer,
|
|
47
|
-
) => {
|
|
48
|
-
window.cc_snippets = window.cc_snippets || {};
|
|
49
|
-
window.cc_snippets[key] = renderer;
|
|
50
|
-
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const getEditableComponentRenderers = () => window.cc_components ?? {};
|
|
54
|
-
export const getEditableSnippetRenderers = () => window.cc_snippets ?? {};
|
|
55
|
-
|
|
56
|
-
export const realizeAPIValue = async (value: unknown): Promise<unknown> => {
|
|
57
|
-
if (_cloudcannon.isAPICollection(value)) {
|
|
58
|
-
const items = await value.items();
|
|
59
|
-
return Promise.all(items.map(realizeAPIValue));
|
|
60
|
-
}
|
|
61
|
-
if (_cloudcannon.isAPIFile(value)) {
|
|
62
|
-
return value.data.get();
|
|
63
|
-
}
|
|
64
|
-
if (_cloudcannon.isAPIDataset(value)) {
|
|
65
|
-
const items = await value.items();
|
|
66
|
-
if (Array.isArray(items)) {
|
|
67
|
-
return Promise.all(items.map(realizeAPIValue));
|
|
68
|
-
}
|
|
69
|
-
return items.data.get();
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export { _cloudcannon as CloudCannon };
|