@cloudcannon/editable-regions 0.0.5 → 0.0.6

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.
@@ -0,0 +1,39 @@
1
+ import styleContent from "../../styles/ui/editable-region-button.css?inline";
2
+
3
+ export default class EditableRegionButton extends HTMLElement {
4
+ private shadow?: ShadowRoot;
5
+
6
+ connectedCallback() {
7
+ if (this.shadow) {
8
+ return;
9
+ }
10
+
11
+ this.shadow = this.attachShadow({ mode: "open" });
12
+
13
+ this.render(this.shadow);
14
+ }
15
+
16
+ render(shadow: ShadowRoot) {
17
+ const style = document.createElement("style");
18
+ style.textContent = styleContent;
19
+ shadow.appendChild(style);
20
+
21
+ const button = document.createElement("button");
22
+ button.innerHTML = `<cc-icon name='${this.getAttribute("icon")}'></cc-icon>${this.getAttribute("text")}`;
23
+ shadow.appendChild(button);
24
+
25
+ button.addEventListener("click", (e) => {
26
+ this.dispatchEvent(
27
+ new CustomEvent("button-click", { detail: { originalEvent: e } }),
28
+ );
29
+ });
30
+ }
31
+ }
32
+
33
+ customElements.define("editable-region-button", EditableRegionButton);
34
+
35
+ declare global {
36
+ interface HTMLElementTagNameMap {
37
+ "editable-region-button": EditableRegionButton;
38
+ }
39
+ }
@@ -65,6 +65,14 @@ export default class EditableRegionErrorCard extends HTMLElement {
65
65
  body.appendChild(stack);
66
66
  }
67
67
  }
68
+
69
+ if (this.hasAttribute("hint")) {
70
+ const hint = document.createElement("p");
71
+ hint.className = "hint";
72
+ hint.innerHTML = this.getAttribute("hint") ?? "";
73
+
74
+ body.appendChild(hint);
75
+ }
68
76
  }
69
77
  }
70
78
 
package/helpers/checks.ts CHANGED
@@ -100,3 +100,10 @@ export const isEditableArrayItem = (el?: Element | null): boolean => {
100
100
  (el instanceof HTMLElement && el.dataset.editable === "array-item")
101
101
  );
102
102
  };
103
+
104
+ export const isEditableArray = (el?: Element | null): boolean => {
105
+ return (
106
+ el?.tagName === "EDITABLE-ARRAY" ||
107
+ (el instanceof HTMLElement && el.dataset.editable === "array")
108
+ );
109
+ };
@@ -52,7 +52,10 @@ export const hydrateDataEditableRegions = (root: Element) => {
52
52
  return;
53
53
  }
54
54
 
