@cloudcannon/editable-regions 0.0.6 → 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-array-item-controls.ts +3 -7
- package/components/ui/editable-component-controls.ts +17 -2
- 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 +8 -12
- package/nodes/editable-array.ts +93 -18
- package/nodes/editable-component.ts +3 -3
- package/nodes/editable-image.ts +4 -2
- package/nodes/editable-snippet.ts +2 -2
- package/nodes/editable-source.ts +1 -1
- package/nodes/editable-text.ts +5 -2
- package/nodes/editable.ts +70 -45
- 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,
|
|
@@ -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
|
};
|
|
@@ -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[] = [];
|
|
@@ -44,11 +44,11 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
isValidDropzone(e: DragEvent): boolean {
|
|
47
|
-
|
|
48
|
-
if (!source || !e.dataTransfer) {
|
|
47
|
+
if (!e.dataTransfer) {
|
|
49
48
|
return false;
|
|
50
49
|
}
|
|
51
50
|
|
|
51
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
52
52
|
if (e.dataTransfer?.types.includes(source.toLowerCase())) {
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
@@ -96,11 +96,11 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
onDragStart(e: DragEvent): void {
|
|
99
|
-
|
|
100
|
-
if (!source || !e.dataTransfer || !this.element.dataset.prop) {
|
|
99
|
+
if (!e.dataTransfer || !this.element.dataset.prop) {
|
|
101
100
|
return;
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
104
104
|
const clientRect = this.element.getBoundingClientRect();
|
|
105
105
|
|
|
106
106
|
e.stopPropagation();
|
|
@@ -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
|
|
|
@@ -423,11 +423,7 @@ export default class EditableArrayItem extends EditableComponent {
|
|
|
423
423
|
return;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
const source = this.parent?.contextBase?.
|
|
427
|
-
if (!source) {
|
|
428
|
-
throw new Error("Source not found");
|
|
429
|
-
}
|
|
430
|
-
|
|
426
|
+
const source = this.parent?.contextBase?.fullPath ?? "@currentFile";
|
|
431
427
|
const dragType = this.getDragType();
|
|
432
428
|
const sameArrayData = e.dataTransfer.getData(source);
|
|
433
429
|
const otherArrayData = dragType
|
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 = [
|
|
@@ -29,6 +29,7 @@ export default class EditableArray extends Editable {
|
|
|
29
29
|
value:
|
|
30
30
|
| CloudCannonJavaScriptV1APICollection
|
|
31
31
|
| CloudCannonJavaScriptV1APIDataset
|
|
32
|
+
| CloudCannonJavaScriptV1APIFile
|
|
32
33
|
| unknown[]
|
|
33
34
|
| null
|
|
34
35
|
| undefined = undefined;
|
|
@@ -42,6 +43,7 @@ export default class EditableArray extends Editable {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
const __base_context = { ...this.contextBase };
|
|
45
47
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
46
48
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
47
49
|
value = await this.value.items();
|
|
@@ -51,8 +53,13 @@ export default class EditableArray extends Editable {
|
|
|
51
53
|
value = items;
|
|
52
54
|
} else {
|
|
53
55
|
const data = await items.data.get();
|
|
56
|
+
__base_context.file = items;
|
|
54
57
|
value = Array.isArray(data) ? data : [];
|
|
55
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 : [];
|
|
56
63
|
} else if (Array.isArray(this.value)) {
|
|
57
64
|
value = this.value;
|
|
58
65
|
} else {
|
|
@@ -84,7 +91,7 @@ export default class EditableArray extends Editable {
|
|
|
84
91
|
|
|
85
92
|
if (listener.path) {
|
|
86
93
|
listener.editable.pushValue(value, listener, {
|
|
87
|
-
__base_context
|
|
94
|
+
__base_context,
|
|
88
95
|
});
|
|
89
96
|
}
|
|
90
97
|
}
|
|
@@ -113,7 +120,8 @@ export default class EditableArray extends Editable {
|
|
|
113
120
|
!Array.isArray(value) &&
|
|
114
121
|
value !== null &&
|
|
115
122
|
!CloudCannon.isAPICollection(value) &&
|
|
116
|
-
!CloudCannon.isAPIDataset(value)
|
|
123
|
+
!CloudCannon.isAPIDataset(value) &&
|
|
124
|
+
!CloudCannon.isAPIFile(value)
|
|
117
125
|
) {
|
|
118
126
|
this.element.classList.add("errored");
|
|
119
127
|
const error = document.createElement("editable-region-error-card");
|
|
@@ -156,6 +164,7 @@ export default class EditableArray extends Editable {
|
|
|
156
164
|
|
|
157
165
|
private async _update(): Promise<void> {
|
|
158
166
|
let value: unknown[] | CloudCannonJavaScriptV1APIFile[];
|
|
167
|
+
const __base_context = { ...this.contextBase };
|
|
159
168
|
if (CloudCannon.isAPICollection(this.value)) {
|
|
160
169
|
value = await this.value.items();
|
|
161
170
|
} else if (CloudCannon.isAPIDataset(this.value)) {
|
|
@@ -164,14 +173,57 @@ export default class EditableArray extends Editable {
|
|
|
164
173
|
value = items;
|
|
165
174
|
} else {
|
|
166
175
|
const data = await items.data.get();
|
|
176
|
+
__base_context.file = items;
|
|
167
177
|
value = Array.isArray(data) ? data : [];
|
|
168
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 : [];
|
|
169
183
|
} else if (Array.isArray(this.value)) {
|
|
170
184
|
value = this.value;
|
|
171
185
|
} else {
|
|
172
186
|
value = [];
|
|
173
187
|
}
|
|
174
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
|
+
|
|
175
227
|
const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
|
|
176
228
|
|
|
177
229
|
for (const child of this.element.querySelectorAll(
|
|
@@ -192,7 +244,9 @@ export default class EditableArray extends Editable {
|
|
|
192
244
|
if (
|
|
193
245
|
children.length === 0 &&
|
|
194
246
|
!this.element.dataset.component &&
|
|
195
|
-
!this.element.dataset.componentKey
|
|
247
|
+
!this.element.dataset.componentKey &&
|
|
248
|
+
!templates.unkeyed &&
|
|
249
|
+
Object.keys(templates.keyed).length === 0
|
|
196
250
|
) {
|
|
197
251
|
const error = document.createElement("editable-region-error-card");
|
|
198
252
|
error.setAttribute("heading", "Failed to render array editable region");
|
|
@@ -204,7 +258,9 @@ export default class EditableArray extends Editable {
|
|
|
204
258
|
return;
|
|
205
259
|
}
|
|
206
260
|
|
|
207
|
-
|
|
261
|
+
const key = this.element.dataset.idKey ?? this.element.dataset.componentKey;
|
|
262
|
+
|
|
263
|
+
if (!key) {
|
|
208
264
|
while (children.length > value.length) {
|
|
209
265
|
children.pop()?.remove();
|
|
210
266
|
}
|
|
@@ -219,7 +275,12 @@ export default class EditableArray extends Editable {
|
|
|
219
275
|
for (let i = 0; i < value.length; i++) {
|
|
220
276
|
let child = children[i];
|
|
221
277
|
if (!child) {
|
|
222
|
-
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) {
|
|
223
284
|
child = document.createElement(
|
|
224
285
|
"editable-array-item",
|
|
225
286
|
) as EditableArrayItemComponent;
|
|
@@ -240,13 +301,12 @@ export default class EditableArray extends Editable {
|
|
|
240
301
|
child.editable?.pushValue(
|
|
241
302
|
value,
|
|
242
303
|
{ path: `${i}`, editable: child.editable },
|
|
243
|
-
{ __base_context
|
|
304
|
+
{ __base_context },
|
|
244
305
|
);
|
|
245
306
|
}
|
|
246
307
|
return;
|
|
247
308
|
}
|
|
248
309
|
|
|
249
|
-
const key = this.element.dataset.idKey;
|
|
250
310
|
const componentKey = this.element.dataset.componentKey;
|
|
251
311
|
const dataKeys: (string | null)[] = [];
|
|
252
312
|
const componentKeys: (string | null)[] = [];
|
|
@@ -273,18 +333,26 @@ export default class EditableArray extends Editable {
|
|
|
273
333
|
}
|
|
274
334
|
|
|
275
335
|
const childKeys = children.map((element) => {
|
|
276
|
-
return element.dataset.id;
|
|
336
|
+
return element.dataset.id ?? element.dataset.component;
|
|
277
337
|
});
|
|
278
338
|
|
|
279
339
|
const equal = dataKeys?.every((key, i) => key === childKeys[i]);
|
|
280
340
|
if (equal && children.length === dataKeys?.length) {
|
|
281
341
|
children.forEach((child, index) => {
|
|
342
|
+
const componentKey =
|
|
343
|
+
componentKeys[index] || this.element.dataset.component;
|
|
344
|
+
|
|
345
|
+
child.dataset.id = String(childKeys[index]);
|
|
282
346
|
child.dataset.prop = `${index}`;
|
|
283
347
|
child.dataset.length = `${children.length}`;
|
|
348
|
+
if (componentKey) {
|
|
349
|
+
child.dataset.component = componentKey;
|
|
350
|
+
}
|
|
351
|
+
|
|
284
352
|
child.editable?.pushValue(
|
|
285
353
|
value,
|
|
286
354
|
{ path: `${index}`, editable: child.editable },
|
|
287
|
-
{ __base_context
|
|
355
|
+
{ __base_context },
|
|
288
356
|
);
|
|
289
357
|
});
|
|
290
358
|
|
|
@@ -310,6 +378,7 @@ export default class EditableArray extends Editable {
|
|
|
310
378
|
}
|
|
311
379
|
const placeholder = placeholders[i];
|
|
312
380
|
const existingElement = children[i];
|
|
381
|
+
const templateElement = templates.keyed[key] ?? templates.unkeyed;
|
|
313
382
|
const componentKey = componentKeys[i] || this.element.dataset.component;
|
|
314
383
|
|
|
315
384
|
const matchingChildIndex = children.findIndex(
|
|
@@ -319,18 +388,19 @@ export default class EditableArray extends Editable {
|
|
|
319
388
|
let matchingChild = children[matchingChildIndex];
|
|
320
389
|
if (!matchingChild) {
|
|
321
390
|
const clone = children.find((child) => child.dataset.id === key);
|
|
322
|
-
if (
|
|
323
|
-
matchingChild =
|
|
391
|
+
if (templateElement) {
|
|
392
|
+
matchingChild = templateElement.cloneNode(true) as any;
|
|
393
|
+
matchingChild.dataset.editable = "array-item";
|
|
324
394
|
} else if (componentKey) {
|
|
325
395
|
matchingChild = document.createElement(
|
|
326
396
|
"editable-array-item",
|
|
327
397
|
) as EditableArrayItemComponent;
|
|
328
|
-
|
|
329
|
-
matchingChild
|
|
398
|
+
} else if (clone) {
|
|
399
|
+
matchingChild = clone.cloneNode(true) as any;
|
|
330
400
|
} else {
|
|
331
401
|
const error = document.createElement("editable-region-error-card");
|
|
332
402
|
error.setAttribute("heading", "Failed to render array item");
|
|
333
|
-
if (typeof
|
|
403
|
+
if (typeof componentKey === "string") {
|
|
334
404
|
error.setAttribute(
|
|
335
405
|
"message",
|
|
336
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.",
|
|
@@ -338,7 +408,7 @@ export default class EditableArray extends Editable {
|
|
|
338
408
|
error.setAttribute(
|
|
339
409
|
"hint",
|
|
340
410
|
`This may mean that the value for 'data-component-key' is incorrect or that your array data is incorrectly formatted.
|
|
341
|
-
The current value for 'data-component-key' is '${
|
|
411
|
+
The current value for 'data-component-key' is '${componentKey}' and the current value for 'data-id' is '${key}'.
|
|
342
412
|
`,
|
|
343
413
|
);
|
|
344
414
|
} else {
|
|
@@ -360,6 +430,11 @@ export default class EditableArray extends Editable {
|
|
|
360
430
|
|
|
361
431
|
matchingChild.dataset.id = String(key);
|
|
362
432
|
matchingChild.dataset.prop = `${i}`;
|
|
433
|
+
matchingChild.dataset.length = `${dataKeys.length}`;
|
|
434
|
+
|
|
435
|
+
if (componentKey) {
|
|
436
|
+
matchingChild.dataset.component = componentKey;
|
|
437
|
+
}
|
|
363
438
|
|
|
364
439
|
if (existingElement === matchingChild) {
|
|
365
440
|
placeholder.remove();
|
|
@@ -374,7 +449,7 @@ export default class EditableArray extends Editable {
|
|
|
374
449
|
matchingChild.editable.pushValue(
|
|
375
450
|
value,
|
|
376
451
|
{ path: `${i}`, editable: matchingChild.editable },
|
|
377
|
-
{ __base_context
|
|
452
|
+
{ __base_context },
|
|
378
453
|
);
|
|
379
454
|
}
|
|
380
455
|
});
|
|
@@ -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,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
|
export default class EditableImage extends Editable {
|
|
@@ -185,7 +185,9 @@ export default class EditableImage extends Editable {
|
|
|
185
185
|
!!this.element.dataset.propTitle || !!this.element.dataset.prop;
|
|
186
186
|
|
|
187
187
|
this.loadInputConfig().then(() => {
|
|
188
|
-
this.imageEl?.addEventListener("click", () => {
|
|
188
|
+
this.imageEl?.addEventListener("click", (e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
|
|
189
191
|
if (!this.value) {
|
|
190
192
|
throw new Error("Value is not defined");
|
|
191
193
|
}
|
|
@@ -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;
|
|
@@ -80,6 +81,8 @@ export default class EditableText extends Editable {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
mount(): void {
|
|
84
|
+
this.element.addEventListener("click", (e) => e.preventDefault());
|
|
85
|
+
|
|
83
86
|
this.element.addEventListener("blur", () => {
|
|
84
87
|
this.focused = false;
|
|
85
88
|
this.element.dispatchEvent(
|
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 {
|
|
@@ -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;
|
|
@@ -317,9 +319,12 @@ export default class Editable {
|
|
|
317
319
|
let parentEditable: Editable | undefined;
|
|
318
320
|
let parent = this.element.parentElement;
|
|
319
321
|
while (parent) {
|
|
320
|
-
if (hasEditable(parent)) {
|
|
322
|
+
if (hasEditable(parent) && !parentEditable) {
|
|
321
323
|
parentEditable = parent.editable;
|
|
322
|
-
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (parent.tagName === "A") {
|
|
327
|
+
parent.draggable = false;
|
|
323
328
|
}
|
|
324
329
|
parent = parent.parentElement;
|
|
325
330
|
}
|
|
@@ -331,7 +336,7 @@ export default class Editable {
|
|
|
331
336
|
return;
|
|
332
337
|
}
|
|
333
338
|
|
|
334
|
-
const { collection, file, dataset, source, absolute } =
|
|
339
|
+
const { collection, file, dataset, source, absolute, currentFile } =
|
|
335
340
|
this.parseSource(propPath);
|
|
336
341
|
|
|
337
342
|
const listener = {
|
|
@@ -349,8 +354,15 @@ export default class Editable {
|
|
|
349
354
|
// Any single data path should only be able to refer to a single absolute API object
|
|
350
355
|
const obj = collection || dataset || file;
|
|
351
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}]`;
|
|
352
364
|
const handleAPIChange = () => {
|
|
353
|
-
this.pushValue(obj, listener);
|
|
365
|
+
this.pushValue(obj, listener, { __base_context: { fullPath } });
|
|
354
366
|
};
|
|
355
367
|
this.APIListeners.push({
|
|
356
368
|
obj,
|
|
@@ -425,22 +437,6 @@ export default class Editable {
|
|
|
425
437
|
}
|
|
426
438
|
|
|
427
439
|
filePromise.then((file) => {
|
|
428
|
-
if (typeof source !== "string") {
|
|
429
|
-
if (options.action === "get-input-config") {
|
|
430
|
-
options.callback({
|
|
431
|
-
options: {
|
|
432
|
-
disable_reorder: true,
|
|
433
|
-
disable_remove: true,
|
|
434
|
-
disable_add: true,
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
throw new Error(
|
|
440
|
-
`Failed to resolve source for API call: ${options.source}`,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
440
|
switch (options.action) {
|
|
445
441
|
case "edit":
|
|
446
442
|
file?.data.edit({ slug: source, position: options.position });
|
|
@@ -474,7 +470,17 @@ export default class Editable {
|
|
|
474
470
|
});
|
|
475
471
|
break;
|
|
476
472
|
case "get-input-config":
|
|
477
|
-
|
|
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
|
+
}
|
|
478
484
|
break;
|
|
479
485
|
}
|
|
480
486
|
});
|
|
@@ -516,40 +522,58 @@ export default class Editable {
|
|
|
516
522
|
});
|
|
517
523
|
}
|
|
518
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
|
+
|
|
519
547
|
parseSource(source?: string) {
|
|
520
548
|
let collection: CloudCannonJavaScriptV1APICollection | undefined;
|
|
521
549
|
let file: CloudCannonJavaScriptV1APIFile | undefined;
|
|
522
550
|
let dataset: CloudCannonJavaScriptV1APIDataset | undefined;
|
|
523
551
|
let absolute = false;
|
|
552
|
+
let currentFile = false;
|
|
524
553
|
|
|
525
|
-
const collectionMatch = source
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (collectionMatch?.groups) {
|
|
529
|
-
const { key, rest } = collectionMatch.groups;
|
|
554
|
+
const collectionMatch = this.matchSourcePart("collections", source);
|
|
555
|
+
if (collectionMatch) {
|
|
556
|
+
const { key, rest } = collectionMatch;
|
|
530
557
|
collection = CloudCannon.collection(key);
|
|
531
558
|
source = rest;
|
|
532
559
|
absolute = true;
|
|
533
560
|
} else {
|
|
534
|
-
const fileMatch = source
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const { path, rest } = fileMatch.groups;
|
|
539
|
-
file = CloudCannon.file(path);
|
|
561
|
+
const fileMatch = this.matchSourcePart("file", source);
|
|
562
|
+
if (fileMatch) {
|
|
563
|
+
const { key, rest } = fileMatch;
|
|
564
|
+
file = CloudCannon.file(key);
|
|
540
565
|
source = rest;
|
|
541
566
|
absolute = true;
|
|
542
567
|
} else {
|
|
543
|
-
const dataMatch = source
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (dataMatch?.groups) {
|
|
547
|
-
const { key, rest } = dataMatch.groups;
|
|
568
|
+
const dataMatch = this.matchSourcePart("data", source);
|
|
569
|
+
if (dataMatch) {
|
|
570
|
+
const { key, rest } = dataMatch;
|
|
548
571
|
dataset = CloudCannon.dataset(key);
|
|
549
572
|
source = rest;
|
|
550
573
|
absolute = true;
|
|
551
574
|
} else {
|
|
552
575
|
file = CloudCannon.currentFile();
|
|
576
|
+
currentFile = true;
|
|
553
577
|
}
|
|
554
578
|
}
|
|
555
579
|
}
|
|
@@ -566,10 +590,11 @@ export default class Editable {
|
|
|
566
590
|
return {
|
|
567
591
|
collection,
|
|
568
592
|
file,
|
|
569
|
-
source,
|
|
593
|
+
source: source ?? "",
|
|
570
594
|
absolute,
|
|
571
595
|
snippets,
|
|
572
596
|
dataset,
|
|
597
|
+
currentFile,
|
|
573
598
|
};
|
|
574
599
|
}
|
|
575
600
|
}
|
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 };
|