@cloudcannon/editable-regions 0.0.20-rc.3 → 0.0.20
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/README.md +885 -0
- package/helpers/cloudcannon.mjs +20 -9
- package/helpers/hydrate-editable-regions.ts +36 -3
- package/integrations/hugo/browser/errors.ts +8 -2
- package/integrations/react.mjs +63 -1
- package/integrations/svelte/EditableRegions.svelte +80 -0
- package/integrations/{svelte.mjs → svelte/index.mjs} +3 -1
- package/integrations/vue.mjs +97 -1
- package/nodes/editable-array-item.ts +5 -1
- package/nodes/editable-component.ts +10 -4
- package/nodes/editable-image.ts +80 -75
- package/nodes/editable.ts +28 -5
- package/package.json +17 -16
- package/types/react.d.ts +20 -6
- package/types/svelte.d.ts +10 -0
- package/types/vue.d.ts +14 -1
- package/integrations/liquid/README.md +0 -677
package/helpers/cloudcannon.mjs
CHANGED
|
@@ -14,24 +14,26 @@
|
|
|
14
14
|
* }} ExtendedWindow
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/** @type {ExtendedWindow} */
|
|
18
|
-
const extendedWindow = /** @type {any} */ (
|
|
17
|
+
/** @type {ExtendedWindow | undefined} */
|
|
18
|
+
const extendedWindow = /** @type {any} */ (
|
|
19
|
+
typeof window === "undefined" ? undefined : window
|
|
20
|
+
);
|
|
19
21
|
|
|
20
|
-
/** @type {CloudCannonVisualEditorAPIV1} */
|
|
22
|
+
/** @type {CloudCannonVisualEditorAPIV1 | undefined} */
|
|
21
23
|
let _cloudcannon;
|
|
22
24
|
|
|
23
25
|
/** @type {Promise<void>} */
|
|
24
26
|
export const apiLoadedPromise = new Promise((resolve) => {
|
|
25
|
-
if (extendedWindow
|
|
27
|
+
if (extendedWindow?.CloudCannonAPI) {
|
|
26
28
|
_cloudcannon = /** @type {any} */ (
|
|
27
29
|
extendedWindow.CloudCannonAPI.useVersion("v1", true)
|
|
28
30
|
);
|
|
29
31
|
resolve();
|
|
30
|
-
} else {
|
|
32
|
+
} else if (typeof document !== "undefined") {
|
|
31
33
|
document.addEventListener(
|
|
32
34
|
"cloudcannon:load",
|
|
33
35
|
() => {
|
|
34
|
-
if (extendedWindow
|
|
36
|
+
if (extendedWindow?.CloudCannonAPI) {
|
|
35
37
|
_cloudcannon = /** @type {any} */ (
|
|
36
38
|
extendedWindow.CloudCannonAPI.useVersion("v1", true)
|
|
37
39
|
);
|
|
@@ -48,6 +50,9 @@ export const apiLoadedPromise = new Promise((resolve) => {
|
|
|
48
50
|
* @param {ComponentRenderer} renderer
|
|
49
51
|
*/
|
|
50
52
|
export const addEditableComponentRenderer = (key, renderer) => {
|
|
53
|
+
if (!extendedWindow) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
51
56
|
extendedWindow.cc_components = extendedWindow.cc_components || {};
|
|
52
57
|
extendedWindow.cc_components[key] = renderer;
|
|
53
58
|
};
|
|
@@ -57,15 +62,18 @@ export const addEditableComponentRenderer = (key, renderer) => {
|
|
|
57
62
|
* @param {ComponentRenderer} renderer
|
|
58
63
|
*/
|
|
59
64
|
export const addEditableSnippetRenderer = (key, renderer) => {
|
|
65
|
+
if (!extendedWindow) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
60
68
|
extendedWindow.cc_snippets = extendedWindow.cc_snippets || {};
|
|
61
69
|
extendedWindow.cc_snippets[key] = renderer;
|
|
62
70
|
};
|
|
63
71
|
|
|
64
72
|
export const getEditableComponentRenderers = () =>
|
|
65
|
-
extendedWindow
|
|
73
|
+
extendedWindow?.cc_components ?? {};
|
|
66
74
|
|
|
67
75
|
export const getEditableSnippetRenderers = () =>
|
|
68
|
-
extendedWindow
|
|
76
|
+
extendedWindow?.cc_snippets ?? {};
|
|
69
77
|
|
|
70
78
|
/**
|
|
71
79
|
* Resolves CloudCannon API objects (collections, files, datasets) to plain data.
|
|
@@ -91,13 +99,16 @@ export const realizeAPIValue = async (value) => {
|
|
|
91
99
|
};
|
|
92
100
|
|
|
93
101
|
export const addCustomEditableRegion = (key, region) => {
|
|
102
|
+
if (!extendedWindow || typeof document === "undefined") {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
94
105
|
extendedWindow.editableRegionMap ??= {};
|
|
95
106
|
extendedWindow.editableRegionMap[key] = region;
|
|
96
107
|
extendedWindow.hydrateDataEditableRegions(document.body);
|
|
97
108
|
};
|
|
98
109
|
|
|
99
110
|
export const getCustomEditableRegions = () => {
|
|
100
|
-
return extendedWindow
|
|
111
|
+
return extendedWindow?.editableRegionMap ?? {};
|
|
101
112
|
};
|
|
102
113
|
|
|
103
114
|
export { _cloudcannon as CloudCannon };
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { hasEditable } from "../nodes/editable.js";
|
|
11
11
|
import { isEditableWebcomponent } from "./checks";
|
|
12
12
|
|
|
13
|
+
const DYNAMIC_EDITABLE_TYPE = "_dynamic";
|
|
14
|
+
|
|
13
15
|
const baseEditableMap: Record<string, typeof Editable | undefined> = {
|
|
14
16
|
array: EditableArray,
|
|
15
17
|
"array-item": EditableArrayItem,
|
|
@@ -20,12 +22,23 @@ const baseEditableMap: Record<string, typeof Editable | undefined> = {
|
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
export const dehydrateDataEditableRegions = (root: Element) => {
|
|
23
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
root instanceof HTMLElement &&
|
|
27
|
+
hasEditable(root) &&
|
|
28
|
+
root.dataset.editable !== DYNAMIC_EDITABLE_TYPE
|
|
29
|
+
) {
|
|
24
30
|
root.editable.disconnect();
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
root.querySelectorAll("[data-editable]").forEach((element) => {
|
|
28
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
!(element instanceof HTMLElement) ||
|
|
36
|
+
element.dataset.editable === DYNAMIC_EDITABLE_TYPE
|
|
37
|
+
) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (hasEditable(element)) {
|
|
29
42
|
element.editable.disconnect();
|
|
30
43
|
}
|
|
31
44
|
});
|
|
@@ -39,6 +52,7 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
39
52
|
if (
|
|
40
53
|
root instanceof HTMLElement &&
|
|
41
54
|
root.dataset.editable &&
|
|
55
|
+
root.dataset.editable !== DYNAMIC_EDITABLE_TYPE &&
|
|
42
56
|
!isEditableWebcomponent(root)
|
|
43
57
|
) {
|
|
44
58
|
if ("editable" in root && root.editable instanceof Editable) {
|
|
@@ -64,6 +78,10 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
64
78
|
return;
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
if (element.dataset.editable === DYNAMIC_EDITABLE_TYPE) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
const Editable = editableMap[element.dataset.editable];
|
|
68
86
|
if (!Editable) {
|
|
69
87
|
const error = document.createElement("editable-region-error-card");
|
|
@@ -95,4 +113,19 @@ export const hydrateDataEditableRegions = (root: Element) => {
|
|
|
95
113
|
});
|
|
96
114
|
};
|
|
97
115
|
|
|
98
|
-
|
|
116
|
+
const extendedWindow = /** @type {any} */ (window);
|
|
117
|
+
|
|
118
|
+
Object.assign(extendedWindow, {
|
|
119
|
+
editableRegions: {
|
|
120
|
+
Editable,
|
|
121
|
+
EditableArray,
|
|
122
|
+
EditableArrayItem,
|
|
123
|
+
EditableComponent,
|
|
124
|
+
EditableImage,
|
|
125
|
+
EditableSource,
|
|
126
|
+
EditableText,
|
|
127
|
+
},
|
|
128
|
+
hydrateDataEditableRegions,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
document.dispatchEvent(new CustomEvent("editable-regions:load"));
|
|
@@ -43,7 +43,13 @@ export function parseHugoTemplateError(raw: string): ParsedHugoError {
|
|
|
43
43
|
return { frames, call, message: rest };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
/** An error enhanced with an optional editor-context hint. */
|
|
47
|
+
export type HugoError = Error & { hint?: string };
|
|
48
|
+
|
|
49
|
+
export function enhanceHugoError(
|
|
50
|
+
message: string,
|
|
51
|
+
componentKey: string,
|
|
52
|
+
): HugoError {
|
|
47
53
|
const parsed = parseHugoTemplateError(message);
|
|
48
54
|
|
|
49
55
|
let hint = "";
|
|
@@ -76,7 +82,7 @@ export function enhanceHugoError(message: string, componentKey: string): Error {
|
|
|
76
82
|
|
|
77
83
|
const error = new Error(
|
|
78
84
|
`Failed to render Hugo component "${componentKey}": ${sentence}`,
|
|
79
|
-
) as
|
|
85
|
+
) as HugoError;
|
|
80
86
|
|
|
81
87
|
if (hint) {
|
|
82
88
|
error.hint = hint;
|
package/integrations/react.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createElement, useEffect, useRef } from "react";
|
|
2
4
|
import { flushSync } from "react-dom";
|
|
3
5
|
import { createRoot } from "react-dom/client";
|
|
4
6
|
import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
|
|
@@ -26,3 +28,63 @@ export const registerReactComponent = (key, component) => {
|
|
|
26
28
|
|
|
27
29
|
addEditableComponentRenderer(key, wrappedComponent);
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Root editable region component for React (e.g. Next.js App Router pages).
|
|
34
|
+
* Keeps editable regions from mutating the DOM before frameworks like Next.js finish hydrating.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ tag?: string } & Record<string, any>} props
|
|
37
|
+
* All other props and attributes are forwarded to the rendered element.
|
|
38
|
+
*/
|
|
39
|
+
export const EditableRegions = ({ tag: Tag = "div", ...attrs }) => {
|
|
40
|
+
const elementRef = useRef(null);
|
|
41
|
+
/** @type {{ current: any | null }} */
|
|
42
|
+
const editableRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
/** @param {any} regions */
|
|
46
|
+
const connectEditable = (regions) => {
|
|
47
|
+
if (!regions || editableRef.current) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
editableRef.current = new regions.Editable(elementRef.current);
|
|
52
|
+
editableRef.current.connect();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onLoad = () => {
|
|
56
|
+
connectEditable(/** @type {any} */ (window).editableRegions);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (/** @type {any} */ (window).editableRegions) {
|
|
60
|
+
connectEditable(/** @type {any} */ (window).editableRegions);
|
|
61
|
+
} else {
|
|
62
|
+
document.addEventListener("editable-regions:load", onLoad, {
|
|
63
|
+
once: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
document.removeEventListener("editable-regions:load", onLoad);
|
|
69
|
+
editableRef.current?.disconnect();
|
|
70
|
+
editableRef.current = null;
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
/** @type {Record<string, any>} */
|
|
75
|
+
const forwarded = {};
|
|
76
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
77
|
+
if (/^data-(prop|literal)/i.test(name)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
forwarded[name] = value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return createElement(Tag, {
|
|
85
|
+
...forwarded,
|
|
86
|
+
ref: elementRef,
|
|
87
|
+
"data-editable": "_dynamic",
|
|
88
|
+
"data-prop": "",
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onDestroy, onMount } from "svelte";
|
|
3
|
+
|
|
4
|
+
// Element type to render. Defaults to "div".
|
|
5
|
+
/** @type {string | undefined} */
|
|
6
|
+
export let tag = "div";
|
|
7
|
+
|
|
8
|
+
/** @type {any} */
|
|
9
|
+
let editable = null;
|
|
10
|
+
/** @type {HTMLElement | undefined} */
|
|
11
|
+
let element;
|
|
12
|
+
/** @type {(() => void) | null} */
|
|
13
|
+
let pendingLoadHandler = null;
|
|
14
|
+
|
|
15
|
+
// data-prop and data-literal attributes are controlled by this component
|
|
16
|
+
// itself; user-supplied ones are ignored.
|
|
17
|
+
$: restProps = filterProps($$restProps);
|
|
18
|
+
|
|
19
|
+
function filterProps(props) {
|
|
20
|
+
const filtered = {};
|
|
21
|
+
for (const [name, value] of Object.entries(props)) {
|
|
22
|
+
if (/^data-(prop|literal)/i.test(name)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
filtered[name] = value;
|
|
27
|
+
}
|
|
28
|
+
return filtered;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMount(() => {
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} regions
|
|
34
|
+
*/
|
|
35
|
+
const connectEditable = (regions) => {
|
|
36
|
+
if (!regions || editable) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
editable = new regions.Editable(element);
|
|
41
|
+
editable.connect();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (window.editableRegions) {
|
|
45
|
+
connectEditable(window.editableRegions);
|
|
46
|
+
} else {
|
|
47
|
+
pendingLoadHandler = () => {
|
|
48
|
+
connectEditable(window.editableRegions);
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener(
|
|
51
|
+
"editable-regions:load",
|
|
52
|
+
pendingLoadHandler,
|
|
53
|
+
{ once: true },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
onDestroy(() => {
|
|
59
|
+
if (pendingLoadHandler) {
|
|
60
|
+
document.removeEventListener(
|
|
61
|
+
"editable-regions:load",
|
|
62
|
+
pendingLoadHandler,
|
|
63
|
+
);
|
|
64
|
+
pendingLoadHandler = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
editable?.disconnect();
|
|
68
|
+
editable = null;
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<svelte:element
|
|
73
|
+
this={tag}
|
|
74
|
+
{...restProps}
|
|
75
|
+
bind:this={element}
|
|
76
|
+
data-editable="_dynamic"
|
|
77
|
+
data-prop=""
|
|
78
|
+
>
|
|
79
|
+
<slot />
|
|
80
|
+
</svelte:element>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
export { default as EditableRegions } from "./EditableRegions.svelte";
|
|
2
|
+
|
|
3
|
+
import { addEditableComponentRenderer } from "../../helpers/cloudcannon.mjs";
|
|
2
4
|
|
|
3
5
|
/** @type{((component: any, args: { target: HTMLElement, props: unknown }) => void) | undefined} */
|
|
4
6
|
let mount;
|
package/integrations/vue.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createApp,
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
onMounted,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
ref,
|
|
8
|
+
} from "vue";
|
|
2
9
|
import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
|
|
3
10
|
|
|
4
11
|
/**
|
|
@@ -26,3 +33,92 @@ export const registerVueComponent = (key, component) => {
|
|
|
26
33
|
|
|
27
34
|
addEditableComponentRenderer(key, wrappedComponent);
|
|
28
35
|
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Root editable region component for Vue.
|
|
39
|
+
* Keeps editable regions from mutating the DOM before frameworks like Nuxt finish hydrating.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} [props.tag] - Element type to render. Defaults to "div".
|
|
42
|
+
* All other props and attributes are forwarded to the rendered element.
|
|
43
|
+
*/
|
|
44
|
+
export const EditableRegions = defineComponent({
|
|
45
|
+
name: "EditableRegions",
|
|
46
|
+
props: {
|
|
47
|
+
tag: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: "div",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
inheritAttrs: false,
|
|
53
|
+
/**
|
|
54
|
+
* @param {{ tag: string }} props
|
|
55
|
+
* @param {{ attrs: any; slots: any }} context
|
|
56
|
+
*/
|
|
57
|
+
setup(props, { attrs, slots }) {
|
|
58
|
+
const element = ref(null);
|
|
59
|
+
/** @type {any} */
|
|
60
|
+
let editable = null;
|
|
61
|
+
/** @type {(() => void) | null} */
|
|
62
|
+
let pendingLoadHandler = null;
|
|
63
|
+
|
|
64
|
+
/** @param {any} regions */
|
|
65
|
+
const connectEditable = (regions) => {
|
|
66
|
+
if (!regions || editable) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
editable = new regions.Editable(element.value);
|
|
71
|
+
editable.connect();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
onMounted(() => {
|
|
75
|
+
const regions = /** @type {any} */ (window).editableRegions;
|
|
76
|
+
if (regions) {
|
|
77
|
+
connectEditable(regions);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pendingLoadHandler = () => {
|
|
82
|
+
connectEditable(/** @type {any} */ (window).editableRegions);
|
|
83
|
+
};
|
|
84
|
+
document.addEventListener("editable-regions:load", pendingLoadHandler, {
|
|
85
|
+
once: true,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
onUnmounted(() => {
|
|
90
|
+
if (pendingLoadHandler) {
|
|
91
|
+
document.removeEventListener(
|
|
92
|
+
"editable-regions:load",
|
|
93
|
+
pendingLoadHandler,
|
|
94
|
+
);
|
|
95
|
+
pendingLoadHandler = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
editable?.disconnect();
|
|
99
|
+
editable = null;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
/** @type {Record<string, any>} */
|
|
104
|
+
const forwarded = {};
|
|
105
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
106
|
+
if (/^data-(prop|literal)/i.test(name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
forwarded[name] = value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return h(
|
|
113
|
+
props.tag,
|
|
114
|
+
{
|
|
115
|
+
...forwarded,
|
|
116
|
+
ref: element,
|
|
117
|
+
"data-editable": "_dynamic",
|
|
118
|
+
"data-prop": "",
|
|
119
|
+
},
|
|
120
|
+
/** @type {any} */ (slots.default?.()),
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
@@ -18,12 +18,16 @@ export const hasEditableArrayItem = <T extends object>(
|
|
|
18
18
|
export default class EditableArrayItem extends EditableComponent {
|
|
19
19
|
parent: EditableArray | null = null;
|
|
20
20
|
|
|
21
|
-
protected
|
|
21
|
+
declare protected controlsElement?: EditableArrayItemControls;
|
|
22
22
|
|
|
23
23
|
private inputConfig?: any;
|
|
24
24
|
private structureStrings: string[] = [];
|
|
25
25
|
|
|
26
26
|
shouldMount(): boolean {
|
|
27
|
+
if (this.mounted) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
return this.value !== undefined;
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -25,13 +25,19 @@ export default class EditableComponent extends Editable {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
shouldMount(): boolean {
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
28
|
+
if (this.mounted) {
|
|
29
|
+
return false;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
key.startsWith("prop"),
|
|
32
|
+
const hasNoProps = Object.keys(this.element.dataset).every(
|
|
33
|
+
(key) => !key.startsWith("prop"),
|
|
34
34
|
);
|
|
35
|
+
|
|
36
|
+
if (hasNoProps) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return super.shouldMount();
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
validateConfiguration(): boolean {
|
package/nodes/editable-image.ts
CHANGED
|
@@ -185,87 +185,92 @@ export default class EditableImage extends Editable {
|
|
|
185
185
|
this.configuredTitle =
|
|
186
186
|
!!this.element.dataset.propTitle || !!this.element.dataset.prop;
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// Loading the input config costs a round trip per configured prop, so the
|
|
189
|
+
// listener is attached up front and the click waits for it. Attaching after
|
|
190
|
+
// the load resolves instead leaves a window where clicks are dropped.
|
|
191
|
+
const inputConfigLoaded = this.loadInputConfig();
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
193
|
+
this.imageEl?.addEventListener("click", async (e) => {
|
|
194
|
+
e.preventDefault();
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
if ("src" in this.value) {
|
|
198
|
-
data.src = this.value.src;
|
|
199
|
-
}
|
|
200
|
-
if ("alt" in this.value) {
|
|
201
|
-
data.alt = this.value.alt;
|
|
202
|
-
}
|
|
203
|
-
if ("title" in this.value) {
|
|
204
|
-
data.title = this.value.title;
|
|
205
|
-
}
|
|
196
|
+
await inputConfigLoaded;
|
|
206
197
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
198
|
+
if (!this.value) {
|
|
199
|
+
throw new Error("Value is not defined");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data: this["value"] = {};
|
|
203
|
+
if ("src" in this.value) {
|
|
204
|
+
data.src = this.value.src;
|
|
205
|
+
}
|
|
206
|
+
if ("alt" in this.value) {
|
|
207
|
+
data.alt = this.value.alt;
|
|
208
|
+
}
|
|
209
|
+
if ("title" in this.value) {
|
|
210
|
+
data.title = this.value.title;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.panelId = await CloudCannon.createCustomDataPanel({
|
|
214
|
+
title: "Edit Image",
|
|
215
|
+
data,
|
|
216
|
+
position: this.imageEl?.getBoundingClientRect(),
|
|
217
|
+
config: {
|
|
218
|
+
_inputs: {
|
|
219
|
+
src: {
|
|
220
|
+
label: "Image",
|
|
221
|
+
type: "image",
|
|
222
|
+
...this.inputConfig.src,
|
|
223
|
+
},
|
|
224
|
+
alt: {
|
|
225
|
+
comment:
|
|
226
|
+
"A description which provides information about this image if for some reason it cannot be viewed.",
|
|
227
|
+
...this.inputConfig.alt,
|
|
228
|
+
},
|
|
229
|
+
title: {
|
|
230
|
+
comment: "Displayed when hovering over the image.",
|
|
231
|
+
...this.inputConfig.title,
|
|
227
232
|
},
|
|
228
233
|
},
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
},
|
|
235
|
+
onChange: (value): void => {
|
|
236
|
+
if (!value || typeof value !== "object") {
|
|
237
|
+
throw new Error("Invalid image data");
|
|
238
|
+
}
|
|
233
239
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
});
|
|
240
|
+
if (
|
|
241
|
+
"src" in value &&
|
|
242
|
+
this.configuredSrc &&
|
|
243
|
+
value.src !== this.value?.src
|
|
244
|
+
) {
|
|
245
|
+
this.dispatchSet(
|
|
246
|
+
this.element.dataset.propSrc ??
|
|
247
|
+
`${this.element.dataset.prop}.src`,
|
|
248
|
+
value.src,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
"alt" in value &&
|
|
253
|
+
this.configuredAlt &&
|
|
254
|
+
value.alt !== this.value?.alt
|
|
255
|
+
) {
|
|
256
|
+
this.dispatchSet(
|
|
257
|
+
this.element.dataset.propAlt ??
|
|
258
|
+
`${this.element.dataset.prop}.alt`,
|
|
259
|
+
value.alt,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
"title" in value &&
|
|
264
|
+
this.configuredTitle &&
|
|
265
|
+
value.title !== this.value?.title
|
|
266
|
+
) {
|
|
267
|
+
this.dispatchSet(
|
|
268
|
+
this.element.dataset.propTitle ??
|
|
269
|
+
`${this.element.dataset.prop}.title`,
|
|
270
|
+
value.title,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
269
274
|
});
|
|
270
275
|
});
|
|
271
276
|
}
|