@cloudcannon/editable-regions 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/editable-array-item-controls.ts +9 -0
- package/components/ui/editable-component-controls.ts +67 -11
- package/helpers/checks.ts +5 -1
- package/helpers/cloudcannon.mjs +0 -2
- package/integrations/astro/astro-integration.mjs +109 -56
- package/integrations/astro/index.mjs +21 -8
- package/nodes/editable-component.ts +87 -92
- package/package.json +3 -3
- package/styles/editable-array-item.css +5 -3
- package/styles/editable-component.css +5 -3
- package/styles/ui/editable-component-controls.css +21 -0
|
@@ -93,7 +93,16 @@ export default class EditableArrayItemControls extends EditableComponentControls
|
|
|
93
93
|
|
|
94
94
|
this.dragHandle = document.createElement("button");
|
|
95
95
|
this.dragHandle.type = "button";
|
|
96
|
+
|
|
97
|
+
if (this.usePopoverAPI) {
|
|
98
|
+
//@ts-expect-error: Typescript doesn't yet support the popover API
|
|
99
|
+
this.dragHandle.style.anchorName = this.anchorName;
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
this.dragHandle.onclick = (e) => {
|
|
103
|
+
if (this.hasAttribute("open")) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
97
106
|
e.stopPropagation();
|
|
98
107
|
e.preventDefault();
|
|
99
108
|
this.toggleContextMenu();
|
|
@@ -6,13 +6,28 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
6
6
|
protected buttonRow?: HTMLDivElement;
|
|
7
7
|
|
|
8
8
|
private editButton?: HTMLButtonElement;
|
|
9
|
+
protected usePopoverAPI = false;
|
|
10
|
+
protected contextMenuId =
|
|
11
|
+
`context-menu-popover-${Math.random().toString(36).substring(2, 11)}`;
|
|
12
|
+
protected anchorName = `--${this.contextMenuId}`;
|
|
13
|
+
protected hostAnchorName =
|
|
14
|
+
`--cc-editable-host-${Math.random().toString(36).substring(2, 11)}`;
|
|
9
15
|
|
|
10
16
|
toggleContextMenu() {
|
|
11
|
-
if (this.contextMenu
|
|
12
|
-
|
|
17
|
+
if (!this.contextMenu || this.contextMenu.childElementCount === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (this.usePopoverAPI) {
|
|
22
|
+
this.contextMenu.togglePopover();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (this.contextMenu.classList.contains("open")) {
|
|
27
|
+
this.contextMenu.classList.remove("open");
|
|
13
28
|
this.removeAttribute("open");
|
|
14
|
-
} else
|
|
15
|
-
this.contextMenu
|
|
29
|
+
} else {
|
|
30
|
+
this.contextMenu.classList.add("open");
|
|
16
31
|
this.setAttribute("open", "true");
|
|
17
32
|
}
|
|
18
33
|
}
|
|
@@ -28,6 +43,13 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
28
43
|
|
|
29
44
|
this.contextMenu = document.createElement("ul");
|
|
30
45
|
this.contextMenu.classList.add("context-menu");
|
|
46
|
+
|
|
47
|
+
if (this.usePopoverAPI) {
|
|
48
|
+
this.contextMenu.setAttribute("popover", "auto");
|
|
49
|
+
//@ts-expect-error: Typescript doesn't yet support the popover API
|
|
50
|
+
this.contextMenu.style.positionAnchor = this.anchorName;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
shadow.appendChild(this.contextMenu);
|
|
32
54
|
|
|
33
55
|
this.editButton = document.createElement("button");
|
|
@@ -40,17 +62,32 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
40
62
|
|
|
41
63
|
this.onclick = (e) => {
|
|
42
64
|
e.preventDefault();
|
|
43
|
-
this.contextMenu?.classList.remove("open");
|
|
44
65
|
this.removeAttribute("open");
|
|
45
|
-
};
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
if (this.usePopoverAPI) {
|
|
68
|
+
this.contextMenu?.hidePopover();
|
|
69
|
+
} else {
|
|
70
|
+
this.contextMenu?.classList.remove("open");
|
|
50
71
|
}
|
|
51
|
-
this.contextMenu?.classList.remove("open");
|
|
52
|
-
this.removeAttribute("open");
|
|
53
72
|
};
|
|
73
|
+
|
|
74
|
+
if (this.usePopoverAPI && this.contextMenu) {
|
|
75
|
+
this.contextMenu.ontoggle = (e: ToggleEvent) => {
|
|
76
|
+
if (e.newState === "open") {
|
|
77
|
+
this.setAttribute("open", "true");
|
|
78
|
+
} else {
|
|
79
|
+
this.removeAttribute("open");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
this.onblur = (e) => {
|
|
84
|
+
if ((e.relatedTarget as HTMLElement)?.tagName === "A") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.removeAttribute("open");
|
|
88
|
+
this.contextMenu?.classList.remove("open");
|
|
89
|
+
};
|
|
90
|
+
}
|
|
54
91
|
}
|
|
55
92
|
|
|
56
93
|
connectedCallback() {
|
|
@@ -58,8 +95,27 @@ export default class EditableComponentControls extends HTMLElement {
|
|
|
58
95
|
return;
|
|
59
96
|
}
|
|
60
97
|
|
|
98
|
+
this.usePopoverAPI =
|
|
99
|
+
"popover" in HTMLElement.prototype && CSS.supports("anchor-name", "--a");
|
|
100
|
+
|
|
61
101
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
62
102
|
this.render(this.shadow);
|
|
103
|
+
|
|
104
|
+
if (this.usePopoverAPI && this.parentElement) {
|
|
105
|
+
const existing = getComputedStyle(this.parentElement)
|
|
106
|
+
.getPropertyValue("anchor-name")
|
|
107
|
+
.trim();
|
|
108
|
+
|
|
109
|
+
if (existing && existing !== "none") {
|
|
110
|
+
this.hostAnchorName = existing.split(",")[0].trim();
|
|
111
|
+
} else {
|
|
112
|
+
// @ts-expect-error
|
|
113
|
+
this.parentElement.style.anchorName = this.hostAnchorName;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// @ts-expect-error
|
|
117
|
+
this.style.positionAnchor = this.hostAnchorName;
|
|
118
|
+
}
|
|
63
119
|
}
|
|
64
120
|
}
|
|
65
121
|
|
package/helpers/checks.ts
CHANGED
|
@@ -46,7 +46,11 @@ export const isEditableElement = (el: unknown): boolean => {
|
|
|
46
46
|
);
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
export const areEqualEditables = (a:
|
|
49
|
+
export const areEqualEditables = (a: Element, b: Element) => {
|
|
50
|
+
if (!(a instanceof HTMLElement) || !(b instanceof HTMLElement)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
if (getEditableType(a) !== getEditableType(b)) {
|
|
51
55
|
return false;
|
|
52
56
|
}
|
package/helpers/cloudcannon.mjs
CHANGED
|
@@ -55,7 +55,6 @@ export const apiLoadedPromise = new Promise((resolve) => {
|
|
|
55
55
|
export const addEditableComponentRenderer = (key, renderer) => {
|
|
56
56
|
extendedWindow.cc_components = extendedWindow.cc_components || {};
|
|
57
57
|
extendedWindow.cc_components[key] = renderer;
|
|
58
|
-
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
59
58
|
};
|
|
60
59
|
|
|
61
60
|
/**
|
|
@@ -67,7 +66,6 @@ export const addEditableComponentRenderer = (key, renderer) => {
|
|
|
67
66
|
export const addEditableSnippetRenderer = (key, renderer) => {
|
|
68
67
|
extendedWindow.cc_snippets = extendedWindow.cc_snippets || {};
|
|
69
68
|
extendedWindow.cc_snippets[key] = renderer;
|
|
70
|
-
document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
|
|
71
69
|
};
|
|
72
70
|
|
|
73
71
|
/**
|
|
@@ -4,6 +4,33 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
/** @type{string[]} */
|
|
5
5
|
const SUPPORTED_VIRTUAL_MODULES = ["assets", "content"];
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @param {*} original
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
function wrapTransform(original) {
|
|
12
|
+
/**
|
|
13
|
+
* @this {*}
|
|
14
|
+
* @param {*} source
|
|
15
|
+
* @param {*} id
|
|
16
|
+
* @param {*} options
|
|
17
|
+
*/
|
|
18
|
+
return function (source, id, options) {
|
|
19
|
+
if (this.environment?.name === "client") {
|
|
20
|
+
const proxy = new Proxy(this, {
|
|
21
|
+
get(target, prop) {
|
|
22
|
+
if (prop === "environment") {
|
|
23
|
+
return { ...target.environment, name: "ssr" };
|
|
24
|
+
}
|
|
25
|
+
return Reflect.get(target, prop);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return original.call(proxy, source, id, { ...options, ssr: true });
|
|
29
|
+
}
|
|
30
|
+
return original.call(this, source, id, options);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
/**
|
|
8
35
|
* @return {import("astro").AstroIntegration}
|
|
9
36
|
*/
|
|
@@ -14,70 +41,96 @@ export default () => {
|
|
|
14
41
|
"astro:config:setup": ({ updateConfig }) => {
|
|
15
42
|
updateConfig({
|
|
16
43
|
vite: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
vite.define ??= {};
|
|
27
|
-
vite.define.ENV_CLIENT = true;
|
|
44
|
+
plugins: [
|
|
45
|
+
{
|
|
46
|
+
enforce: "pre",
|
|
47
|
+
name: "vite-plugin-editable-regions",
|
|
48
|
+
applyToEnvironment(environment) {
|
|
49
|
+
return environment.name === "client";
|
|
50
|
+
},
|
|
51
|
+
config(config) {
|
|
52
|
+
config.environments ??= {};
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
obj &&
|
|
33
|
-
typeof obj === "object" &&
|
|
34
|
-
"name" in obj &&
|
|
35
|
-
obj.name === "astro:build"
|
|
36
|
-
);
|
|
37
|
-
});
|
|
54
|
+
config.environments.ssr ??= {};
|
|
55
|
+
config.environments.ssr.define ??= {};
|
|
56
|
+
config.environments.ssr.define.ENV_CLIENT = false;
|
|
38
57
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
typeof astroBuildPlugin.transform === "function"
|
|
43
|
-
) {
|
|
44
|
-
const original = astroBuildPlugin.transform;
|
|
45
|
-
astroBuildPlugin.transform = function (source, id, options) {
|
|
46
|
-
return original.bind(this)(source, id, { ...options, ssr: true });
|
|
47
|
-
};
|
|
48
|
-
}
|
|
58
|
+
config.environments.astro ??= {};
|
|
59
|
+
config.environments.astro.define ??= {};
|
|
60
|
+
config.environments.astro.define.ENV_CLIENT = false;
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
config.environments.prerender ??= {};
|
|
63
|
+
config.environments.prerender.define ??= {};
|
|
64
|
+
config.environments.prerender.define.ENV_CLIENT = false;
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
66
|
+
config.environments.client ??= {};
|
|
67
|
+
config.environments.client.define ??= {};
|
|
68
|
+
config.environments.client.define.ENV_CLIENT = true;
|
|
69
|
+
},
|
|
70
|
+
configResolved(config) {
|
|
71
|
+
const flatPlugins = config.plugins?.flat(10);
|
|
72
|
+
const astroBuildPlugin = flatPlugins?.find((obj) => {
|
|
73
|
+
return (
|
|
74
|
+
obj &&
|
|
75
|
+
typeof obj === "object" &&
|
|
76
|
+
"name" in obj &&
|
|
77
|
+
obj.name === "astro:build"
|
|
78
|
+
);
|
|
79
|
+
});
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
if (
|
|
82
|
+
astroBuildPlugin &&
|
|
83
|
+
"transform" in astroBuildPlugin &&
|
|
84
|
+
typeof astroBuildPlugin.transform === "object" &&
|
|
85
|
+
astroBuildPlugin.transform &&
|
|
86
|
+
"handler" in astroBuildPlugin.transform &&
|
|
87
|
+
typeof astroBuildPlugin.transform.handler === "function"
|
|
88
|
+
) {
|
|
89
|
+
astroBuildPlugin.transform.handler = wrapTransform(
|
|
90
|
+
astroBuildPlugin.transform.handler,
|
|
91
|
+
);
|
|
92
|
+
} else if (
|
|
93
|
+
astroBuildPlugin &&
|
|
94
|
+
"transform" in astroBuildPlugin &&
|
|
95
|
+
typeof astroBuildPlugin.transform === "function"
|
|
96
|
+
) {
|
|
97
|
+
astroBuildPlugin.transform = wrapTransform(
|
|
98
|
+
astroBuildPlugin.transform,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
resolveId: {
|
|
103
|
+
order: "pre",
|
|
104
|
+
handler(id) {
|
|
105
|
+
if (id.startsWith("astro:")) {
|
|
106
|
+
const type = id
|
|
107
|
+
.replace("astro:", "")
|
|
108
|
+
.replace("/client", "")
|
|
109
|
+
.replace("/server", "");
|
|
67
110
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
111
|
+
let dir = "";
|
|
112
|
+
if (typeof __dirname !== "undefined") {
|
|
113
|
+
dir = __dirname;
|
|
114
|
+
} else {
|
|
115
|
+
dir = dirname(fileURLToPath(import.meta.url));
|
|
116
|
+
}
|
|
71
117
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
if (type === "env" && id.endsWith("/server")) {
|
|
119
|
+
return join(dir, "modules", "secrets.js");
|
|
120
|
+
}
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return join(dir, "modules", `${type}.js`);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
81
134
|
},
|
|
82
135
|
},
|
|
83
136
|
};
|
|
@@ -131,31 +131,44 @@ export const registerAstroComponent = (key, component) => {
|
|
|
131
131
|
props,
|
|
132
132
|
resolve: () => "editable-region-placeholder",
|
|
133
133
|
/**
|
|
134
|
-
* @param {*}
|
|
135
|
-
* @param {*} props
|
|
136
|
-
* @param {*} slots
|
|
134
|
+
* @param {*} args
|
|
137
135
|
*/
|
|
138
|
-
createAstro(
|
|
136
|
+
createAstro(...args) {
|
|
137
|
+
if (args.length < 2 || args.length > 3) {
|
|
138
|
+
console.warn(
|
|
139
|
+
`[CloudCannon] createAstro called with unexpected number of arguments (${args.length})`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let astroGlobal = SSRResult;
|
|
144
|
+
let componentProps, componentSlots;
|
|
145
|
+
|
|
146
|
+
if (args.length === 2) {
|
|
147
|
+
[componentProps, componentSlots] = args;
|
|
148
|
+
} else {
|
|
149
|
+
[astroGlobal, componentProps, componentSlots] = args;
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
const astroSlots = {
|
|
140
153
|
/**
|
|
141
154
|
* @param {string} name
|
|
142
155
|
* @returns boolean
|
|
143
156
|
*/
|
|
144
157
|
has: (name) => {
|
|
145
|
-
if (!
|
|
146
|
-
return Boolean(
|
|
158
|
+
if (!componentSlots) return false;
|
|
159
|
+
return Boolean(componentSlots[name]);
|
|
147
160
|
},
|
|
148
161
|
/**
|
|
149
162
|
* @param {string} name
|
|
150
163
|
* @returns string
|
|
151
164
|
*/
|
|
152
165
|
render: (name) => {
|
|
153
|
-
return renderSlotToString(SSRResult,
|
|
166
|
+
return renderSlotToString(SSRResult, componentSlots[name]);
|
|
154
167
|
},
|
|
155
168
|
};
|
|
156
169
|
return {
|
|
157
170
|
__proto__: astroGlobal,
|
|
158
|
-
props,
|
|
171
|
+
props: componentProps,
|
|
159
172
|
slots: astroSlots,
|
|
160
173
|
request: new Request(window.location.href),
|
|
161
174
|
};
|
|
@@ -171,100 +171,112 @@ export default class EditableComponent extends Editable {
|
|
|
171
171
|
targetNode?: ChildNode | null,
|
|
172
172
|
renderNode?: ChildNode | null,
|
|
173
173
|
): void {
|
|
174
|
-
let
|
|
174
|
+
let nextTargetChild: ChildNode | null | undefined =
|
|
175
175
|
targetNode?.firstChild ?? undefined;
|
|
176
|
-
let
|
|
176
|
+
let nextRenderChild: ChildNode | null | undefined =
|
|
177
177
|
renderNode?.firstChild ?? undefined;
|
|
178
|
-
while (renderChild || targetChild) {
|
|
179
|
-
const nextTargetChild: ChildNode | null | undefined =
|
|
180
|
-
targetChild?.nextSibling ?? undefined;
|
|
181
|
-
const nextRenderChild: ChildNode | null | undefined =
|
|
182
|
-
renderChild?.nextSibling ?? undefined;
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
while (nextRenderChild || nextTargetChild) {
|
|
180
|
+
const targetChild: ChildNode | null | undefined = nextTargetChild;
|
|
181
|
+
const renderChild: ChildNode | null | undefined = nextRenderChild;
|
|
182
|
+
|
|
183
|
+
nextTargetChild = targetChild?.nextSibling ?? undefined;
|
|
184
|
+
nextRenderChild = renderChild?.nextSibling ?? undefined;
|
|
185
|
+
|
|
186
|
+
// There is an existing node in the DOM but no rendered node in it's place
|
|
187
|
+
if (!renderChild && targetChild) {
|
|
188
|
+
targetNode?.removeChild(targetChild);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// There is a node to be rendered, but no existing node in the DOM in it's place
|
|
193
|
+
if (renderChild && !targetChild) {
|
|
194
|
+
targetNode?.appendChild(renderChild);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!targetChild || !renderChild) {
|
|
199
|
+
throw new Error("Illegal rendering state, both children should exist ");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The existing child is an element and the rendered child is a node (i.e. some text)
|
|
203
|
+
if (targetChild instanceof Element && !(renderChild instanceof Element)) {
|
|
204
|
+
// Render the (text) node before the element and skip to the next rendered child
|
|
189
205
|
targetChild.before(renderChild);
|
|
190
|
-
|
|
206
|
+
nextTargetChild = targetChild;
|
|
191
207
|
continue;
|
|
192
208
|
}
|
|
193
209
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
!(targetChild instanceof Element)
|
|
198
|
-
) {
|
|
210
|
+
// The existing child is a node (i.e. some text) and the rendered child is an element
|
|
211
|
+
if (renderChild instanceof Element && !(targetChild instanceof Element)) {
|
|
212
|
+
// Delete the (text) node and skip to the next existing child
|
|
199
213
|
targetChild.remove();
|
|
200
|
-
|
|
214
|
+
nextRenderChild = renderChild;
|
|
201
215
|
continue;
|
|
202
216
|
}
|
|
203
217
|
|
|
218
|
+
// Both existing and rendered children are nodes (i.e. some text)
|
|
204
219
|
if (
|
|
205
|
-
renderChild &&
|
|
206
|
-
targetChild &&
|
|
207
220
|
!(renderChild instanceof Element) &&
|
|
208
221
|
!(targetChild instanceof Element)
|
|
209
222
|
) {
|
|
223
|
+
// Render the offscreen node's content into the existing node.
|
|
210
224
|
targetChild.nodeValue = renderChild.nodeValue;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Both children are elements but are different types
|
|
229
|
+
if (renderChild.nodeName !== targetChild.nodeName) {
|
|
230
|
+
targetChild.replaceWith(renderChild);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Both existing and rendered children are the same kind of element, and neither is editable
|
|
235
|
+
if (!isEditableElement(renderChild) && !isEditableElement(targetChild)) {
|
|
236
|
+
// Update the existing element to match the rendered element and recurse their subtrees
|
|
237
|
+
this.updateNode(targetChild, renderChild);
|
|
238
|
+
this.updateTree(targetChild, renderChild);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
!(renderChild instanceof HTMLElement) ||
|
|
244
|
+
!(targetChild instanceof HTMLElement)
|
|
216
245
|
) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} else if (hasEditable(targetChild)) {
|
|
244
|
-
this.updateEditable(renderChild, targetChild);
|
|
245
|
-
}
|
|
246
|
-
} else if (hasEditable(targetChild)) {
|
|
247
|
-
this.updateEditable(renderChild, targetChild);
|
|
248
|
-
}
|
|
249
|
-
} else if (renderChild && targetChild) {
|
|
250
|
-
if (
|
|
251
|
-
renderChild.nodeName !== targetChild.nodeName ||
|
|
252
|
-
isEditableElement(renderChild) ||
|
|
253
|
-
isEditableElement(targetChild)
|
|
254
|
-
) {
|
|
255
|
-
targetChild.replaceWith(renderChild);
|
|
256
|
-
} else {
|
|
257
|
-
this.updateNode(targetChild, renderChild);
|
|
258
|
-
this.updateTree(targetChild, renderChild);
|
|
259
|
-
}
|
|
260
|
-
} else if (renderChild) {
|
|
261
|
-
targetNode?.appendChild(renderChild);
|
|
262
|
-
} else if (targetChild) {
|
|
263
|
-
targetNode?.removeChild(targetChild);
|
|
246
|
+
throw new Error("Illegal state, both children should be elements");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The existing and rendered child either aren't both editable or are different kinds of editables
|
|
250
|
+
if (!areEqualEditables(renderChild, targetChild)) {
|
|
251
|
+
targetChild.replaceWith(renderChild);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!isEditableElement(renderChild) || !isEditableElement(targetChild)) {
|
|
256
|
+
throw new Error("Illegal state, both children should be editable");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Both elements are editable but the existing element hasn't hydrated
|
|
260
|
+
if (!hasEditable(targetChild)) {
|
|
261
|
+
targetChild.replaceWith(renderChild);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The existing and rendered child are the same kind of non-text editable.
|
|
266
|
+
if (!isEditableText(targetChild)) {
|
|
267
|
+
this.updateEditable(renderChild, targetChild);
|
|
268
|
+
continue;
|
|
264
269
|
}
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
|
|
271
|
+
// The existing element is currently focused or is already the same as the rendered element
|
|
272
|
+
if (
|
|
273
|
+
(hasEditableText(targetChild) && targetChild.editable.focused) ||
|
|
274
|
+
targetChild?.nodeName === renderChild.nodeName
|
|
275
|
+
) {
|
|
276
|
+
this.updateEditable(renderChild, targetChild);
|
|
277
|
+
} else {
|
|
278
|
+
targetChild.replaceWith(renderChild);
|
|
279
|
+
}
|
|
268
280
|
}
|
|
269
281
|
}
|
|
270
282
|
|
|
@@ -351,23 +363,6 @@ export default class EditableComponent extends Editable {
|
|
|
351
363
|
);
|
|
352
364
|
}
|
|
353
365
|
|
|
354
|
-
setupListeners(): void {
|
|
355
|
-
super.setupListeners();
|
|
356
|
-
const key = this.element.dataset.component;
|
|
357
|
-
if (!key) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const component = this.getComponents()?.[key];
|
|
362
|
-
if (!component) {
|
|
363
|
-
document.addEventListener(
|
|
364
|
-
`editable-regions:registered-${key}`,
|
|
365
|
-
() => this.update(),
|
|
366
|
-
{ once: true },
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
366
|
mount(): void {
|
|
372
367
|
if (!this.controlsElement) {
|
|
373
368
|
let editPath: string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcannon/editable-regions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Visual Editing for the CloudCannon CMS.",
|
|
6
6
|
"keywords": [
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"./internal/styles": "./styles/index.js"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@biomejs/biome": "2.
|
|
57
|
-
"@cloudcannon/javascript-api": "0.0.
|
|
56
|
+
"@biomejs/biome": "2.4.4",
|
|
57
|
+
"@cloudcannon/javascript-api": "0.0.12",
|
|
58
58
|
"@types/js-beautify": "1.14.3",
|
|
59
59
|
"@types/react": "19.2.8",
|
|
60
60
|
"@types/react-dom": "18.3.1",
|
|
@@ -9,7 +9,9 @@ editable-array-item {
|
|
|
9
9
|
will-change: contents;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
@supports not (anchor-name: --a) {
|
|
13
|
+
editable-array-item,
|
|
14
|
+
[data-editable="array-item"] {
|
|
15
|
+
position: relative;
|
|
16
|
+
}
|
|
15
17
|
}
|
|
@@ -9,7 +9,9 @@ editable-component {
|
|
|
9
9
|
will-change: contents;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
@supports not (anchor-name: --a) {
|
|
13
|
+
editable-component,
|
|
14
|
+
[data-editable="component"] {
|
|
15
|
+
position: relative;
|
|
16
|
+
}
|
|
15
17
|
}
|
|
@@ -90,6 +90,27 @@ button {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
.context-menu:popover-open {
|
|
94
|
+
display: block;
|
|
95
|
+
pointer-events: auto;
|
|
96
|
+
inset: auto;
|
|
97
|
+
margin: 4px 4px 0 0;
|
|
98
|
+
position: fixed;
|
|
99
|
+
top: anchor(bottom);
|
|
100
|
+
right: anchor(right);
|
|
101
|
+
position-try-fallbacks: flip-block;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@supports (anchor-name: --a) {
|
|
105
|
+
:host {
|
|
106
|
+
position: fixed;
|
|
107
|
+
top: anchor(top);
|
|
108
|
+
right: anchor(right);
|
|
109
|
+
margin-top: var(--ccve-editable-outline-width);
|
|
110
|
+
margin-right: var(--ccve-editable-outline-width);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
@keyframes fade-in-up {
|
|
94
115
|
from {
|
|
95
116
|
opacity: 0.5;
|