@cloudcannon/editable-regions 0.0.7 → 0.0.9

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.
@@ -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.js";
9
+ import { apiLoadedPromise } from "../helpers/cloudcannon.mjs";
10
10
  import {
11
11
  dehydrateDataEditableRegions,
12
12
  hydrateDataEditableRegions,
@@ -22,11 +22,13 @@ export default class EditableRegionButton extends HTMLElement {
22
22
  button.innerHTML = `<cc-icon name='${this.getAttribute("icon")}'></cc-icon>${this.getAttribute("text")}`;
23
23
  shadow.appendChild(button);
24
24
 
25
- button.addEventListener("click", (e) => {
26
- this.dispatchEvent(
27
- new CustomEvent("button-click", { detail: { originalEvent: e } }),
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
@@ -1,6 +1,6 @@
1
+ import Editable from "../nodes/editable.js";
1
2
  import EditableArrayItem from "../nodes/editable-array-item.js";
2
3
  import EditableText from "../nodes/editable-text.js";
3
- import Editable from "../nodes/editable.js";
4
4
 
5
5
  const TAG_NAMES = [
6
6
  "EDITABLE-TEXT",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Promise that resolves when the CloudCannon API is loaded
3
+ * @type {Promise<void>}
4
+ */
5
+ export const apiLoadedPromise: Promise<void>;
6
+ export function addEditableComponentRenderer(key: string, renderer: ComponentRenderer): void;
7
+ export function addEditableSnippetRenderer(key: string, renderer: ComponentRenderer): void;
8
+ export function getEditableComponentRenderers(): Record<string, ComponentRenderer>;
9
+ export function getEditableSnippetRenderers(): Record<string, ComponentRenderer>;
10
+ export function realizeAPIValue(value: unknown): Promise<unknown>;
11
+ export { _cloudcannon as CloudCannon };
12
+ export type CloudCannonEditorWindow = import("@cloudcannon/javascript-api").CloudCannonEditorWindow;
13
+ export type CloudCannonJavaScriptV1API = import("@cloudcannon/javascript-api").CloudCannonJavaScriptV1API;
14
+ export type ComponentRenderer = (props: any) => HTMLElement | Promise<HTMLElement>;
15
+ export type ExtendedWindow = CloudCannonEditorWindow & {
16
+ cc_components?: Record<string, ComponentRenderer>;
17
+ cc_snippets?: Record<string, ComponentRenderer>;
18
+ };
19
+ /** @type {CloudCannonJavaScriptV1API} */
20
+ declare let _cloudcannon: CloudCannonJavaScriptV1API;
@@ -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?.unshift({
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 (e) {
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 (err) {
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
  );
@@ -1,4 +1,4 @@
1
- import { CloudCannon } from "../../../helpers/cloudcannon";
1
+ import { CloudCannon } from "../../../helpers/cloudcannon.mjs";
2
2
 
3
3
  /**
4
4
  * @param {string} collectionKey
@@ -24,7 +24,7 @@ addFrameworkRenderer({
24
24
  try {
25
25
  const reactNode = Component(props);
26
26
  return { html: renderToStaticMarkup(reactNode) };
27
- } catch (err) {
27
+ } catch (_err) {
28
28
  const id = queueForClientSideRender((node) => {
29
29
  const reactNode = createElement(Component, props, null);
30
30
  const root = createRoot(node);
@@ -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.js";
9
+ import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.mjs";
10
10
  import type EditableArray from "./editable-array.js";
11
11
  import EditableComponent from "./editable-component.js";
12
12
 
13
13
  export default class EditableArrayItem extends EditableComponent {
14
14
  parent: EditableArray | null = null;
15
15
 
16
- protected controlsElement?: EditableArrayItemControls;
16
+ protected declare controlsElement?: EditableArrayItemControls;
17
17
 
18
18
  private inputConfig?: any;
19
19
  private structureStrings: string[] = [];
@@ -292,7 +292,7 @@ export default class EditableArrayItem extends EditableComponent {
292
292
  this.controlsElement = document.createElement(
293
293
  "editable-array-item-controls",
294
294
  );
295
- this.controlsElement.addEventListener("edit", (e: any) => {
295
+ this.controlsElement.addEventListener("edit", (_e: any) => {
296
296
  this.dispatchEdit(this.element.dataset.prop);
297
297
  });
298
298
 
@@ -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.js";
10
- import type EditableArrayItem from "./editable-array-item.js";
9
+ import { CloudCannon } from "../helpers/cloudcannon.mjs";
11
10
  import Editable, { type EditableListener } from "./editable.js";
11
+ import type EditableArrayItem from "./editable-array-item.js";
12
12
  import "../components/ui/editable-region-button.js";
13
13
 
14
14
  const arrayDirectionValues = [
@@ -186,6 +186,44 @@ export default class EditableArray extends Editable {
186
186
  value = [];
187
187
  }
188
188
 
189
+ const templates: {
190
+ keyed: Record<
191
+ string,
192
+ HTMLElement & {
193
+ editable?: EditableArrayItem;
194
+ }
195
+ >;
196
+ unkeyed?: HTMLElement & {
197
+ editable?: EditableArrayItem;
198
+ };
199
+ } = { keyed: {} };
200
+
201
+ for (let i = 0; i < this.element.children.length; i += 1) {
202
+ const childEl = this.element.children[i];
203
+ if (
204
+ childEl instanceof HTMLTemplateElement &&
205
+ typeof childEl.dataset.cloudcannonIgnore !== "string"
206
+ ) {
207
+ const key = childEl.dataset.id;
208
+ const content = childEl.content;
209
+ let templateEl: HTMLElement;
210
+ if (content.childElementCount === 1) {
211
+ templateEl = content.children[0] as HTMLElement;
212
+ } else {
213
+ templateEl = document.createElement(
214
+ "editable-array-item",
215
+ ) as HTMLElement;
216
+ templateEl.append(content.cloneNode(true));
217
+ }
218
+
219
+ if (typeof key === "string") {
220
+ templates.keyed[key] = templateEl;
221
+ } else {
222
+ templates.unkeyed = templateEl;
223
+ }
224
+ }
225
+ }
226
+
189
227
  const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
190
228
 
191
229
  for (const child of this.element.querySelectorAll(
@@ -206,7 +244,9 @@ export default class EditableArray extends Editable {
206
244
  if (
207
245
  children.length === 0 &&
208
246
  !this.element.dataset.component &&
209
- !this.element.dataset.componentKey
247
+ !this.element.dataset.componentKey &&
248
+ !templates.unkeyed &&
249
+ Object.keys(templates.keyed).length === 0
210
250
  ) {
211
251
  const error = document.createElement("editable-region-error-card");
212
252
  error.setAttribute("heading", "Failed to render array editable region");
@@ -218,7 +258,9 @@ export default class EditableArray extends Editable {
218
258
  return;
219
259
  }
220
260
 
221
- if (!this.element.dataset.idKey) {
261
+ const key = this.element.dataset.idKey ?? this.element.dataset.componentKey;
262
+
263
+ if (!key) {
222
264
  while (children.length > value.length) {
223
265
  children.pop()?.remove();
224
266
  }
@@ -233,7 +275,12 @@ export default class EditableArray extends Editable {
233
275
  for (let i = 0; i < value.length; i++) {
234
276
  let child = children[i];
235
277
  if (!child) {
236
- if (this.element.dataset.component) {
278
+ if (templates.unkeyed) {
279
+ child = templates.unkeyed.cloneNode(true) as HTMLElement & {
280
+ editable?: EditableArrayItem;
281
+ };
282
+ child.dataset.editable = "array-item";
283
+ } else if (this.element.dataset.component) {
237
284
  child = document.createElement(
238
285
  "editable-array-item",
239
286
  ) as EditableArrayItemComponent;
@@ -260,7 +307,6 @@ export default class EditableArray extends Editable {
260
307
  return;
261
308
  }
262
309
 
263
- const key = this.element.dataset.idKey;
264
310
  const componentKey = this.element.dataset.componentKey;
265
311
  const dataKeys: (string | null)[] = [];
266
312
  const componentKeys: (string | null)[] = [];
@@ -287,14 +333,22 @@ export default class EditableArray extends Editable {
287
333
  }
288
334
 
289
335
  const childKeys = children.map((element) => {
290
- return element.dataset.id;
336
+ return element.dataset.id ?? element.dataset.component;
291
337
  });
292
338
 
293
339
  const equal = dataKeys?.every((key, i) => key === childKeys[i]);
294
340
  if (equal && children.length === dataKeys?.length) {
295
341
  children.forEach((child, index) => {
342
+ const componentKey =
343
+ componentKeys[index] || this.element.dataset.component;
344
+
345
+ child.dataset.id = String(childKeys[index]);
296
346
  child.dataset.prop = `${index}`;
297
347
  child.dataset.length = `${children.length}`;
348
+ if (componentKey) {
349
+ child.dataset.component = componentKey;
350
+ }
351
+
298
352
  child.editable?.pushValue(
299
353
  value,
300
354
  { path: `${index}`, editable: child.editable },
@@ -324,6 +378,7 @@ export default class EditableArray extends Editable {
324
378
  }
325
379
  const placeholder = placeholders[i];
326
380
  const existingElement = children[i];
381
+ const templateElement = templates.keyed[key] ?? templates.unkeyed;
327
382
  const componentKey = componentKeys[i] || this.element.dataset.component;
328
383
 
329
384
  const matchingChildIndex = children.findIndex(
@@ -333,18 +388,19 @@ export default class EditableArray extends Editable {
333
388
  let matchingChild = children[matchingChildIndex];
334
389
  if (!matchingChild) {
335
390
  const clone = children.find((child) => child.dataset.id === key);
336
- if (clone) {
337
- matchingChild = clone.cloneNode(true) as any;
391
+ if (templateElement) {
392
+ matchingChild = templateElement.cloneNode(true) as any;
393
+ matchingChild.dataset.editable = "array-item";
338
394
  } else if (componentKey) {
339
395
  matchingChild = document.createElement(
340
396
  "editable-array-item",
341
397
  ) as EditableArrayItemComponent;
342
- matchingChild.dataset.id = key;
343
- matchingChild.dataset.component = componentKey;
398
+ } else if (clone) {
399
+ matchingChild = clone.cloneNode(true) as any;
344
400
  } else {
345
401
  const error = document.createElement("editable-region-error-card");
346
402
  error.setAttribute("heading", "Failed to render array item");
347
- if (typeof this.element.dataset.componentKey === "string") {
403
+ if (typeof componentKey === "string") {
348
404
  error.setAttribute(
349
405
  "message",
350
406
  "Array editable region has no child with a matching 'data-id' value for this element and the value has no key matching the 'data-component-key' attribute. Please check that the 'data-component-key' attribute for this element is correct and that each element has an entry for that key, or provide a fallback 'data-component' attribute.",
@@ -352,7 +408,7 @@ export default class EditableArray extends Editable {
352
408
  error.setAttribute(
353
409
  "hint",
354
410
  `This may mean that the value for 'data-component-key' is incorrect or that your array data is incorrectly formatted.
355
- The current value for 'data-component-key' is '${this.element.dataset.componentKey}' and the current value for 'data-id' is '${key}'.
411
+ The current value for 'data-component-key' is '${componentKey}' and the current value for 'data-id' is '${key}'.
356
412
  `,
357
413
  );
358
414
  } else {
@@ -374,6 +430,11 @@ export default class EditableArray extends Editable {
374
430
 
375
431
  matchingChild.dataset.id = String(key);
376
432
  matchingChild.dataset.prop = `${i}`;
433
+ matchingChild.dataset.length = `${dataKeys.length}`;
434
+
435
+ if (componentKey) {
436
+ matchingChild.dataset.component = componentKey;
437
+ }
377
438
 
378
439
  if (existingElement === matchingChild) {
379
440
  placeholder.remove();
@@ -12,7 +12,7 @@ import type EditableComponentControls from "../components/ui/editable-component-
12
12
  import {
13
13
  getEditableComponentRenderers,
14
14
  realizeAPIValue,
15
- } from "../helpers/cloudcannon.js";
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(this.value);
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", (e: any) => {
335
+ this.controlsElement.addEventListener("edit", (_e: any) => {
336
336
  this.dispatchEdit(editPath);
337
337
  });
338
338
  this.element.append(this.controlsElement);
@@ -1,4 +1,4 @@
1
- import { CloudCannon } from "../helpers/cloudcannon.js";
1
+ import { CloudCannon } from "../helpers/cloudcannon.mjs";
2
2
  import Editable from "./editable.js";
3
3
 
4
4
  export default class EditableImage extends Editable {
@@ -1,6 +1,6 @@
1
- import { getEditableSnippetRenderers } from "../helpers/cloudcannon.js";
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() {
@@ -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.js";
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;
@@ -1,4 +1,4 @@
1
- import { CloudCannon } from "../helpers/cloudcannon.js";
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
- value: string | null | undefined;
10
+
11
+ declare value: string | null | undefined;
11
12
 
12
13
  validateConfiguration(): boolean {
13
14
  const prop = this.element.dataset.prop;
package/nodes/editable.ts CHANGED
@@ -4,7 +4,7 @@ import type {
4
4
  CloudCannonJavaScriptV1APIFile,
5
5
  } from "@cloudcannon/javascript-api";
6
6
  import { hasEditable } from "../helpers/checks";
7
- import { CloudCannon } from "../helpers/cloudcannon";
7
+ import { CloudCannon } from "../helpers/cloudcannon.mjs";
8
8
  import { loadingPromise } from "../helpers/loading";
9
9
 
10
10
  export interface EditableListener {
@@ -522,6 +522,28 @@ export default class Editable {
522
522
  });
523
523
  }
524
524
 
525
+ matchSourcePart(type: "collections" | "data" | "file", source?: string) {
526
+ if (!source) {
527
+ return;
528
+ }
529
+
530
+ if (!source.startsWith(`@${type}`)) {
531
+ return;
532
+ }
533
+
534
+ const argsPart = source.slice(type.length + 1);
535
+ const brackets = argsPart.match(/^\[+/);
536
+ if (!brackets) {
537
+ return;
538
+ }
539
+ const bracketsLength = brackets[0].length;
540
+ return argsPart.match(
541
+ new RegExp(
542
+ `^\\[{${bracketsLength}}(?<key>.+?)\\]{${bracketsLength}}(\\.(?<rest>.+))?$`,
543
+ ),
544
+ )?.groups;
545
+ }
546
+
525
547
  parseSource(source?: string) {
526
548
  let collection: CloudCannonJavaScriptV1APICollection | undefined;
527
549
  let file: CloudCannonJavaScriptV1APIFile | undefined;
@@ -529,29 +551,23 @@ export default class Editable {
529
551
  let absolute = false;
530
552
  let currentFile = false;
531
553
 
532
- const collectionMatch = source?.match(
533
- /^@collections\[(?<key>[^\]]+)\](\.(?<rest>.+))?$/,
534
- );
535
- if (collectionMatch?.groups) {
536
- const { key, rest } = collectionMatch.groups;
554
+ const collectionMatch = this.matchSourcePart("collections", source);
555
+ if (collectionMatch) {
556
+ const { key, rest } = collectionMatch;
537
557
  collection = CloudCannon.collection(key);
538
558
  source = rest;
539
559
  absolute = true;
540
560
  } else {
541
- const fileMatch = source?.match(
542
- /^@file\[(?<path>[^\]]+)\]\.(?<rest>.+)$/,
543
- );
544
- if (fileMatch?.groups) {
545
- const { path, rest } = fileMatch.groups;
546
- file = CloudCannon.file(path);
561
+ const fileMatch = this.matchSourcePart("file", source);
562
+ if (fileMatch) {
563
+ const { key, rest } = fileMatch;
564
+ file = CloudCannon.file(key);
547
565
  source = rest;
548
566
  absolute = true;
549
567
  } else {
550
- const dataMatch = source?.match(
551
- /^@data\[(?<key>[^\]]+)\](\.(?<rest>.+))?$/,
552
- );
553
- if (dataMatch?.groups) {
554
- const { key, rest } = dataMatch.groups;
568
+ const dataMatch = this.matchSourcePart("data", source);
569
+ if (dataMatch) {
570
+ const { key, rest } = dataMatch;
555
571
  dataset = CloudCannon.dataset(key);
556
572
  source = rest;
557
573
  absolute = true;
package/nodes/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { default as Editable } from "./editable.js";
2
2
  export { default as EditableArray } from "./editable-array.js";
3
3
  export { default as EditableArrayItem } from "./editable-array-item.js";
4
- export { default as EditableText } from "./editable-text.js";
5
4
  export { default as EditableComponent } from "./editable-component.js";
6
5
  export { default as EditableImage } from "./editable-image.js";
7
- export { default as EditableSource } from "./editable-source.js";
8
6
  export { default as EditableSnippet } from "./editable-snippet.js";
7
+ export { default as EditableSource } from "./editable-source.js";
8
+ export { default as EditableText } from "./editable-text.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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": "1.9.4",
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": "18.3.12",
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
- declare module "@cloudcannon/editable-regions/react" {
4
- export function registerReactComponent(key: string, component: unknown): void;
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
  }
@@ -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 };