@caretcms/core 0.1.2 → 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 +109 -13
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -30
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +17 -6
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +57 -14
- package/dist/loader.js.map +1 -1
- package/dist/markdown/block-context.d.ts +21 -0
- package/dist/markdown/block-context.d.ts.map +1 -0
- package/dist/markdown/block-context.js +27 -0
- package/dist/markdown/block-context.js.map +1 -0
- package/dist/markdown/canonical-body.d.ts +30 -0
- package/dist/markdown/canonical-body.d.ts.map +1 -0
- package/dist/markdown/canonical-body.js +54 -0
- package/dist/markdown/canonical-body.js.map +1 -0
- package/dist/markdown/contracts.d.ts +66 -0
- package/dist/markdown/contracts.d.ts.map +1 -0
- package/dist/markdown/contracts.js +102 -0
- package/dist/markdown/contracts.js.map +1 -0
- package/dist/markdown/html-to-snodes.d.ts +26 -0
- package/dist/markdown/html-to-snodes.d.ts.map +1 -0
- package/dist/markdown/html-to-snodes.js +118 -0
- package/dist/markdown/html-to-snodes.js.map +1 -0
- package/dist/markdown/remark.d.ts +35 -0
- package/dist/markdown/remark.d.ts.map +1 -0
- package/dist/markdown/remark.js +66 -0
- package/dist/markdown/remark.js.map +1 -0
- package/dist/markdown/satteri.d.ts +19 -0
- package/dist/markdown/satteri.d.ts.map +1 -0
- package/dist/markdown/satteri.js +144 -0
- package/dist/markdown/satteri.js.map +1 -0
- package/dist/markdown/serialize.d.ts +60 -0
- package/dist/markdown/serialize.d.ts.map +1 -0
- package/dist/markdown/serialize.js +264 -0
- package/dist/markdown/serialize.js.map +1 -0
- package/dist/markdown/splice.d.ts +32 -0
- package/dist/markdown/splice.d.ts.map +1 -0
- package/dist/markdown/splice.js +44 -0
- package/dist/markdown/splice.js.map +1 -0
- package/dist/markdown/stamp.d.ts +68 -0
- package/dist/markdown/stamp.d.ts.map +1 -0
- package/dist/markdown/stamp.js +131 -0
- package/dist/markdown/stamp.js.map +1 -0
- package/dist/runtime/auth/rate-limiter.d.ts.map +1 -1
- package/dist/runtime/auth/rate-limiter.js +5 -0
- package/dist/runtime/auth/rate-limiter.js.map +1 -1
- package/dist/runtime/auth/session.d.ts +3 -3
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +68 -21
- 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/content.d.ts.map +1 -1
- package/dist/runtime/content.js +4 -2
- package/dist/runtime/content.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 +61 -6
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +28 -1
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +76 -0
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +6 -1
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +271 -51
- 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 +13 -1
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +80 -4
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/request-context.d.ts +14 -2
- 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/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +109 -3
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.d.ts.map +1 -1
- package/dist/runtime/rich-allowlist.js +5 -1
- package/dist/runtime/rich-allowlist.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 +33 -15
- 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 +16 -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 +50 -22
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/mutate.d.ts.map +1 -1
- package/dist/runtime/routes/mutate.js +40 -6
- package/dist/runtime/routes/mutate.js.map +1 -1
- package/dist/runtime/routes/publish.d.ts.map +1 -1
- package/dist/runtime/routes/publish.js +5 -2
- package/dist/runtime/routes/publish.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +11 -3
- 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/routes/theme-stylesheet.d.ts.map +1 -1
- package/dist/runtime/routes/theme-stylesheet.js +11 -1
- package/dist/runtime/routes/theme-stylesheet.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/storage/atomic-write.d.ts.map +1 -1
- package/dist/runtime/storage/atomic-write.js +8 -3
- package/dist/runtime/storage/atomic-write.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts +19 -0
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +41 -0
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +8 -0
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +24 -2
- package/dist/runtime/storage/session-overlay-adapter.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/utils.d.ts +10 -0
- package/dist/runtime/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +16 -0
- package/dist/runtime/utils.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 +83 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/cms/admin-entry.css +135 -13
- package/static/cms/admin-entry.js +509 -86
- package/static/cms/editor/guards.js +1 -1
- package/static/cms/editor/image-edit.js +42 -11
- package/static/cms/editor/image.css +7 -1
- package/static/cms/editor/md-block-edit.js +176 -0
- package/static/cms/editor/md-serialize.js +301 -0
- package/static/cms/editor/panel.css +69 -25
- package/static/cms/editor/panel.js +58 -1
- package/static/cms/editor/rich-toolbar.js +4 -3
- package/static/cms/editor/sanitize.js +2 -1
- package/static/cms/editor/sync.js +277 -56
- package/static/cms/editor/text-edit.js +68 -17
- package/static/cms/editor/text-link-interactions.js +4 -4
- package/static/cms/editor/toast.css +114 -2
- package/static/cms/editor/toast.js +121 -22
- package/static/cms/editor/tokens-and-text.css +73 -3
- package/static/cms/editor/toolbar.css +19 -6
- package/static/cms/editor/toolbar.js +64 -23
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +14 -3
- 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"];
|
|
@@ -89,17 +160,22 @@
|
|
|
89
160
|
saveBtn.disabled = !dirty || saving;
|
|
90
161
|
saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
|
|
91
162
|
saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
|
|
92
|
-
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"));
|
|
93
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
|
+
}
|
|
94
170
|
}
|
|
95
171
|
|
|
96
172
|
function showStatus(type, msg) {
|
|
97
173
|
statusEl.textContent = msg;
|
|
98
174
|
statusEl.hidden = false;
|
|
99
|
-
statusEl.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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)");
|
|
103
179
|
}
|
|
104
180
|
|
|
105
181
|
function setNestedValue(obj, path, value) {
|
|
@@ -128,6 +204,7 @@
|
|
|
128
204
|
}
|
|
129
205
|
clearTimeout(dirtyTimer);
|
|
130
206
|
dirtyTimer = setTimeout(updateSaveButton, 300);
|
|
207
|
+
queueEmbeddedPreview();
|
|
131
208
|
}
|
|
132
209
|
|
|
133
210
|
function loginRedirect() {
|
|
@@ -139,6 +216,47 @@
|
|
|
139
216
|
return false;
|
|
140
217
|
}
|
|
141
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
|
+
|
|
142
260
|
/* ─── Image compression + upload ────────────────────────────────── */
|
|
143
261
|
function compressImage(file, maxWidth, quality) {
|
|
144
262
|
maxWidth = maxWidth || 1600;
|
|
@@ -192,9 +310,54 @@
|
|
|
192
310
|
}).then(function (json) { return json.url; });
|
|
193
311
|
}
|
|
194
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
|
+
|
|
195
351
|
/* ─── Save / Delete ─────────────────────────────────────────────── */
|
|
196
352
|
function save() {
|
|
197
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
|
+
}
|
|
198
361
|
saving = true;
|
|
199
362
|
updateSaveButton();
|
|
200
363
|
var payload = {
|
|
@@ -220,14 +383,17 @@
|
|
|
220
383
|
if (!result.ok) {
|
|
221
384
|
if (result.body && result.body.issues) {
|
|
222
385
|
showValidationErrors(result.body.issues);
|
|
223
|
-
showStatus("error", "Validation failed (" + result.body.issues.length + ")");
|
|
386
|
+
showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.body.issues.length + ")");
|
|
224
387
|
var firstErr = fieldsEl.querySelector(".studio-field-error");
|
|
225
388
|
if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
226
389
|
return;
|
|
227
390
|
}
|
|
228
391
|
if (result.status === 409 && result.body && typeof result.body.currentRevision === "number") {
|
|
392
|
+
// Your edits are still in the form; we've refreshed to the server's
|
|
393
|
+
// revision, so saving again overwrites. Don't tell the user to reload
|
|
394
|
+
// (that would throw their edits away).
|
|
229
395
|
entryRevision = result.body.currentRevision;
|
|
230
|
-
showStatus("error", "
|
|
396
|
+
showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere — Save again to overwrite"));
|
|
231
397
|
return;
|
|
232
398
|
}
|
|
233
399
|
throw new Error("save failed");
|
|
@@ -235,17 +401,10 @@
|
|
|
235
401
|
clearFieldErrors();
|
|
236
402
|
if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
|
|
237
403
|
originalJson = JSON.stringify(entryData);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
window.parent.postMessage(
|
|
241
|
-
{ type: "cms:saved", collection: COLLECTION, id: ID, data: deepClone(entryData) },
|
|
242
|
-
window.location.origin,
|
|
243
|
-
);
|
|
244
|
-
} catch (e) { /* noop */ }
|
|
245
|
-
}
|
|
246
|
-
showStatus("saved", "Saved");
|
|
404
|
+
announceChange("cms:saved", entryData);
|
|
405
|
+
showStatus("saved", msg("entry.saved", "Saved"));
|
|
247
406
|
}).catch(function () {
|
|
248
|
-
showStatus("error", "Error");
|
|
407
|
+
showStatus("error", msg("common.error", "Error"));
|
|
249
408
|
}).then(function () {
|
|
250
409
|
saving = false;
|
|
251
410
|
updateSaveButton();
|
|
@@ -259,7 +418,7 @@
|
|
|
259
418
|
|
|
260
419
|
function confirmDelete() {
|
|
261
420
|
deleteConfirmBtn.disabled = true;
|
|
262
|
-
deleteConfirmBtn.textContent = "Deleting…";
|
|
421
|
+
deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
|
|
263
422
|
var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
|
|
264
423
|
if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
|
|
265
424
|
fetch(API + "/mutate", {
|
|
@@ -270,12 +429,13 @@
|
|
|
270
429
|
}).then(function (res) {
|
|
271
430
|
if (handleAuth(res)) return;
|
|
272
431
|
if (!res.ok) throw new Error("delete failed");
|
|
432
|
+
announceChange("cms:deleted", null);
|
|
273
433
|
window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
|
|
274
434
|
}).catch(function () {
|
|
275
435
|
deleteDialog.hidden = true;
|
|
276
436
|
deleteConfirmBtn.disabled = false;
|
|
277
|
-
deleteConfirmBtn.textContent = "Delete";
|
|
278
|
-
showStatus("error", "Delete failed");
|
|
437
|
+
deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
|
|
438
|
+
showStatus("error", msg("entry.deleteFailed", "Delete failed"));
|
|
279
439
|
});
|
|
280
440
|
}
|
|
281
441
|
|
|
@@ -315,6 +475,62 @@
|
|
|
315
475
|
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
316
476
|
}
|
|
317
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
|
+
|
|
318
534
|
/* ─── Field group ───────────────────────────────────────────────── */
|
|
319
535
|
function fieldIdFromPath(path) {
|
|
320
536
|
return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -341,11 +557,16 @@
|
|
|
341
557
|
queueMicrotask(function () {
|
|
342
558
|
var path = wrapper.dataset.fieldPath;
|
|
343
559
|
if (!path) return;
|
|
344
|
-
|
|
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
|
+
});
|
|
345
566
|
if (!control) return;
|
|
346
567
|
var fid = fieldIdFromPath(path);
|
|
347
|
-
|
|
348
|
-
|
|
568
|
+
control.id = fid;
|
|
569
|
+
control.name = path;
|
|
349
570
|
labelEl.htmlFor = control.id;
|
|
350
571
|
});
|
|
351
572
|
|
|
@@ -370,13 +591,7 @@
|
|
|
370
591
|
|
|
371
592
|
// Backfill missing keys with sensible defaults
|
|
372
593
|
if (value === undefined && data) {
|
|
373
|
-
|
|
374
|
-
else if (prop.type === "object") data[key] = {};
|
|
375
|
-
else if (prop.type === "array") data[key] = [];
|
|
376
|
-
else if (prop.type === "boolean") data[key] = false;
|
|
377
|
-
else if (prop.type === "number") data[key] = 0;
|
|
378
|
-
else if (prop.type === "string" && prop.enum) data[key] = prop.enum[0];
|
|
379
|
-
else if (prop.type === "string") data[key] = "";
|
|
594
|
+
data[key] = templateFromSchema(prop);
|
|
380
595
|
}
|
|
381
596
|
var current = data ? data[key] : undefined;
|
|
382
597
|
|
|
@@ -400,7 +615,7 @@
|
|
|
400
615
|
group = createFieldGroup(label, isRequired);
|
|
401
616
|
group.dataset.fieldPath = path;
|
|
402
617
|
group.appendChild(buildSingleImage(current || "", path));
|
|
403
|
-
} else if (prop.type === "string" && prop.format === "textarea") {
|
|
618
|
+
} else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
|
|
404
619
|
group = createFieldGroup(label, isRequired);
|
|
405
620
|
group.dataset.fieldPath = path;
|
|
406
621
|
var ta = document.createElement("textarea");
|
|
@@ -409,7 +624,7 @@
|
|
|
409
624
|
ta.value = current || "";
|
|
410
625
|
ta.addEventListener("input", function () { updateField(path, ta.value); });
|
|
411
626
|
group.appendChild(ta);
|
|
412
|
-
} 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")) {
|
|
413
628
|
group = createFieldGroup(label, isRequired);
|
|
414
629
|
group.dataset.fieldPath = path;
|
|
415
630
|
var emailInput = document.createElement("input");
|
|
@@ -455,7 +670,20 @@
|
|
|
455
670
|
toggle.classList.toggle("active");
|
|
456
671
|
updateField(path, next);
|
|
457
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
|
+
});
|
|
458
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
|
+
}
|
|
459
687
|
} else if (prop.type === "array" && prop.format === "image-gallery") {
|
|
460
688
|
group = createFieldGroup(label, isRequired);
|
|
461
689
|
group.dataset.fieldPath = path;
|
|
@@ -615,7 +843,7 @@
|
|
|
615
843
|
item.draggable = true;
|
|
616
844
|
item.innerHTML =
|
|
617
845
|
'<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
|
|
618
|
-
'<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>' +
|
|
619
847
|
'<span class="caret-gallery-index">' + (idx + 1) + "</span>";
|
|
620
848
|
item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
|
|
621
849
|
e.stopPropagation();
|
|
@@ -660,6 +888,7 @@
|
|
|
660
888
|
fileInput.type = "file";
|
|
661
889
|
fileInput.multiple = true;
|
|
662
890
|
fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
|
|
891
|
+
fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
|
|
663
892
|
fileInput.style.display = "none";
|
|
664
893
|
|
|
665
894
|
function handleFiles(files) {
|
|
@@ -704,16 +933,41 @@
|
|
|
704
933
|
/* ─── Single image ──────────────────────────────────────────────── */
|
|
705
934
|
function buildSingleImage(url, path) {
|
|
706
935
|
var wrapper = document.createElement("div");
|
|
936
|
+
wrapper.className = "caret-single-image";
|
|
707
937
|
|
|
708
938
|
function rerender() {
|
|
709
939
|
wrapper.innerHTML = "";
|
|
710
940
|
var current = getNestedValue(entryData, path) || "";
|
|
711
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
|
+
|
|
712
948
|
if (current) {
|
|
713
|
-
var preview = document.createElement("
|
|
949
|
+
var preview = document.createElement("button");
|
|
950
|
+
preview.type = "button";
|
|
714
951
|
preview.className = "caret-single-image-preview";
|
|
715
|
-
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(); });
|
|
716
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);
|
|
717
971
|
}
|
|
718
972
|
|
|
719
973
|
var row = document.createElement("div");
|
|
@@ -722,6 +976,8 @@
|
|
|
722
976
|
var input = document.createElement("input");
|
|
723
977
|
input.type = "text";
|
|
724
978
|
input.className = "studio-input";
|
|
979
|
+
input.id = fieldIdFromPath(path);
|
|
980
|
+
input.name = path;
|
|
725
981
|
input.style.flex = "1";
|
|
726
982
|
input.value = current;
|
|
727
983
|
input.placeholder = "Image URL";
|
|
@@ -731,12 +987,13 @@
|
|
|
731
987
|
var uploadBtn = document.createElement("button");
|
|
732
988
|
uploadBtn.type = "button";
|
|
733
989
|
uploadBtn.className = "studio-btn-ghost";
|
|
734
|
-
uploadBtn.textContent = "Upload";
|
|
990
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
735
991
|
|
|
736
|
-
var
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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.";
|
|
740
997
|
|
|
741
998
|
uploadBtn.addEventListener("click", function () { fileInput.click(); });
|
|
742
999
|
fileInput.addEventListener("change", function () {
|
|
@@ -744,11 +1001,14 @@
|
|
|
744
1001
|
if (!file) return;
|
|
745
1002
|
uploadBtn.textContent = "Uploading…";
|
|
746
1003
|
uploadBtn.disabled = true;
|
|
747
|
-
uploadFile(file).then(function (
|
|
748
|
-
updateField(path,
|
|
1004
|
+
Promise.all([uploadFile(file), readImageDimensions(file)]).then(function (results) {
|
|
1005
|
+
updateField(path, results[0]);
|
|
1006
|
+
populateImageObject(path, file, results[1]);
|
|
749
1007
|
rerender();
|
|
750
1008
|
}).catch(function () {
|
|
751
|
-
|
|
1009
|
+
help.textContent = "Upload failed. Check the image type and try again.";
|
|
1010
|
+
help.classList.add("is-error");
|
|
1011
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
752
1012
|
uploadBtn.disabled = false;
|
|
753
1013
|
});
|
|
754
1014
|
fileInput.value = "";
|
|
@@ -758,6 +1018,7 @@
|
|
|
758
1018
|
row.appendChild(uploadBtn);
|
|
759
1019
|
row.appendChild(fileInput);
|
|
760
1020
|
wrapper.appendChild(row);
|
|
1021
|
+
wrapper.appendChild(help);
|
|
761
1022
|
}
|
|
762
1023
|
|
|
763
1024
|
rerender();
|
|
@@ -781,7 +1042,7 @@
|
|
|
781
1042
|
tag.textContent = item;
|
|
782
1043
|
var btn = document.createElement("button");
|
|
783
1044
|
btn.type = "button";
|
|
784
|
-
btn.title = "Remove";
|
|
1045
|
+
btn.title = msg("field.remove", "Remove");
|
|
785
1046
|
btn.textContent = "×";
|
|
786
1047
|
btn.addEventListener("click", function () {
|
|
787
1048
|
var next = current.slice();
|
|
@@ -833,6 +1094,31 @@
|
|
|
833
1094
|
function buildObjectArray(items, path, itemSchema) {
|
|
834
1095
|
var wrapper = document.createElement("div");
|
|
835
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
|
+
}
|
|
836
1122
|
|
|
837
1123
|
function rerender() {
|
|
838
1124
|
wrapper.innerHTML = "";
|
|
@@ -842,10 +1128,81 @@
|
|
|
842
1128
|
var card = document.createElement("div");
|
|
843
1129
|
card.className = "caret-object-array-item";
|
|
844
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
|
+
|
|
845
1201
|
var removeBtn = document.createElement("button");
|
|
846
1202
|
removeBtn.type = "button";
|
|
847
1203
|
removeBtn.className = "caret-object-array-remove";
|
|
848
|
-
removeBtn.title = "Remove
|
|
1204
|
+
removeBtn.title = msg("field.remove", "Remove") + " " + singularItemLabel(path);
|
|
1205
|
+
removeBtn.setAttribute("aria-label", msg("field.remove", "Remove") + " " + itemSummary(item, idx));
|
|
849
1206
|
removeBtn.textContent = "×";
|
|
850
1207
|
removeBtn.addEventListener("click", function () {
|
|
851
1208
|
var next = current.slice();
|
|
@@ -853,40 +1210,21 @@
|
|
|
853
1210
|
updateField(path, next);
|
|
854
1211
|
rerender();
|
|
855
1212
|
});
|
|
856
|
-
card.appendChild(removeBtn);
|
|
857
1213
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1214
|
+
actions.appendChild(upBtn);
|
|
1215
|
+
actions.appendChild(downBtn);
|
|
1216
|
+
actions.appendChild(removeBtn);
|
|
1217
|
+
header.appendChild(actions);
|
|
1218
|
+
card.appendChild(header);
|
|
862
1219
|
|
|
863
1220
|
var fieldsCont = document.createElement("div");
|
|
864
1221
|
fieldsCont.className = "caret-object-array-fields";
|
|
865
1222
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
var fieldLabel = (propSchema && propSchema.title) || humanizeKey(key);
|
|
872
|
-
|
|
873
|
-
var row = document.createElement("div");
|
|
874
|
-
row.className = "caret-object-array-row";
|
|
875
|
-
|
|
876
|
-
var keyLabel = document.createElement("span");
|
|
877
|
-
keyLabel.className = "caret-object-array-key";
|
|
878
|
-
keyLabel.textContent = fieldLabel;
|
|
879
|
-
|
|
880
|
-
var input = document.createElement("input");
|
|
881
|
-
input.type = "text";
|
|
882
|
-
input.className = "studio-input";
|
|
883
|
-
input.value = typeof val === "string" ? val : (val == null ? "" : JSON.stringify(val));
|
|
884
|
-
input.addEventListener("input", function () { updateField(itemPath, input.value); });
|
|
885
|
-
|
|
886
|
-
row.appendChild(keyLabel);
|
|
887
|
-
row.appendChild(input);
|
|
888
|
-
fieldsCont.appendChild(row);
|
|
889
|
-
});
|
|
1223
|
+
if (itemSchema && itemSchema.properties) {
|
|
1224
|
+
renderFields(fieldsCont, item, itemSchema, path + "." + idx);
|
|
1225
|
+
} else {
|
|
1226
|
+
renderFieldsFallback(fieldsCont, item, path + "." + idx);
|
|
1227
|
+
}
|
|
890
1228
|
|
|
891
1229
|
card.appendChild(fieldsCont);
|
|
892
1230
|
wrapper.appendChild(card);
|
|
@@ -895,22 +1233,34 @@
|
|
|
895
1233
|
var addBtn = document.createElement("button");
|
|
896
1234
|
addBtn.type = "button";
|
|
897
1235
|
addBtn.className = "caret-object-array-add";
|
|
898
|
-
addBtn.textContent = "+ Add
|
|
1236
|
+
addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
|
|
899
1237
|
addBtn.addEventListener("click", function () {
|
|
900
1238
|
var template;
|
|
901
1239
|
if (itemSchema && itemSchema.properties) {
|
|
902
|
-
template =
|
|
903
|
-
Object.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
});
|
|
1240
|
+
template = templateFromSchema(itemSchema);
|
|
1241
|
+
if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
|
|
1242
|
+
template.id = generatedId(path, current.length);
|
|
1243
|
+
}
|
|
907
1244
|
} else {
|
|
908
1245
|
template = current.length > 0
|
|
909
|
-
? 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
|
+
}))
|
|
910
1250
|
: {};
|
|
911
1251
|
}
|
|
912
1252
|
updateField(path, current.concat([template]));
|
|
913
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
|
+
});
|
|
914
1264
|
});
|
|
915
1265
|
wrapper.appendChild(addBtn);
|
|
916
1266
|
}
|
|
@@ -958,6 +1308,12 @@
|
|
|
958
1308
|
|
|
959
1309
|
info.appendChild(actionSpan);
|
|
960
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
|
+
}
|
|
961
1317
|
row.appendChild(info);
|
|
962
1318
|
|
|
963
1319
|
var restoreBtn = document.createElement("button");
|
|
@@ -1006,6 +1362,7 @@
|
|
|
1006
1362
|
return;
|
|
1007
1363
|
}
|
|
1008
1364
|
historyPanel.hidden = true;
|
|
1365
|
+
announceChange("cms:saved", entryData);
|
|
1009
1366
|
showStatus("saved", "Restored");
|
|
1010
1367
|
}).catch(function () {
|
|
1011
1368
|
showStatus("error", "Restore failed");
|
|
@@ -1014,7 +1371,7 @@
|
|
|
1014
1371
|
|
|
1015
1372
|
/* ─── Wire events ───────────────────────────────────────────────── */
|
|
1016
1373
|
saveBtn.addEventListener("click", save);
|
|
1017
|
-
deleteBtn.addEventListener("click", deleteEntry);
|
|
1374
|
+
if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
|
|
1018
1375
|
historyBtn.addEventListener("click", loadHistory);
|
|
1019
1376
|
historyCloseBtn.addEventListener("click", function () {
|
|
1020
1377
|
historyPanel.hidden = true;
|
|
@@ -1026,6 +1383,41 @@
|
|
|
1026
1383
|
if (e.target === deleteDialog) deleteDialog.hidden = true;
|
|
1027
1384
|
});
|
|
1028
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
|
+
|
|
1029
1421
|
window.addEventListener("keydown", function (e) {
|
|
1030
1422
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
1031
1423
|
e.preventDefault();
|
|
@@ -1063,17 +1455,48 @@
|
|
|
1063
1455
|
if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
|
|
1064
1456
|
|
|
1065
1457
|
loadingEl.hidden = true;
|
|
1066
|
-
|
|
1458
|
+
var initializedMissingEntry = !entry && (IS_NEW || INITIALIZE_IF_MISSING);
|
|
1459
|
+
if (!entry && !initializedMissingEntry) {
|
|
1067
1460
|
notFoundEl.hidden = false;
|
|
1068
1461
|
return;
|
|
1069
1462
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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);
|
|
1073
1469
|
titleEl.textContent = getTitle(entryData);
|
|
1074
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
|
+
}
|
|
1075
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
|
+
}
|
|
1076
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
|
+
}
|
|
1077
1500
|
});
|
|
1078
1501
|
});
|
|
1079
1502
|
}).catch(function () {
|