@cloudcannon/editable-regions 0.0.21 → 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 +2 -0
- package/helpers/error-card.ts +21 -0
- package/helpers/hydrate-editable-regions.ts +13 -16
- package/integrations/hugo/browser/errors.ts +26 -14
- package/integrations/hugo/hugo-module/layouts/partials/editable-regions/find-template-files.html +3 -7
- package/integrations/hugo/renderer/main.go +47 -17
- package/nodes/editable-array-item.ts +6 -6
- package/nodes/editable-array.ts +20 -34
- package/nodes/editable-component.ts +9 -12
- package/nodes/editable-image.ts +2 -6
- package/nodes/editable-source.ts +78 -50
- package/nodes/editable-text.ts +19 -29
- package/package.json +14 -14
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
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
|
|
|
@@ -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(
|
|
48
|
+
if (after.startsWith(TEMPLATE_FRAME_START)) {
|
|
39
49
|
rest = after;
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
|
-
|
|
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
|
|
61
|
-
"
|
|
62
|
-
"
|
|
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 =
|
|
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
|
|
121
|
+
`partial under \`layouts/partials/\` or \`layouts/_partials/\` and ` +
|
|
110
122
|
`rebuild the site.`,
|
|
111
123
|
);
|
|
112
124
|
}
|
package/integrations/hugo/hugo-module/layouts/partials/editable-regions/find-template-files.html
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{{- $files := dict -}}
|
|
2
|
-
{{-
|
|
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
|
-
{{-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
150
|
-
}
|
|
169
|
+
builder.needsFullRebuild = true
|
|
151
170
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
package/nodes/editable-array.ts
CHANGED
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
111
|
-
|
|
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 =
|
|
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;
|
package/nodes/editable-image.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
package/nodes/editable-source.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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: {
|
|
261
|
+
editableType: this.inputConfig ? undefined : "content",
|
|
262
|
+
inputConfig: {
|
|
263
|
+
...this.inputConfig,
|
|
264
|
+
type: "html",
|
|
265
|
+
},
|
|
238
266
|
},
|
|
239
267
|
);
|
|
240
268
|
|
package/nodes/editable-text.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Visual Editing for the CloudCannon CMS.",
|
|
6
6
|
"keywords": [
|
|
@@ -105,30 +105,30 @@
|
|
|
105
105
|
"./internal/styles": "./styles/index.js"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@astrojs/react": "
|
|
108
|
+
"@astrojs/react": "7.0.1",
|
|
109
109
|
"@astrojs/svelte": "9.0.1",
|
|
110
|
-
"@astrojs/vue": "7.0.
|
|
111
|
-
"@biomejs/biome": "2.5.
|
|
110
|
+
"@astrojs/vue": "7.0.3",
|
|
111
|
+
"@biomejs/biome": "2.5.15",
|
|
112
112
|
"@cloudcannon/visual-editor-api": "0.0.20",
|
|
113
113
|
"@sindresorhus/slugify": "3.0.1",
|
|
114
|
-
"@sveltejs/vite-plugin-svelte": "7.3.
|
|
114
|
+
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
|
115
115
|
"@types/js-beautify": "1.14.3",
|
|
116
|
-
"@types/node": "26.5
|
|
116
|
+
"@types/node": "26.6.5",
|
|
117
117
|
"@types/react": "19.3.0",
|
|
118
118
|
"@types/react-dom": "19.3.0",
|
|
119
|
-
"@vitejs/plugin-vue": "6.0.
|
|
120
|
-
"astro": "7.3.
|
|
121
|
-
"happy-dom": "20.14.
|
|
119
|
+
"@vitejs/plugin-vue": "6.0.10",
|
|
120
|
+
"astro": "7.3.8",
|
|
121
|
+
"happy-dom": "20.14.6",
|
|
122
122
|
"js-beautify": "2.0.3",
|
|
123
|
-
"liquidjs": "10.
|
|
123
|
+
"liquidjs": "10.30.0",
|
|
124
124
|
"react": "19.3.0",
|
|
125
125
|
"react-dom": "19.3.0",
|
|
126
126
|
"slugify": "1.6.9",
|
|
127
|
-
"svelte": "5.57.
|
|
127
|
+
"svelte": "5.57.2",
|
|
128
128
|
"typescript": "6.0.3",
|
|
129
|
-
"vitest": "
|
|
130
|
-
"vue": "3.5.
|
|
131
|
-
"vue-tsc": "3.3.
|
|
129
|
+
"vitest": "5.0.3",
|
|
130
|
+
"vue": "3.5.43",
|
|
131
|
+
"vue-tsc": "3.3.13"
|
|
132
132
|
},
|
|
133
133
|
"dependencies": {
|
|
134
134
|
"esbuild": "0.28.2"
|