@caretcms/core 0.2.0 → 0.3.0
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 +81 -3
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +27 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown/satteri.d.ts.map +1 -1
- package/dist/markdown/satteri.js +32 -8
- package/dist/markdown/satteri.js.map +1 -1
- package/dist/runtime/auth/session.d.ts +1 -0
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +16 -0
- package/dist/runtime/auth/session.js.map +1 -1
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +4 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/i18n.d.ts +86 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +168 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +40 -2
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +3 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +9 -0
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +121 -10
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +2 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +7 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +5 -0
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/request-context.d.ts +5 -1
- package/dist/runtime/request-context.d.ts.map +1 -1
- package/dist/runtime/request-context.js +7 -1
- package/dist/runtime/request-context.js.map +1 -1
- package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
- package/dist/runtime/routes/admin-entry.js +114 -18
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/admin.d.ts.map +1 -1
- package/dist/runtime/routes/admin.js +28 -14
- package/dist/runtime/routes/admin.js.map +1 -1
- package/dist/runtime/routes/auth-login.d.ts.map +1 -1
- package/dist/runtime/routes/auth-login.js +4 -0
- package/dist/runtime/routes/auth-login.js.map +1 -1
- package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
- package/dist/runtime/routes/auth-logout.js +7 -2
- package/dist/runtime/routes/auth-logout.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +4 -2
- package/dist/runtime/routes/auth-session.js.map +1 -1
- package/dist/runtime/routes/collection-list.d.ts.map +1 -1
- package/dist/runtime/routes/collection-list.js +64 -41
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +15 -6
- package/dist/runtime/routes/entries.js.map +1 -1
- package/dist/runtime/routes/history.d.ts.map +1 -1
- package/dist/runtime/routes/history.js +4 -1
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +7 -2
- package/dist/runtime/routes/schema.js.map +1 -1
- package/dist/runtime/routes/studio-home.d.ts.map +1 -1
- package/dist/runtime/routes/studio-home.js +38 -19
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/schema-registry.d.ts +8 -1
- package/dist/runtime/schema-registry.d.ts.map +1 -1
- package/dist/runtime/schema-registry.js +47 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/themes/preset-tokens.js +4 -4
- package/dist/runtime/themes/preset-tokens.js.map +1 -1
- package/dist/runtime/views/studio-layout.d.ts +3 -0
- package/dist/runtime/views/studio-layout.d.ts.map +1 -1
- package/dist/runtime/views/studio-layout.js +36 -13
- package/dist/runtime/views/studio-layout.js.map +1 -1
- package/dist/schema-utils.d.ts +7 -0
- package/dist/schema-utils.d.ts.map +1 -1
- package/dist/schema-utils.js +143 -0
- package/dist/schema-utils.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/static/cms/admin-entry.css +135 -13
- package/static/cms/admin-entry.js +506 -117
- package/static/cms/editor/image-edit.js +1 -0
- package/static/cms/editor/panel.js +19 -1
- package/static/cms/editor/sync.js +277 -56
- package/static/cms/editor/tokens-and-text.css +9 -0
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +1 -1
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* POST ${api}/upload FormData(file)
|
|
17
17
|
* GET ${api}/history?collection=&id=
|
|
18
18
|
* POST ${api}/history { collection, id, ts } // restore snapshot
|
|
19
|
+
*
|
|
20
|
+
* Successful mutations are announced on the same-origin
|
|
21
|
+
* `caretcms:content` BroadcastChannel so an open site preview can refresh
|
|
22
|
+
* automatically. Embedded Studio panels keep using postMessage for their
|
|
23
|
+
* faster in-page field patching.
|
|
19
24
|
*/
|
|
20
25
|
(function () {
|
|
21
26
|
"use strict";
|
|
@@ -36,6 +41,16 @@
|
|
|
36
41
|
var MOUNT = CFG.mountPath;
|
|
37
42
|
var COLLECTION = CFG.collection;
|
|
38
43
|
var ID = CFG.id;
|
|
44
|
+
var IS_NEW = CFG.isNew === true;
|
|
45
|
+
var INITIALIZE_IF_MISSING = CFG.initializeIfMissing === true;
|
|
46
|
+
var SAVE_TARGET = CFG.saveTarget === "preview" ? "preview" : "live";
|
|
47
|
+
var MSG = CFG.messages || {};
|
|
48
|
+
function msg(key, fallback) { return typeof MSG[key] === "string" ? MSG[key] : fallback; }
|
|
49
|
+
function htmlEscape(value) {
|
|
50
|
+
return String(value).replace(/[&<>"']/g, function (char) {
|
|
51
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
39
54
|
if (!API || !COLLECTION || !ID) return;
|
|
40
55
|
|
|
41
56
|
/* ─── State ─────────────────────────────────────────────────────── */
|
|
@@ -45,6 +60,11 @@
|
|
|
45
60
|
var originalJson = "";
|
|
46
61
|
var saving = false;
|
|
47
62
|
var dirtyTimer = 0;
|
|
63
|
+
var previewTimer = 0;
|
|
64
|
+
var linkedSelectionTimer = 0;
|
|
65
|
+
var pendingRemoteSelection = null;
|
|
66
|
+
var SYNC_CHANNEL = "caretcms:content";
|
|
67
|
+
var syncChannel = null;
|
|
48
68
|
|
|
49
69
|
/* ─── DOM refs ──────────────────────────────────────────────────── */
|
|
50
70
|
var loadingEl = document.getElementById("loading");
|
|
@@ -53,6 +73,7 @@
|
|
|
53
73
|
var fieldsEl = document.getElementById("fields");
|
|
54
74
|
var titleEl = document.getElementById("entry-title");
|
|
55
75
|
var statusEl = document.getElementById("status-msg");
|
|
76
|
+
var validationWarningEl = document.getElementById("validation-warning");
|
|
56
77
|
var saveBtn = document.getElementById("btn-save");
|
|
57
78
|
var deleteBtn = document.getElementById("btn-delete");
|
|
58
79
|
var historyBtn = document.getElementById("btn-history");
|
|
@@ -67,6 +88,56 @@
|
|
|
67
88
|
/* ─── Helpers ───────────────────────────────────────────────────── */
|
|
68
89
|
function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
69
90
|
|
|
91
|
+
function templateFromSchema(schema) {
|
|
92
|
+
if (!schema || typeof schema !== "object") return "";
|
|
93
|
+
if (schema.default !== undefined) return deepClone(schema.default);
|
|
94
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return deepClone(schema.enum[0]);
|
|
95
|
+
if (schema.type === "object") {
|
|
96
|
+
var objectTemplate = {};
|
|
97
|
+
Object.keys(schema.properties || {}).forEach(function (key) {
|
|
98
|
+
objectTemplate[key] = templateFromSchema(schema.properties[key]);
|
|
99
|
+
});
|
|
100
|
+
return objectTemplate;
|
|
101
|
+
}
|
|
102
|
+
if (schema.type === "array") return [];
|
|
103
|
+
if (schema.type === "boolean") return false;
|
|
104
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
105
|
+
return typeof schema.minimum === "number" ? schema.minimum : 0;
|
|
106
|
+
}
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function generatedId(path, index) {
|
|
111
|
+
var parts = path.split(".").filter(Boolean);
|
|
112
|
+
var key = parts.pop() || "item";
|
|
113
|
+
if (/^\d+$/.test(key)) key = parts.pop() || "item";
|
|
114
|
+
key = key.replace(/ies$/, "y").replace(/s$/, "");
|
|
115
|
+
return (key + "-" + (index + 1) + "-" + Date.now().toString(36))
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
118
|
+
.replace(/^-|-$/g, "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function friendlyFileTitle(filename) {
|
|
122
|
+
return String(filename || "Image")
|
|
123
|
+
.replace(/\.[^.]+$/, "")
|
|
124
|
+
.replace(/[-_]+/g, " ")
|
|
125
|
+
.replace(/\s+/g, " ")
|
|
126
|
+
.trim()
|
|
127
|
+
.replace(/\b\w/g, function (c) { return c.toUpperCase(); }) || "Image";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function singularItemLabel(path) {
|
|
131
|
+
var key = (path.split(".").filter(Boolean).pop() || "item").toLowerCase();
|
|
132
|
+
if (key === "images") return "image";
|
|
133
|
+
if (key === "details") return "detail";
|
|
134
|
+
if (key === "documents") return "document";
|
|
135
|
+
if (key === "videos") return "video";
|
|
136
|
+
if (key === "links") return "link";
|
|
137
|
+
if (key === "resume") return "résumé item";
|
|
138
|
+
return key.replace(/ies$/, "y").replace(/s$/, "") || "item";
|
|
139
|
+
}
|
|
140
|
+
|
|
70
141
|
function getTitle(data) {
|
|
71
142
|
if (!data) return "Untitled";
|
|
72
143
|
var keys = ["name", "title", "question", "company_name", "headline", "label"];
|
|
@@ -80,31 +151,6 @@
|
|
|
80
151
|
return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
|
81
152
|
}
|
|
82
153
|
|
|
83
|
-
// Coerce a text-input value back to the field's declared (or original) type so
|
|
84
|
-
// editing an object-array's numeric/boolean/nested field doesn't turn it into a
|
|
85
|
-
// string. Unparseable input returns the raw string (server validation reports it).
|
|
86
|
-
function coerceScalarValue(raw, propSchema, originalVal) {
|
|
87
|
-
var type = (propSchema && propSchema.type) ||
|
|
88
|
-
(typeof originalVal === "number" ? "number" :
|
|
89
|
-
typeof originalVal === "boolean" ? "boolean" :
|
|
90
|
-
(originalVal && typeof originalVal === "object" ? "object" : "string"));
|
|
91
|
-
if (type === "number" || type === "integer") {
|
|
92
|
-
if (raw.trim() === "") return null;
|
|
93
|
-
var n = Number(raw);
|
|
94
|
-
return Number.isFinite(n) ? n : raw;
|
|
95
|
-
}
|
|
96
|
-
if (type === "boolean") {
|
|
97
|
-
var t = raw.trim().toLowerCase();
|
|
98
|
-
if (t === "true") return true;
|
|
99
|
-
if (t === "false") return false;
|
|
100
|
-
return raw;
|
|
101
|
-
}
|
|
102
|
-
if (type === "object" || type === "array") {
|
|
103
|
-
try { return JSON.parse(raw); } catch (e) { return raw; }
|
|
104
|
-
}
|
|
105
|
-
return raw;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
154
|
function isDirty() {
|
|
109
155
|
return entryData !== null && JSON.stringify(entryData) !== originalJson;
|
|
110
156
|
}
|
|
@@ -114,17 +160,22 @@
|
|
|
114
160
|
saveBtn.disabled = !dirty || saving;
|
|
115
161
|
saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
|
|
116
162
|
saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
|
|
117
|
-
saveBtn.textContent = saving ? "Saving…" : "Save";
|
|
163
|
+
saveBtn.textContent = saving ? msg("common.saving", "Saving…") : (SAVE_TARGET === "preview" ? msg("entry.saveDraft", "Save draft") : msg("entry.saveLive", "Save live"));
|
|
118
164
|
titleEl.textContent = getTitle(entryData);
|
|
165
|
+
if (entryData !== null) {
|
|
166
|
+
if (saving) showStatus("saving", msg("entry.savingChanges", "Saving changes…"));
|
|
167
|
+
else if (dirty) showStatus("dirty", msg("entry.unsaved", "Unsaved changes"));
|
|
168
|
+
else showStatus("saved", msg("entry.allSaved", "All changes saved"));
|
|
169
|
+
}
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
function showStatus(type, msg) {
|
|
122
173
|
statusEl.textContent = msg;
|
|
123
174
|
statusEl.hidden = false;
|
|
124
|
-
statusEl.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
statusEl.dataset.status = type;
|
|
176
|
+
statusEl.style.color = type === "saved"
|
|
177
|
+
? "var(--studio-green)"
|
|
178
|
+
: (type === "dirty" || type === "saving" ? "var(--studio-text-muted)" : "var(--studio-red)");
|
|
128
179
|
}
|
|
129
180
|
|
|
130
181
|
function setNestedValue(obj, path, value) {
|
|
@@ -153,6 +204,7 @@
|
|
|
153
204
|
}
|
|
154
205
|
clearTimeout(dirtyTimer);
|
|
155
206
|
dirtyTimer = setTimeout(updateSaveButton, 300);
|
|
207
|
+
queueEmbeddedPreview();
|
|
156
208
|
}
|
|
157
209
|
|
|
158
210
|
function loginRedirect() {
|
|
@@ -164,6 +216,47 @@
|
|
|
164
216
|
return false;
|
|
165
217
|
}
|
|
166
218
|
|
|
219
|
+
function queueEmbeddedPreview() {
|
|
220
|
+
if (window.parent === window || !entryData) return;
|
|
221
|
+
clearTimeout(previewTimer);
|
|
222
|
+
previewTimer = setTimeout(function () {
|
|
223
|
+
try {
|
|
224
|
+
window.parent.postMessage({
|
|
225
|
+
type: "cms:preview",
|
|
226
|
+
collection: COLLECTION,
|
|
227
|
+
id: ID,
|
|
228
|
+
data: deepClone(entryData),
|
|
229
|
+
embedded: true,
|
|
230
|
+
studioPath: window.location.pathname + window.location.search,
|
|
231
|
+
}, window.location.origin);
|
|
232
|
+
} catch (e) { /* visual preview is a progressive enhancement */ }
|
|
233
|
+
}, 80);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function announceChange(type, data) {
|
|
237
|
+
var message = {
|
|
238
|
+
type: type,
|
|
239
|
+
collection: COLLECTION,
|
|
240
|
+
id: ID,
|
|
241
|
+
data: data ? deepClone(data) : null,
|
|
242
|
+
embedded: window.parent !== window,
|
|
243
|
+
studioPath: window.location.pathname + window.location.search,
|
|
244
|
+
savedAt: Date.now(),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (window.parent !== window) {
|
|
248
|
+
try { window.parent.postMessage(message, window.location.origin); } catch (e) { /* noop */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if ("BroadcastChannel" in window) {
|
|
252
|
+
try {
|
|
253
|
+
var channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
254
|
+
channel.postMessage(message);
|
|
255
|
+
setTimeout(function () { channel.close(); }, 0);
|
|
256
|
+
} catch (e) { /* automatic preview sync is a progressive enhancement */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
167
260
|
/* ─── Image compression + upload ────────────────────────────────── */
|
|
168
261
|
function compressImage(file, maxWidth, quality) {
|
|
169
262
|
maxWidth = maxWidth || 1600;
|
|
@@ -217,9 +310,54 @@
|
|
|
217
310
|
}).then(function (json) { return json.url; });
|
|
218
311
|
}
|
|
219
312
|
|
|
313
|
+
function readImageDimensions(file) {
|
|
314
|
+
return createImageBitmap(file).then(function (bitmap) {
|
|
315
|
+
var width = bitmap.width;
|
|
316
|
+
var height = bitmap.height;
|
|
317
|
+
bitmap.close();
|
|
318
|
+
if (width > 1600) {
|
|
319
|
+
height = Math.round((height * 1600) / width);
|
|
320
|
+
width = 1600;
|
|
321
|
+
}
|
|
322
|
+
return { width: width, height: height };
|
|
323
|
+
}).catch(function () { return null; });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function populateImageObject(path, file, dimensions) {
|
|
327
|
+
var parts = path.split(".");
|
|
328
|
+
if (parts.length < 2 || parts[parts.length - 1] !== "src") return;
|
|
329
|
+
var parentPath = parts.slice(0, -1).join(".");
|
|
330
|
+
var current = getNestedValue(entryData, parentPath);
|
|
331
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
332
|
+
|
|
333
|
+
var next = deepClone(current);
|
|
334
|
+
var title = friendlyFileTitle(file.name);
|
|
335
|
+
if (Object.prototype.hasOwnProperty.call(next, "id") && !next.id) {
|
|
336
|
+
next.id = generatedId(parentPath, Number(parts[parts.length - 2]) || 0);
|
|
337
|
+
}
|
|
338
|
+
if (Object.prototype.hasOwnProperty.call(next, "title") && !next.title) next.title = title;
|
|
339
|
+
if (Object.prototype.hasOwnProperty.call(next, "alt") && !next.alt) next.alt = title;
|
|
340
|
+
if (dimensions && Object.prototype.hasOwnProperty.call(next, "width")) next.width = dimensions.width;
|
|
341
|
+
if (dimensions && Object.prototype.hasOwnProperty.call(next, "height")) next.height = dimensions.height;
|
|
342
|
+
updateField(parentPath, next);
|
|
343
|
+
|
|
344
|
+
["id", "title", "alt", "width", "height"].forEach(function (key) {
|
|
345
|
+
if (!Object.prototype.hasOwnProperty.call(next, key)) return;
|
|
346
|
+
var control = document.getElementById(fieldIdFromPath(parentPath + "." + key));
|
|
347
|
+
if (control && "value" in control) control.value = String(next[key]);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
220
351
|
/* ─── Save / Delete ─────────────────────────────────────────────── */
|
|
221
352
|
function save() {
|
|
222
353
|
if (!entryData || saving) return;
|
|
354
|
+
if (SAVE_TARGET === "live") {
|
|
355
|
+
var liveConfirmationKey = "caretcms:confirmed-live-save";
|
|
356
|
+
var confirmed = false;
|
|
357
|
+
try { confirmed = sessionStorage.getItem(liveConfirmationKey) === "1"; } catch (e) { /* storage may be disabled */ }
|
|
358
|
+
if (!confirmed && !window.confirm(msg("entry.liveConfirm", "Save these changes directly to the live site?"))) return;
|
|
359
|
+
try { sessionStorage.setItem(liveConfirmationKey, "1"); } catch (e) { /* confirmation still applies to this save */ }
|
|
360
|
+
}
|
|
223
361
|
saving = true;
|
|
224
362
|
updateSaveButton();
|
|
225
363
|
var payload = {
|
|
@@ -245,7 +383,7 @@
|
|
|
245
383
|
if (!result.ok) {
|
|
246
384
|
if (result.body && result.body.issues) {
|
|
247
385
|
showValidationErrors(result.body.issues);
|
|
248
|
-
showStatus("error", "Validation failed (" + result.body.issues.length + ")");
|
|
386
|
+
showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.body.issues.length + ")");
|
|
249
387
|
var firstErr = fieldsEl.querySelector(".studio-field-error");
|
|
250
388
|
if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
251
389
|
return;
|
|
@@ -255,7 +393,7 @@
|
|
|
255
393
|
// revision, so saving again overwrites. Don't tell the user to reload
|
|
256
394
|
// (that would throw their edits away).
|
|
257
395
|
entryRevision = result.body.currentRevision;
|
|
258
|
-
showStatus("error", "Changed elsewhere — Save again to overwrite");
|
|
396
|
+
showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere — Save again to overwrite"));
|
|
259
397
|
return;
|
|
260
398
|
}
|
|
261
399
|
throw new Error("save failed");
|
|
@@ -263,17 +401,10 @@
|
|
|
263
401
|
clearFieldErrors();
|
|
264
402
|
if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
|
|
265
403
|
originalJson = JSON.stringify(entryData);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
window.parent.postMessage(
|
|
269
|
-
{ type: "cms:saved", collection: COLLECTION, id: ID, data: deepClone(entryData) },
|
|
270
|
-
window.location.origin,
|
|
271
|
-
);
|
|
272
|
-
} catch (e) { /* noop */ }
|
|
273
|
-
}
|
|
274
|
-
showStatus("saved", "Saved");
|
|
404
|
+
announceChange("cms:saved", entryData);
|
|
405
|
+
showStatus("saved", msg("entry.saved", "Saved"));
|
|
275
406
|
}).catch(function () {
|
|
276
|
-
showStatus("error", "Error");
|
|
407
|
+
showStatus("error", msg("common.error", "Error"));
|
|
277
408
|
}).then(function () {
|
|
278
409
|
saving = false;
|
|
279
410
|
updateSaveButton();
|
|
@@ -287,7 +418,7 @@
|
|
|
287
418
|
|
|
288
419
|
function confirmDelete() {
|
|
289
420
|
deleteConfirmBtn.disabled = true;
|
|
290
|
-
deleteConfirmBtn.textContent = "Deleting…";
|
|
421
|
+
deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
|
|
291
422
|
var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
|
|
292
423
|
if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
|
|
293
424
|
fetch(API + "/mutate", {
|
|
@@ -298,12 +429,13 @@
|
|
|
298
429
|
}).then(function (res) {
|
|
299
430
|
if (handleAuth(res)) return;
|
|
300
431
|
if (!res.ok) throw new Error("delete failed");
|
|
432
|
+
announceChange("cms:deleted", null);
|
|
301
433
|
window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
|
|
302
434
|
}).catch(function () {
|
|
303
435
|
deleteDialog.hidden = true;
|
|
304
436
|
deleteConfirmBtn.disabled = false;
|
|
305
|
-
deleteConfirmBtn.textContent = "Delete";
|
|
306
|
-
showStatus("error", "Delete failed");
|
|
437
|
+
deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
|
|
438
|
+
showStatus("error", msg("entry.deleteFailed", "Delete failed"));
|
|
307
439
|
});
|
|
308
440
|
}
|
|
309
441
|
|
|
@@ -343,6 +475,62 @@
|
|
|
343
475
|
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
344
476
|
}
|
|
345
477
|
|
|
478
|
+
function fieldControl(path) {
|
|
479
|
+
return fieldsEl.querySelector("#" + cssEscape(fieldIdFromPath(path)));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function showLinkedSelection(group) {
|
|
483
|
+
window.clearTimeout(linkedSelectionTimer);
|
|
484
|
+
fieldsEl.querySelectorAll(".caret-field-selected").forEach(function (selected) {
|
|
485
|
+
selected.classList.remove("caret-field-selected");
|
|
486
|
+
});
|
|
487
|
+
group.classList.add("caret-field-selected");
|
|
488
|
+
linkedSelectionTimer = window.setTimeout(function () {
|
|
489
|
+
group.classList.remove("caret-field-selected");
|
|
490
|
+
}, 1500);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function applyRemoteSelection(message) {
|
|
494
|
+
if (
|
|
495
|
+
!message
|
|
496
|
+
|| message.collection !== COLLECTION
|
|
497
|
+
|| message.id !== ID
|
|
498
|
+
|| typeof message.field !== "string"
|
|
499
|
+
) return;
|
|
500
|
+
|
|
501
|
+
if (!entryData) {
|
|
502
|
+
pendingRemoteSelection = message;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
var control = fieldControl(message.field);
|
|
507
|
+
var group = control && control.closest(".caret-field-group");
|
|
508
|
+
if (!group) return;
|
|
509
|
+
showLinkedSelection(group);
|
|
510
|
+
group.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function announceFieldSelection(path) {
|
|
514
|
+
var message = {
|
|
515
|
+
type: "cms:field-selected",
|
|
516
|
+
collection: COLLECTION,
|
|
517
|
+
id: ID,
|
|
518
|
+
field: path,
|
|
519
|
+
source: "studio",
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (window.parent !== window) {
|
|
523
|
+
try {
|
|
524
|
+
window.parent.postMessage(Object.assign({ embedded: true }, message), window.location.origin);
|
|
525
|
+
} catch (e) { /* embedded selection linking is a progressive enhancement */ }
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
if (syncChannel) syncChannel.postMessage(message);
|
|
531
|
+
} catch (e) { /* cross-tab selection linking is a progressive enhancement */ }
|
|
532
|
+
}
|
|
533
|
+
|
|
346
534
|
/* ─── Field group ───────────────────────────────────────────────── */
|
|
347
535
|
function fieldIdFromPath(path) {
|
|
348
536
|
return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -369,11 +557,16 @@
|
|
|
369
557
|
queueMicrotask(function () {
|
|
370
558
|
var path = wrapper.dataset.fieldPath;
|
|
371
559
|
if (!path) return;
|
|
372
|
-
|
|
560
|
+
// Nested object/array groups contain other field groups. Only claim a
|
|
561
|
+
// control whose nearest field-group owner is this wrapper; otherwise an
|
|
562
|
+
// outer `hero` group can overwrite `hero.headline`'s stable identity.
|
|
563
|
+
var control = Array.from(wrapper.querySelectorAll("input, select, textarea")).find(function (candidate) {
|
|
564
|
+
return candidate.closest(".caret-field-group") === wrapper;
|
|
565
|
+
});
|
|
373
566
|
if (!control) return;
|
|
374
567
|
var fid = fieldIdFromPath(path);
|
|
375
|
-
|
|
376
|
-
|
|
568
|
+
control.id = fid;
|
|
569
|
+
control.name = path;
|
|
377
570
|
labelEl.htmlFor = control.id;
|
|
378
571
|
});
|
|
379
572
|
|
|
@@ -398,13 +591,7 @@
|
|
|
398
591
|
|
|
399
592
|
// Backfill missing keys with sensible defaults
|
|
400
593
|
if (value === undefined && data) {
|
|
401
|
-
|
|
402
|
-
else if (prop.type === "object") data[key] = {};
|
|
403
|
-
else if (prop.type === "array") data[key] = [];
|
|
404
|
-
else if (prop.type === "boolean") data[key] = false;
|
|
405
|
-
else if (prop.type === "number") data[key] = 0;
|
|
406
|
-
else if (prop.type === "string" && prop.enum) data[key] = prop.enum[0];
|
|
407
|
-
else if (prop.type === "string") data[key] = "";
|
|
594
|
+
data[key] = templateFromSchema(prop);
|
|
408
595
|
}
|
|
409
596
|
var current = data ? data[key] : undefined;
|
|
410
597
|
|
|
@@ -428,7 +615,7 @@
|
|
|
428
615
|
group = createFieldGroup(label, isRequired);
|
|
429
616
|
group.dataset.fieldPath = path;
|
|
430
617
|
group.appendChild(buildSingleImage(current || "", path));
|
|
431
|
-
} else if (prop.type === "string" && prop.format === "textarea") {
|
|
618
|
+
} else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
|
|
432
619
|
group = createFieldGroup(label, isRequired);
|
|
433
620
|
group.dataset.fieldPath = path;
|
|
434
621
|
var ta = document.createElement("textarea");
|
|
@@ -437,7 +624,7 @@
|
|
|
437
624
|
ta.value = current || "";
|
|
438
625
|
ta.addEventListener("input", function () { updateField(path, ta.value); });
|
|
439
626
|
group.appendChild(ta);
|
|
440
|
-
} else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri")) {
|
|
627
|
+
} else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri" || prop.format === "url")) {
|
|
441
628
|
group = createFieldGroup(label, isRequired);
|
|
442
629
|
group.dataset.fieldPath = path;
|
|
443
630
|
var emailInput = document.createElement("input");
|
|
@@ -483,7 +670,20 @@
|
|
|
483
670
|
toggle.classList.toggle("active");
|
|
484
671
|
updateField(path, next);
|
|
485
672
|
});
|
|
673
|
+
toggle.setAttribute("role", "switch");
|
|
674
|
+
toggle.setAttribute("aria-label", label);
|
|
675
|
+
toggle.setAttribute("aria-checked", current ? "true" : "false");
|
|
676
|
+
toggle.addEventListener("click", function () {
|
|
677
|
+
toggle.setAttribute("aria-checked", toggle.classList.contains("active") ? "true" : "false");
|
|
678
|
+
});
|
|
486
679
|
group.appendChild(toggle);
|
|
680
|
+
if (key === "published") {
|
|
681
|
+
group.classList.add("caret-field-with-help");
|
|
682
|
+
var publishHelp = document.createElement("p");
|
|
683
|
+
publishHelp.className = "caret-field-help";
|
|
684
|
+
publishHelp.textContent = "Turn this on and Save when the entry is ready to appear on the live site.";
|
|
685
|
+
group.appendChild(publishHelp);
|
|
686
|
+
}
|
|
487
687
|
} else if (prop.type === "array" && prop.format === "image-gallery") {
|
|
488
688
|
group = createFieldGroup(label, isRequired);
|
|
489
689
|
group.dataset.fieldPath = path;
|
|
@@ -643,7 +843,7 @@
|
|
|
643
843
|
item.draggable = true;
|
|
644
844
|
item.innerHTML =
|
|
645
845
|
'<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
|
|
646
|
-
'<button class="caret-gallery-remove" type="button" title="Remove">×</button>' +
|
|
846
|
+
'<button class="caret-gallery-remove" type="button" title="' + htmlEscape(msg("field.remove", "Remove")) + '">×</button>' +
|
|
647
847
|
'<span class="caret-gallery-index">' + (idx + 1) + "</span>";
|
|
648
848
|
item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
|
|
649
849
|
e.stopPropagation();
|
|
@@ -688,6 +888,7 @@
|
|
|
688
888
|
fileInput.type = "file";
|
|
689
889
|
fileInput.multiple = true;
|
|
690
890
|
fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
|
|
891
|
+
fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
|
|
691
892
|
fileInput.style.display = "none";
|
|
692
893
|
|
|
693
894
|
function handleFiles(files) {
|
|
@@ -732,16 +933,41 @@
|
|
|
732
933
|
/* ─── Single image ──────────────────────────────────────────────── */
|
|
733
934
|
function buildSingleImage(url, path) {
|
|
734
935
|
var wrapper = document.createElement("div");
|
|
936
|
+
wrapper.className = "caret-single-image";
|
|
735
937
|
|
|
736
938
|
function rerender() {
|
|
737
939
|
wrapper.innerHTML = "";
|
|
738
940
|
var current = getNestedValue(entryData, path) || "";
|
|
739
941
|
|
|
942
|
+
var fileInput = document.createElement("input");
|
|
943
|
+
fileInput.type = "file";
|
|
944
|
+
fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
|
|
945
|
+
fileInput.setAttribute("aria-label", msg("field.uploadOrReplaceImage", "Upload or replace image"));
|
|
946
|
+
fileInput.style.display = "none";
|
|
947
|
+
|
|
740
948
|
if (current) {
|
|
741
|
-
var preview = document.createElement("
|
|
949
|
+
var preview = document.createElement("button");
|
|
950
|
+
preview.type = "button";
|
|
742
951
|
preview.className = "caret-single-image-preview";
|
|
743
|
-
preview.
|
|
952
|
+
preview.title = msg("field.replaceImage", "Replace image");
|
|
953
|
+
preview.setAttribute("aria-label", msg("field.replaceImage", "Replace image"));
|
|
954
|
+
var image = document.createElement("img");
|
|
955
|
+
image.src = current;
|
|
956
|
+
image.alt = "Current image preview";
|
|
957
|
+
var overlay = document.createElement("span");
|
|
958
|
+
overlay.className = "caret-single-image-overlay";
|
|
959
|
+
overlay.textContent = msg("field.replaceImage", "Replace image");
|
|
960
|
+
preview.appendChild(image);
|
|
961
|
+
preview.appendChild(overlay);
|
|
962
|
+
preview.addEventListener("click", function () { fileInput.click(); });
|
|
744
963
|
wrapper.appendChild(preview);
|
|
964
|
+
} else {
|
|
965
|
+
var emptyPreview = document.createElement("button");
|
|
966
|
+
emptyPreview.type = "button";
|
|
967
|
+
emptyPreview.className = "caret-single-image-empty";
|
|
968
|
+
emptyPreview.textContent = "+ Choose an image";
|
|
969
|
+
emptyPreview.addEventListener("click", function () { fileInput.click(); });
|
|
970
|
+
wrapper.appendChild(emptyPreview);
|
|
745
971
|
}
|
|
746
972
|
|
|
747
973
|
var row = document.createElement("div");
|
|
@@ -750,6 +976,8 @@
|
|
|
750
976
|
var input = document.createElement("input");
|
|
751
977
|
input.type = "text";
|
|
752
978
|
input.className = "studio-input";
|
|
979
|
+
input.id = fieldIdFromPath(path);
|
|
980
|
+
input.name = path;
|
|
753
981
|
input.style.flex = "1";
|
|
754
982
|
input.value = current;
|
|
755
983
|
input.placeholder = "Image URL";
|
|
@@ -759,12 +987,13 @@
|
|
|
759
987
|
var uploadBtn = document.createElement("button");
|
|
760
988
|
uploadBtn.type = "button";
|
|
761
989
|
uploadBtn.className = "studio-btn-ghost";
|
|
762
|
-
uploadBtn.textContent = "Upload";
|
|
990
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
763
991
|
|
|
764
|
-
var
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
992
|
+
var help = document.createElement("p");
|
|
993
|
+
help.className = "caret-image-help";
|
|
994
|
+
help.textContent = current
|
|
995
|
+
? "Click the preview or Replace to upload a new image. You can also paste a URL. For gallery images, blank title and alt fields are filled from the filename—review them before saving."
|
|
996
|
+
: "Upload an image or paste its URL.";
|
|
768
997
|
|
|
769
998
|
uploadBtn.addEventListener("click", function () { fileInput.click(); });
|
|
770
999
|
fileInput.addEventListener("change", function () {
|
|
@@ -772,11 +1001,14 @@
|
|
|
772
1001
|
if (!file) return;
|
|
773
1002
|
uploadBtn.textContent = "Uploading…";
|
|
774
1003
|
uploadBtn.disabled = true;
|
|
775
|
-
uploadFile(file).then(function (
|
|
776
|
-
updateField(path,
|
|
1004
|
+
Promise.all([uploadFile(file), readImageDimensions(file)]).then(function (results) {
|
|
1005
|
+
updateField(path, results[0]);
|
|
1006
|
+
populateImageObject(path, file, results[1]);
|
|
777
1007
|
rerender();
|
|
778
1008
|
}).catch(function () {
|
|
779
|
-
|
|
1009
|
+
help.textContent = "Upload failed. Check the image type and try again.";
|
|
1010
|
+
help.classList.add("is-error");
|
|
1011
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
780
1012
|
uploadBtn.disabled = false;
|
|
781
1013
|
});
|
|
782
1014
|
fileInput.value = "";
|
|
@@ -786,6 +1018,7 @@
|
|
|
786
1018
|
row.appendChild(uploadBtn);
|
|
787
1019
|
row.appendChild(fileInput);
|
|
788
1020
|
wrapper.appendChild(row);
|
|
1021
|
+
wrapper.appendChild(help);
|
|
789
1022
|
}
|
|
790
1023
|
|
|
791
1024
|
rerender();
|
|
@@ -809,7 +1042,7 @@
|
|
|
809
1042
|
tag.textContent = item;
|
|
810
1043
|
var btn = document.createElement("button");
|
|
811
1044
|
btn.type = "button";
|
|
812
|
-
btn.title = "Remove";
|
|
1045
|
+
btn.title = msg("field.remove", "Remove");
|
|
813
1046
|
btn.textContent = "×";
|
|
814
1047
|
btn.addEventListener("click", function () {
|
|
815
1048
|
var next = current.slice();
|
|
@@ -861,6 +1094,31 @@
|
|
|
861
1094
|
function buildObjectArray(items, path, itemSchema) {
|
|
862
1095
|
var wrapper = document.createElement("div");
|
|
863
1096
|
wrapper.className = "caret-object-array";
|
|
1097
|
+
var draggedIndex = null;
|
|
1098
|
+
|
|
1099
|
+
function itemSummary(item, idx) {
|
|
1100
|
+
var summaryKeys = ["title", "label", "name", "alt", "src"];
|
|
1101
|
+
for (var i = 0; i < summaryKeys.length; i++) {
|
|
1102
|
+
var value = item && item[summaryKeys[i]];
|
|
1103
|
+
if (typeof value === "string" && value.trim()) {
|
|
1104
|
+
if (summaryKeys[i] === "src") {
|
|
1105
|
+
var clean = value.split("?")[0].split("/").pop();
|
|
1106
|
+
return clean || (singularItemLabel(path) + " " + (idx + 1));
|
|
1107
|
+
}
|
|
1108
|
+
return value;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return singularItemLabel(path).replace(/^./, function (c) { return c.toUpperCase(); }) + " " + (idx + 1);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function moveItem(current, from, to) {
|
|
1115
|
+
if (to < 0 || to >= current.length || from === to) return;
|
|
1116
|
+
var next = current.slice();
|
|
1117
|
+
var moved = next.splice(from, 1)[0];
|
|
1118
|
+
next.splice(to, 0, moved);
|
|
1119
|
+
updateField(path, next);
|
|
1120
|
+
rerender();
|
|
1121
|
+
}
|
|
864
1122
|
|
|
865
1123
|
function rerender() {
|
|
866
1124
|
wrapper.innerHTML = "";
|
|
@@ -870,10 +1128,81 @@
|
|
|
870
1128
|
var card = document.createElement("div");
|
|
871
1129
|
card.className = "caret-object-array-item";
|
|
872
1130
|
|
|
1131
|
+
var header = document.createElement("div");
|
|
1132
|
+
header.className = "caret-object-array-header";
|
|
1133
|
+
|
|
1134
|
+
var dragHandle = document.createElement("button");
|
|
1135
|
+
dragHandle.type = "button";
|
|
1136
|
+
dragHandle.className = "caret-object-array-drag";
|
|
1137
|
+
dragHandle.draggable = true;
|
|
1138
|
+
dragHandle.title = msg("field.drag", "Drag to reorder");
|
|
1139
|
+
dragHandle.setAttribute("aria-label", msg("field.drag", "Drag to reorder") + " " + itemSummary(item, idx));
|
|
1140
|
+
dragHandle.textContent = "⠿";
|
|
1141
|
+
dragHandle.addEventListener("dragstart", function (event) {
|
|
1142
|
+
draggedIndex = idx;
|
|
1143
|
+
card.classList.add("caret-object-array-dragging");
|
|
1144
|
+
if (event.dataTransfer) {
|
|
1145
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1146
|
+
event.dataTransfer.setData("text/plain", String(idx));
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
dragHandle.addEventListener("dragend", function () {
|
|
1150
|
+
draggedIndex = null;
|
|
1151
|
+
wrapper.querySelectorAll(".caret-object-array-item").forEach(function (row) {
|
|
1152
|
+
row.classList.remove("caret-object-array-dragging", "caret-object-array-drag-over");
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
header.appendChild(dragHandle);
|
|
1156
|
+
|
|
1157
|
+
var idxLabel = document.createElement("span");
|
|
1158
|
+
idxLabel.className = "caret-object-array-index";
|
|
1159
|
+
idxLabel.textContent = itemSummary(item, idx);
|
|
1160
|
+
header.appendChild(idxLabel);
|
|
1161
|
+
|
|
1162
|
+
card.addEventListener("dragover", function (event) {
|
|
1163
|
+
if (draggedIndex === null || draggedIndex === idx) return;
|
|
1164
|
+
event.preventDefault();
|
|
1165
|
+
card.classList.add("caret-object-array-drag-over");
|
|
1166
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
1167
|
+
});
|
|
1168
|
+
card.addEventListener("dragleave", function () {
|
|
1169
|
+
card.classList.remove("caret-object-array-drag-over");
|
|
1170
|
+
});
|
|
1171
|
+
card.addEventListener("drop", function (event) {
|
|
1172
|
+
event.preventDefault();
|
|
1173
|
+
card.classList.remove("caret-object-array-drag-over");
|
|
1174
|
+
if (draggedIndex === null) return;
|
|
1175
|
+
var from = draggedIndex;
|
|
1176
|
+
draggedIndex = null;
|
|
1177
|
+
moveItem(current, from, idx);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
var actions = document.createElement("div");
|
|
1181
|
+
actions.className = "caret-object-array-actions";
|
|
1182
|
+
|
|
1183
|
+
var upBtn = document.createElement("button");
|
|
1184
|
+
upBtn.type = "button";
|
|
1185
|
+
upBtn.className = "caret-object-array-move";
|
|
1186
|
+
upBtn.title = msg("field.moveUp", "Move up");
|
|
1187
|
+
upBtn.setAttribute("aria-label", msg("field.moveUp", "Move up") + " " + itemSummary(item, idx));
|
|
1188
|
+
upBtn.textContent = "↑";
|
|
1189
|
+
upBtn.disabled = idx === 0;
|
|
1190
|
+
upBtn.addEventListener("click", function () { moveItem(current, idx, idx - 1); });
|
|
1191
|
+
|
|
1192
|
+
var downBtn = document.createElement("button");
|
|
1193
|
+
downBtn.type = "button";
|
|
1194
|
+
downBtn.className = "caret-object-array-move";
|
|
1195
|
+
downBtn.title = msg("field.moveDown", "Move down");
|
|
1196
|
+
downBtn.setAttribute("aria-label", msg("field.moveDown", "Move down") + " " + itemSummary(item, idx));
|
|
1197
|
+
downBtn.textContent = "↓";
|
|
1198
|
+
downBtn.disabled = idx === current.length - 1;
|
|
1199
|
+
downBtn.addEventListener("click", function () { moveItem(current, idx, idx + 1); });
|
|
1200
|
+
|
|
873
1201
|
var removeBtn = document.createElement("button");
|
|
874
1202
|
removeBtn.type = "button";
|
|
875
1203
|
removeBtn.className = "caret-object-array-remove";
|
|
876
|
-
removeBtn.title = "Remove
|
|
1204
|
+
removeBtn.title = msg("field.remove", "Remove") + " " + singularItemLabel(path);
|
|
1205
|
+
removeBtn.setAttribute("aria-label", msg("field.remove", "Remove") + " " + itemSummary(item, idx));
|
|
877
1206
|
removeBtn.textContent = "×";
|
|
878
1207
|
removeBtn.addEventListener("click", function () {
|
|
879
1208
|
var next = current.slice();
|
|
@@ -881,46 +1210,21 @@
|
|
|
881
1210
|
updateField(path, next);
|
|
882
1211
|
rerender();
|
|
883
1212
|
});
|
|
884
|
-
card.appendChild(removeBtn);
|
|
885
1213
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1214
|
+
actions.appendChild(upBtn);
|
|
1215
|
+
actions.appendChild(downBtn);
|
|
1216
|
+
actions.appendChild(removeBtn);
|
|
1217
|
+
header.appendChild(actions);
|
|
1218
|
+
card.appendChild(header);
|
|
890
1219
|
|
|
891
1220
|
var fieldsCont = document.createElement("div");
|
|
892
1221
|
fieldsCont.className = "caret-object-array-fields";
|
|
893
1222
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
var fieldLabel = (propSchema && propSchema.title) || humanizeKey(key);
|
|
900
|
-
|
|
901
|
-
var row = document.createElement("div");
|
|
902
|
-
row.className = "caret-object-array-row";
|
|
903
|
-
|
|
904
|
-
var keyLabel = document.createElement("span");
|
|
905
|
-
keyLabel.className = "caret-object-array-key";
|
|
906
|
-
keyLabel.textContent = fieldLabel;
|
|
907
|
-
|
|
908
|
-
var input = document.createElement("input");
|
|
909
|
-
input.type = "text";
|
|
910
|
-
input.className = "studio-input";
|
|
911
|
-
input.value = typeof val === "string" ? val : (val == null ? "" : JSON.stringify(val));
|
|
912
|
-
// Write back in the field's declared (or original) type, not always a
|
|
913
|
-
// string — otherwise editing a number/boolean/nested field silently
|
|
914
|
-
// rewrites it as text. Unparseable input falls back to the raw string
|
|
915
|
-
// so server-side validation can report it.
|
|
916
|
-
input.addEventListener("input", function () {
|
|
917
|
-
updateField(itemPath, coerceScalarValue(input.value, propSchema, val));
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
row.appendChild(keyLabel);
|
|
921
|
-
row.appendChild(input);
|
|
922
|
-
fieldsCont.appendChild(row);
|
|
923
|
-
});
|
|
1223
|
+
if (itemSchema && itemSchema.properties) {
|
|
1224
|
+
renderFields(fieldsCont, item, itemSchema, path + "." + idx);
|
|
1225
|
+
} else {
|
|
1226
|
+
renderFieldsFallback(fieldsCont, item, path + "." + idx);
|
|
1227
|
+
}
|
|
924
1228
|
|
|
925
1229
|
card.appendChild(fieldsCont);
|
|
926
1230
|
wrapper.appendChild(card);
|
|
@@ -929,22 +1233,34 @@
|
|
|
929
1233
|
var addBtn = document.createElement("button");
|
|
930
1234
|
addBtn.type = "button";
|
|
931
1235
|
addBtn.className = "caret-object-array-add";
|
|
932
|
-
addBtn.textContent = "+ Add
|
|
1236
|
+
addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
|
|
933
1237
|
addBtn.addEventListener("click", function () {
|
|
934
1238
|
var template;
|
|
935
1239
|
if (itemSchema && itemSchema.properties) {
|
|
936
|
-
template =
|
|
937
|
-
Object.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
});
|
|
1240
|
+
template = templateFromSchema(itemSchema);
|
|
1241
|
+
if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
|
|
1242
|
+
template.id = generatedId(path, current.length);
|
|
1243
|
+
}
|
|
941
1244
|
} else {
|
|
942
1245
|
template = current.length > 0
|
|
943
|
-
? Object.fromEntries(Object.keys(current[0]).map(function (k) {
|
|
1246
|
+
? Object.fromEntries(Object.keys(current[0]).map(function (k) {
|
|
1247
|
+
var sample = current[0][k];
|
|
1248
|
+
return [k, typeof sample === "number" ? 0 : (typeof sample === "boolean" ? false : (Array.isArray(sample) ? [] : (sample && typeof sample === "object" ? {} : "")))];
|
|
1249
|
+
}))
|
|
944
1250
|
: {};
|
|
945
1251
|
}
|
|
946
1252
|
updateField(path, current.concat([template]));
|
|
947
1253
|
rerender();
|
|
1254
|
+
var cards = wrapper.querySelectorAll(".caret-object-array-item");
|
|
1255
|
+
var newest = cards[cards.length - 1];
|
|
1256
|
+
var firstControl = newest && newest.querySelector("input, textarea, select, button.caret-single-image-empty");
|
|
1257
|
+
// Focus immediately. Deferring focus to the next animation frame can
|
|
1258
|
+
// steal it back after a fast user (or assistive automation) has already
|
|
1259
|
+
// moved to another control in the new row.
|
|
1260
|
+
if (firstControl) firstControl.focus();
|
|
1261
|
+
requestAnimationFrame(function () {
|
|
1262
|
+
if (newest) newest.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1263
|
+
});
|
|
948
1264
|
});
|
|
949
1265
|
wrapper.appendChild(addBtn);
|
|
950
1266
|
}
|
|
@@ -992,6 +1308,12 @@
|
|
|
992
1308
|
|
|
993
1309
|
info.appendChild(actionSpan);
|
|
994
1310
|
info.appendChild(timeSpan);
|
|
1311
|
+
if (item.editor && typeof item.editor.id === "string") {
|
|
1312
|
+
var editorSpan = document.createElement("span");
|
|
1313
|
+
editorSpan.style.cssText = "font-size:0.7rem;color:var(--studio-text-dim);";
|
|
1314
|
+
editorSpan.textContent = item.editor.name || item.editor.email || item.editor.id;
|
|
1315
|
+
info.appendChild(editorSpan);
|
|
1316
|
+
}
|
|
995
1317
|
row.appendChild(info);
|
|
996
1318
|
|
|
997
1319
|
var restoreBtn = document.createElement("button");
|
|
@@ -1040,6 +1362,7 @@
|
|
|
1040
1362
|
return;
|
|
1041
1363
|
}
|
|
1042
1364
|
historyPanel.hidden = true;
|
|
1365
|
+
announceChange("cms:saved", entryData);
|
|
1043
1366
|
showStatus("saved", "Restored");
|
|
1044
1367
|
}).catch(function () {
|
|
1045
1368
|
showStatus("error", "Restore failed");
|
|
@@ -1048,7 +1371,7 @@
|
|
|
1048
1371
|
|
|
1049
1372
|
/* ─── Wire events ───────────────────────────────────────────────── */
|
|
1050
1373
|
saveBtn.addEventListener("click", save);
|
|
1051
|
-
deleteBtn.addEventListener("click", deleteEntry);
|
|
1374
|
+
if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
|
|
1052
1375
|
historyBtn.addEventListener("click", loadHistory);
|
|
1053
1376
|
historyCloseBtn.addEventListener("click", function () {
|
|
1054
1377
|
historyPanel.hidden = true;
|
|
@@ -1060,6 +1383,41 @@
|
|
|
1060
1383
|
if (e.target === deleteDialog) deleteDialog.hidden = true;
|
|
1061
1384
|
});
|
|
1062
1385
|
|
|
1386
|
+
window.addEventListener("message", function (event) {
|
|
1387
|
+
if (event.origin !== window.location.origin) return;
|
|
1388
|
+
if (event.data && event.data.type === "cms:field-selected") {
|
|
1389
|
+
applyRemoteSelection(event.data);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Focusing a Studio control locates its live-page counterpart without
|
|
1394
|
+
// stealing focus from the editor. The temporary blue cue is distinct from
|
|
1395
|
+
// green save confirmation.
|
|
1396
|
+
fieldsEl.addEventListener("focusin", function (event) {
|
|
1397
|
+
var control = event.target instanceof Element
|
|
1398
|
+
? event.target.closest("input, select, textarea, button")
|
|
1399
|
+
: null;
|
|
1400
|
+
var group = control && control.closest("[data-field-path]");
|
|
1401
|
+
var path = group && group.dataset.fieldPath;
|
|
1402
|
+
if (!path) return;
|
|
1403
|
+
showLinkedSelection(group);
|
|
1404
|
+
announceFieldSelection(path);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
if ("BroadcastChannel" in window) {
|
|
1408
|
+
try {
|
|
1409
|
+
syncChannel = new BroadcastChannel(SYNC_CHANNEL);
|
|
1410
|
+
syncChannel.addEventListener("message", function (event) {
|
|
1411
|
+
if (event.data && event.data.type === "cms:field-selected") {
|
|
1412
|
+
applyRemoteSelection(event.data);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
window.addEventListener("pagehide", function () { syncChannel.close(); }, { once: true });
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
syncChannel = null;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1063
1421
|
window.addEventListener("keydown", function (e) {
|
|
1064
1422
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
1065
1423
|
e.preventDefault();
|
|
@@ -1097,17 +1455,48 @@
|
|
|
1097
1455
|
if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
|
|
1098
1456
|
|
|
1099
1457
|
loadingEl.hidden = true;
|
|
1100
|
-
|
|
1458
|
+
var initializedMissingEntry = !entry && (IS_NEW || INITIALIZE_IF_MISSING);
|
|
1459
|
+
if (!entry && !initializedMissingEntry) {
|
|
1101
1460
|
notFoundEl.hidden = false;
|
|
1102
1461
|
return;
|
|
1103
1462
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1463
|
+
var initialData = entry
|
|
1464
|
+
? entry.data
|
|
1465
|
+
: (schemaJson && schemaJson.template) || templateFromSchema(entrySchema);
|
|
1466
|
+
entryData = deepClone(initialData);
|
|
1467
|
+
entryRevision = entry && typeof entry.revision === "number" ? entry.revision : 0;
|
|
1468
|
+
originalJson = JSON.stringify(initialData);
|
|
1107
1469
|
titleEl.textContent = getTitle(entryData);
|
|
1108
1470
|
renderFields(fieldsEl, entryData, entrySchema, "");
|
|
1471
|
+
if (validationWarningEl && entry && Array.isArray(entry.validationIssues) && entry.validationIssues.length > 0) {
|
|
1472
|
+
validationWarningEl.textContent = "This stored entry has invalid fields: " + entry.validationIssues.map(function (issue) {
|
|
1473
|
+
return (issue.path || "entry") + " — " + issue.message;
|
|
1474
|
+
}).join("; ") + ". Correct them and Save to restore public delivery.";
|
|
1475
|
+
validationWarningEl.hidden = false;
|
|
1476
|
+
}
|
|
1109
1477
|
updateSaveButton();
|
|
1478
|
+
if (IS_NEW || initializedMissingEntry) {
|
|
1479
|
+
var guideTitle = document.getElementById("editor-guide-title");
|
|
1480
|
+
var guideCopy = document.getElementById("editor-guide-copy");
|
|
1481
|
+
if (guideTitle) guideTitle.textContent = msg("entry.newGuideTitle", "Entry created with safe defaults");
|
|
1482
|
+
if (guideCopy) guideCopy.textContent = msg("entry.newGuideCopy", "Fill in the fields below. Save becomes available after your first change. In the sidebar Studio, text and image edits preview on the page as you work; Save confirms them and refreshes the visual preview for structural changes. The Published switch controls whether signed-out visitors can see this entry.");
|
|
1483
|
+
}
|
|
1110
1484
|
editorEl.hidden = false;
|
|
1485
|
+
if (window.parent !== window) {
|
|
1486
|
+
try {
|
|
1487
|
+
window.parent.postMessage({
|
|
1488
|
+
type: "cms:entry-ready",
|
|
1489
|
+
collection: COLLECTION,
|
|
1490
|
+
id: ID,
|
|
1491
|
+
embedded: true,
|
|
1492
|
+
}, window.location.origin);
|
|
1493
|
+
} catch (e) { /* embedded field linking is a progressive enhancement */ }
|
|
1494
|
+
}
|
|
1495
|
+
if (pendingRemoteSelection) {
|
|
1496
|
+
var pending = pendingRemoteSelection;
|
|
1497
|
+
pendingRemoteSelection = null;
|
|
1498
|
+
applyRemoteSelection(pending);
|
|
1499
|
+
}
|
|
1111
1500
|
});
|
|
1112
1501
|
});
|
|
1113
1502
|
}).catch(function () {
|