@cloudcannon/editable-regions 0.0.20-rc.3 → 0.0.21

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.
@@ -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
- this.loadInputConfig().then(() => {
189
- this.imageEl?.addEventListener("click", async (e) => {
190
- e.preventDefault();
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
- if (!this.value) {
193
- throw new Error("Value is not defined");
194
- }
193
+ this.imageEl?.addEventListener("click", async (e) => {
194
+ e.preventDefault();
195
195
 
196
- const data: this["value"] = {};
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
- this.panelId = await CloudCannon.createCustomDataPanel({
208
- title: "Edit Image",
209
- data,
210
- position: this.imageEl?.getBoundingClientRect(),
211
- config: {
212
- _inputs: {
213
- src: {
214
- label: "Image",
215
- type: "image",
216
- ...this.inputConfig.src,
217
- },
218
- alt: {
219
- comment:
220
- "A description which provides information about this image if for some reason it cannot be viewed.",
221
- ...this.inputConfig.alt,
222
- },
223
- title: {
224
- comment: "Displayed when hovering over the image.",
225
- ...this.inputConfig.title,
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
- onChange: (value): void => {
230
- if (!value || typeof value !== "object") {
231
- throw new Error("Invalid image data");
232
- }
234
+ },
235
+ onChange: (value): void => {
236
+ if (!value || typeof value !== "object") {
237
+ throw new Error("Invalid image data");
238
+ }
233
239
 
234
- if (
235
- "src" in value &&
236
- this.configuredSrc &&
237
- value.src !== this.value?.src
238
- ) {
239
- this.dispatchSet(
240
- this.element.dataset.propSrc ??
241
- `${this.element.dataset.prop}.src`,
242
- value.src,
243
- );
244
- }
245
- if (
246
- "alt" in value &&
247
- this.configuredAlt &&
248
- value.alt !== this.value?.alt
249
- ) {
250
- this.dispatchSet(
251
- this.element.dataset.propAlt ??
252
- `${this.element.dataset.prop}.alt`,
253
- value.alt,
254
- );
255
- }
256
- if (
257
- "title" in value &&
258
- this.configuredTitle &&
259
- value.title !== this.value?.title
260
- ) {
261
- this.dispatchSet(
262
- this.element.dataset.propTitle ??
263
- `${this.element.dataset.prop}.title`,
264
- value.title,
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
  }
package/nodes/editable.ts CHANGED
@@ -174,7 +174,19 @@ export default class Editable {
174
174
  return true;
175
175
  }
176
176
 
177
+ private parentMounted(): boolean {
178
+ if (this.parent) {
179
+ return this.parent.mounted;
180
+ }
181
+
182
+ return !this.pendingParentElement;
183
+ }
184
+
177
185
  shouldMount() {
186
+ if (this.mounted) {
187
+ return false;
188
+ }
189
+
178
190
  return this.value !== undefined;
179
191
  }
180
192
 
@@ -211,8 +223,10 @@ export default class Editable {
211
223
  contexts?: { [key: string]: EditableContext },
212
224
  ): Promise<unknown> {
213
225
  const { key, path } = listener ?? {};
226
+ let shouldValidate = false;
214
227
 
215
228
  if (typeof path === "string") {
229
+ shouldValidate = true;
216
230
  const { value: resolvedValue, context: newContext } =
217
231
  await this.lookupPathAndContext(path, value, contexts);
218
232
 
@@ -255,6 +269,7 @@ export default class Editable {
255
269
  ({ key }) => !!key,
256
270
  );
257
271
  if (filteredSpecialPropsListener.length > 0) {
272
+ shouldValidate = true;
258
273
  newValue = filteredSpecialPropsListener.reduce(
259
274
  (acc, { key, path }) => {
260
275
  if (key && path) {
@@ -271,6 +286,7 @@ export default class Editable {
271
286
  }
272
287
 
273
288
  if (Object.entries(literalProps).length > 0) {
289
+ shouldValidate = true;
274
290
  newValue = Object.entries(literalProps).reduce(
275
291
  (acc, [key, val]) => {
276
292
  (acc as any)[key] = structuredClone(val);
@@ -282,7 +298,11 @@ export default class Editable {
282
298
  );
283
299
  }
284
300
 
285
- return this.validateValue(newValue);
301
+ if (shouldValidate) {
302
+ newValue = this.validateValue(newValue);
303
+ }
304
+
305
+ return newValue;
286
306
  }
287
307
 
288
308
  async pushValue(
@@ -299,15 +319,18 @@ export default class Editable {
299
319
  contexts,
300
320
  );
301
321
 
302
- if (typeof newValue === "undefined" || !this.shouldUpdate(newValue)) {
322
+ if (
323
+ typeof newValue === "undefined" ||
324
+ !this.shouldUpdate(newValue) ||
325
+ !this.parentMounted()
326
+ ) {
303
327
  return;
304
328
  }
305
329
 
306
330
  this.value = newValue;
307
- if (this.connected && !this.mounted) {
331
+ if (this.connected && this.shouldMount()) {
308
332
  this.mounted = true;
309
333
  this.mount();
310
- return this.update(partialSubtree);
311
334
  }
312
335
 
313
336
  if (this.mounted) {
@@ -440,7 +463,7 @@ export default class Editable {
440
463
  this.connectPromise = apiLoadedPromise.then(() => {
441
464
  this.setupListeners();
442
465
  this.connected = true;
443
- if (!this.mounted && this.shouldMount()) {
466
+ if (this.parentMounted() && this.shouldMount()) {
444
467
  this.mounted = true;
445
468
  this.mount();
446
469
  this.update();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.20-rc.3",
3
+ "version": "0.0.21",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -37,7 +37,8 @@
37
37
  "test:build-astro-fixture": "npm --prefix test/unit/_fixtures/astro install --silent && npm --prefix test/unit/_fixtures/astro run build",
38
38
  "test:build-eleventy-fixture": "npm --prefix test/unit/_fixtures/eleventy install --silent && npm --prefix test/unit/_fixtures/eleventy run build",
39
39
  "test:build-eleventy-plugin-config": "npm --prefix test/unit/_fixtures/eleventy-plugin-config install --silent && npm --prefix test/unit/_fixtures/eleventy-plugin-config run build",
40
- "test:build-hugo-fixture": "npm --prefix test/unit/_fixtures/hugo run build"
40
+ "test:build-hugo-fixture": "npm --prefix test/unit/_fixtures/hugo run build",
41
+ "test:integration": "npm run build:hugo && bash run-integration-tests.sh"
41
42
  },
42
43
  "files": [
43
44
  "components",
@@ -77,7 +78,8 @@
77
78
  },
78
79
  "./svelte": {
79
80
  "types": "./types/svelte.d.ts",
80
- "default": "./integrations/svelte.mjs"
81
+ "svelte": "./integrations/svelte/index.mjs",
82
+ "default": "./integrations/svelte/index.mjs"
81
83
  },
82
84
  "./vue": {
83
85
  "types": "./types/vue.d.ts",
@@ -103,30 +105,30 @@
103
105
  "./internal/styles": "./styles/index.js"
104
106
  },
105
107
  "devDependencies": {
106
- "@astrojs/react": "6.0.2",
108
+ "@astrojs/react": "6.0.5",
107
109
  "@astrojs/svelte": "9.0.1",
108
110
  "@astrojs/vue": "7.0.2",
109
- "@biomejs/biome": "2.5.8",
111
+ "@biomejs/biome": "2.5.13",
110
112
  "@cloudcannon/visual-editor-api": "0.0.20",
111
- "@sindresorhus/slugify": "3.0.0",
113
+ "@sindresorhus/slugify": "3.0.1",
112
114
  "@sveltejs/vite-plugin-svelte": "7.3.0",
113
115
  "@types/js-beautify": "1.14.3",
114
- "@types/node": "26.2.0",
115
- "@types/react": "19.2.18",
116
- "@types/react-dom": "19.2.4",
117
- "@vitejs/plugin-vue": "6.0.8",
118
- "astro": "7.2.2",
119
- "happy-dom": "20.11.2",
116
+ "@types/node": "26.5.1",
117
+ "@types/react": "19.3.0",
118
+ "@types/react-dom": "19.3.0",
119
+ "@vitejs/plugin-vue": "6.0.9",
120
+ "astro": "7.3.2",
121
+ "happy-dom": "20.14.5",
120
122
  "js-beautify": "2.0.3",
121
123
  "liquidjs": "10.29.0",
122
- "react": "19.2.8",
123
- "react-dom": "19.2.8",
124
+ "react": "19.3.0",
125
+ "react-dom": "19.3.0",
124
126
  "slugify": "1.6.9",
125
- "svelte": "5.56.9",
127
+ "svelte": "5.57.0",
126
128
  "typescript": "6.0.3",
127
- "vitest": "4.1.10",
128
- "vue": "3.5.41",
129
- "vue-tsc": "3.3.10"
129
+ "vitest": "^4.1.11",
130
+ "vue": "3.5.42",
131
+ "vue-tsc": "3.3.11"
130
132
  },
131
133
  "dependencies": {
132
134
  "esbuild": "0.28.2"
package/types/react.d.ts CHANGED
@@ -1,32 +1,38 @@
1
1
  /// <reference path="./cloudcannon.d.ts" />
2
2
 
3
- import type { HTMLAttributes, RefAttributes } from "react";
3
+ import type { FC, HTMLAttributes, RefAttributes } from "react";
4
4
 
5
5
  export function registerReactComponent(key: string, component: unknown): void;
6
6
 
7
+ export const EditableRegions: FC<
8
+ HTMLAttributes<HTMLElement> & {
9
+ tag?: string;
10
+ }
11
+ >;
12
+
7
13
  declare global {
8
14
  namespace React.JSX {
9
15
  interface IntrinsicElements {
10
16
  "editable-component": RefAttributes<HTMLElement> &
11
- Omit<HTMLAttributes<HTMLElement>, "className"> & {
17
+ HTMLAttributes<HTMLElement> & {
12
18
  class?: string;
13
19
  "data-prop": string;
14
20
  "data-component": string;
15
21
  };
16
22
  "editable-text": RefAttributes<HTMLElement> &
17
- Omit<HTMLAttributes<HTMLElement>, "className"> & {
23
+ HTMLAttributes<HTMLElement> & {
18
24
  class?: string;
19
25
  "data-prop": string;
20
26
  "data-type"?: "block" | "text" | "span";
21
27
  };
22
28
  "editable-source": RefAttributes<HTMLElement> &
23
- Omit<HTMLAttributes<HTMLElement>, "className"> & {
29
+ HTMLAttributes<HTMLElement> & {
24
30
  class?: string;
25
31
  "data-path": string;
26
32
  "data-key": string;
27
33
  };
28
34
  "editable-array": RefAttributes<HTMLElement> &
29
- Omit<HTMLAttributes<HTMLElement>, "className"> & {
35
+ HTMLAttributes<HTMLElement> & {
30
36
  class?: string;
31
37
  "data-prop": string;
32
38
  "data-id-key"?: string;
@@ -39,11 +45,19 @@ declare global {
39
45
  | "row-reverse";
40
46
  };
41
47
  "editable-array-item": RefAttributes<HTMLElement> &
42
- Omit<HTMLAttributes<HTMLElement>, "className"> & {
48
+ HTMLAttributes<HTMLElement> & {
43
49
  class?: string;
44
50
  "data-id"?: string;
45
51
  "data-component"?: string;
46
52
  };
53
+ "editable-image": RefAttributes<HTMLElement> &
54
+ HTMLAttributes<HTMLElement> & {
55
+ class?: string;
56
+ "data-prop"?: string;
57
+ "data-prop-src"?: string;
58
+ "data-prop-alt"?: string;
59
+ "data-prop-title"?: string;
60
+ };
47
61
  }
48
62
  }
49
63
  }
package/types/svelte.d.ts CHANGED
@@ -1,3 +1,13 @@
1
1
  /// <reference path="./cloudcannon.d.ts" />
2
2
 
3
+ import type { SvelteComponentTyped } from "svelte";
4
+
3
5
  export function registerSvelteComponent(key: string, component: unknown): void;
6
+
7
+ export const EditableRegions: import("svelte").ComponentType<
8
+ SvelteComponentTyped<
9
+ {
10
+ tag?: string;
11
+ } & Record<string, any>
12
+ >
13
+ >;
package/types/vue.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  /// <reference path="./cloudcannon.d.ts" />
2
2
 
3
- import type { HTMLAttributes } from "vue";
3
+ import type { DefineComponent, HTMLAttributes } from "vue";
4
4
 
5
5
  export function registerVueComponent(key: string, component: unknown): void;
6
6
 
7
+ export const EditableRegions: DefineComponent<
8
+ Omit<HTMLAttributes, "data-prop"> & {
9
+ tag?: string;
10
+ }
11
+ >;
12
+
7
13
  declare global {
8
14
  namespace JSX {
9
15
  interface IntrinsicElements {
@@ -17,6 +23,13 @@ declare global {
17
23
  "data-prop": string;
18
24
  "data-type"?: "block" | "text" | "span";
19
25
  };
26
+ "editable-image": HTMLAttributes & {
27
+ class?: string;
28
+ "data-prop"?: string;
29
+ "data-prop-src"?: string;
30
+ "data-prop-alt"?: string;
31
+ "data-prop-title"?: string;
32
+ };
20
33
  "editable-source": HTMLAttributes & {
21
34
  class?: string;
22
35
  "data-path": string;