55
- if (!element.dataset.editable || element.dataset.cloudcannonIgnore) {
55
+ if (
56
+ typeof element.dataset.editable !== "string" ||
57
+ typeof element.dataset.cloudcannonIgnore === "string"
58
+ ) {
56
59
  return;
57
60
  }
58
61
 
@@ -62,7 +65,17 @@ export const hydrateDataEditableRegions = (root: Element) => {
62
65
  error.setAttribute("heading", "Failed to render editable region");
63
66
  error.setAttribute(
64
67
  "message",
65
- `Unrecognized editable type: "${element.dataset.editable}". The supported types are: ${Object.keys(editableMap).join(", ")}`,
68
+ `This element has an invalid editable type for the 'data-editable' HTML attribute. The supported editable types are ${Object.keys(
69
+ editableMap,
70
+ )
71
+ .map((type) => `"${type}"`)
72
+ .join(
73
+ ", ",
74
+ )} but instead received the type "${element.dataset.editable}". Please make sure that this element has a valid "data-editable" attribute.`,
75
+ );
76
+ error.setAttribute(
77
+ "hint",
78
+ "If this is intentional you can use the 'data-cloudcannon-ignore' attribute to exclude this element from editable regions.",
66
79
  );
67
80
  element.replaceWith(error);
68
81
  return;
@@ -2,10 +2,12 @@ import "../components/ui/editable-array-item-controls.js";
2
2
  import type EditableArrayItemControls from "../components/ui/editable-array-item-controls.js";
3
3
  import {
4
4
  hasEditableArrayItem,
5
+ isEditableArray,
5
6
  isEditableArrayItem,
7
+ isEditableElement,
6
8
  } from "../helpers/checks.js";
7
9
  import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.js";
8
- import EditableArray from "./editable-array.js";
10
+ import type EditableArray from "./editable-array.js";
9
11
  import EditableComponent from "./editable-component.js";
10
12
 
11
13
  export default class EditableArrayItem extends EditableComponent {
@@ -14,32 +16,25 @@ export default class EditableArrayItem extends EditableComponent {
14
16
  protected controlsElement?: EditableArrayItemControls;
15
17
 
16
18
  private inputConfig?: any;
19
+ private structureStrings: string[] = [];
17
20
 
18
21
  shouldMount(): boolean {
19
22
  return this.value !== undefined;
20
23
  }
21
24
 
22
25
  validateConfiguration(): boolean {
23
- const key = this.element.dataset.component;
24
- if (key) {
25
- const component = this.getComponents()[key];
26
- if (!component) {
27
- this.element.classList.add("errored");
28
- const error = document.createElement("editable-region-error-card");
29
- error.setAttribute("heading", "Failed to render component");
30
- error.setAttribute("message", `Couldn't find component '${key}'`);
31
- this.element.replaceChildren(error);
32
- return false;
33
- }
26
+ let parentElement = this.element.parentElement;
27
+ while (parentElement && !isEditableElement(parentElement)) {
28
+ parentElement = parentElement.parentElement;
34
29
  }
35
30
 
36
- if (!this.parent || !(this.parent instanceof EditableArray)) {
31
+ if (!parentElement || !isEditableArray(parentElement)) {
37
32
  this.element.classList.add("errored");
38
33
  const error = document.createElement("editable-region-error-card");
39
34
  error.setAttribute("heading", "Failed to render array item");
40
35
  error.setAttribute(
41
36
  "message",
42
- "Parent array editable not found. Array items must be a descendant of an array editable.",
37
+ "Array item editable regions must be nested inside an array editable region but this element has no parent editable region. Please check that this element is inside an array editable region.",
43
38
  );
44
39
  this.element.replaceChildren(error);
45
40
  return false;
@@ -54,15 +49,38 @@ export default class EditableArrayItem extends EditableComponent {
54
49
  return false;
55
50
  }
56
51
 
52
+ if (e.dataTransfer?.types.includes(source.toLowerCase())) {
53
+ return true;
54
+ }
55
+
57
56
  const dragType = this.getDragType();
58
57
 
59
- if (
60
- !e.dataTransfer?.types.includes(source.toLowerCase()) &&
61
- (!dragType || !e.dataTransfer.types.includes(dragType))
62
- ) {
58
+ if (!dragType || !e.dataTransfer.types.includes(dragType)) {
63
59
  return false;
64
60
  }
65
61
 
62
+ if (dragType === "cc:structure") {
63
+ if (this.structureStrings.length === 0) {
64
+ return false;
65
+ }
66
+
67
+ const structureString = e.dataTransfer.types
68
+ .find((type) => type.startsWith("structure:"))
69
+ ?.replace(/^structure:/, "");
70
+
71
+ if (!structureString) {
72
+ return false;
73
+ }
74
+
75
+ const targetStructure = this.structureStrings.find(
76
+ (targetValue: unknown) => structureString === targetValue,
77
+ );
78
+
79
+ if (!targetStructure) {
80
+ return false;
81
+ }
82
+ }
83
+
66
84
  return true;
67
85
  }
68
86
 
@@ -77,6 +95,47 @@ export default class EditableArrayItem extends EditableComponent {
77
95
  this.element.style.boxShadow = this.getDraggingBoxShadow(e);
78
96
  }
79
97
 
98
+ onDragStart(e: DragEvent): void {
99
+ const source = this.parent?.contextBase?.filePath;
100
+ if (!source || !e.dataTransfer || !this.element.dataset.prop) {
101
+ return;
102
+ }
103
+
104
+ const clientRect = this.element.getBoundingClientRect();
105
+
106
+ e.stopPropagation();
107
+ this.element.classList.add("dragging");
108
+
109
+ e.dataTransfer.setDragImage(this.element, clientRect.width - 35, 35);
110
+ e.dataTransfer.effectAllowed = "move";
111
+
112
+ const id = Math.random().toString(36).slice(2);
113
+ this.element.id = id;
114
+
115
+ const data = {
116
+ index: this.element.dataset.prop,
117
+ sourceId: id,
118
+ value: this.value,
119
+ };
120
+
121
+ if (this.inputConfig?.options?.structures?.values?.length > 0) {
122
+ const structure = CloudCannon.findStructure(
123
+ this.inputConfig?.options?.structures,
124
+ this.value,
125
+ );
126
+ if (structure) {
127
+ e.dataTransfer.setData(`structure:${JSON.stringify(structure)}`, "");
128
+ }
129
+ }
130
+
131
+ const payload = JSON.stringify(data);
132
+ e.dataTransfer?.setData(source, payload);
133
+ const dragType = this.getDragType();
134
+ if (dragType) {
135
+ e.dataTransfer?.setData(dragType, payload);
136
+ }
137
+ }
138
+
80
139
  getDragType(): string | undefined {
81
140
  if (this.inputConfig?.options?.structures?.values?.length) {
82
141
  return "cc:structure";
@@ -300,43 +359,9 @@ export default class EditableArrayItem extends EditableComponent {
300
359
  this.element.remove();
301
360
  });
302
361
 
303
- this.controlsElement.addEventListener("dragstart", (e: DragEvent) => {
304
- const source = this.parent?.contextBase?.filePath;
305
- if (!source || !e.dataTransfer || !this.element.dataset.prop) {
306
- return;
307
- }
308
-
309
- const clientRect = this.element.getBoundingClientRect();
310
-
311
- e.stopPropagation();
312
- this.element.classList.add("dragging");
313
-
314
- e.dataTransfer.setDragImage(this.element, clientRect.width - 35, 35);
315
- e.dataTransfer.effectAllowed = "move";
316
-
317
- const id = Math.random().toString(36).slice(2);
318
- this.element.id = id;
319
-
320
- const data: Record<string, any> = {
321
- index: this.element.dataset.prop,
322
- sourceId: id,
323
- value: this.value,
324
- };
325
-
326
- if (this.inputConfig?.options?.structures?.values?.length > 0) {
327
- data.structure = CloudCannon.findStructure(
328
- this.inputConfig?.options?.structures,
329
- this.value,
330
- );
331
- }
332
-
333
- const payload = JSON.stringify(data);
334
- e.dataTransfer?.setData(source, payload);
335
- const dragType = this.getDragType();
336
- if (dragType) {
337
- e.dataTransfer?.setData(dragType, payload);
338
- }
339
- });
362
+ this.controlsElement.addEventListener("dragstart", (e: DragEvent) =>
363
+ this.onDragStart(e),
364
+ );
340
365
 
341
366
  this.updateControls();
342
367
 
@@ -358,6 +383,12 @@ export default class EditableArrayItem extends EditableComponent {
358
383
  (inputConfig as any)?.options?.disable_add ?? false;
359
384
 
360
385
  this.inputConfig = inputConfig;
386
+ if (this.inputConfig?.options?.structures?.values?.length > 0) {
387
+ this.structureStrings =
388
+ this.inputConfig.options.structures.values.map((value: unknown) =>
389
+ JSON.stringify(value).toLowerCase(),
390
+ );
391
+ }
361
392
  this.element.append(this.controlsElement);
362
393
  });
363
394
  }
@@ -428,25 +459,7 @@ export default class EditableArrayItem extends EditableComponent {
428
459
  this.dispatchArrayMove(fromIndex, newIndex);
429
460
  }
430
461
  } else if (otherArrayData) {
431
- const { index, sourceId, value, structure } =
432
- JSON.parse(otherArrayData);
433
- if (dragType === "cc:structure") {
434
- if (!this.inputConfig?.options?.structures?.values) {
435
- return;
436
- }
437
-
438
- const targetStructure = CloudCannon.findStructure(
439
- this.inputConfig.options.structures,
440
- this.value,
441
- );
442
- if (!targetStructure) {
443
- return;
444
- }
445
-
446
- if (JSON.stringify(structure) !== JSON.stringify(targetStructure)) {
447
- return;
448
- }
449
- }
462
+ const { index, sourceId, value } = JSON.parse(otherArrayData);
450
463
 
451
464
  const sourceElement = document.getElementById(sourceId);
452
465
  if (sourceElement && hasEditableArrayItem(sourceElement)) {
@@ -4,10 +4,12 @@ import type {
4
4
  CloudCannonJavaScriptV1APIFile,
5
5
  } from "@cloudcannon/javascript-api";
6
6
  import type EditableArrayItemComponent from "../components/editable-array-item-component.js";
7
- import { hasEditableArrayItem, isEditableElement } from "../helpers/checks.js";
7
+ import type EditableRegionButton from "../components/ui/editable-region-button.js";
8
+ import { isEditableElement } from "../helpers/checks.js";
8
9
  import { CloudCannon } from "../helpers/cloudcannon.js";
9
10
  import type EditableArrayItem from "./editable-array-item.js";
10
11
  import Editable, { type EditableListener } from "./editable.js";
12
+ import "../components/ui/editable-region-button.js";
11
13
 
12
14
  const arrayDirectionValues = [
13
15
  "row",
@@ -33,6 +35,7 @@ export default class EditableArray extends Editable {
33
35
 
34
36
  private updatePromise: Promise<void> | undefined;
35
37
  private needsReupdate = false;
38
+ private addButton?: EditableRegionButton;
36
39
 
37
40
  async registerListener(listener: EditableListener): Promise<void> {
38
41
  if (!this.value) {
@@ -94,7 +97,10 @@ export default class EditableArray extends Editable {
94
97
  this.element.classList.add("errored");
95
98
  const error = document.createElement("editable-region-error-card");
96
99
  error.setAttribute("heading", "Failed to render array editable");
97
- error.setAttribute("message", "Missing required attribute data-prop");
100
+ error.setAttribute(
101
+ "message",
102
+ "Array editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
103
+ );
98
104
  this.element.replaceChildren(error);
99
105
  return false;
100
106
  }
@@ -114,8 +120,19 @@ export default class EditableArray extends Editable {
114
120
  error.setAttribute("heading", "Failed to render array editable");
115
121
  error.setAttribute(
116
122
  "message",
117
- `Illegal value type: ${typeof value}. Supported types are array.`,
123
+ `Array editable regions expect to receive a value of type "array" but instead received a value of type '${typeof value}'.`,
118
124
  );
125
+ if (this.contextBase?.fullPath) {
126
+ error.setAttribute(
127
+ "hint",
128
+ `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
129
+ );
130
+ } else {
131
+ error.setAttribute(
132
+ "hint",
133
+ `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
134
+ );
135
+ }
119
136
  this.element.replaceChildren(error);
120
137
  return;
121
138
  }
@@ -158,7 +175,7 @@ export default class EditableArray extends Editable {
158
175
  const children: (HTMLElement & { editable?: EditableArrayItem })[] = [];
159
176
 
160
177
  for (const child of this.element.querySelectorAll(
161
- "editable-array-item,[data-editable='array-item']",
178
+ "editable-array-item,[data-editable='array-item'],array-placeholder",
162
179
  )) {
163
180
  let parent = child.parentElement;
164
181
  while (parent instanceof HTMLElement && !isEditableElement(parent)) {
@@ -172,12 +189,33 @@ export default class EditableArray extends Editable {
172
189
  children.push(child as any);
173
190
  }
174
191
 
192
+ if (
193
+ children.length === 0 &&
194
+ !this.element.dataset.component &&
195
+ !this.element.dataset.componentKey
196
+ ) {
197
+ const error = document.createElement("editable-region-error-card");
198
+ error.setAttribute("heading", "Failed to render array editable region");
199
+ error.setAttribute(
200
+ "message",
201
+ "Array editable regions with no child array items must have either a 'data-component' attribute or a 'data-component-key' attribute. Please add an item to this array then save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
202
+ );
203
+ this.element.replaceChildren(error);
204
+ return;
205
+ }
206
+
175
207
  if (!this.element.dataset.idKey) {
176
208
  while (children.length > value.length) {
177
209
  children.pop()?.remove();
178
210
  }
179
211
 
180
- const firstChild = children[0];
212
+ if (value.length === 0 && this.addButton) {
213
+ this.element.appendChild(this.addButton);
214
+ return;
215
+ }
216
+
217
+ this.addButton?.remove();
218
+
181
219
  for (let i = 0; i < value.length; i++) {
182
220
  let child = children[i];
183
221
  if (!child) {
@@ -185,12 +223,11 @@ export default class EditableArray extends Editable {
185
223
  child = document.createElement(
186
224
  "editable-array-item",
187
225
  ) as EditableArrayItemComponent;
188
- } else if (firstChild) {
226
+ } else {
227
+ // Empty arrays should be caught by the error case above so children[0] should always exist
189
228
  child = children[0].cloneNode(true) as HTMLElement & {
190
229
  editable?: EditableArrayItem;
191
230
  };
192
- } else {
193
- child = document.createElement("array-placeholder");
194
231
  }
195
232
  this.element.appendChild(child);
196
233
  }
@@ -226,9 +263,12 @@ export default class EditableArray extends Editable {
226
263
  }
227
264
 
228
265
  if (data && typeof data === "object" && componentKey) {
229
- componentKeys.push(String((data as any)[componentKey]));
230
- } else {
231
- componentKeys.push(null);
266
+ const component = (data as any)[componentKey];
267
+ if (typeof component !== "undefined" && component !== null) {
268
+ componentKeys.push(String((data as any)[componentKey]));
269
+ } else {
270
+ componentKeys.push(null);
271
+ }
232
272
  }
233
273
  }
234
274
 
@@ -247,6 +287,12 @@ export default class EditableArray extends Editable {
247
287
  { __base_context: this.contextBase ?? {} },
248
288
  );
249
289
  });
290
+
291
+ if (dataKeys.length === 0 && this.addButton) {
292
+ this.element.appendChild(this.addButton);
293
+ } else {
294
+ this.addButton?.remove();
295
+ }
250
296
  return;
251
297
  }
252
298
 
@@ -264,6 +310,8 @@ export default class EditableArray extends Editable {
264
310
  }
265
311
  const placeholder = placeholders[i];
266
312
  const existingElement = children[i];
313
+ const componentKey = componentKeys[i] || this.element.dataset.component;
314
+
267
315
  const matchingChildIndex = children.findIndex(
268
316
  (child, i) => child.dataset.id === key && !moved[i],
269
317
  );
@@ -273,17 +321,38 @@ export default class EditableArray extends Editable {
273
321
  const clone = children.find((child) => child.dataset.id === key);
274
322
  if (clone) {
275
323
  matchingChild = clone.cloneNode(true) as any;
276
- } else {
324
+ } else if (componentKey) {
277
325
  matchingChild = document.createElement(
278
326
  "editable-array-item",
279
327
  ) as EditableArrayItemComponent;
280
328
  matchingChild.dataset.id = key;
281
-
282
- const componentKey =
283
- componentKeys[i] || this.element.dataset.component;
284
- if (componentKey) {
285
- matchingChild.dataset.component = componentKey;
329
+ matchingChild.dataset.component = componentKey;
330
+ } else {
331
+ const error = document.createElement("editable-region-error-card");
332
+ error.setAttribute("heading", "Failed to render array item");
333
+ if (typeof this.element.dataset.componentKey === "string") {
334
+ error.setAttribute(
335
+ "message",
336
+ "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.",
337
+ );
338
+ error.setAttribute(
339
+ "hint",
340
+ `This may mean that the value for 'data-component-key' is incorrect or that your array data is incorrectly formatted.
341
+ The current value for 'data-component-key' is '${this.element.dataset.componentKey}' and the current value for 'data-id' is '${key}'.
342
+ `,
343
+ );
344
+ } else {
345
+ error.setAttribute(
346
+ "message",
347
+ "Array editable region has no child with a matching 'data-id' value for this element and no 'data-component' or 'data-component-key' attribute. Please save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
348
+ );
349
+ error.setAttribute(
350
+ "hint",
351
+ `The full value of "data-id" for this item is "${key}"`,
352
+ );
286
353
  }
354
+ matchingChild = document.createElement("array-placeholder");
355
+ matchingChild.append(error);
287
356
  }
288
357
  } else {
289
358
  moved[matchingChildIndex] = true;
@@ -315,6 +384,12 @@ export default class EditableArray extends Editable {
315
384
  child.remove();
316
385
  }
317
386
  });
387
+
388
+ if (dataKeys.length === 0 && this.addButton) {
389
+ this.element.appendChild(this.addButton);
390
+ } else {
391
+ this.addButton?.remove();
392
+ }
318
393
  }
319
394
 
320
395
  calculateArrayDirection(): ArrayDirection {
@@ -335,5 +410,21 @@ export default class EditableArray extends Editable {
335
410
 
336
411
  mount(): void {
337
412
  this.arrayDirection = this.calculateArrayDirection();
413
+
414
+ this.addButton = document.createElement("editable-region-button");
415
+ this.addButton.setAttribute("icon", "add");
416
+ this.addButton.setAttribute("text", "Add Item");
417
+ this.addButton.addEventListener("button-click", () => {
418
+ this.element.dispatchEvent(
419
+ new CustomEvent("cloudcannon-api", {
420
+ bubbles: true,
421
+ detail: {
422
+ source: this.element.dataset.prop,
423
+ action: "add-array-item",
424
+ newIndex: 0,
425
+ },
426
+ }),
427
+ );
428
+ });
338
429
  }
339
430
  }
@@ -42,7 +42,7 @@ export default class EditableComponent extends Editable {
42
42
  error.setAttribute("heading", "Failed to render component");
43
43
  error.setAttribute(
44
44
  "message",
45
- "Component key(data-component) not provided",
45
+ "Component editable regions require a 'data-component' HTML attribute but none was provided. Please check that this element has a valid 'data-component' attribute.",
46
46
  );
47
47
  this.element.replaceChildren(error);
48
48
  return false;
@@ -11,11 +11,14 @@ export default class EditableImage extends Editable {
11
11
  configuredAlt = false;
12
12
  configuredTitle = false;
13
13
 
14
- displayError(heading: string, message: string) {
14
+ displayError(heading: string, message: string, hint?: string) {
15
15
  this.element.classList.add("errored");
16
16
  const error = document.createElement("editable-region-error-card");
17
17
  error.setAttribute("heading", heading);
18
18
  error.setAttribute("message", message);
19
+ if (hint) {
20
+ error.setAttribute("hint", hint);
21
+ }
19
22
  if (this.imageEl) {
20
23
  this.imageEl?.replaceWith(error);
21
24
  } else {
@@ -32,7 +35,7 @@ export default class EditableImage extends Editable {
32
35
  if (!(child instanceof HTMLImageElement)) {
33
36
  this.displayError(
34
37
  "Failed to render image editable region",
35
- "Image editable region requires an image element as its child.",
38
+ "Image editable regions must contain a child HTML element of type 'img'. Please check that this element has a child 'img' element.",
36
39
  );
37
40
  return false;
38
41
  }
@@ -47,7 +50,7 @@ export default class EditableImage extends Editable {
47
50
  ) {
48
51
  this.displayError(
49
52
  "Failed to render image editable region",
50
- "Atleast one of data-prop, data-prop-src, data-prop-alt, or data-prop-title is required.",
53
+ "Image editable regions require atleast one valid 'data-prop-*' HTML attribute. The valid attributes are 'data-prop', 'data-prop-src', 'data-prop-alt', and 'data-prop-title'. Please check that this element has atleast one of these attributes.",
51
54
  );
52
55
  return false;
53
56
  }
@@ -58,7 +61,10 @@ export default class EditableImage extends Editable {
58
61
  if (typeof value !== "object") {
59
62
  this.displayError(
60
63
  "Failed to render image editable region",
61
- `Illegal value type: ${typeof value}. Supported types are object.`,
64
+ `Image editable regions expect to receive a value of type "object" but instead received a value of type '${typeof value}'.`,
65
+ this.contextBase?.fullPath
66
+ ? `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`
67
+ : `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
62
68
  );
63
69
  return;
64
70
  }
@@ -67,32 +73,30 @@ export default class EditableImage extends Editable {
67
73
  return value;
68
74
  }
69
75
 
70
- if ("src" in value && typeof value.src !== "string" && value.src !== null) {
71
- this.displayError(
72
- "Failed to render image editable region",
73
- `Illegal value type for "src": ${typeof value.src}. Supported types are string.`,
74
- );
75
- return;
76
- }
77
-
78
- if ("alt" in value && typeof value.alt !== "string" && value.alt !== null) {
79
- this.displayError(
80
- "Failed to render image editable region",
81
- `Illegal value type for "alt": ${typeof value.alt}. Supported types are string.`,
82
- );
83
- return;
84
- }
76
+ for (const key of ["src", "alt", "title"]) {
77
+ if (
78
+ key in value &&
79
+ typeof value[key as keyof typeof value] !== "string" &&
80
+ value[key as keyof typeof value] !== null
81
+ ) {
82
+ let hint: string;
83
+ if (this.contexts[key]?.fullPath) {
84
+ hint = `This may mean that the 'data-prop-${key}' attribute is incorrectly set for this element, the full 'data-prop-${key}' path was '${this.contexts[key]?.fullPath}'.`;
85
+ } else if (typeof this.element.dataset[key] === "string") {
86
+ hint = `This may mean that the 'data-prop-${key}' attribute is incorrectly set for this element.`;
87
+ } else if (this.contextBase?.fullPath) {
88
+ hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`;
89
+ } else {
90
+ hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element.`;
91
+ }
85
92
 
86
- if (
87
- "title" in value &&
88
- typeof value.title !== "string" &&
89
- value.title !== null
90
- ) {
91
- this.displayError(
92
- "Failed to render image editable region",
93
- `Illegal value type for "title": ${typeof value.title}. Supported types are string.`,
94
- );
95
- return;
93
+ this.displayError(
94
+ "Failed to render image editable region",
95
+ `Image editable regions expect the "${key}" key to have a value of type "string" but instead it was a value of type '${typeof value[key as keyof typeof value]}'.`,
96
+ hint,
97
+ );
98
+ return;
99
+ }
96
100
  }
97
101
 
98
102
  const unexpectedKey = Object.keys(value).find(
@@ -100,9 +104,21 @@ export default class EditableImage extends Editable {
100
104
  );
101
105
 
102
106
  if (unexpectedKey) {
107
+ let hint: string | undefined;
108
+ const capitalizedUnexpectedKey =
109
+ unexpectedKey.charAt(0).toUpperCase() + unexpectedKey.slice(1);
110
+
111
+ if (this.element.dataset[`prop${capitalizedUnexpectedKey}`]) {
112
+ hint = `Try removing the 'data-prop-${unexpectedKey}' HTML attribute from this element.`;
113
+ } else if (this.contextBase?.fullPath) {
114
+ hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`;
115
+ } else {
116
+ hint = `This may mean that the 'data-prop' attribute is incorrectly set for this element.`;
117
+ }
103
118
  this.displayError(
104
119
  "Failed to render image editable region",
105
- `Unexpected key "${unexpectedKey}" in image editable region. Supported keys are "src", "alt", and "title".`,
120
+ `Image editable region received an unexpected value key "${unexpectedKey}". The supported values are "src", "alt", and "title". Please check that your data is correctly formatted.`,
121
+ hint,
106
122
  );
107
123
  return;
108
124
  }
@@ -51,6 +51,7 @@ export default class EditableSnippet extends EditableComponent {
51
51
  const { snippets, source } = this.parseSource(options.source);
52
52
 
53
53
  if (options.action === "get-input-config") {
54
+ // TODO: This should actually load the input config, including the snippet in the cascade
54
55
  return false;
55
56
  }
56
57
 
@@ -5,7 +5,7 @@ import EditableText from "./editable-text.js";
5
5
 
6
6
  const INDENTATION_REGEX = /^([ \t]+)[^\s]/gm;
7
7
  const TAG_REGEX =
8
- /<\s*(?<closing>\/?)\s*(?<tagname>[-a-z]+)(\s+[^>]+)*?\s*(?<selfclosing>\/?)\s*>/gi;
8
+ /<\s*(?<closing>\/?)\s*(?<tagname>[-a-z0-9]+)(\s+[^>]+)*?\s*(?<selfclosing>\/?)\s*>/gi;
9
9
 
10
10
  const HTML_VOID_ELEMENT: Record<string, boolean> = {
11
11
  area: true,
@@ -38,6 +38,11 @@ export default class EditableSource extends EditableText {
38
38
  if (!this.element.dataset.path) {
39
39
  return;
40
40
  }
41
+
42
+ if (!this.element.dataset.path.startsWith("/")) {
43
+ this.element.dataset.path = `/${this.element.dataset.path}`;
44
+ }
45
+
41
46
  this.file = CloudCannon.file(this.element.dataset.path);
42
47
  this.file.addEventListener("change", () => {
43
48
  this.file?.get().then(this.pushValue.bind(this));
@@ -51,7 +56,10 @@ export default class EditableSource extends EditableText {
51
56
  this.element.classList.add("errored");
52
57
  const error = document.createElement("editable-region-error-card");
53
58
  error.setAttribute("heading", "Failed to render source editable region");
54
- error.setAttribute("message", "Missing required attribute data-path");
59
+ error.setAttribute(
60
+ "message",
61
+ "Source editable regions require a 'data-path' HTML attribute but none was provided. Please check that this element has a valid 'data-path' attribute.",
62
+ );
55
63
  this.element.replaceChildren(error);
56
64
  return false;
57
65
  }
@@ -61,7 +69,10 @@ export default class EditableSource extends EditableText {
61
69
  this.element.classList.add("errored");
62
70
  const error = document.createElement("editable-region-error-card");
63
71
  error.setAttribute("heading", "Failed to render source editable region");
64
- error.setAttribute("message", "Missing required attribute data-key");
72
+ error.setAttribute(
73
+ "message",
74
+ "Source editable regions require a 'data-key' HTML attribute but none was provided. Please check that this element has a valid 'data-key' attribute.",
75
+ );
65
76
  this.element.replaceChildren(error);
66
77
  return false;
67
78
  }
@@ -75,7 +86,11 @@ export default class EditableSource extends EditableText {
75
86
  error.setAttribute("heading", "Failed to render source editable region");
76
87
  error.setAttribute(
77
88
  "message",
78
- `Illegal value type: ${typeof value}. Supported types are string.`,
89
+ "The provided 'data-path' HTML attribute references a file that does not exist. Please check that the file exists and that the 'data-path' attribute on this element is correct.",
90
+ );
91
+ error.setAttribute(
92
+ "hint",
93
+ `The current value of the "data-path" attribute is "${this.element.dataset.path}"`,
79
94
  );
80
95
  this.element.replaceChildren(error);
81
96
  return;
@@ -92,7 +107,11 @@ export default class EditableSource extends EditableText {
92
107
  );
93
108
  error.setAttribute(
94
109
  "message",
95
- "Failed to find element with matching data-key attribute",
110
+ "Failed to find an element matching the provided 'data-key' attribute.",
111
+ );
112
+ error.setAttribute(
113
+ "hint",
114
+ `This might mean that your 'data-path' attribute is incorrect. The current value of the "data-path" attribute is "${this.element.dataset.path}" and the current value of the "data-key" attribute is "${this.element.dataset.key}".`,
96
115
  );
97
116
  this.element.replaceChildren(error);
98
117
  return;
@@ -111,7 +130,11 @@ export default class EditableSource extends EditableText {
111
130
  );
112
131
  error.setAttribute(
113
132
  "message",
114
- "Found duplicate data-key attribute. Make sure all source editables have unique data-key attributes",
133
+ `Source editable regions require that all 'data-key' attributes in the same file have unique values but the current file contains multiple instances of the key '${this.element.dataset.key}'. Please make sure all 'data-key' attributes are unique within the same file.`,
134
+ );
135
+ error.setAttribute(
136
+ "hint",
137
+ `The current value of the 'data-path' attribute is '${this.element.dataset.path}'"'.`,
115
138
  );
116
139
  this.element.replaceChildren(error);
117
140
  return;
@@ -15,7 +15,10 @@ export default class EditableText extends Editable {
15
15
  this.element.classList.add("errored");
16
16
  const error = document.createElement("editable-region-error-card");
17
17
  error.setAttribute("heading", "Failed to render text editable region");
18
- error.setAttribute("message", "Missing required attribute data-prop");
18
+ error.setAttribute(
19
+ "message",
20
+ "Text editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
21
+ );
19
22
  this.element.replaceChildren(error);
20
23
  return false;
21
24
  }
@@ -30,7 +33,7 @@ export default class EditableText extends Editable {
30
33
  error.setAttribute("heading", "Failed to render text editable region");
31
34
  error.setAttribute(
32
35
  "message",
33
- `Unsupported element type: "${elementType}". Supported element types are span, text, and block.`,
36
+ `Text editable region received an invalid type for the 'data-type' HTML attribute. The provided element type was '${elementType}' but the supported element types are 'span', 'text', and 'block'. Please set the 'data-type' attribute to one of the supported types.`,
34
37
  );
35
38
  this.element.replaceChildren(error);
36
39
  return false;
@@ -45,8 +48,19 @@ export default class EditableText extends Editable {
45
48
  error.setAttribute("heading", "Failed to render text editable region");
46
49
  error.setAttribute(
47
50
  "message",
48
- `Illegal value type: ${typeof value}. Supported types are string.`,
51
+ `Text editable regions expect to receive a value of type "string" but instead received a value of type '${typeof value}'.`,
49
52
  );
53
+ if (this.contextBase?.fullPath) {
54
+ error.setAttribute(
55
+ "hint",
56
+ `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
57
+ );
58
+ } else {
59
+ error.setAttribute(
60
+ "hint",
61
+ `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
62
+ );
63
+ }
50
64
  this.element.replaceChildren(error);
51
65
  return;
52
66
  }
package/nodes/editable.ts CHANGED
@@ -286,6 +286,10 @@ export default class Editable {
286
286
  }
287
287
 
288
288
  connect(): void {
289
+ if (!this.validateConfiguration()) {
290
+ return;
291
+ }
292
+
289
293
  if (this.disconnecting) {
290
294
  this.needsReconnect = true;
291
295
  return;
@@ -295,13 +299,11 @@ export default class Editable {
295
299
  }
296
300
  this.connectPromise = loadingPromise.then(() => {
297
301
  this.setupListeners();
298
- if (this.validateConfiguration()) {
299
- this.connected = true;
300
- if (!this.mounted && this.shouldMount()) {
301
- this.mounted = true;
302
- this.mount();
303
- this.update();
304
- }
302
+ this.connected = true;
303
+ if (!this.mounted && this.shouldMount()) {
304
+ this.mounted = true;
305
+ this.mount();
306
+ this.update();
305
307
  }
306
308
  });
307
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -1,3 +1,8 @@
1
1
  editable-text {
2
2
  display: inline-block;
3
3
  }
4
+
5
+ editable-text,
6
+ [data-editable="text"] {
7
+ min-width: 10px;
8
+ }
@@ -0,0 +1,36 @@
1
+ button {
2
+ all: initial;
3
+ -webkit-appearance: none;
4
+ -moz-appearance: none;
5
+ appearance: none;
6
+ text-decoration: none;
7
+ text-align: center;
8
+ box-sizing: border-box;
9
+
10
+ font-size: 16px;
11
+ font-weight: bold;
12
+ font-family: var(--ccrt-font-family);
13
+ line-height: 1.4;
14
+ color: var(--ccrt-color-cc-blue);
15
+ --cc-icon-fill: var(--ccrt-color-cc-blue);
16
+
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+
21
+ border: var(--ccrt-border-width) solid var(--ccrt-color-alto);
22
+ border-radius: var(--ccrt-border-radius);
23
+
24
+ padding: 0 var(--ccrt-gap);
25
+ height: 48px;
26
+ }
27
+
28
+ button:hover {
29
+ background-color: rgba(226, 234, 250);
30
+ border-color: color-mix(
31
+ in srgb,
32
+ var(--ccrt-color-cc-blue) 80%,
33
+ var(--ccrt-color-carbon)
34
+ );
35
+ cursor: pointer;
36
+ }