@cloudcannon/editable-regions 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -888,3 +888,5 @@ If you *can* move the value into front matter or a data file, prefer a text regi
888
888
  | `data-path` | String | Yes | The template file the text lives in, relative to the site root. A leading `/` is optional. This is a file path, not a selector. |
889
889
  | `data-key` | String | Yes | The name identifying this region within the file. Must be unique among all `data-key` values in the same file. |
890
890
  | `data-type` | `"span"` \| `"text"` \| `"block"` | No | Controls the rich text editing surface, as with [text regions](#text). Source content is edited as HTML. |
891
+ | `data-input-config` | JSON object | No | Inline input configuration for the editor, as a JSON object. Takes precedence over `data-input`. |
892
+ | `data-input` | String | No | The slug of the CloudCannon input configuration to resolve for this region, used when `data-input-config` isn't set. |
@@ -0,0 +1,21 @@
1
+ import type EditableRegionErrorCard from "../components/ui/editable-region-error-card.js";
2
+
3
+ /**
4
+ * Builds an `<editable-region-error-card>` with the given content. The caller
5
+ * decides where to place it (usually `element.replaceChildren(...)`).
6
+ */
7
+ export function createErrorCard(
8
+ heading: string,
9
+ message?: string,
10
+ hint?: string,
11
+ ): EditableRegionErrorCard {
12
+ const error = document.createElement("editable-region-error-card");
13
+ error.setAttribute("heading", heading);
14
+ if (typeof message === "string") {
15
+ error.setAttribute("message", message);
16
+ }
17
+ if (typeof hint === "string") {
18
+ error.setAttribute("hint", hint);
19
+ }
20
+ return error;
21
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from "../nodes";
10
10
  import { hasEditable } from "../nodes/editable.js";
11
11
  import { isEditableWebcomponent } from "./checks";
12
+ import { createErrorCard } from "./error-card.js";
12
13
 
13
14
  const DYNAMIC_EDITABLE_TYPE = "_dynamic";
14
15
 
@@ -84,23 +85,19 @@ export const hydrateDataEditableRegions = (root: Element) => {
84
85
 
85
86
  const Editable = editableMap[element.dataset.editable];
86
87
  if (!Editable) {
87
- const error = document.createElement("editable-region-error-card");
88
- error.setAttribute("heading", "Failed to render editable region");
89
- error.setAttribute(
90
- "message",
91
- `This element has an invalid editable type for the 'data-editable' HTML attribute. The supported editable types are ${Object.keys(
92
- editableMap,
93
- )
94
- .map((type) => `"${type}"`)
95
- .join(
96
- ", ",
97
- )} but instead received the type "${element.dataset.editable}". Please make sure that this element has a valid "data-editable" attribute.`,
88
+ element.replaceWith(
89
+ createErrorCard(
90
+ "Failed to render editable region",
91
+ `This element has an invalid editable type for the 'data-editable' HTML attribute. The supported editable types are ${Object.keys(
92
+ editableMap,
93
+ )
94
+ .map((type) => `"${type}"`)
95
+ .join(
96
+ ", ",
97
+ )} but instead received the type "${element.dataset.editable}". Please make sure that this element has a valid "data-editable" attribute.`,
98
+ "If this is intentional you can use the 'data-cloudcannon-ignore' attribute to exclude this element from editable regions.",
99
+ ),
98
100
  );
99
- error.setAttribute(
100
- "hint",
101
- "If this is intentional you can use the 'data-cloudcannon-ignore' attribute to exclude this element from editable regions.",
102
- );
103
- element.replaceWith(error);
104
101
  return;
105
102
  }
106
103
 
@@ -368,9 +368,10 @@ async function generateLiveEditingSource(
368
368
 
369
369
  for (const [i, filePath] of allLiquidFiles.entries()) {
370
370
  const id = `liquidFile_${i}`;
371
- source += `import ${id} from "./${filePath}";
371
+ const importPath = toPosixPath(filePath);
372
+ source += `import ${id} from ${JSON.stringify(`./${importPath}`)};
372
373
 
373
- window.cc_liquid_files["${filePath}"] = ${id};
374
+ window.cc_liquid_files[${JSON.stringify(importPath)}] = ${id};
374
375
  `;
375
376
  }
376
377
 
@@ -537,6 +538,14 @@ async function findAllLiquidFiles(
537
538
  return allFiles;
538
539
  }
539
540
 
541
+ /**
542
+ * @param {string} filePath
543
+ * @returns {string}
544
+ */
545
+ function toPosixPath(filePath) {
546
+ return filePath.split(path.sep).join("/");
547
+ }
548
+
540
549
  /**
541
550
  * @param {Object} options
542
551
  * @param {string} options.directory
@@ -625,7 +634,8 @@ function emitImportRegistrations(liquidOptions) {
625
634
  liquidOptions?.[optionKey] ?? {},
626
635
  ).entries()) {
627
636
  const id = `${optionKey}_${i}`;
628
- imports += `\nimport ${id} from "./${file}";\n`;
637
+ const importPath = toPosixPath(String(file));
638
+ imports += `\nimport ${id} from ${JSON.stringify(`./${importPath}`)};\n`;
629
639
  body += `${registerFn}(${JSON.stringify(name)}, ${id});\n`;
630
640
  }
631
641
  }
@@ -6,6 +6,7 @@
6
6
  const TEMPLATE_FRAME_RE =
7
7
  /^template: (.+?): executing ".*?" at .*?: error calling (.+?): /;
8
8
  const FAILED_SEPARATOR = ": execute of template failed: ";
9
+ const TEMPLATE_FRAME_START = "template: ";
9
10
 
10
11
  export interface ParsedHugoError {
11
12
  /** Template refs (file:line:column), outermost call first. */
@@ -14,12 +15,21 @@ export interface ParsedHugoError {
14
15
  call?: string;
15
16
  /** The underlying error, with the template-call chain stripped. */
16
17
  message: string;
18
+ /** Wrapper text ahead of the first template frame (e.g. a build-error prefix). */
19
+ prefix?: string;
17
20
  }
18
21
 
19
22
  export function parseHugoTemplateError(raw: string): ParsedHugoError {
23
+ const trimmed = raw.trim();
24
+ // Build errors arrive wrapped ("editor site build failed: ...") ahead of the
25
+ // template chain, so start parsing at the first frame and keep the wrapper
26
+ // as context.
27
+ const start = trimmed.indexOf(TEMPLATE_FRAME_START);
28
+ const prefix = start > 0 ? trimmed.slice(0, start) : "";
29
+ let rest = start > 0 ? trimmed.slice(start) : trimmed;
30
+
20
31
  const frames: string[] = [];
21
32
  let call: string | undefined;
22
- let rest = raw.trim();
23
33
  for (;;) {
24
34
  const match = rest.match(TEMPLATE_FRAME_RE);
25
35
  if (!match) break;
@@ -35,12 +45,18 @@ export function parseHugoTemplateError(raw: string): ParsedHugoError {
35
45
  const separator = rest.indexOf(FAILED_SEPARATOR);
36
46
  const after =
37
47
  separator === -1 ? "" : rest.slice(separator + FAILED_SEPARATOR.length);
38
- if (after.startsWith("template: ")) {
48
+ if (after.startsWith(TEMPLATE_FRAME_START)) {
39
49
  rest = after;
40
50
  }
41
51
  }
42
52
  }
43
- return { frames, call, message: rest };
53
+
54
+ // No template chain (a content/config error, say) — keep the whole message.
55
+ if (frames.length === 0) {
56
+ return { frames, message: trimmed };
57
+ }
58
+
59
+ return { frames, call, message: rest, prefix: prefix || undefined };
44
60
  }
45
61
 
46
62
  /** An error enhanced with an optional editor-context hint. */
@@ -57,10 +73,9 @@ export function enhanceHugoError(
57
73
  if (/partial .* not found/i.test(message)) {
58
74
  hint =
59
75
  "This partial isn't in the bundled template snapshot. Check that it " +
60
- "lives under one of the directories in " +
61
- "`params.editable_regions.template_dirs` (by default the partials, " +
62
- "render hooks, and shortcodes of your configured layout dir) " +
63
- "and rebuild the site.";
76
+ "lives under `layouts/partials/` or `layouts/_partials/` in your " +
77
+ "site, theme, or a module (or under one of the directories in " +
78
+ "`params.editable_regions.template_dirs`, if set) and rebuild the site.";
64
79
  } else if (parsed.frames.length > 0) {
65
80
  hint =
66
81
  "The partial errored while rendering in the editor. If the code " +
@@ -68,17 +83,14 @@ export function enhanceHugoError(
68
83
  "`{{ if not site.Params.env_client }}`, " +
69
84
  "or supply an editor-safe version via " +
70
85
  "`params.editable_regions.templates_overrides`.";
71
- } else if (/logged \d+ errors/i.test(message)) {
72
- hint =
73
- "Hugo logged errors during the render — open the browser console " +
74
- "for the underlying messages.";
75
86
  }
76
87
 
77
88
  const primary =
78
89
  parsed.frames.length > 0 && parsed.call
79
- ? `${parsed.call}: ${parsed.message}`
90
+ ? `${parsed.prefix ?? ""}${parsed.call}: ${parsed.message}`
80
91
  : parsed.message;
81
- const sentence = /[.!?]$/.test(primary) ? primary : `${primary}.`;
92
+ const sentence =
93
+ primary.includes("\n") || /[.!?]$/.test(primary) ? primary : `${primary}.`;
82
94
 
83
95
  const error = new Error(
84
96
  `Failed to render Hugo component "${componentKey}": ${sentence}`,
@@ -106,7 +118,7 @@ export function missingComponentError(componentKey: string): Error {
106
118
  return new Error(
107
119
  `No Hugo partial found for component "${componentKey}". This partial ` +
108
120
  `isn't captured in the editor's template bundle. Make sure it's a ` +
109
- `partial, shortcode, or render hook under your layout tree and ` +
121
+ `partial under \`layouts/partials/\` or \`layouts/_partials/\` and ` +
110
122
  `rebuild the site.`,
111
123
  );
112
124
  }
@@ -1,5 +1,7 @@
1
1
  {{- $files := dict -}}
2
- {{- $subDirs := slice "partials" "shortcodes" -}}
2
+ {{- /* Legacy and Hugo 0.146+ template dir names. `_markup` matches at any
3
+ depth, covering `_default/_markup`, `_markup`, and `<section>/_markup`. */ -}}
4
+ {{- $subDirs := slice "partials" "shortcodes" "_partials" "_shortcodes" "_markup" -}}
3
5
 
4
6
  {{- $deps := .deps -}}
5
7
  {{- $exts := .exts -}}
@@ -36,12 +38,6 @@
36
38
 
37
39
  {{- if in $subDirs $entry.Name -}}
38
40
  {{- $files = merge $files (partial "editable-regions/find-files-with-extension.html" (dict "dir" $path "exts" $exts "ignore" $ignore)) -}}
39
- {{- else if eq $entry.Name "_default" -}}
40
- {{- range $defaultEntry := readDir $path -}}
41
- {{- if eq $defaultEntry.Name "_markup" -}}
42
- {{- $files = merge $files (partial "editable-regions/find-files-with-extension.html" (dict "dir" (path.Join $path "_markup") "exts" $exts "ignore" $ignore)) -}}
43
- {{- end -}}
44
- {{- end -}}
45
41
  {{- end -}}
46
42
 
47
43
  {{- $files = merge $files (partial "editable-regions/find-template-files.html" (dict "dir" $path "deps" $deps "exts" $exts "ignore" $ignore)) -}}
@@ -36,9 +36,15 @@ const dispatchView = `{{- with .cc_requests -}}
36
36
  key-sorted; the browser demuxes by id, not position. */ -}}
37
37
  {{- range $id, $req := . -}}
38
38
  {{- $partial := $req.partial -}}
39
- {{- $found := templates.Exists (printf "partials/%s" $partial) -}}
40
- {{- $found = or $found (templates.Exists (printf "partials/%s.html" $partial)) -}}
41
- {{- $found = or $found (templates.Exists (printf "partials/%s.htm" $partial)) -}}
39
+ {{- /* templates.Exists matches exact paths: Hugo registers legacy
40
+ partials/ files under both partials/ and _partials/, but files in
41
+ _partials/ (Hugo 0.146+) only under _partials/. */ -}}
42
+ {{- $found := false -}}
43
+ {{- range $dir := slice "_partials" "partials" -}}
44
+ {{- $found = or $found (templates.Exists (printf "%s/%s" $dir $partial)) -}}
45
+ {{- $found = or $found (templates.Exists (printf "%s/%s.html" $dir $partial)) -}}
46
+ {{- $found = or $found (templates.Exists (printf "%s/%s.htm" $dir $partial)) -}}
47
+ {{- end -}}
42
48
  {{- if not $found -}}
43
49
  <div data-cc-render="{{ $id }}"><cc-missing-partial data-name="{{ $partial }}"></cc-missing-partial></div>
44
50
  {{- else -}}
@@ -61,6 +67,9 @@ type editorSiteBuilder struct {
61
67
  Sites *hugolib.HugoSites
62
68
  changedFiles []string
63
69
  removedFiles []string
70
+ // needsFullRebuild forces the next build to be a full rebuild (no change
71
+ // events), used to recover from a failed build without replaying events.
72
+ needsFullRebuild bool
64
73
  }
65
74
 
66
75
  func editorFlags() config.Provider {
@@ -134,30 +143,54 @@ func (builder *editorSiteBuilder) createSites() error {
134
143
  return nil
135
144
  }
136
145
 
146
+ // build clears pending changes even when it fails, as `hugo server` does, so a
147
+ // failed build can't leave the builder permanently dirty and replay the same
148
+ // failing events on every render.
137
149
  func (builder *editorSiteBuilder) build() error {
138
- var events []fsnotify.Event
150
+ events := builder.changeEvents()
151
+ builder.changedFiles = nil
152
+ builder.removedFiles = nil
153
+
139
154
  if builder.Sites == nil {
140
155
  if err := builder.createSites(); err != nil {
141
156
  return err
142
157
  }
143
- } else {
144
- events = builder.changeEvents()
158
+ events = nil
159
+ } else if builder.needsFullRebuild {
160
+ // A previous build failed, which can leave Hugo's incremental state
161
+ // inconsistent (replaying events has panicked), so recover with a full
162
+ // rebuild instead of replaying events.
163
+ events = nil
145
164
  }
165
+ builder.needsFullRebuild = false
146
166
 
147
167
  err := builder.Sites.Build(hugolib.BuildCfg{NoBuildLock: true}, events...)
148
168
  if err != nil {
149
- return err
150
- }
169
+ builder.needsFullRebuild = true
151
170
 
152
- builder.changedFiles = nil
153
- builder.removedFiles = nil
154
-
155
- if n := builder.Sites.NumLogErrors(); n > 0 {
156
- err = fmt.Errorf("logged %d errors", n)
171
+ // Logged errors surface only as "logged N error(s)"; include the
172
+ // messages so the editor's error card shows the cause.
173
+ if logged := capErrorLines(strings.TrimSpace(builder.Sites.Log.Errors()), 5); logged != "" {
174
+ err = fmt.Errorf("%w\n\n%s", err, logged)
175
+ }
157
176
  }
158
177
  return err
159
178
  }
160
179
 
180
+ // capErrorLines trims s to at most max lines, noting how many were dropped, so
181
+ // the editor's error card stays readable.
182
+ func capErrorLines(s string, max int) string {
183
+ if s == "" {
184
+ return ""
185
+ }
186
+ lines := strings.Split(s, "\n")
187
+ if len(lines) <= max {
188
+ return s
189
+ }
190
+ lines = append(lines[:max:max], fmt.Sprintf("… %d more line(s)", len(lines)-max))
191
+ return strings.Join(lines, "\n")
192
+ }
193
+
161
194
  // buildIfDirty runs the initial build when the site doesn't exist yet, then
162
195
  // an incremental build only when files changed since the last build, so
163
196
  // renders with nothing pending skip the build entirely.
@@ -170,10 +203,7 @@ func (builder *editorSiteBuilder) buildIfDirty() error {
170
203
  return nil
171
204
  }
172
205
 
173
- if err := builder.build(); err != nil {
174
- return fmt.Errorf("build after pending content changes: %w", err)
175
- }
176
- return nil
206
+ return builder.build()
177
207
  }
178
208
 
179
209
  func (builder *editorSiteBuilder) writeFile(filename, content string) {
@@ -334,7 +364,9 @@ func (builder *editorSiteBuilder) targetPage(target string) page.Page {
334
364
  if f == nil {
335
365
  continue
336
366
  }
337
- if filepath.Join(builder.Cfg.Base.ContentDir, f.Path()) != target {
367
+ // `target` is slash-normalized; `filepath.Join` uses the OS
368
+ // separator, so normalize it too or Windows never matches.
369
+ if filepath.ToSlash(filepath.Join(builder.Cfg.Base.ContentDir, f.Path())) != target {
338
370
  continue
339
371
  }
340
372
  return p
@@ -6,6 +6,7 @@ import {
6
6
  isEditableElement,
7
7
  } from "../helpers/checks.js";
8
8
  import { CloudCannon, realizeAPIValue } from "../helpers/cloudcannon.mjs";
9
+ import { createErrorCard } from "../helpers/error-card.js";
9
10
  import type EditableArray from "./editable-array.js";
10
11
  import EditableComponent from "./editable-component.js";
11
12
 
@@ -39,13 +40,12 @@ export default class EditableArrayItem extends EditableComponent {
39
40
 
40
41
  if (!parentElement || !isEditableArray(parentElement)) {
41
42
  this.element.classList.add("errored");
42
- const error = document.createElement("editable-region-error-card");
43
- error.setAttribute("heading", "Failed to render array item");
44
- error.setAttribute(
45
- "message",
46
- "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
+ this.element.replaceChildren(
44
+ createErrorCard(
45
+ "Failed to render array item",
46
+ "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.",
47
+ ),
47
48
  );
48
- this.element.replaceChildren(error);
49
49
  return false;
50
50
  }
51
51
 
@@ -7,6 +7,7 @@ import type EditableArrayItemComponent from "../components/editable-array-item-c
7
7
  import type EditableRegionButton from "../components/ui/editable-region-button.js";
8
8
  import { isEditableElement } from "../helpers/checks.js";
9
9
  import { CloudCannon } from "../helpers/cloudcannon.mjs";
10
+ import { createErrorCard } from "../helpers/error-card.js";
10
11
  import Editable, { type EditableListener } from "./editable.js";
11
12
  import type EditableArrayItem from "./editable-array-item.js";
12
13
  import "../components/ui/editable-region-button.js";
@@ -105,13 +106,12 @@ export default class EditableArray extends Editable {
105
106
  const prop = this.element.dataset.prop;
106
107
  if (typeof prop !== "string") {
107
108
  this.element.classList.add("errored");
108
- const error = document.createElement("editable-region-error-card");
109
- error.setAttribute("heading", "Failed to render array editable");
110
- error.setAttribute(
111
- "message",
112
- "Array editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
109
+ this.element.replaceChildren(
110
+ createErrorCard(
111
+ "Failed to render array editable",
112
+ "Array editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
113
+ ),
113
114
  );
114
- this.element.replaceChildren(error);
115
115
  return false;
116
116
  }
117
117
 
@@ -127,24 +127,15 @@ export default class EditableArray extends Editable {
127
127
  !CloudCannon.isAPIFile(value)
128
128
  ) {
129
129
  this.element.classList.add("errored");
130
- const error = document.createElement("editable-region-error-card");
131
- error.setAttribute("heading", "Failed to render array editable");
132
- error.setAttribute(
133
- "message",
134
- `Array editable regions expect to receive a value of type "array" but instead received a value of type '${typeof value}'.`,
130
+ this.element.replaceChildren(
131
+ createErrorCard(
132
+ "Failed to render array editable",
133
+ `Array editable regions expect to receive a value of type "array" but instead received a value of type '${typeof value}'.`,
134
+ this.contextBase?.fullPath
135
+ ? `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`
136
+ : `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
137
+ ),
135
138
  );
136
- if (this.contextBase?.fullPath) {
137
- error.setAttribute(
138
- "hint",
139
- `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
140
- );
141
- } else {
142
- error.setAttribute(
143
- "hint",
144
- `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
145
- );
146
- }
147
- this.element.replaceChildren(error);
148
139
  return;
149
140
  }
150
141
  return value;
@@ -304,16 +295,12 @@ export default class EditableArray extends Editable {
304
295
  editable?: EditableArrayItem;
305
296
  };
306
297
  } else {
307
- const error = document.createElement("editable-region-error-card");
308
- error.setAttribute(
309
- "heading",
310
- "Failed to render array editable region",
311
- );
312
- error.setAttribute(
313
- "message",
314
- "Array editable region contains an array item that cannot be rendered. Either a 'data-component' attribute or a 'data-component-key' attribute is required to render array items that weren't previously on the page. Please save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
298
+ this.element.replaceChildren(
299
+ createErrorCard(
300
+ "Failed to render array editable region",
301
+ "Array editable region contains an array item that cannot be rendered. Either a 'data-component' attribute or a 'data-component-key' attribute is required to render array items that weren't previously on the page. Please save and rebuild to see your changes or add a 'data-component' or 'data-component-key' attribute to this element.",
302
+ ),
315
303
  );
316
- this.element.replaceChildren(error);
317
304
  return;
318
305
  }
319
306
  this.element.appendChild(child);
@@ -431,8 +418,7 @@ export default class EditableArray extends Editable {
431
418
  } else if (clone) {
432
419
  matchingChild = clone.cloneNode(true) as any;
433
420
  } else {
434
- const error = document.createElement("editable-region-error-card");
435
- error.setAttribute("heading", "Failed to render array item");
421
+ const error = createErrorCard("Failed to render array item");
436
422
  if (typeof componentKey === "string") {
437
423
  error.setAttribute(
438
424
  "message",
@@ -6,6 +6,7 @@ import {
6
6
  import Editable, { hasEditable } from "./editable.js";
7
7
  import { hasEditableText } from "./editable-text.js";
8
8
  import "../components/ui/editable-region-error-card.js";
9
+ import { createErrorCard } from "../helpers/error-card.js";
9
10
  import "../components/ui/editable-component-controls.js";
10
11
  import type EditableComponentControls from "../components/ui/editable-component-controls.js";
11
12
  import {
@@ -44,13 +45,12 @@ export default class EditableComponent extends Editable {
44
45
  const key = this.element.dataset.component;
45
46
  if (!key) {
46
47
  this.element.classList.add("errored");
47
- const error = document.createElement("editable-region-error-card");
48
- error.setAttribute("heading", "Failed to render component");
49
- error.setAttribute(
50
- "message",
51
- "Component editable regions require a 'data-component' HTML attribute but none was provided. Please check that this element has a valid 'data-component' attribute.",
48
+ this.element.replaceChildren(
49
+ createErrorCard(
50
+ "Failed to render component",
51
+ "Component editable regions require a 'data-component' HTML attribute but none was provided. Please check that this element has a valid 'data-component' attribute.",
52
+ ),
52
53
  );
53
- this.element.replaceChildren(error);
54
54
  return false;
55
55
  }
56
56
 
@@ -107,10 +107,8 @@ export default class EditableComponent extends Editable {
107
107
 
108
108
  if (!component) {
109
109
  this.element.classList.add("errored");
110
- const error = document.createElement("editable-region-error-card");
111
- error.setAttribute("heading", "Failed to render component");
112
- error.setAttribute(
113
- "message",
110
+ const error = createErrorCard(
111
+ "Failed to render component",
114
112
  `Failed to find a registered component with the key "${key}". This may mean that the provided "data-component" attribute is incorrect or that the component hasn't been registered.`,
115
113
  );
116
114
 
@@ -146,8 +144,7 @@ export default class EditableComponent extends Editable {
146
144
  rootEl = this.santiseComponentOutput(await component(value));
147
145
  } catch (err: unknown) {
148
146
  this.element.classList.add("errored");
149
- const error = document.createElement("editable-region-error-card");
150
- error.setAttribute("heading", `Failed to render component: ${key}`);
147
+ const error = createErrorCard(`Failed to render component: ${key}`);
151
148
  error.error = err;
152
149
  this.element.replaceChildren(error);
153
150
  return;
@@ -1,4 +1,5 @@
1
1
  import { CloudCannon } from "../helpers/cloudcannon.mjs";
2
+ import { createErrorCard } from "../helpers/error-card.js";
2
3
  import Editable from "./editable.js";
3
4
 
4
5
  export default class EditableImage extends Editable {
@@ -14,12 +15,7 @@ export default class EditableImage extends Editable {
14
15
 
15
16
  displayError(heading: string, message: string, hint?: string) {
16
17
  this.element.classList.add("errored");
17
- const error = document.createElement("editable-region-error-card");
18
- error.setAttribute("heading", heading);
19
- error.setAttribute("message", message);
20
- if (hint) {
21
- error.setAttribute("hint", hint);
22
- }
18
+ const error = createErrorCard(heading, message, hint);
23
19
  if (this.imageEl) {
24
20
  this.imageEl?.replaceWith(error);
25
21
  } else {
@@ -1,6 +1,7 @@
1
1
  import type { CloudCannonVisualEditorAPIV1File } from "@cloudcannon/visual-editor-api";
2
2
  import { html as beautifyHtml } from "js-beautify";
3
3
  import { CloudCannon } from "../helpers/cloudcannon.mjs";
4
+ import { createErrorCard } from "../helpers/error-card.js";
4
5
  import EditableText from "./editable-text.js";
5
6
 
6
7
  const INDENTATION_REGEX = /^([ \t]+)[^\s]/gm;
@@ -26,6 +27,7 @@ const HTML_VOID_ELEMENT: Record<string, boolean> = {
26
27
 
27
28
  export default class EditableSource extends EditableText {
28
29
  file?: CloudCannonVisualEditorAPIV1File;
30
+ inputConfig?: Record<string, unknown>;
29
31
  format = {
30
32
  leading: "",
31
33
  trailing: "",
@@ -60,45 +62,76 @@ export default class EditableSource extends EditableText {
60
62
  const path = this.element.dataset.path;
61
63
  if (typeof path !== "string") {
62
64
  this.element.classList.add("errored");
63
- const error = document.createElement("editable-region-error-card");
64
- error.setAttribute("heading", "Failed to render source editable region");
65
- error.setAttribute(
66
- "message",
67
- "Source editable regions require a 'data-path' HTML attribute but none was provided. Please check that this element has a valid 'data-path' attribute.",
65
+ this.element.replaceChildren(
66
+ createErrorCard(
67
+ "Failed to render source editable region",
68
+ "Source editable regions require a 'data-path' HTML attribute but none was provided. Please check that this element has a valid 'data-path' attribute.",
69
+ ),
68
70
  );
69
- this.element.replaceChildren(error);
70
71
  return false;
71
72
  }
72
73
 
73
74
  const key = this.element.dataset.key;
74
75
  if (typeof key !== "string") {
75
76
  this.element.classList.add("errored");
76
- const error = document.createElement("editable-region-error-card");
77
- error.setAttribute("heading", "Failed to render source editable region");
78
- error.setAttribute(
79
- "message",
80
- "Source editable regions require a 'data-key' HTML attribute but none was provided. Please check that this element has a valid 'data-key' attribute.",
77
+ this.element.replaceChildren(
78
+ createErrorCard(
79
+ "Failed to render source editable region",
80
+ "Source editable regions require a 'data-key' HTML attribute but none was provided. Please check that this element has a valid 'data-key' attribute.",
81
+ ),
81
82
  );
82
- this.element.replaceChildren(error);
83
83
  return false;
84
84
  }
85
+
86
+ const inlineInputConfig = this.element.dataset.inputConfig;
87
+ if (typeof inlineInputConfig === "string") {
88
+ try {
89
+ const inputConfig = JSON.parse(inlineInputConfig);
90
+ if (
91
+ !inputConfig ||
92
+ typeof inputConfig !== "object" ||
93
+ Array.isArray(inputConfig)
94
+ ) {
95
+ const actualType = Array.isArray(inputConfig)
96
+ ? "array"
97
+ : inputConfig === null
98
+ ? "null"
99
+ : typeof inputConfig;
100
+ this.element.classList.add("errored");
101
+ this.element.replaceChildren(
102
+ createErrorCard(
103
+ "Failed to render source editable region",
104
+ `The value provided for the 'data-input-config' HTML attribute is invalid. The value should be of type "object", but instead was a value of type "${actualType}". Please check that this attribute is a valid JSON object.`,
105
+ ),
106
+ );
107
+ return false;
108
+ }
109
+ this.inputConfig = inputConfig;
110
+ } catch (err: unknown) {
111
+ this.element.classList.add("errored");
112
+ this.element.replaceChildren(
113
+ createErrorCard(
114
+ "Failed to render source editable region",
115
+ "Failed to parse the value provided for the 'data-input-config' HTML attribute as JSON. Please check that this attribute is a valid JSON object.",
116
+ err instanceof Error ? err.message : String(err),
117
+ ),
118
+ );
119
+ return false;
120
+ }
121
+ }
85
122
  return true;
86
123
  }
87
124
 
88
125
  validateValue(value: unknown): string | null | undefined {
89
126
  if (typeof value !== "string" && value !== null) {
90
127
  this.element.classList.add("errored");
91
- const error = document.createElement("editable-region-error-card");
92
- error.setAttribute("heading", "Failed to render source editable region");
93
- error.setAttribute(
94
- "message",
95
- "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.",
96
- );
97
- error.setAttribute(
98
- "hint",
99
- `The current value of the "data-path" attribute is "${this.element.dataset.path}"`,
128
+ this.element.replaceChildren(
129
+ createErrorCard(
130
+ "Failed to render source editable region",
131
+ "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.",
132
+ `The current value of the "data-path" attribute is "${this.element.dataset.path}"`,
133
+ ),
100
134
  );
101
- this.element.replaceChildren(error);
102
135
  return;
103
136
  }
104
137
 
@@ -106,20 +139,13 @@ export default class EditableSource extends EditableText {
106
139
  const keyIndex = value.indexOf(`data-key="${this.element.dataset.key}"`);
107
140
  if (keyIndex === -1) {
108
141
  this.element.classList.add("errored");
109
- const error = document.createElement("editable-region-error-card");
110
- error.setAttribute(
111
- "heading",
112
- "Failed to render source editable region",
113
- );
114
- error.setAttribute(
115
- "message",
116
- "Failed to find an element matching the provided 'data-key' attribute.",
117
- );
118
- error.setAttribute(
119
- "hint",
120
- `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}".`,
142
+ this.element.replaceChildren(
143
+ createErrorCard(
144
+ "Failed to render source editable region",
145
+ "Failed to find an element matching the provided 'data-key' attribute.",
146
+ `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}".`,
147
+ ),
121
148
  );
122
- this.element.replaceChildren(error);
123
149
  return;
124
150
  }
125
151
 
@@ -129,20 +155,13 @@ export default class EditableSource extends EditableText {
129
155
  );
130
156
  if (nextKeyIndex !== -1) {
131
157
  this.element.classList.add("errored");
132
- const error = document.createElement("editable-region-error-card");
133
- error.setAttribute(
134
- "heading",
135
- "Failed to render source editable region",
136
- );
137
- error.setAttribute(
138
- "message",
139
- `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.`,
158
+ this.element.replaceChildren(
159
+ createErrorCard(
160
+ "Failed to render source editable region",
161
+ `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.`,
162
+ `The current value of the 'data-path' attribute is '${this.element.dataset.path}'.`,
163
+ ),
140
164
  );
141
- error.setAttribute(
142
- "hint",
143
- `The current value of the 'data-path' attribute is '${this.element.dataset.path}'"'.`,
144
- );
145
- this.element.replaceChildren(error);
146
165
  return;
147
166
  }
148
167
  }
@@ -228,13 +247,22 @@ export default class EditableSource extends EditableText {
228
247
  return this.editor;
229
248
  }
230
249
 
250
+ if (!this.inputConfig && typeof this.element.dataset.input === "string") {
251
+ this.inputConfig = await this.file?.getInputConfig({
252
+ slug: this.element.dataset.input,
253
+ });
254
+ }
255
+
231
256
  this.editor = await CloudCannon.createTextEditableRegion(
232
257
  this.element,
233
258
  this.onChange.bind(this),
234
259
  {
235
260
  elementType: this.element.dataset.type,
236
- editableType: "content",
237
- inputConfig: { type: "html" },
261
+ editableType: this.inputConfig ? undefined : "content",
262
+ inputConfig: {
263
+ ...this.inputConfig,
264
+ type: "html",
265
+ },
238
266
  },
239
267
  );
240
268
 
@@ -1,4 +1,5 @@
1
1
  import { CloudCannon } from "../helpers/cloudcannon.mjs";
2
+ import { createErrorCard } from "../helpers/error-card.js";
2
3
  import Editable from "./editable.js";
3
4
 
4
5
  type EditableFocusEvent = CustomEvent<number>;
@@ -20,13 +21,12 @@ export default class EditableText extends Editable {
20
21
  const prop = this.element.dataset.prop;
21
22
  if (typeof prop !== "string") {
22
23
  this.element.classList.add("errored");
23
- const error = document.createElement("editable-region-error-card");
24
- error.setAttribute("heading", "Failed to render text editable region");
25
- error.setAttribute(
26
- "message",
27
- "Text editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
24
+ this.element.replaceChildren(
25
+ createErrorCard(
26
+ "Failed to render text editable region",
27
+ "Text editable regions require a 'data-prop' HTML attribute but none was provided. Please check that this element has a valid 'data-prop' attribute.",
28
+ ),
28
29
  );
29
- this.element.replaceChildren(error);
30
30
  return false;
31
31
  }
32
32
 
@@ -36,13 +36,12 @@ export default class EditableText extends Editable {
36
36
  !["span", "text", "block"].includes(elementType)
37
37
  ) {
38
38
  this.element.classList.add("errored");
39
- const error = document.createElement("editable-region-error-card");
40
- error.setAttribute("heading", "Failed to render text editable region");
41
- error.setAttribute(
42
- "message",
43
- `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.`,
39
+ this.element.replaceChildren(
40
+ createErrorCard(
41
+ "Failed to render text editable region",
42
+ `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.`,
43
+ ),
44
44
  );
45
- this.element.replaceChildren(error);
46
45
  return false;
47
46
  }
48
47
  return true;
@@ -51,24 +50,15 @@ export default class EditableText extends Editable {
51
50
  validateValue(value: unknown): string | null | undefined {
52
51
  if (typeof value !== "string" && value !== null) {
53
52
  this.element.classList.add("errored");
54
- const error = document.createElement("editable-region-error-card");
55
- error.setAttribute("heading", "Failed to render text editable region");
56
- error.setAttribute(
57
- "message",
58
- `Text editable regions expect to receive a value of type "string" but instead received a value of type '${typeof value}'.`,
53
+ this.element.replaceChildren(
54
+ createErrorCard(
55
+ "Failed to render text editable region",
56
+ `Text editable regions expect to receive a value of type "string" but instead received a value of type '${typeof value}'.`,
57
+ this.contextBase?.fullPath
58
+ ? `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`
59
+ : `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
60
+ ),
59
61
  );
60
- if (this.contextBase?.fullPath) {
61
- error.setAttribute(
62
- "hint",
63
- `This may mean that the 'data-prop' attribute is incorrectly set for this element, the full 'data-prop' path was '${this.contextBase?.fullPath}'.`,
64
- );
65
- } else {
66
- error.setAttribute(
67
- "hint",
68
- `This may mean that the 'data-prop' attribute is incorrectly set for this element.`,
69
- );
70
- }
71
- this.element.replaceChildren(error);
72
62
  return;
73
63
  }
74
64
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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",
@@ -104,30 +105,30 @@
104
105
  "./internal/styles": "./styles/index.js"
105
106
  },
106
107
  "devDependencies": {
107
- "@astrojs/react": "6.0.5",
108
+ "@astrojs/react": "7.0.1",
108
109
  "@astrojs/svelte": "9.0.1",
109
- "@astrojs/vue": "7.0.2",
110
- "@biomejs/biome": "2.5.13",
110
+ "@astrojs/vue": "7.0.3",
111
+ "@biomejs/biome": "2.5.15",
111
112
  "@cloudcannon/visual-editor-api": "0.0.20",
112
113
  "@sindresorhus/slugify": "3.0.1",
113
- "@sveltejs/vite-plugin-svelte": "7.3.0",
114
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
114
115
  "@types/js-beautify": "1.14.3",
115
- "@types/node": "26.5.1",
116
+ "@types/node": "26.6.5",
116
117
  "@types/react": "19.3.0",
117
118
  "@types/react-dom": "19.3.0",
118
- "@vitejs/plugin-vue": "6.0.8",
119
- "astro": "7.3.2",
120
- "happy-dom": "20.14.5",
119
+ "@vitejs/plugin-vue": "6.0.10",
120
+ "astro": "7.3.8",
121
+ "happy-dom": "20.14.6",
121
122
  "js-beautify": "2.0.3",
122
- "liquidjs": "10.29.0",
123
+ "liquidjs": "10.30.0",
123
124
  "react": "19.3.0",
124
125
  "react-dom": "19.3.0",
125
126
  "slugify": "1.6.9",
126
- "svelte": "5.57.0",
127
+ "svelte": "5.57.2",
127
128
  "typescript": "6.0.3",
128
- "vitest": "^4.1.11",
129
- "vue": "3.5.42",
130
- "vue-tsc": "3.3.11"
129
+ "vitest": "5.0.3",
130
+ "vue": "3.5.43",
131
+ "vue-tsc": "3.3.13"
131
132
  },
132
133
  "dependencies": {
133
134
  "esbuild": "0.28.2"