@cloudcannon/editable-regions 0.0.2
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/LICENSE +5 -0
- package/README.md +5 -0
- package/components/editable-array-component.ts +26 -0
- package/components/editable-array-item-component.ts +26 -0
- package/components/editable-component-component.ts +26 -0
- package/components/editable-image-component.ts +26 -0
- package/components/editable-snippet-component.ts +30 -0
- package/components/editable-source-component.ts +26 -0
- package/components/editable-text-component.ts +26 -0
- package/components/index.ts +35 -0
- package/components/ui/editable-array-item-controls.ts +123 -0
- package/components/ui/editable-component-controls.ts +57 -0
- package/components/ui/editable-region-error-card.ts +77 -0
- package/helpers/checks.ts +126 -0
- package/helpers/cloudcannon.ts +67 -0
- package/helpers/hydrate-editable-regions.ts +71 -0
- package/integrations/astro/astro-integration.mjs +110 -0
- package/integrations/astro/index.mjs +193 -0
- package/integrations/astro/modules/actions.js +74 -0
- package/integrations/astro/modules/assets.js +38 -0
- package/integrations/astro/modules/client-router.astro +5 -0
- package/integrations/astro/modules/content.js +116 -0
- package/integrations/astro/modules/i18n.js +76 -0
- package/integrations/astro/modules/image.astro +33 -0
- package/integrations/astro/modules/middleware.js +27 -0
- package/integrations/astro/modules/picture.astro +7 -0
- package/integrations/astro/modules/transitions.js +63 -0
- package/integrations/astro/react-renderer.mjs +40 -0
- package/integrations/react.mjs +32 -0
- package/nodes/editable-array-item.ts +427 -0
- package/nodes/editable-array.ts +241 -0
- package/nodes/editable-component.ts +273 -0
- package/nodes/editable-image.ts +253 -0
- package/nodes/editable-snippet.ts +148 -0
- package/nodes/editable-source.ts +245 -0
- package/nodes/editable-text.ts +163 -0
- package/nodes/editable.ts +471 -0
- package/nodes/index.ts +8 -0
- package/package.json +64 -0
- package/styles/editable-array-item.css +8 -0
- package/styles/editable-component.css +8 -0
- package/styles/editable-image.css +4 -0
- package/styles/editable-snippet.css +12 -0
- package/styles/editable-source.css +3 -0
- package/styles/editable-text.css +3 -0
- package/styles/index.css +101 -0
- package/styles/index.ts +6 -0
- package/styles/ui/editable-component-controls.css +104 -0
- package/styles/ui/editable-region-error-card.css +25 -0
- package/types/astro.d.ts +19 -0
- package/types/cloudcannon.d.ts +11 -0
- package/types/modules.d.ts +12 -0
- package/types/react.d.ts +5 -0
- package/types/vite.d.ts +9 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { CloudCannon } from "../helpers/cloudcannon.js";
|
|
2
|
+
import Editable from "./editable.js";
|
|
3
|
+
|
|
4
|
+
type EditableFocusEvent = CustomEvent<number>;
|
|
5
|
+
|
|
6
|
+
export default class EditableText extends Editable {
|
|
7
|
+
editor?: any;
|
|
8
|
+
focused = false;
|
|
9
|
+
focusIndex = 0;
|
|
10
|
+
value: string | null | undefined;
|
|
11
|
+
|
|
12
|
+
validateConfiguration(): boolean {
|
|
13
|
+
const prop = this.element.dataset.prop;
|
|
14
|
+
if (typeof prop !== "string") {
|
|
15
|
+
this.element.classList.add("errored");
|
|
16
|
+
const error = document.createElement("editable-region-error-card");
|
|
17
|
+
error.setAttribute("heading", "Failed to render text editable region");
|
|
18
|
+
error.setAttribute("message", "Missing required attribute data-prop");
|
|
19
|
+
this.element.replaceChildren(error);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const elementType = this.element.dataset.type;
|
|
24
|
+
if (
|
|
25
|
+
typeof elementType === "string" &&
|
|
26
|
+
!["span", "text", "block"].includes(elementType)
|
|
27
|
+
) {
|
|
28
|
+
this.element.classList.add("errored");
|
|
29
|
+
const error = document.createElement("editable-region-error-card");
|
|
30
|
+
error.setAttribute("heading", "Failed to render text editable region");
|
|
31
|
+
error.setAttribute(
|
|
32
|
+
"message",
|
|
33
|
+
`Unsupported element type: "${elementType}". Supported element types are span, text, and block.`,
|
|
34
|
+
);
|
|
35
|
+
this.element.replaceChildren(error);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
validateValue(value: unknown): string | null | undefined {
|
|
42
|
+
if (typeof value !== "string" && value !== null) {
|
|
43
|
+
this.element.classList.add("errored");
|
|
44
|
+
const error = document.createElement("editable-region-error-card");
|
|
45
|
+
error.setAttribute("heading", "Failed to render text editable region");
|
|
46
|
+
error.setAttribute(
|
|
47
|
+
"message",
|
|
48
|
+
`Illegal value type: ${typeof value}. Supported types are string.`,
|
|
49
|
+
);
|
|
50
|
+
this.element.replaceChildren(error);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
shouldUpdate(value: string) {
|
|
57
|
+
return (
|
|
58
|
+
!this.focused &&
|
|
59
|
+
value !== this.value &&
|
|
60
|
+
(typeof value === "string" || value === null)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
update(): void {
|
|
65
|
+
this.editor?.setContent(this.value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
mount(): void {
|
|
69
|
+
this.element.addEventListener("blur", () => {
|
|
70
|
+
this.focused = false;
|
|
71
|
+
this.element.dispatchEvent(
|
|
72
|
+
new CustomEvent("editable:blur", {
|
|
73
|
+
bubbles: true,
|
|
74
|
+
detail: this.focusIndex,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.element.addEventListener("focus", () => {
|
|
80
|
+
this.focusIndex += 1;
|
|
81
|
+
this.element.dispatchEvent(
|
|
82
|
+
new CustomEvent("editable:focus", {
|
|
83
|
+
bubbles: true,
|
|
84
|
+
detail: this.focusIndex,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.element.addEventListener("editable:focus", (e: EditableFocusEvent) => {
|
|
90
|
+
this.focused = true;
|
|
91
|
+
this.focusIndex = e.detail;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.element.addEventListener("editable:blur", (e: EditableFocusEvent) => {
|
|
95
|
+
if (e.detail >= this.focusIndex) {
|
|
96
|
+
this.focused = false;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (typeof this.element.dataset.deferMount === "string") {
|
|
101
|
+
this.element.onclick = () => {
|
|
102
|
+
this.focused = true;
|
|
103
|
+
this.mountEditor().then(() => {
|
|
104
|
+
this.element.focus();
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!this.editor) {
|
|
111
|
+
this.mountEditor();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async mountEditor(): Promise<any> {
|
|
116
|
+
if (this.editor) {
|
|
117
|
+
return this.editor;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const source = this.resolveSource();
|
|
121
|
+
|
|
122
|
+
if (!source) {
|
|
123
|
+
throw new Error("Source not found");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputConfig = source.endsWith("@content")
|
|
127
|
+
? { type: "markdown" }
|
|
128
|
+
: await this.dispatchGetInputConfig(this.element.dataset.prop);
|
|
129
|
+
|
|
130
|
+
this.editor = await CloudCannon.createTextEditableRegion(
|
|
131
|
+
this.element,
|
|
132
|
+
this.onChange.bind(this),
|
|
133
|
+
{
|
|
134
|
+
elementType: this.element.dataset.type,
|
|
135
|
+
editableType: source.endsWith("@content") ? "content" : undefined,
|
|
136
|
+
inputConfig,
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (typeof this.value === "string") {
|
|
141
|
+
this.update();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this.editor;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onChange(value?: string | null) {
|
|
148
|
+
const source = this.element.dataset.prop;
|
|
149
|
+
if (typeof source !== "string") {
|
|
150
|
+
throw new Error("Source not found");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.value = value;
|
|
154
|
+
this.dispatchSet(source, value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
declare global {
|
|
159
|
+
interface HTMLElementEventMap {
|
|
160
|
+
"editable:focus": EditableFocusEvent;
|
|
161
|
+
"editable:blur": EditableFocusEvent;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CloudCannonJavaScriptV1APICollection,
|
|
3
|
+
CloudCannonJavaScriptV1APIDataset,
|
|
4
|
+
CloudCannonJavaScriptV1APIFile,
|
|
5
|
+
} from "@cloudcannon/javascript-api";
|
|
6
|
+
import { hasEditable } from "../helpers/checks";
|
|
7
|
+
import { CloudCannon, loadedPromise } from "../helpers/cloudcannon";
|
|
8
|
+
|
|
9
|
+
export interface EditableListener {
|
|
10
|
+
editable: Editable;
|
|
11
|
+
key?: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface APIListener {
|
|
16
|
+
obj:
|
|
17
|
+
| CloudCannonJavaScriptV1APIFile
|
|
18
|
+
| CloudCannonJavaScriptV1APICollection
|
|
19
|
+
| CloudCannonJavaScriptV1APIDataset;
|
|
20
|
+
fn: () => void;
|
|
21
|
+
event: "change" | "delete";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class Editable {
|
|
25
|
+
APIListeners: APIListener[] = [];
|
|
26
|
+
listeners: EditableListener[] = [];
|
|
27
|
+
value: unknown = undefined;
|
|
28
|
+
parent: Editable | null = null;
|
|
29
|
+
element: HTMLElement;
|
|
30
|
+
mounted = false;
|
|
31
|
+
connected = false;
|
|
32
|
+
|
|
33
|
+
propsBase: unknown;
|
|
34
|
+
props: Record<string, unknown> = {};
|
|
35
|
+
|
|
36
|
+
constructor(element: HTMLElement) {
|
|
37
|
+
this.element = element;
|
|
38
|
+
(element as any).editable = this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async lookupPath(path: string, obj: unknown): Promise<any> {
|
|
42
|
+
if (!path) {
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
return path.split(".").reduce(async (acc, key) => {
|
|
46
|
+
acc = await acc;
|
|
47
|
+
|
|
48
|
+
if (CloudCannon.isAPICollection(acc)) {
|
|
49
|
+
acc = await acc.items();
|
|
50
|
+
} else if (CloudCannon.isAPIFile(acc)) {
|
|
51
|
+
if (key === "@content") {
|
|
52
|
+
return acc.content.get();
|
|
53
|
+
}
|
|
54
|
+
acc = await acc.data.get();
|
|
55
|
+
} else if (CloudCannon.isAPIDataset(acc)) {
|
|
56
|
+
const items = await acc.items();
|
|
57
|
+
if (Array.isArray(items)) {
|
|
58
|
+
acc = items;
|
|
59
|
+
} else {
|
|
60
|
+
acc = await items.data.get();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (acc && typeof acc === "object" && key in acc) {
|
|
65
|
+
return (acc as any)[key];
|
|
66
|
+
}
|
|
67
|
+
}, obj);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
shouldUpdate(_value: unknown) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getNewValue(
|
|
75
|
+
value: unknown,
|
|
76
|
+
listener?: EditableListener,
|
|
77
|
+
): Promise<unknown> {
|
|
78
|
+
const { key, path } = listener ?? {};
|
|
79
|
+
const resolvedValue = path ? await this.lookupPath(path, value) : value;
|
|
80
|
+
if (!key) {
|
|
81
|
+
this.propsBase = resolvedValue;
|
|
82
|
+
} else {
|
|
83
|
+
this.props[key] = resolvedValue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Object.entries(this.props).length === 0) {
|
|
87
|
+
return this.validateValue(this.propsBase);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const newValue = Object.entries(this.props).reduce(
|
|
91
|
+
(acc, [key, val]) => {
|
|
92
|
+
(acc as any)[key] = structuredClone(val);
|
|
93
|
+
return acc;
|
|
94
|
+
},
|
|
95
|
+
structuredClone(this.propsBase ?? {}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return this.validateValue(newValue);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async pushValue(value: unknown, listener?: EditableListener): Promise<void> {
|
|
102
|
+
const newValue = await this.getNewValue(value, listener);
|
|
103
|
+
|
|
104
|
+
if (typeof newValue === "undefined" || !this.shouldUpdate(newValue)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.value = newValue;
|
|
109
|
+
if (this.connected && !this.mounted) {
|
|
110
|
+
this.mounted = true;
|
|
111
|
+
this.mount();
|
|
112
|
+
return this.update();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.mounted) {
|
|
116
|
+
return this.update();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
update(): void {
|
|
121
|
+
this.listeners.forEach((listener) =>
|
|
122
|
+
listener.editable.pushValue(this.value, listener),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
validateValue(value: unknown): unknown {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
registerListener(listener: EditableListener): void {
|
|
131
|
+
if (
|
|
132
|
+
this.listeners.find(
|
|
133
|
+
({ editable: other, key }) =>
|
|
134
|
+
listener.editable.element === other.element && listener.key === key,
|
|
135
|
+
)
|
|
136
|
+
) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.value !== undefined) {
|
|
141
|
+
listener.editable.pushValue(this.value, listener);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.listeners.push(listener);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
deregisterListener(target: Editable): void {
|
|
148
|
+
this.listeners = this.listeners.filter(
|
|
149
|
+
({ editable }) => editable.element !== target.element,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
disconnect(): void {
|
|
154
|
+
this.parent?.deregisterListener(this);
|
|
155
|
+
this.parent = null;
|
|
156
|
+
this.APIListeners.forEach(({ obj, fn, event }) =>
|
|
157
|
+
obj.removeEventListener(event, fn),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resolveSource(source?: string): string | undefined {
|
|
162
|
+
if (typeof source !== "string") {
|
|
163
|
+
return this.parent
|
|
164
|
+
? this.parent.resolveSource(this.element.dataset.prop)
|
|
165
|
+
: this.element.dataset.prop;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const [part, ...rest] = source.split(".");
|
|
169
|
+
const propKey = part.charAt(0).toUpperCase() + part.slice(1);
|
|
170
|
+
const propPath = this.element.dataset[`prop${propKey}`];
|
|
171
|
+
|
|
172
|
+
if (propPath) {
|
|
173
|
+
rest.unshift(propPath);
|
|
174
|
+
return this.parent
|
|
175
|
+
? this.parent.resolveSource(rest.join("."))
|
|
176
|
+
: rest.join(".");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof this.element.dataset.prop !== "string") {
|
|
180
|
+
throw new Error(`Failed to resolve source "${source}"`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.element.dataset.prop) {
|
|
184
|
+
source = `${this.element.dataset.prop}.${source}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this.parent && !source.startsWith("@")
|
|
188
|
+
? this.parent.resolveSource(source)
|
|
189
|
+
: source;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
connect(): void {
|
|
193
|
+
loadedPromise.then(() => {
|
|
194
|
+
this.setupListeners();
|
|
195
|
+
if (this.validateConfiguration()) {
|
|
196
|
+
this.connected = true;
|
|
197
|
+
if (this.value !== undefined && !this.mounted) {
|
|
198
|
+
this.mounted = true;
|
|
199
|
+
this.mount();
|
|
200
|
+
this.update();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setupListeners(): void {
|
|
207
|
+
let parentEditable: Editable | undefined;
|
|
208
|
+
let parent = this.element.parentElement;
|
|
209
|
+
while (parent) {
|
|
210
|
+
if (hasEditable(parent)) {
|
|
211
|
+
parentEditable = parent.editable;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
parent = parent.parentElement;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.parent = parentEditable || null;
|
|
218
|
+
|
|
219
|
+
let hasProps = false;
|
|
220
|
+
Object.entries(this.element.dataset).forEach(
|
|
221
|
+
async ([propName, propPath]) => {
|
|
222
|
+
if (!propName.startsWith("prop") || typeof propPath !== "string") {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
hasProps = true;
|
|
227
|
+
|
|
228
|
+
const { collection, file, dataset, source, absolute } =
|
|
229
|
+
this.parseSource(propPath);
|
|
230
|
+
|
|
231
|
+
const listener = {
|
|
232
|
+
editable: this,
|
|
233
|
+
key:
|
|
234
|
+
propName === "prop"
|
|
235
|
+
? undefined
|
|
236
|
+
: propName.substring(4).toLowerCase(),
|
|
237
|
+
path: source,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (!absolute && parentEditable) {
|
|
241
|
+
parentEditable.registerListener(listener);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Any single data path should only be able to refer to a single absolute API object
|
|
246
|
+
const obj = collection || dataset || file;
|
|
247
|
+
if (obj) {
|
|
248
|
+
const handleAPIChange = () => {
|
|
249
|
+
this.pushValue(obj, listener);
|
|
250
|
+
};
|
|
251
|
+
this.APIListeners.push({
|
|
252
|
+
obj,
|
|
253
|
+
fn: handleAPIChange,
|
|
254
|
+
event: "change",
|
|
255
|
+
});
|
|
256
|
+
obj.addEventListener("change", handleAPIChange);
|
|
257
|
+
handleAPIChange();
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.element.addEventListener("cloudcannon-api", async (e: any) => {
|
|
263
|
+
if (e.target !== this.element) {
|
|
264
|
+
if (!e.detail.source) {
|
|
265
|
+
e.detail.source = this.element.dataset.prop;
|
|
266
|
+
} else {
|
|
267
|
+
const source = e.detail.source;
|
|
268
|
+
const [part, ...rest] = source.split(".");
|
|
269
|
+
const propKey = part.charAt(0).toUpperCase() + part.slice(1);
|
|
270
|
+
const propPath = this.element.dataset[`prop${propKey}`];
|
|
271
|
+
|
|
272
|
+
if (propPath) {
|
|
273
|
+
rest.unshift(propPath);
|
|
274
|
+
e.detail.source = rest.join(".");
|
|
275
|
+
} else if (this.element.dataset.prop) {
|
|
276
|
+
e.detail.source = `${this.element.dataset.prop}.${source}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { absolute } = this.parseSource(e.detail.source);
|
|
282
|
+
if (!this.parent || absolute) {
|
|
283
|
+
if (this.executeApiCall(e.detail)) {
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!hasProps) {
|
|
290
|
+
this.mount();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
executeApiCall(options: any): boolean {
|
|
295
|
+
let { file, collection, source, dataset } = this.parseSource(
|
|
296
|
+
options.source,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
let filePromise: Promise<CloudCannonJavaScriptV1APIFile | undefined>;
|
|
300
|
+
if (!file) {
|
|
301
|
+
if (collection && source) {
|
|
302
|
+
const parts = source.split(".");
|
|
303
|
+
const first = Number(parts.shift());
|
|
304
|
+
filePromise = collection.items().then((items) => items[first]);
|
|
305
|
+
source = parts.join(".");
|
|
306
|
+
} else if (dataset) {
|
|
307
|
+
filePromise = dataset.items().then((items) => {
|
|
308
|
+
if (Array.isArray(items) && source) {
|
|
309
|
+
const parts = source.split(".");
|
|
310
|
+
const first = Number(parts.shift());
|
|
311
|
+
source = parts.join(".");
|
|
312
|
+
return items[first];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (CloudCannon.isAPIFile(items)) {
|
|
316
|
+
return items;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
filePromise = Promise.resolve(undefined);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
filePromise = Promise.resolve(file);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
filePromise.then((file) => {
|
|
327
|
+
if (typeof source !== "string") {
|
|
328
|
+
if (options.action === "get-input-config") {
|
|
329
|
+
options.callback({
|
|
330
|
+
options: {
|
|
331
|
+
disable_reorder: true,
|
|
332
|
+
disable_remove: true,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Failed to resolve source for API call: ${options.source}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
switch (options.action) {
|
|
342
|
+
case "edit":
|
|
343
|
+
file?.data.edit({ slug: source });
|
|
344
|
+
break;
|
|
345
|
+
case "set":
|
|
346
|
+
if (source?.endsWith("@content")) {
|
|
347
|
+
file?.content.set(options.value);
|
|
348
|
+
} else if (source) {
|
|
349
|
+
file?.data.set({ slug: source, value: options.value });
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case "add-array-item":
|
|
353
|
+
file?.data.addArrayItem({
|
|
354
|
+
slug: source,
|
|
355
|
+
index: options.newIndex,
|
|
356
|
+
value: options.value,
|
|
357
|
+
});
|
|
358
|
+
break;
|
|
359
|
+
case "remove-array-item":
|
|
360
|
+
file?.data.removeArrayItem({
|
|
361
|
+
slug: source,
|
|
362
|
+
index: options.fromIndex,
|
|
363
|
+
});
|
|
364
|
+
break;
|
|
365
|
+
case "move-array-item":
|
|
366
|
+
file?.data.moveArrayItem({
|
|
367
|
+
slug: source,
|
|
368
|
+
index: options.fromIndex,
|
|
369
|
+
toIndex: options.toIndex,
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case "get-input-config":
|
|
373
|
+
file?.getInputConfig({ slug: source }).then(options.callback);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
mount(): void {}
|
|
382
|
+
|
|
383
|
+
validateConfiguration(): boolean {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
dispatchSet(source: string, value: unknown) {
|
|
388
|
+
this.element.dispatchEvent(
|
|
389
|
+
new CustomEvent("cloudcannon-api", {
|
|
390
|
+
bubbles: true,
|
|
391
|
+
detail: {
|
|
392
|
+
action: "set",
|
|
393
|
+
source,
|
|
394
|
+
value,
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async dispatchGetInputConfig(source?: string): Promise<any> {
|
|
401
|
+
return new Promise((resolve) => {
|
|
402
|
+
this.element.dispatchEvent(
|
|
403
|
+
new CustomEvent("cloudcannon-api", {
|
|
404
|
+
bubbles: true,
|
|
405
|
+
detail: {
|
|
406
|
+
action: "get-input-config",
|
|
407
|
+
source,
|
|
408
|
+
callback: resolve,
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
parseSource(source?: string) {
|
|
416
|
+
let collection: CloudCannonJavaScriptV1APICollection | undefined;
|
|
417
|
+
let file: CloudCannonJavaScriptV1APIFile | undefined;
|
|
418
|
+
let dataset: CloudCannonJavaScriptV1APIDataset | undefined;
|
|
419
|
+
let absolute = false;
|
|
420
|
+
|
|
421
|
+
const collectionMatch = source?.match(
|
|
422
|
+
/^@collections\[(?<key>[^\]]+)\](\.(?<rest>.+))?$/,
|
|
423
|
+
);
|
|
424
|
+
if (collectionMatch?.groups) {
|
|
425
|
+
const { key, rest } = collectionMatch.groups;
|
|
426
|
+
collection = CloudCannon.collection(key);
|
|
427
|
+
source = rest;
|
|
428
|
+
absolute = true;
|
|
429
|
+
} else {
|
|
430
|
+
const fileMatch = source?.match(
|
|
431
|
+
/^@file\[(?<path>[^\]]+)\]\.(?<rest>.+)$/,
|
|
432
|
+
);
|
|
433
|
+
if (fileMatch?.groups) {
|
|
434
|
+
const { path, rest } = fileMatch.groups;
|
|
435
|
+
file = CloudCannon.file(path);
|
|
436
|
+
source = rest;
|
|
437
|
+
absolute = true;
|
|
438
|
+
} else {
|
|
439
|
+
const dataMatch = source?.match(
|
|
440
|
+
/^@data\[(?<key>[^\]]+)\](\.(?<rest>.+))?$/,
|
|
441
|
+
);
|
|
442
|
+
if (dataMatch?.groups) {
|
|
443
|
+
const { key, rest } = dataMatch.groups;
|
|
444
|
+
dataset = CloudCannon.dataset(key);
|
|
445
|
+
source = rest;
|
|
446
|
+
absolute = true;
|
|
447
|
+
} else {
|
|
448
|
+
file = CloudCannon.currentFile();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const snippets = [];
|
|
454
|
+
let snippetMatch = source?.match(/@snippet\[(?<id>[^\]]+)\]\.(?<rest>.+)$/);
|
|
455
|
+
while (snippetMatch?.groups) {
|
|
456
|
+
const { id, rest } = snippetMatch.groups;
|
|
457
|
+
snippets.push(id);
|
|
458
|
+
source = rest;
|
|
459
|
+
snippetMatch = source.match(/@snippet\[(?<id>[^\]]+)\]\.(?<rest>.+)$/);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
collection,
|
|
464
|
+
file,
|
|
465
|
+
source,
|
|
466
|
+
absolute,
|
|
467
|
+
snippets,
|
|
468
|
+
dataset,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
package/nodes/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as Editable } from "./editable.js";
|
|
2
|
+
export { default as EditableArray } from "./editable-array.js";
|
|
3
|
+
export { default as EditableArrayItem } from "./editable-array-item.js";
|
|
4
|
+
export { default as EditableText } from "./editable-text.js";
|
|
5
|
+
export { default as EditableComponent } from "./editable-component.js";
|
|
6
|
+
export { default as EditableImage } from "./editable-image.js";
|
|
7
|
+
export { default as EditableSource } from "./editable-source.js";
|
|
8
|
+
export { default as EditableSnippet } from "./editable-snippet.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudcannon/editable-regions",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Visual Editing for the CloudCannon CMS.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cloudcannon"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "CloudCannon <support@cloudcannon.com>",
|
|
11
|
+
"homepage": "https://github.com/CloudCannon/editable-regions#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/CloudCannon/editable-regions.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/CloudCannon/editable-regions/issues",
|
|
18
|
+
"email": "support@cloudcannon.com"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"typecheck:watch": "tsc --noEmit --watch",
|
|
23
|
+
"lint-autofix": "biome check --fix",
|
|
24
|
+
"lint": "biome check"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"components",
|
|
28
|
+
"helpers",
|
|
29
|
+
"integrations",
|
|
30
|
+
"nodes",
|
|
31
|
+
"styles",
|
|
32
|
+
"types"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
"./*": null,
|
|
36
|
+
"./astro": {
|
|
37
|
+
"default": "./integrations/astro/index.mjs",
|
|
38
|
+
"types": "./types/astro.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./astro-react-renderer": {
|
|
41
|
+
"default": "./integrations/astro/react-renderer.mjs",
|
|
42
|
+
"types": "./types/astro.d.ts"
|
|
43
|
+
},
|
|
44
|
+
"./astro-integration": {
|
|
45
|
+
"default": "./integrations/astro/astro-integration.mjs",
|
|
46
|
+
"types": "./types/astro.d.ts"
|
|
47
|
+
},
|
|
48
|
+
"./react": {
|
|
49
|
+
"default": "./integrations/react.mjs",
|
|
50
|
+
"types": "./types/react.d.ts"
|
|
51
|
+
},
|
|
52
|
+
"./internal/components": "./components/index.js",
|
|
53
|
+
"./internal/styles": "./styles/index.js"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "1.9.4",
|
|
57
|
+
"@cloudcannon/javascript-api": "0.0.9",
|
|
58
|
+
"@types/js-beautify": "1.14.3",
|
|
59
|
+
"@types/react": "18.3.12",
|
|
60
|
+
"@types/react-dom": "18.3.1",
|
|
61
|
+
"astro": "^5.14.1",
|
|
62
|
+
"typescript": "5.9.3"
|
|
63
|
+
}
|
|
64
|
+
}
|