@caretcms/core 0.2.0 → 0.4.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 +195 -12
- package/dist/index.d.ts +26 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -12
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +35 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown/paragraphs.d.ts +14 -0
- package/dist/markdown/paragraphs.d.ts.map +1 -0
- package/dist/markdown/paragraphs.js +61 -0
- package/dist/markdown/paragraphs.js.map +1 -0
- package/dist/markdown/remark.d.ts.map +1 -1
- package/dist/markdown/remark.js +1 -0
- package/dist/markdown/remark.js.map +1 -1
- package/dist/markdown/satteri.d.ts.map +1 -1
- package/dist/markdown/satteri.js +35 -9
- package/dist/markdown/satteri.js.map +1 -1
- package/dist/markdown/stamp.d.ts +0 -9
- package/dist/markdown/stamp.d.ts.map +1 -1
- package/dist/markdown/stamp.js +6 -0
- package/dist/markdown/stamp.js.map +1 -1
- package/dist/providers/deployment/github.d.ts +27 -0
- package/dist/providers/deployment/github.d.ts.map +1 -0
- package/dist/providers/deployment/github.js +180 -0
- package/dist/providers/deployment/github.js.map +1 -0
- package/dist/providers/deployment/simulated.d.ts +10 -0
- package/dist/providers/deployment/simulated.d.ts.map +1 -0
- package/dist/providers/deployment/simulated.js +29 -0
- package/dist/providers/deployment/simulated.js.map +1 -0
- 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/authorization.d.ts +11 -0
- package/dist/runtime/authorization.d.ts.map +1 -0
- package/dist/runtime/authorization.js +29 -0
- package/dist/runtime/authorization.js.map +1 -0
- package/dist/runtime/collection-policy.d.ts +11 -0
- package/dist/runtime/collection-policy.d.ts.map +1 -0
- package/dist/runtime/collection-policy.js +44 -0
- package/dist/runtime/collection-policy.js.map +1 -0
- 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-errors.d.ts +9 -0
- package/dist/runtime/content-errors.d.ts.map +1 -0
- package/dist/runtime/content-errors.js +17 -0
- package/dist/runtime/content-errors.js.map +1 -0
- package/dist/runtime/content.d.ts.map +1 -1
- package/dist/runtime/content.js +13 -4
- package/dist/runtime/content.js.map +1 -1
- package/dist/runtime/deployment-status.d.ts +14 -0
- package/dist/runtime/deployment-status.d.ts.map +1 -0
- package/dist/runtime/deployment-status.js +57 -0
- package/dist/runtime/deployment-status.js.map +1 -0
- package/dist/runtime/draft-state.d.ts +16 -0
- package/dist/runtime/draft-state.d.ts.map +1 -0
- package/dist/runtime/draft-state.js +29 -0
- package/dist/runtime/draft-state.js.map +1 -0
- package/dist/runtime/i18n.d.ts +97 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +190 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +61 -4
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +12 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +54 -4
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +2 -0
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +220 -57
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +4 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +18 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish-recovery.d.ts +32 -0
- package/dist/runtime/publish-recovery.d.ts.map +1 -0
- package/dist/runtime/publish-recovery.js +98 -0
- package/dist/runtime/publish-recovery.js.map +1 -0
- package/dist/runtime/publish.d.ts +10 -4
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +105 -63
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/rebuild-webhook.d.ts +4 -0
- package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
- package/dist/runtime/rebuild-webhook.js +12 -2
- package/dist/runtime/rebuild-webhook.js.map +1 -1
- package/dist/runtime/request-context.d.ts +7 -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/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +28 -2
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.js +1 -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 +129 -21
- 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 +10 -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 +213 -66
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/deployment.d.ts +5 -0
- package/dist/runtime/routes/deployment.d.ts.map +1 -0
- package/dist/runtime/routes/deployment.js +38 -0
- package/dist/runtime/routes/deployment.js.map +1 -0
- package/dist/runtime/routes/draft.d.ts.map +1 -1
- package/dist/runtime/routes/draft.js +10 -3
- package/dist/runtime/routes/draft.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +87 -43
- 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 +40 -21
- 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 +4 -1
- 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 +127 -33
- 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 -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 +41 -20
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/routes/upload.d.ts.map +1 -1
- package/dist/runtime/routes/upload.js +3 -0
- package/dist/runtime/routes/upload.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 +49 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/storage/entry-commit.d.ts +8 -0
- package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
- package/dist/runtime/storage/entry-commit.js +45 -0
- package/dist/runtime/storage/entry-commit.js.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/filesystem-adapter.js +12 -3
- package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
- package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.js +156 -13
- package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.js +6 -0
- package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +18 -20
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +100 -13
- package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
- package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.js +48 -5
- package/dist/runtime/storage/sidecar-meta-store.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.map +1 -1
- package/dist/runtime/utils.js +3 -4
- 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 +165 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -3
- package/static/cms/admin-entry.css +144 -13
- package/static/cms/admin-entry.js +539 -586
- package/static/cms/dev-toolbar/app.js +78 -19
- package/static/cms/editor/config.js +35 -15
- package/static/cms/editor/content-map.css +8 -1
- package/static/cms/editor/content-map.js +62 -23
- package/static/cms/editor/deployment-status.js +66 -0
- package/static/cms/editor/draft-client.js +87 -0
- package/static/cms/editor/guards.js +20 -0
- package/static/cms/editor/helpers.js +30 -1
- package/static/cms/editor/highlight.js +1 -0
- package/static/cms/editor/image-edit.js +44 -1
- package/static/cms/editor/image-utils.js +23 -7
- package/static/cms/editor/link-follow.js +122 -41
- package/static/cms/editor/link-ui.css +10 -1
- package/static/cms/editor/linkify.js +2 -0
- package/static/cms/editor/md-block-edit.js +140 -37
- package/static/cms/editor/md-paragraphs.js +98 -0
- package/static/cms/editor/md-serialize.js +1 -1
- package/static/cms/editor/panel.js +53 -7
- package/static/cms/editor/preview-mode.js +44 -0
- package/static/cms/editor/publish-result.js +100 -0
- package/static/cms/editor/rich-toolbar.js +58 -15
- package/static/cms/editor/sanitize.js +6 -2
- package/static/cms/editor/save-queue.js +75 -17
- package/static/cms/editor/section-controls/activation.js +3 -0
- package/static/cms/editor/section-controls/api.js +25 -7
- package/static/cms/editor/section-controls/index.js +60 -8
- package/static/cms/editor/section-controls/model.js +39 -4
- package/static/cms/editor/section-controls/mutations.js +15 -0
- package/static/cms/editor/section-controls/page-context.js +9 -0
- package/static/cms/editor/section-controls/pickers.js +53 -2
- package/static/cms/editor/section-controls/reorder.js +23 -0
- package/static/cms/editor/section-controls/selection.js +6 -0
- package/static/cms/editor/section-controls/spacing-drag.js +16 -0
- package/static/cms/editor/section-controls/ui-elements.js +7 -0
- package/static/cms/editor/section-controls/utils.js +7 -0
- package/static/cms/editor/section-controls.css +18 -3
- package/static/cms/editor/security.js +13 -2
- package/static/cms/editor/stega-hydrate.js +17 -4
- package/static/cms/editor/sync.js +475 -56
- package/static/cms/editor/text-edit.js +37 -3
- package/static/cms/editor/text-link-interactions.js +59 -17
- package/static/cms/editor/toast.js +23 -9
- package/static/cms/editor/tokens-and-text.css +9 -0
- package/static/cms/editor/toolbar.css +5 -0
- package/static/cms/editor/toolbar.js +91 -101
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +1 -0
- package/static/cms/studio/entry-loader.js +102 -0
- package/static/cms/studio/field-model.js +110 -0
- package/static/cms/studio/fields.js +319 -0
- package/static/cms/studio/history-client.js +74 -0
- package/static/cms/studio/mutation-client.js +74 -0
- package/static/cms/studio/sync-client.js +107 -0
- package/static/cms/studio/upload-client.js +109 -0
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { deepClone, templateFromSchema, generatedId, friendlyFileTitle, singularItemLabel, getTitle, humanizeKey, fieldIdFromPath, getNestedValue, setNestedValue } from './studio/field-model.js';
|
|
2
|
+
import { createFieldGroup, createFieldRenderer } from './studio/fields.js';
|
|
3
|
+
import { createStudioMutationClient } from './studio/mutation-client.js';
|
|
4
|
+
import { createStudioHistoryClient } from './studio/history-client.js';
|
|
5
|
+
import { createStudioUploadClient, isSupportedImage } from './studio/upload-client.js';
|
|
6
|
+
import { createStudioEntryLoader } from './studio/entry-loader.js';
|
|
7
|
+
import { createStudioSync } from './studio/sync-client.js';
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('../../src/schema-utils.js').JsonSchemaNode} Schema */
|
|
10
|
+
/** @typedef {{ path: string, message: string }} ValidationIssue */
|
|
11
|
+
/** @typedef {{ apiBasePath?: string, mountPath?: string, collection?: string, id?: string, isNew?: boolean, initializeIfMissing?: boolean, saveTarget?: string, previewPath?: string, editable?: boolean, messages?: Record<string, string> }} EntryConfig */
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* Studio entry editor — runtime.
|
|
3
15
|
*
|
|
@@ -16,6 +28,11 @@
|
|
|
16
28
|
* POST ${api}/upload FormData(file)
|
|
17
29
|
* GET ${api}/history?collection=&id=
|
|
18
30
|
* POST ${api}/history { collection, id, ts } // restore snapshot
|
|
31
|
+
*
|
|
32
|
+
* Successful mutations are announced on the same-origin
|
|
33
|
+
* `caretcms:content` BroadcastChannel so an open site preview can refresh
|
|
34
|
+
* automatically. Embedded Studio panels keep using postMessage for their
|
|
35
|
+
* faster in-page field patching.
|
|
19
36
|
*/
|
|
20
37
|
(function () {
|
|
21
38
|
"use strict";
|
|
@@ -24,290 +41,262 @@
|
|
|
24
41
|
var pageRoot = document.getElementById("cms-entry-page");
|
|
25
42
|
if (!configEl || !pageRoot) return;
|
|
26
43
|
|
|
44
|
+
/** @type {EntryConfig} */
|
|
27
45
|
var CFG;
|
|
28
46
|
try {
|
|
29
|
-
CFG = JSON.parse(configEl.textContent || "{}");
|
|
47
|
+
CFG = /** @type {EntryConfig} */ (JSON.parse(configEl.textContent || "{}"));
|
|
30
48
|
} catch (e) {
|
|
31
49
|
console.error("[caret] invalid entry config", e);
|
|
32
50
|
return;
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
var API = CFG.apiBasePath;
|
|
36
|
-
var MOUNT = CFG.mountPath;
|
|
37
|
-
var COLLECTION = CFG.collection;
|
|
38
|
-
var ID = CFG.id;
|
|
53
|
+
var API = typeof CFG.apiBasePath === "string" ? CFG.apiBasePath : "";
|
|
54
|
+
var MOUNT = typeof CFG.mountPath === "string" ? CFG.mountPath : "";
|
|
55
|
+
var COLLECTION = typeof CFG.collection === "string" ? CFG.collection : "";
|
|
56
|
+
var ID = typeof CFG.id === "string" ? CFG.id : "";
|
|
57
|
+
var IS_NEW = CFG.isNew === true;
|
|
58
|
+
var INITIALIZE_IF_MISSING = CFG.initializeIfMissing === true;
|
|
59
|
+
var SAVE_TARGET = CFG.saveTarget === "preview" ? "preview" : "live";
|
|
60
|
+
var PREVIEW_PATH = typeof CFG.previewPath === "string" ? CFG.previewPath : null;
|
|
61
|
+
/** @type {Record<string, string>} */
|
|
62
|
+
var MSG = CFG.messages || {};
|
|
63
|
+
/** @param {string} key @param {string} fallback */
|
|
64
|
+
function msg(key, fallback) { return typeof MSG[key] === "string" ? MSG[key] : fallback; }
|
|
65
|
+
/** @param {unknown} value */
|
|
66
|
+
function htmlEscape(value) {
|
|
67
|
+
/** @type {Record<string, string>} */
|
|
68
|
+
var entities = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
69
|
+
return String(value).replace(/[&<>"']/g, function (char) {
|
|
70
|
+
return entities[char] || char;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
39
73
|
if (!API || !COLLECTION || !ID) return;
|
|
74
|
+
var mutationClient = createStudioMutationClient({ apiBasePath: API });
|
|
75
|
+
var historyClient = createStudioHistoryClient({ apiBasePath: API });
|
|
76
|
+
var uploadClient = createStudioUploadClient({ apiBasePath: API, onUnauthorized: loginRedirect });
|
|
77
|
+
var entryLoader = createStudioEntryLoader({ apiBasePath: API });
|
|
40
78
|
|
|
41
79
|
/* ─── State ─────────────────────────────────────────────────────── */
|
|
80
|
+
/** @type {Record<string, unknown> | null} */
|
|
42
81
|
var entryData = null;
|
|
82
|
+
/** @type {Schema | null} */
|
|
43
83
|
var entrySchema = null;
|
|
84
|
+
/** @type {number | undefined} */
|
|
44
85
|
var entryRevision = undefined;
|
|
45
86
|
var originalJson = "";
|
|
46
87
|
var saving = false;
|
|
47
88
|
var dirtyTimer = 0;
|
|
89
|
+
var linkedSelectionTimer = 0;
|
|
90
|
+
/** @type {Record<string, unknown> | null} */
|
|
91
|
+
var pendingRemoteSelection = null;
|
|
92
|
+
/** @type {string | null} */
|
|
93
|
+
var publicationFieldName = null;
|
|
94
|
+
var studioSync = createStudioSync({
|
|
95
|
+
collection: COLLECTION,
|
|
96
|
+
id: ID,
|
|
97
|
+
previewPath: PREVIEW_PATH,
|
|
98
|
+
origin: window.location.origin,
|
|
99
|
+
isEmbedded: window.parent !== window,
|
|
100
|
+
studioPath: function () { return window.location.pathname + window.location.search; },
|
|
101
|
+
postToParent: function (message) { window.parent.postMessage(message, window.location.origin); },
|
|
102
|
+
onSelection: applyRemoteSelection,
|
|
103
|
+
addWindowMessageListener: function (listener) { window.addEventListener("message", listener); },
|
|
104
|
+
addPagehideListener: function (listener) { window.addEventListener("pagehide", listener, { once: true }); },
|
|
105
|
+
openChannel: "BroadcastChannel" in window ? function (name) { return new BroadcastChannel(name); } : undefined,
|
|
106
|
+
});
|
|
48
107
|
|
|
49
108
|
/* ─── DOM refs ──────────────────────────────────────────────────── */
|
|
50
|
-
var loadingEl = document.getElementById("loading");
|
|
51
|
-
var notFoundEl = document.getElementById("not-found");
|
|
52
|
-
var editorEl = document.getElementById("editor");
|
|
53
|
-
var fieldsEl = document.getElementById("fields");
|
|
54
|
-
var titleEl = document.getElementById("entry-title");
|
|
55
|
-
var statusEl = document.getElementById("status-msg");
|
|
56
|
-
var
|
|
57
|
-
var
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
var
|
|
61
|
-
var
|
|
62
|
-
var
|
|
63
|
-
var
|
|
64
|
-
var
|
|
65
|
-
var
|
|
109
|
+
var loadingEl = /** @type {HTMLElement} */ (document.getElementById("loading"));
|
|
110
|
+
var notFoundEl = /** @type {HTMLElement} */ (document.getElementById("not-found"));
|
|
111
|
+
var editorEl = /** @type {HTMLElement} */ (document.getElementById("editor"));
|
|
112
|
+
var fieldsEl = /** @type {HTMLElement} */ (document.getElementById("fields"));
|
|
113
|
+
var titleEl = /** @type {HTMLElement} */ (document.getElementById("entry-title"));
|
|
114
|
+
var statusEl = /** @type {HTMLElement} */ (document.getElementById("status-msg"));
|
|
115
|
+
var validationWarningEl = /** @type {HTMLElement | null} */ (document.getElementById("validation-warning"));
|
|
116
|
+
var saveBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-save"));
|
|
117
|
+
var deleteBtn = /** @type {HTMLButtonElement | null} */ (document.getElementById("btn-delete"));
|
|
118
|
+
var historyBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-history"));
|
|
119
|
+
var historyPanel = /** @type {HTMLElement} */ (document.getElementById("history-panel"));
|
|
120
|
+
var historyList = /** @type {HTMLElement} */ (document.getElementById("history-list"));
|
|
121
|
+
var historyCloseBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-history-close"));
|
|
122
|
+
var deleteDialog = /** @type {HTMLElement} */ (document.getElementById("delete-dialog"));
|
|
123
|
+
var deleteNameEl = /** @type {HTMLElement} */ (document.getElementById("delete-entry-name"));
|
|
124
|
+
var deleteCancelBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-delete-cancel"));
|
|
125
|
+
var deleteConfirmBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-delete-confirm"));
|
|
126
|
+
/** @type {HTMLElement | null} */
|
|
127
|
+
var deleteRestoreFocus = null;
|
|
66
128
|
|
|
67
129
|
/* ─── Helpers ───────────────────────────────────────────────────── */
|
|
68
|
-
function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
69
|
-
|
|
70
|
-
function getTitle(data) {
|
|
71
|
-
if (!data) return "Untitled";
|
|
72
|
-
var keys = ["name", "title", "question", "company_name", "headline", "label"];
|
|
73
|
-
for (var i = 0; i < keys.length; i++) {
|
|
74
|
-
if (typeof data[keys[i]] === "string" && data[keys[i]]) return data[keys[i]];
|
|
75
|
-
}
|
|
76
|
-
return "Untitled";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function humanizeKey(key) {
|
|
80
|
-
return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
|
81
|
-
}
|
|
82
|
-
|
|
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
130
|
|
|
108
131
|
function isDirty() {
|
|
109
132
|
return entryData !== null && JSON.stringify(entryData) !== originalJson;
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
|
|
135
|
+
/** @param {boolean} [preserveStatus] */
|
|
136
|
+
function updateSaveButton(preserveStatus) {
|
|
113
137
|
var dirty = isDirty();
|
|
114
|
-
saveBtn.disabled = !dirty || saving;
|
|
138
|
+
saveBtn.disabled = CFG.editable === false || !dirty || saving;
|
|
115
139
|
saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
|
|
116
140
|
saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
|
|
117
|
-
saveBtn.textContent = saving ? "Saving…" : "Save";
|
|
141
|
+
saveBtn.textContent = saving ? msg("common.saving", "Saving…") : (SAVE_TARGET === "preview" ? msg("entry.saveDraft", "Save draft") : msg("entry.saveLive", "Save live"));
|
|
118
142
|
titleEl.textContent = getTitle(entryData);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
statusEl.hidden = false;
|
|
124
|
-
statusEl.style.color = type === "saved" ? "var(--studio-green)" : "var(--studio-red)";
|
|
125
|
-
if (type === "saved") {
|
|
126
|
-
setTimeout(function () { statusEl.hidden = true; }, 2000);
|
|
143
|
+
if (entryData !== null && !preserveStatus) {
|
|
144
|
+
if (saving) showStatus("saving", msg("entry.savingChanges", "Saving changes…"));
|
|
145
|
+
else if (dirty) showStatus("dirty", msg("entry.unsaved", "Unsaved changes"));
|
|
146
|
+
else showStatus("saved", msg("entry.allSaved", "All changes saved"));
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
cur = cur[k];
|
|
139
|
-
}
|
|
140
|
-
cur[keys[keys.length - 1]] = value;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function getNestedValue(obj, path) {
|
|
144
|
-
return path.split(".").reduce(function (o, k) { return o == null ? undefined : o[k]; }, obj);
|
|
150
|
+
/** @param {string} type @param {string} message */
|
|
151
|
+
function showStatus(type, message) {
|
|
152
|
+
statusEl.textContent = message;
|
|
153
|
+
statusEl.hidden = false;
|
|
154
|
+
statusEl.dataset.status = type;
|
|
155
|
+
statusEl.style.color = type === "saved"
|
|
156
|
+
? "var(--studio-green)"
|
|
157
|
+
: (type === "dirty" || type === "saving" ? "var(--studio-text-muted)" : "var(--studio-red)");
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
/** @param {string} path @param {unknown} value */
|
|
147
161
|
function updateField(path, value) {
|
|
162
|
+
if (!entryData) return;
|
|
148
163
|
setNestedValue(entryData, path, value);
|
|
149
164
|
if (!saving) {
|
|
150
|
-
saveBtn.disabled = false;
|
|
165
|
+
saveBtn.disabled = CFG.editable === false;
|
|
151
166
|
saveBtn.classList.remove("studio-save-clean", "studio-save-saving");
|
|
152
167
|
saveBtn.classList.add("studio-save-dirty");
|
|
153
168
|
}
|
|
154
169
|
clearTimeout(dirtyTimer);
|
|
155
170
|
dirtyTimer = setTimeout(updateSaveButton, 300);
|
|
171
|
+
studioSync.queuePreview(entryData);
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
function loginRedirect() {
|
|
159
175
|
window.location.href = MOUNT + "?redirect=" + encodeURIComponent(window.location.pathname);
|
|
160
176
|
}
|
|
161
177
|
|
|
178
|
+
/** @param {Response} res */
|
|
162
179
|
function handleAuth(res) {
|
|
163
180
|
if (res.status === 401) { loginRedirect(); return true; }
|
|
164
181
|
return false;
|
|
165
182
|
}
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
var isPng = file.type === "image/png";
|
|
191
|
-
var outType = isPng ? "image/png" : "image/webp";
|
|
192
|
-
return new Promise(function (resolve) {
|
|
193
|
-
canvas.toBlob(function (blob) {
|
|
194
|
-
if (!blob || blob.size >= file.size) return resolve(file);
|
|
195
|
-
var ext = isPng ? ".png" : ".webp";
|
|
196
|
-
resolve(new File([blob], file.name.replace(/\.[^.]+$/, ext), { type: outType }));
|
|
197
|
-
}, outType, isPng ? undefined : quality);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
184
|
+
/** @param {string} path @param {File} file @param {{ width: number, height: number } | null} dimensions */
|
|
185
|
+
function populateImageObject(path, file, dimensions) {
|
|
186
|
+
var parts = path.split(".");
|
|
187
|
+
if (parts.length < 2 || parts[parts.length - 1] !== "src") return;
|
|
188
|
+
var parentPath = parts.slice(0, -1).join(".");
|
|
189
|
+
var current = getNestedValue(entryData, parentPath);
|
|
190
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
191
|
+
|
|
192
|
+
var next = /** @type {Record<string, unknown>} */ (deepClone(current));
|
|
193
|
+
var title = friendlyFileTitle(file.name);
|
|
194
|
+
if (Object.prototype.hasOwnProperty.call(next, "id") && !next.id) {
|
|
195
|
+
next.id = generatedId(parentPath, Number(parts[parts.length - 2]) || 0);
|
|
196
|
+
}
|
|
197
|
+
if (Object.prototype.hasOwnProperty.call(next, "title") && !next.title) next.title = title;
|
|
198
|
+
if (Object.prototype.hasOwnProperty.call(next, "alt") && !next.alt) next.alt = title;
|
|
199
|
+
if (dimensions && Object.prototype.hasOwnProperty.call(next, "width")) next.width = dimensions.width;
|
|
200
|
+
if (dimensions && Object.prototype.hasOwnProperty.call(next, "height")) next.height = dimensions.height;
|
|
201
|
+
updateField(parentPath, next);
|
|
202
|
+
|
|
203
|
+
["id", "title", "alt", "width", "height"].forEach(function (key) {
|
|
204
|
+
if (!Object.prototype.hasOwnProperty.call(next, key)) return;
|
|
205
|
+
var control = document.getElementById(fieldIdFromPath(parentPath + "." + key));
|
|
206
|
+
if (control && "value" in control) control.value = String(next[key]);
|
|
200
207
|
});
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
function uploadFile(file) {
|
|
204
|
-
return compressImage(file).then(function (compressed) {
|
|
205
|
-
var fd = new FormData();
|
|
206
|
-
fd.append("file", compressed);
|
|
207
|
-
return fetch(API + "/upload", {
|
|
208
|
-
method: "POST",
|
|
209
|
-
body: fd,
|
|
210
|
-
headers: { "x-caret-request": "1" },
|
|
211
|
-
credentials: "same-origin",
|
|
212
|
-
});
|
|
213
|
-
}).then(function (res) {
|
|
214
|
-
if (handleAuth(res)) throw new Error("Unauthorized");
|
|
215
|
-
if (!res.ok) throw new Error("Upload failed");
|
|
216
|
-
return res.json();
|
|
217
|
-
}).then(function (json) { return json.url; });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
210
|
/* ─── Save / Delete ─────────────────────────────────────────────── */
|
|
221
211
|
function save() {
|
|
222
212
|
if (!entryData || saving) return;
|
|
213
|
+
if (SAVE_TARGET === "live") {
|
|
214
|
+
var liveConfirmationKey = "caretcms:confirmed-live-save";
|
|
215
|
+
var confirmed = false;
|
|
216
|
+
try { confirmed = sessionStorage.getItem(liveConfirmationKey) === "1"; } catch (e) { /* storage may be disabled */ }
|
|
217
|
+
if (!confirmed && !window.confirm(msg("entry.liveConfirm", "Save these changes directly to the live site?"))) return;
|
|
218
|
+
try { sessionStorage.setItem(liveConfirmationKey, "1"); } catch (e) { /* confirmation still applies to this save */ }
|
|
219
|
+
}
|
|
223
220
|
saving = true;
|
|
224
221
|
updateSaveButton();
|
|
222
|
+
var preserveSaveStatus = false;
|
|
223
|
+
/** @type {{ collection: string, id: string, data: Record<string, unknown>, expectedRevision?: number }} */
|
|
225
224
|
var payload = {
|
|
226
|
-
type: "put_entry",
|
|
227
225
|
collection: COLLECTION,
|
|
228
226
|
id: ID,
|
|
229
227
|
data: entryData,
|
|
230
228
|
};
|
|
231
229
|
if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (handleAuth(res)) return null;
|
|
240
|
-
return res.json().catch(function () { return null; }).then(function (body) {
|
|
241
|
-
return { ok: res.ok, status: res.status, body: body };
|
|
242
|
-
});
|
|
243
|
-
}).then(function (result) {
|
|
244
|
-
if (!result) return;
|
|
245
|
-
if (!result.ok) {
|
|
246
|
-
if (result.body && result.body.issues) {
|
|
247
|
-
showValidationErrors(result.body.issues);
|
|
248
|
-
showStatus("error", "Validation failed (" + result.body.issues.length + ")");
|
|
231
|
+
mutationClient.save(payload).then(function (result) {
|
|
232
|
+
if (result.kind === "unauthorized") { loginRedirect(); return; }
|
|
233
|
+
if (result.kind === "validation") {
|
|
234
|
+
preserveSaveStatus = true;
|
|
235
|
+
showValidationErrors(result.issues);
|
|
236
|
+
showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.issues.length + ")");
|
|
249
237
|
var firstErr = fieldsEl.querySelector(".studio-field-error");
|
|
250
238
|
if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
251
239
|
return;
|
|
252
|
-
|
|
253
|
-
|
|
240
|
+
}
|
|
241
|
+
if (result.kind === "conflict") {
|
|
242
|
+
preserveSaveStatus = true;
|
|
254
243
|
// Your edits are still in the form; we've refreshed to the server's
|
|
255
244
|
// revision, so saving again overwrites. Don't tell the user to reload
|
|
256
245
|
// (that would throw their edits away).
|
|
257
|
-
entryRevision = result.
|
|
258
|
-
showStatus("error", "Changed elsewhere — Save again to overwrite");
|
|
246
|
+
entryRevision = result.currentRevision;
|
|
247
|
+
showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere — Save again to overwrite"));
|
|
259
248
|
return;
|
|
260
|
-
}
|
|
261
|
-
throw new Error("save failed");
|
|
262
249
|
}
|
|
250
|
+
if (result.kind === "error") throw new Error("save failed");
|
|
263
251
|
clearFieldErrors();
|
|
264
|
-
if (
|
|
252
|
+
if (typeof result.revision === "number") entryRevision = result.revision;
|
|
265
253
|
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");
|
|
254
|
+
studioSync.announceChange("cms:saved", entryData);
|
|
255
|
+
showStatus("saved", msg("entry.saved", "Saved"));
|
|
275
256
|
}).catch(function () {
|
|
276
|
-
|
|
257
|
+
preserveSaveStatus = true;
|
|
258
|
+
showStatus("error", msg("common.error", "Error"));
|
|
277
259
|
}).then(function () {
|
|
278
260
|
saving = false;
|
|
279
|
-
updateSaveButton();
|
|
261
|
+
updateSaveButton(preserveSaveStatus);
|
|
280
262
|
});
|
|
281
263
|
}
|
|
282
264
|
|
|
283
265
|
function deleteEntry() {
|
|
284
266
|
deleteNameEl.textContent = ID;
|
|
267
|
+
deleteRestoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : deleteBtn;
|
|
285
268
|
deleteDialog.hidden = false;
|
|
269
|
+
deleteCancelBtn.focus();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function closeDeleteDialog() {
|
|
273
|
+
deleteDialog.hidden = true;
|
|
274
|
+
var restore = deleteRestoreFocus || deleteBtn;
|
|
275
|
+
deleteRestoreFocus = null;
|
|
276
|
+
if (restore instanceof HTMLElement && restore.isConnected) restore.focus();
|
|
286
277
|
}
|
|
287
278
|
|
|
288
279
|
function confirmDelete() {
|
|
289
280
|
deleteConfirmBtn.disabled = true;
|
|
290
|
-
deleteConfirmBtn.textContent = "Deleting…";
|
|
291
|
-
|
|
281
|
+
deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
|
|
282
|
+
/** @type {{ collection: string, id: string, expectedRevision?: number }} */
|
|
283
|
+
var payload = { collection: COLLECTION, id: ID };
|
|
292
284
|
if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
body: JSON.stringify(payload),
|
|
298
|
-
}).then(function (res) {
|
|
299
|
-
if (handleAuth(res)) return;
|
|
300
|
-
if (!res.ok) throw new Error("delete failed");
|
|
285
|
+
mutationClient.remove(payload).then(function (result) {
|
|
286
|
+
if (result === "unauthorized") { loginRedirect(); return; }
|
|
287
|
+
if (result !== "deleted") throw new Error("delete failed");
|
|
288
|
+
studioSync.announceChange("cms:deleted", null);
|
|
301
289
|
window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
|
|
302
290
|
}).catch(function () {
|
|
303
|
-
|
|
291
|
+
closeDeleteDialog();
|
|
304
292
|
deleteConfirmBtn.disabled = false;
|
|
305
|
-
deleteConfirmBtn.textContent = "Delete";
|
|
306
|
-
showStatus("error", "Delete failed");
|
|
293
|
+
deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
|
|
294
|
+
showStatus("error", msg("entry.deleteFailed", "Delete failed"));
|
|
307
295
|
});
|
|
308
296
|
}
|
|
309
297
|
|
|
310
298
|
/* ─── Validation rendering ──────────────────────────────────────── */
|
|
299
|
+
/** @param {Element} group @param {string} message */
|
|
311
300
|
function showFieldError(group, message) {
|
|
312
301
|
var errEl = group.querySelector(".studio-error-text");
|
|
313
302
|
if (message) {
|
|
@@ -317,7 +306,7 @@
|
|
|
317
306
|
group.appendChild(errEl);
|
|
318
307
|
}
|
|
319
308
|
errEl.textContent = message;
|
|
320
|
-
errEl.hidden = false;
|
|
309
|
+
/** @type {HTMLElement} */ (errEl).hidden = false;
|
|
321
310
|
group.classList.add("studio-field-error");
|
|
322
311
|
} else if (errEl) {
|
|
323
312
|
errEl.remove();
|
|
@@ -330,6 +319,7 @@
|
|
|
330
319
|
fieldsEl.querySelectorAll(".caret-field-group").forEach(function (el) { el.classList.remove("studio-field-error"); });
|
|
331
320
|
}
|
|
332
321
|
|
|
322
|
+
/** @param {ValidationIssue[]} issues */
|
|
333
323
|
function showValidationErrors(issues) {
|
|
334
324
|
clearFieldErrors();
|
|
335
325
|
issues.forEach(function (issue) {
|
|
@@ -338,303 +328,77 @@
|
|
|
338
328
|
});
|
|
339
329
|
}
|
|
340
330
|
|
|
331
|
+
/** @param {string} value */
|
|
341
332
|
function cssEscape(value) {
|
|
342
333
|
if (window.CSS && CSS.escape) return CSS.escape(value);
|
|
343
334
|
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
344
335
|
}
|
|
345
336
|
|
|
346
|
-
|
|
347
|
-
function
|
|
348
|
-
return "
|
|
337
|
+
/** @param {string} path */
|
|
338
|
+
function fieldControl(path) {
|
|
339
|
+
return fieldsEl.querySelector("#" + cssEscape(fieldIdFromPath(path)));
|
|
349
340
|
}
|
|
350
341
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
labelEl.className = "studio-label";
|
|
357
|
-
labelEl.textContent = label;
|
|
358
|
-
if (isRequired === false) {
|
|
359
|
-
var opt = document.createElement("span");
|
|
360
|
-
opt.textContent = " (optional)";
|
|
361
|
-
opt.style.cssText = "color:var(--studio-text-dim);font-weight:400;text-transform:none;letter-spacing:normal;";
|
|
362
|
-
labelEl.appendChild(opt);
|
|
363
|
-
}
|
|
364
|
-
wrapper.appendChild(labelEl);
|
|
365
|
-
|
|
366
|
-
// Once children are populated, link the label to the first form control
|
|
367
|
-
// for screen-reader accessibility. Path is read from data-fieldPath
|
|
368
|
-
// (set by the caller after createFieldGroup returns).
|
|
369
|
-
queueMicrotask(function () {
|
|
370
|
-
var path = wrapper.dataset.fieldPath;
|
|
371
|
-
if (!path) return;
|
|
372
|
-
var control = wrapper.querySelector("input, select, textarea");
|
|
373
|
-
if (!control) return;
|
|
374
|
-
var fid = fieldIdFromPath(path);
|
|
375
|
-
if (!control.id) control.id = fid;
|
|
376
|
-
if (!control.name) control.name = path;
|
|
377
|
-
labelEl.htmlFor = control.id;
|
|
342
|
+
/** @param {Element} group */
|
|
343
|
+
function showLinkedSelection(group) {
|
|
344
|
+
window.clearTimeout(linkedSelectionTimer);
|
|
345
|
+
fieldsEl.querySelectorAll(".caret-field-selected").forEach(function (selected) {
|
|
346
|
+
selected.classList.remove("caret-field-selected");
|
|
378
347
|
});
|
|
379
|
-
|
|
380
|
-
|
|
348
|
+
group.classList.add("caret-field-selected");
|
|
349
|
+
linkedSelectionTimer = window.setTimeout(function () {
|
|
350
|
+
group.classList.remove("caret-field-selected");
|
|
351
|
+
}, 1500);
|
|
381
352
|
}
|
|
382
353
|
|
|
383
|
-
|
|
384
|
-
function
|
|
385
|
-
if (
|
|
386
|
-
|
|
354
|
+
/** @param {Record<string, unknown>} message */
|
|
355
|
+
function applyRemoteSelection(message) {
|
|
356
|
+
if (
|
|
357
|
+
!message
|
|
358
|
+
|| message.collection !== COLLECTION
|
|
359
|
+
|| message.id !== ID
|
|
360
|
+
|| typeof message.field !== "string"
|
|
361
|
+
) return;
|
|
362
|
+
|
|
363
|
+
if (!entryData) {
|
|
364
|
+
pendingRemoteSelection = message;
|
|
387
365
|
return;
|
|
388
366
|
}
|
|
389
367
|
|
|
390
|
-
var
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
var label = prop.title || humanizeKey(key);
|
|
396
|
-
var isRequired = required.has(key);
|
|
397
|
-
var value = data ? data[key] : undefined;
|
|
398
|
-
|
|
399
|
-
// Backfill missing keys with sensible defaults
|
|
400
|
-
if (value === undefined && data) {
|
|
401
|
-
if (prop.default !== undefined) data[key] = deepClone(prop.default);
|
|
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] = "";
|
|
408
|
-
}
|
|
409
|
-
var current = data ? data[key] : undefined;
|
|
410
|
-
|
|
411
|
-
var group;
|
|
412
|
-
|
|
413
|
-
if (prop.type === "string" && prop.enum) {
|
|
414
|
-
group = createFieldGroup(label, isRequired);
|
|
415
|
-
group.dataset.fieldPath = path;
|
|
416
|
-
var select = document.createElement("select");
|
|
417
|
-
select.className = "studio-input";
|
|
418
|
-
prop.enum.forEach(function (opt) {
|
|
419
|
-
var option = document.createElement("option");
|
|
420
|
-
option.value = opt;
|
|
421
|
-
option.textContent = String(opt).charAt(0).toUpperCase() + String(opt).slice(1);
|
|
422
|
-
if (opt === current) option.selected = true;
|
|
423
|
-
select.appendChild(option);
|
|
424
|
-
});
|
|
425
|
-
select.addEventListener("change", function () { updateField(path, select.value); });
|
|
426
|
-
group.appendChild(select);
|
|
427
|
-
} else if (prop.type === "string" && prop.format === "image") {
|
|
428
|
-
group = createFieldGroup(label, isRequired);
|
|
429
|
-
group.dataset.fieldPath = path;
|
|
430
|
-
group.appendChild(buildSingleImage(current || "", path));
|
|
431
|
-
} else if (prop.type === "string" && prop.format === "textarea") {
|
|
432
|
-
group = createFieldGroup(label, isRequired);
|
|
433
|
-
group.dataset.fieldPath = path;
|
|
434
|
-
var ta = document.createElement("textarea");
|
|
435
|
-
ta.className = "studio-input";
|
|
436
|
-
ta.rows = 4;
|
|
437
|
-
ta.value = current || "";
|
|
438
|
-
ta.addEventListener("input", function () { updateField(path, ta.value); });
|
|
439
|
-
group.appendChild(ta);
|
|
440
|
-
} else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri")) {
|
|
441
|
-
group = createFieldGroup(label, isRequired);
|
|
442
|
-
group.dataset.fieldPath = path;
|
|
443
|
-
var emailInput = document.createElement("input");
|
|
444
|
-
emailInput.type = prop.format === "email" ? "email" : "url";
|
|
445
|
-
emailInput.className = "studio-input";
|
|
446
|
-
emailInput.value = current || "";
|
|
447
|
-
emailInput.addEventListener("input", function () { updateField(path, emailInput.value); });
|
|
448
|
-
group.appendChild(emailInput);
|
|
449
|
-
} else if (prop.type === "string") {
|
|
450
|
-
group = createFieldGroup(label, isRequired);
|
|
451
|
-
group.dataset.fieldPath = path;
|
|
452
|
-
var input;
|
|
453
|
-
if (typeof current === "string" && current.length > 100) {
|
|
454
|
-
input = document.createElement("textarea");
|
|
455
|
-
input.rows = 4;
|
|
456
|
-
} else {
|
|
457
|
-
input = document.createElement("input");
|
|
458
|
-
input.type = "text";
|
|
459
|
-
}
|
|
460
|
-
input.className = "studio-input";
|
|
461
|
-
input.value = current || "";
|
|
462
|
-
input.addEventListener("input", function () { updateField(path, input.value); });
|
|
463
|
-
group.appendChild(input);
|
|
464
|
-
} else if (prop.type === "number" || prop.type === "integer") {
|
|
465
|
-
group = createFieldGroup(label, isRequired);
|
|
466
|
-
group.dataset.fieldPath = path;
|
|
467
|
-
var num = document.createElement("input");
|
|
468
|
-
num.type = "number";
|
|
469
|
-
num.className = "studio-input";
|
|
470
|
-
num.value = (current == null ? 0 : current);
|
|
471
|
-
if (prop.minimum !== undefined) num.min = prop.minimum;
|
|
472
|
-
if (prop.maximum !== undefined) num.max = prop.maximum;
|
|
473
|
-
num.addEventListener("input", function () { updateField(path, Number(num.value) || 0); });
|
|
474
|
-
group.appendChild(num);
|
|
475
|
-
} else if (prop.type === "boolean") {
|
|
476
|
-
group = createFieldGroup(label, isRequired, true);
|
|
477
|
-
group.dataset.fieldPath = path;
|
|
478
|
-
var toggle = document.createElement("button");
|
|
479
|
-
toggle.type = "button";
|
|
480
|
-
toggle.className = "studio-toggle" + (current ? " active" : "");
|
|
481
|
-
toggle.addEventListener("click", function () {
|
|
482
|
-
var next = !toggle.classList.contains("active");
|
|
483
|
-
toggle.classList.toggle("active");
|
|
484
|
-
updateField(path, next);
|
|
485
|
-
});
|
|
486
|
-
group.appendChild(toggle);
|
|
487
|
-
} else if (prop.type === "array" && prop.format === "image-gallery") {
|
|
488
|
-
group = createFieldGroup(label, isRequired);
|
|
489
|
-
group.dataset.fieldPath = path;
|
|
490
|
-
group.appendChild(buildImageGallery(current || [], path));
|
|
491
|
-
} else if (prop.type === "array" && prop.items && prop.items.type === "object") {
|
|
492
|
-
group = createFieldGroup(label, isRequired);
|
|
493
|
-
group.dataset.fieldPath = path;
|
|
494
|
-
group.appendChild(buildObjectArray(current || [], path, prop.items));
|
|
495
|
-
} else if (prop.type === "array" && prop.items && prop.items.type === "string") {
|
|
496
|
-
group = createFieldGroup(label, isRequired);
|
|
497
|
-
group.dataset.fieldPath = path;
|
|
498
|
-
group.appendChild(buildTagList(current || [], path));
|
|
499
|
-
} else if (prop.type === "object" && prop.properties) {
|
|
500
|
-
group = createFieldGroup(label, isRequired);
|
|
501
|
-
group.dataset.fieldPath = path;
|
|
502
|
-
var nested = document.createElement("div");
|
|
503
|
-
nested.className = "caret-nested";
|
|
504
|
-
renderFields(nested, current || {}, prop, path);
|
|
505
|
-
group.appendChild(nested);
|
|
506
|
-
} else {
|
|
507
|
-
group = createFieldGroup(label, isRequired);
|
|
508
|
-
group.dataset.fieldPath = path;
|
|
509
|
-
var jsonTa = document.createElement("textarea");
|
|
510
|
-
jsonTa.className = "studio-input";
|
|
511
|
-
jsonTa.style.fontFamily = "var(--studio-font-mono)";
|
|
512
|
-
jsonTa.style.fontSize = "11px";
|
|
513
|
-
jsonTa.rows = 4;
|
|
514
|
-
jsonTa.value = JSON.stringify(current, null, 2);
|
|
515
|
-
jsonTa.addEventListener("input", function () {
|
|
516
|
-
try { updateField(path, JSON.parse(jsonTa.value)); } catch (e) { /* ignore until valid */ }
|
|
517
|
-
});
|
|
518
|
-
group.appendChild(jsonTa);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
container.appendChild(group);
|
|
522
|
-
});
|
|
368
|
+
var control = fieldControl(message.field);
|
|
369
|
+
var group = control && control.closest(".caret-field-group");
|
|
370
|
+
if (!group) return;
|
|
371
|
+
showLinkedSelection(group);
|
|
372
|
+
group.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
|
|
523
373
|
}
|
|
524
374
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
g.appendChild(buildImageGallery(value, path));
|
|
536
|
-
container.appendChild(g);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if ((key === "image" || key === "thumbnail" || key === "bg_image") && typeof value === "string") {
|
|
540
|
-
var g2 = createFieldGroup(label, true);
|
|
541
|
-
g2.dataset.fieldPath = path;
|
|
542
|
-
g2.appendChild(buildSingleImage(value, path));
|
|
543
|
-
container.appendChild(g2);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
|
|
547
|
-
var g3 = createFieldGroup(label, true);
|
|
548
|
-
g3.dataset.fieldPath = path;
|
|
549
|
-
g3.appendChild(buildTagList(value, path));
|
|
550
|
-
container.appendChild(g3);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (Array.isArray(value) && value.every(function (v) { return typeof v === "object" && v !== null && !Array.isArray(v); })) {
|
|
554
|
-
var g4 = createFieldGroup(label, true);
|
|
555
|
-
g4.dataset.fieldPath = path;
|
|
556
|
-
g4.appendChild(buildObjectArray(value, path, null));
|
|
557
|
-
container.appendChild(g4);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
561
|
-
var g5 = createFieldGroup(label, true);
|
|
562
|
-
g5.dataset.fieldPath = path;
|
|
563
|
-
var nested = document.createElement("div");
|
|
564
|
-
nested.className = "caret-nested";
|
|
565
|
-
renderFieldsFallback(nested, value, path);
|
|
566
|
-
g5.appendChild(nested);
|
|
567
|
-
container.appendChild(g5);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
if (typeof value === "boolean") {
|
|
571
|
-
var g6 = createFieldGroup(label, true, true);
|
|
572
|
-
g6.dataset.fieldPath = path;
|
|
573
|
-
var t = document.createElement("button");
|
|
574
|
-
t.type = "button";
|
|
575
|
-
t.className = "studio-toggle" + (value ? " active" : "");
|
|
576
|
-
t.addEventListener("click", function () {
|
|
577
|
-
var next = !t.classList.contains("active");
|
|
578
|
-
t.classList.toggle("active");
|
|
579
|
-
updateField(path, next);
|
|
580
|
-
});
|
|
581
|
-
g6.appendChild(t);
|
|
582
|
-
container.appendChild(g6);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (typeof value === "number") {
|
|
586
|
-
var g7 = createFieldGroup(label, true);
|
|
587
|
-
g7.dataset.fieldPath = path;
|
|
588
|
-
var n = document.createElement("input");
|
|
589
|
-
n.type = "number";
|
|
590
|
-
n.className = "studio-input";
|
|
591
|
-
n.value = value;
|
|
592
|
-
n.addEventListener("input", function () { updateField(path, Number(n.value) || 0); });
|
|
593
|
-
g7.appendChild(n);
|
|
594
|
-
container.appendChild(g7);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
if (typeof value === "string") {
|
|
598
|
-
var g8 = createFieldGroup(label, true);
|
|
599
|
-
g8.dataset.fieldPath = path;
|
|
600
|
-
var i;
|
|
601
|
-
if (value.length > 100) { i = document.createElement("textarea"); i.rows = 4; }
|
|
602
|
-
else { i = document.createElement("input"); i.type = "text"; }
|
|
603
|
-
i.className = "studio-input";
|
|
604
|
-
i.value = value;
|
|
605
|
-
i.addEventListener("input", function () { updateField(path, i.value); });
|
|
606
|
-
g8.appendChild(i);
|
|
607
|
-
container.appendChild(g8);
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
// Fallback to JSON
|
|
611
|
-
var g9 = createFieldGroup(label, true);
|
|
612
|
-
g9.dataset.fieldPath = path;
|
|
613
|
-
var jt = document.createElement("textarea");
|
|
614
|
-
jt.className = "studio-input";
|
|
615
|
-
jt.style.fontFamily = "var(--studio-font-mono)";
|
|
616
|
-
jt.style.fontSize = "11px";
|
|
617
|
-
jt.rows = 4;
|
|
618
|
-
jt.value = JSON.stringify(value, null, 2);
|
|
619
|
-
jt.addEventListener("input", function () {
|
|
620
|
-
try { updateField(path, JSON.parse(jt.value)); } catch (e) { /* ignore */ }
|
|
621
|
-
});
|
|
622
|
-
g9.appendChild(jt);
|
|
623
|
-
container.appendChild(g9);
|
|
624
|
-
});
|
|
625
|
-
}
|
|
375
|
+
/* ─── Field rendering ───────────────────────────────────────────── */
|
|
376
|
+
|
|
377
|
+
const { renderFields, renderFieldsFallback } = createFieldRenderer({
|
|
378
|
+
updateField,
|
|
379
|
+
buildSingleImage,
|
|
380
|
+
buildImageGallery,
|
|
381
|
+
buildObjectArray,
|
|
382
|
+
buildTagList,
|
|
383
|
+
getPublicationField: () => publicationFieldName,
|
|
384
|
+
});
|
|
626
385
|
|
|
627
386
|
/* ─── Image gallery ─────────────────────────────────────────────── */
|
|
387
|
+
/** @param {unknown} images @param {string} path */
|
|
628
388
|
function buildImageGallery(images, path) {
|
|
629
389
|
var wrapper = document.createElement("div");
|
|
630
390
|
|
|
631
391
|
function rerender() {
|
|
632
392
|
wrapper.innerHTML = "";
|
|
633
|
-
var
|
|
393
|
+
var galleryValue = getNestedValue(entryData, path);
|
|
394
|
+
var current = Array.isArray(galleryValue)
|
|
395
|
+
? galleryValue.filter(function (value) { return typeof value === "string"; })
|
|
396
|
+
: [];
|
|
634
397
|
|
|
635
398
|
if (current.length > 0) {
|
|
636
399
|
var grid = document.createElement("div");
|
|
637
400
|
grid.className = "caret-gallery-grid";
|
|
401
|
+
/** @type {number | null} */
|
|
638
402
|
var dragIdx = null;
|
|
639
403
|
|
|
640
404
|
current.forEach(function (url, idx) {
|
|
@@ -643,9 +407,9 @@
|
|
|
643
407
|
item.draggable = true;
|
|
644
408
|
item.innerHTML =
|
|
645
409
|
'<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
|
|
646
|
-
'<button class="caret-gallery-remove" type="button" title="Remove">×</button>' +
|
|
410
|
+
'<button class="caret-gallery-remove" type="button" title="' + htmlEscape(msg("field.remove", "Remove")) + '">×</button>' +
|
|
647
411
|
'<span class="caret-gallery-index">' + (idx + 1) + "</span>";
|
|
648
|
-
item.querySelector(".caret-gallery-remove")
|
|
412
|
+
item.querySelector(".caret-gallery-remove")?.addEventListener("click", function (e) {
|
|
649
413
|
e.stopPropagation();
|
|
650
414
|
var imgs = current.slice();
|
|
651
415
|
imgs.splice(idx, 1);
|
|
@@ -688,11 +452,13 @@
|
|
|
688
452
|
fileInput.type = "file";
|
|
689
453
|
fileInput.multiple = true;
|
|
690
454
|
fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
|
|
455
|
+
fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
|
|
691
456
|
fileInput.style.display = "none";
|
|
692
457
|
|
|
458
|
+
/** @param {ArrayLike<File>} files */
|
|
693
459
|
function handleFiles(files) {
|
|
694
|
-
var valid = Array.
|
|
695
|
-
return
|
|
460
|
+
var valid = Array.from(files).filter(function (f) {
|
|
461
|
+
return isSupportedImage(f);
|
|
696
462
|
});
|
|
697
463
|
if (!valid.length) return;
|
|
698
464
|
dropzone.innerHTML =
|
|
@@ -700,8 +466,11 @@
|
|
|
700
466
|
'<p style="margin:0;font-size:0.75rem;color:var(--studio-text-muted);">Uploading ' + valid.length + " image" + (valid.length > 1 ? "s" : "") + "…</p>";
|
|
701
467
|
dropzone.style.pointerEvents = "none";
|
|
702
468
|
|
|
703
|
-
var
|
|
704
|
-
|
|
469
|
+
var existingImages = getNestedValue(entryData, path);
|
|
470
|
+
var imgs = Array.isArray(existingImages)
|
|
471
|
+
? existingImages.filter(function (value) { return typeof value === "string"; })
|
|
472
|
+
: [];
|
|
473
|
+
Promise.allSettled(valid.map(function (f) { return uploadClient.upload(f); })).then(function (results) {
|
|
705
474
|
results.forEach(function (r) { if (r.status === "fulfilled") imgs.push(r.value); });
|
|
706
475
|
updateField(path, imgs);
|
|
707
476
|
rerender();
|
|
@@ -714,10 +483,11 @@
|
|
|
714
483
|
dropzone.addEventListener("drop", function (e) {
|
|
715
484
|
e.preventDefault();
|
|
716
485
|
dropzone.classList.remove("drag-over");
|
|
717
|
-
|
|
486
|
+
var files = e.dataTransfer && e.dataTransfer.files;
|
|
487
|
+
if (files && files.length) handleFiles(files);
|
|
718
488
|
});
|
|
719
489
|
fileInput.addEventListener("change", function () {
|
|
720
|
-
if (fileInput.files.length) handleFiles(fileInput.files);
|
|
490
|
+
if (fileInput.files && fileInput.files.length) handleFiles(fileInput.files);
|
|
721
491
|
fileInput.value = "";
|
|
722
492
|
});
|
|
723
493
|
|
|
@@ -730,18 +500,45 @@
|
|
|
730
500
|
}
|
|
731
501
|
|
|
732
502
|
/* ─── Single image ──────────────────────────────────────────────── */
|
|
503
|
+
/** @param {unknown} url @param {string} path */
|
|
733
504
|
function buildSingleImage(url, path) {
|
|
734
505
|
var wrapper = document.createElement("div");
|
|
506
|
+
wrapper.className = "caret-single-image";
|
|
735
507
|
|
|
736
508
|
function rerender() {
|
|
737
509
|
wrapper.innerHTML = "";
|
|
738
|
-
var
|
|
510
|
+
var imageValue = getNestedValue(entryData, path);
|
|
511
|
+
var current = typeof imageValue === "string" ? imageValue : "";
|
|
512
|
+
|
|
513
|
+
var fileInput = document.createElement("input");
|
|
514
|
+
fileInput.type = "file";
|
|
515
|
+
fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
|
|
516
|
+
fileInput.setAttribute("aria-label", msg("field.uploadOrReplaceImage", "Upload or replace image"));
|
|
517
|
+
fileInput.style.display = "none";
|
|
739
518
|
|
|
740
519
|
if (current) {
|
|
741
|
-
var preview = document.createElement("
|
|
520
|
+
var preview = document.createElement("button");
|
|
521
|
+
preview.type = "button";
|
|
742
522
|
preview.className = "caret-single-image-preview";
|
|
743
|
-
preview.
|
|
523
|
+
preview.title = msg("field.replaceImage", "Replace image");
|
|
524
|
+
preview.setAttribute("aria-label", msg("field.replaceImage", "Replace image"));
|
|
525
|
+
var image = document.createElement("img");
|
|
526
|
+
image.src = current;
|
|
527
|
+
image.alt = "Current image preview";
|
|
528
|
+
var overlay = document.createElement("span");
|
|
529
|
+
overlay.className = "caret-single-image-overlay";
|
|
530
|
+
overlay.textContent = msg("field.replaceImage", "Replace image");
|
|
531
|
+
preview.appendChild(image);
|
|
532
|
+
preview.appendChild(overlay);
|
|
533
|
+
preview.addEventListener("click", function () { fileInput.click(); });
|
|
744
534
|
wrapper.appendChild(preview);
|
|
535
|
+
} else {
|
|
536
|
+
var emptyPreview = document.createElement("button");
|
|
537
|
+
emptyPreview.type = "button";
|
|
538
|
+
emptyPreview.className = "caret-single-image-empty";
|
|
539
|
+
emptyPreview.textContent = "+ Choose an image";
|
|
540
|
+
emptyPreview.addEventListener("click", function () { fileInput.click(); });
|
|
541
|
+
wrapper.appendChild(emptyPreview);
|
|
745
542
|
}
|
|
746
543
|
|
|
747
544
|
var row = document.createElement("div");
|
|
@@ -750,6 +547,8 @@
|
|
|
750
547
|
var input = document.createElement("input");
|
|
751
548
|
input.type = "text";
|
|
752
549
|
input.className = "studio-input";
|
|
550
|
+
input.id = fieldIdFromPath(path);
|
|
551
|
+
input.name = path;
|
|
753
552
|
input.style.flex = "1";
|
|
754
553
|
input.value = current;
|
|
755
554
|
input.placeholder = "Image URL";
|
|
@@ -759,24 +558,29 @@
|
|
|
759
558
|
var uploadBtn = document.createElement("button");
|
|
760
559
|
uploadBtn.type = "button";
|
|
761
560
|
uploadBtn.className = "studio-btn-ghost";
|
|
762
|
-
uploadBtn.textContent = "Upload";
|
|
561
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
763
562
|
|
|
764
|
-
var
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
563
|
+
var help = document.createElement("p");
|
|
564
|
+
help.className = "caret-image-help";
|
|
565
|
+
help.textContent = current
|
|
566
|
+
? "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."
|
|
567
|
+
: "Upload an image or paste its URL.";
|
|
768
568
|
|
|
769
569
|
uploadBtn.addEventListener("click", function () { fileInput.click(); });
|
|
770
570
|
fileInput.addEventListener("change", function () {
|
|
771
571
|
var file = fileInput.files && fileInput.files[0];
|
|
772
572
|
if (!file) return;
|
|
573
|
+
var selectedFile = file;
|
|
773
574
|
uploadBtn.textContent = "Uploading…";
|
|
774
575
|
uploadBtn.disabled = true;
|
|
775
|
-
|
|
776
|
-
updateField(path,
|
|
576
|
+
Promise.all([uploadClient.upload(selectedFile), uploadClient.readDimensions(selectedFile)]).then(function (results) {
|
|
577
|
+
updateField(path, results[0]);
|
|
578
|
+
populateImageObject(path, selectedFile, results[1]);
|
|
777
579
|
rerender();
|
|
778
580
|
}).catch(function () {
|
|
779
|
-
|
|
581
|
+
help.textContent = "Upload failed. Check the image type and try again.";
|
|
582
|
+
help.classList.add("is-error");
|
|
583
|
+
uploadBtn.textContent = current ? "Replace" : "Upload";
|
|
780
584
|
uploadBtn.disabled = false;
|
|
781
585
|
});
|
|
782
586
|
fileInput.value = "";
|
|
@@ -786,6 +590,7 @@
|
|
|
786
590
|
row.appendChild(uploadBtn);
|
|
787
591
|
row.appendChild(fileInput);
|
|
788
592
|
wrapper.appendChild(row);
|
|
593
|
+
wrapper.appendChild(help);
|
|
789
594
|
}
|
|
790
595
|
|
|
791
596
|
rerender();
|
|
@@ -793,12 +598,16 @@
|
|
|
793
598
|
}
|
|
794
599
|
|
|
795
600
|
/* ─── Tag list ──────────────────────────────────────────────────── */
|
|
601
|
+
/** @param {unknown} items @param {string} path */
|
|
796
602
|
function buildTagList(items, path) {
|
|
797
603
|
var wrapper = document.createElement("div");
|
|
798
604
|
|
|
799
605
|
function rerender() {
|
|
800
606
|
wrapper.innerHTML = "";
|
|
801
|
-
var
|
|
607
|
+
var tagValue = getNestedValue(entryData, path);
|
|
608
|
+
var current = Array.isArray(tagValue)
|
|
609
|
+
? tagValue.filter(function (value) { return typeof value === "string"; })
|
|
610
|
+
: [];
|
|
802
611
|
|
|
803
612
|
if (current.length > 0) {
|
|
804
613
|
var tagsEl = document.createElement("div");
|
|
@@ -809,7 +618,7 @@
|
|
|
809
618
|
tag.textContent = item;
|
|
810
619
|
var btn = document.createElement("button");
|
|
811
620
|
btn.type = "button";
|
|
812
|
-
btn.title = "Remove";
|
|
621
|
+
btn.title = msg("field.remove", "Remove");
|
|
813
622
|
btn.textContent = "×";
|
|
814
623
|
btn.addEventListener("click", function () {
|
|
815
624
|
var next = current.slice();
|
|
@@ -858,22 +667,125 @@
|
|
|
858
667
|
}
|
|
859
668
|
|
|
860
669
|
/* ─── Object array ──────────────────────────────────────────────── */
|
|
670
|
+
/** @param {unknown} items @param {string} path @param {Schema | null | undefined} itemSchema */
|
|
861
671
|
function buildObjectArray(items, path, itemSchema) {
|
|
862
672
|
var wrapper = document.createElement("div");
|
|
863
673
|
wrapper.className = "caret-object-array";
|
|
674
|
+
/** @type {number | null} */
|
|
675
|
+
var draggedIndex = null;
|
|
676
|
+
|
|
677
|
+
/** @param {Record<string, unknown>} item @param {number} idx */
|
|
678
|
+
function itemSummary(item, idx) {
|
|
679
|
+
var summaryKeys = ["title", "label", "name", "alt", "src"];
|
|
680
|
+
for (var i = 0; i < summaryKeys.length; i++) {
|
|
681
|
+
var value = item && item[summaryKeys[i]];
|
|
682
|
+
if (typeof value === "string" && value.trim()) {
|
|
683
|
+
if (summaryKeys[i] === "src") {
|
|
684
|
+
var clean = value.split("?")[0].split("/").pop();
|
|
685
|
+
return clean || (singularItemLabel(path) + " " + (idx + 1));
|
|
686
|
+
}
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return singularItemLabel(path).replace(/^./, function (c) { return c.toUpperCase(); }) + " " + (idx + 1);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** @param {Record<string, unknown>[]} current @param {number} from @param {number} to */
|
|
694
|
+
function moveItem(current, from, to) {
|
|
695
|
+
if (to < 0 || to >= current.length || from === to) return;
|
|
696
|
+
var next = current.slice();
|
|
697
|
+
var moved = next.splice(from, 1)[0];
|
|
698
|
+
next.splice(to, 0, moved);
|
|
699
|
+
updateField(path, next);
|
|
700
|
+
rerender();
|
|
701
|
+
}
|
|
864
702
|
|
|
865
703
|
function rerender() {
|
|
866
704
|
wrapper.innerHTML = "";
|
|
867
|
-
var
|
|
705
|
+
var arrayValue = getNestedValue(entryData, path);
|
|
706
|
+
var current = Array.isArray(arrayValue)
|
|
707
|
+
? arrayValue.filter(function (value) { return value !== null && typeof value === "object" && !Array.isArray(value); })
|
|
708
|
+
: [];
|
|
868
709
|
|
|
869
710
|
current.forEach(function (item, idx) {
|
|
870
711
|
var card = document.createElement("div");
|
|
871
712
|
card.className = "caret-object-array-item";
|
|
872
713
|
|
|
714
|
+
var header = document.createElement("div");
|
|
715
|
+
header.className = "caret-object-array-header";
|
|
716
|
+
|
|
717
|
+
var dragHandle = document.createElement("button");
|
|
718
|
+
dragHandle.type = "button";
|
|
719
|
+
dragHandle.className = "caret-object-array-drag";
|
|
720
|
+
dragHandle.draggable = true;
|
|
721
|
+
dragHandle.title = msg("field.drag", "Drag to reorder");
|
|
722
|
+
dragHandle.setAttribute("aria-label", msg("field.drag", "Drag to reorder") + " " + itemSummary(item, idx));
|
|
723
|
+
dragHandle.textContent = "⠿";
|
|
724
|
+
dragHandle.addEventListener("dragstart", function (event) {
|
|
725
|
+
draggedIndex = idx;
|
|
726
|
+
card.classList.add("caret-object-array-dragging");
|
|
727
|
+
if (event.dataTransfer) {
|
|
728
|
+
event.dataTransfer.effectAllowed = "move";
|
|
729
|
+
event.dataTransfer.setData("text/plain", String(idx));
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
dragHandle.addEventListener("dragend", function () {
|
|
733
|
+
draggedIndex = null;
|
|
734
|
+
wrapper.querySelectorAll(".caret-object-array-item").forEach(function (row) {
|
|
735
|
+
row.classList.remove("caret-object-array-dragging", "caret-object-array-drag-over");
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
header.appendChild(dragHandle);
|
|
739
|
+
|
|
740
|
+
var idxLabel = document.createElement("span");
|
|
741
|
+
idxLabel.className = "caret-object-array-index";
|
|
742
|
+
idxLabel.textContent = itemSummary(item, idx);
|
|
743
|
+
header.appendChild(idxLabel);
|
|
744
|
+
|
|
745
|
+
card.addEventListener("dragover", function (event) {
|
|
746
|
+
if (draggedIndex === null || draggedIndex === idx) return;
|
|
747
|
+
event.preventDefault();
|
|
748
|
+
card.classList.add("caret-object-array-drag-over");
|
|
749
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
750
|
+
});
|
|
751
|
+
card.addEventListener("dragleave", function () {
|
|
752
|
+
card.classList.remove("caret-object-array-drag-over");
|
|
753
|
+
});
|
|
754
|
+
card.addEventListener("drop", function (event) {
|
|
755
|
+
event.preventDefault();
|
|
756
|
+
card.classList.remove("caret-object-array-drag-over");
|
|
757
|
+
if (draggedIndex === null) return;
|
|
758
|
+
var from = draggedIndex;
|
|
759
|
+
draggedIndex = null;
|
|
760
|
+
moveItem(current, from, idx);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
var actions = document.createElement("div");
|
|
764
|
+
actions.className = "caret-object-array-actions";
|
|
765
|
+
|
|
766
|
+
var upBtn = document.createElement("button");
|
|
767
|
+
upBtn.type = "button";
|
|
768
|
+
upBtn.className = "caret-object-array-move";
|
|
769
|
+
upBtn.title = msg("field.moveUp", "Move up");
|
|
770
|
+
upBtn.setAttribute("aria-label", msg("field.moveUp", "Move up") + " " + itemSummary(item, idx));
|
|
771
|
+
upBtn.textContent = "↑";
|
|
772
|
+
upBtn.disabled = idx === 0;
|
|
773
|
+
upBtn.addEventListener("click", function () { moveItem(current, idx, idx - 1); });
|
|
774
|
+
|
|
775
|
+
var downBtn = document.createElement("button");
|
|
776
|
+
downBtn.type = "button";
|
|
777
|
+
downBtn.className = "caret-object-array-move";
|
|
778
|
+
downBtn.title = msg("field.moveDown", "Move down");
|
|
779
|
+
downBtn.setAttribute("aria-label", msg("field.moveDown", "Move down") + " " + itemSummary(item, idx));
|
|
780
|
+
downBtn.textContent = "↓";
|
|
781
|
+
downBtn.disabled = idx === current.length - 1;
|
|
782
|
+
downBtn.addEventListener("click", function () { moveItem(current, idx, idx + 1); });
|
|
783
|
+
|
|
873
784
|
var removeBtn = document.createElement("button");
|
|
874
785
|
removeBtn.type = "button";
|
|
875
786
|
removeBtn.className = "caret-object-array-remove";
|
|
876
|
-
removeBtn.title = "Remove
|
|
787
|
+
removeBtn.title = msg("field.remove", "Remove") + " " + singularItemLabel(path);
|
|
788
|
+
removeBtn.setAttribute("aria-label", msg("field.remove", "Remove") + " " + itemSummary(item, idx));
|
|
877
789
|
removeBtn.textContent = "×";
|
|
878
790
|
removeBtn.addEventListener("click", function () {
|
|
879
791
|
var next = current.slice();
|
|
@@ -881,46 +793,21 @@
|
|
|
881
793
|
updateField(path, next);
|
|
882
794
|
rerender();
|
|
883
795
|
});
|
|
884
|
-
card.appendChild(removeBtn);
|
|
885
796
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
797
|
+
actions.appendChild(upBtn);
|
|
798
|
+
actions.appendChild(downBtn);
|
|
799
|
+
actions.appendChild(removeBtn);
|
|
800
|
+
header.appendChild(actions);
|
|
801
|
+
card.appendChild(header);
|
|
890
802
|
|
|
891
803
|
var fieldsCont = document.createElement("div");
|
|
892
804
|
fieldsCont.className = "caret-object-array-fields";
|
|
893
805
|
|
|
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
|
-
});
|
|
806
|
+
if (itemSchema && itemSchema.properties) {
|
|
807
|
+
renderFields(fieldsCont, item, itemSchema, path + "." + idx);
|
|
808
|
+
} else {
|
|
809
|
+
renderFieldsFallback(fieldsCont, item, path + "." + idx);
|
|
810
|
+
}
|
|
924
811
|
|
|
925
812
|
card.appendChild(fieldsCont);
|
|
926
813
|
wrapper.appendChild(card);
|
|
@@ -929,22 +816,38 @@
|
|
|
929
816
|
var addBtn = document.createElement("button");
|
|
930
817
|
addBtn.type = "button";
|
|
931
818
|
addBtn.className = "caret-object-array-add";
|
|
932
|
-
addBtn.textContent = "+ Add
|
|
819
|
+
addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
|
|
933
820
|
addBtn.addEventListener("click", function () {
|
|
821
|
+
/** @type {Record<string, unknown>} */
|
|
934
822
|
var template;
|
|
935
823
|
if (itemSchema && itemSchema.properties) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
824
|
+
var templateValue = templateFromSchema(itemSchema);
|
|
825
|
+
template = templateValue && typeof templateValue === "object" && !Array.isArray(templateValue)
|
|
826
|
+
? /** @type {Record<string, unknown>} */ (templateValue)
|
|
827
|
+
: {};
|
|
828
|
+
if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
|
|
829
|
+
template.id = generatedId(path, current.length);
|
|
830
|
+
}
|
|
941
831
|
} else {
|
|
942
832
|
template = current.length > 0
|
|
943
|
-
? Object.fromEntries(Object.keys(current[0]).map(function (k) {
|
|
833
|
+
? Object.fromEntries(Object.keys(current[0]).map(function (k) {
|
|
834
|
+
var sample = current[0][k];
|
|
835
|
+
return [k, typeof sample === "number" ? 0 : (typeof sample === "boolean" ? false : (Array.isArray(sample) ? [] : (sample && typeof sample === "object" ? {} : "")))];
|
|
836
|
+
}))
|
|
944
837
|
: {};
|
|
945
838
|
}
|
|
946
839
|
updateField(path, current.concat([template]));
|
|
947
840
|
rerender();
|
|
841
|
+
var cards = wrapper.querySelectorAll(".caret-object-array-item");
|
|
842
|
+
var newest = cards[cards.length - 1];
|
|
843
|
+
var firstControl = newest && newest.querySelector("input, textarea, select, button.caret-single-image-empty");
|
|
844
|
+
// Focus immediately. Deferring focus to the next animation frame can
|
|
845
|
+
// steal it back after a fast user (or assistive automation) has already
|
|
846
|
+
// moved to another control in the new row.
|
|
847
|
+
if (firstControl instanceof HTMLElement) firstControl.focus();
|
|
848
|
+
requestAnimationFrame(function () {
|
|
849
|
+
if (newest) newest.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
850
|
+
});
|
|
948
851
|
});
|
|
949
852
|
wrapper.appendChild(addBtn);
|
|
950
853
|
}
|
|
@@ -957,16 +860,13 @@
|
|
|
957
860
|
function loadHistory() {
|
|
958
861
|
historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">Loading…</p>';
|
|
959
862
|
historyPanel.hidden = false;
|
|
863
|
+
historyBtn.setAttribute("aria-expanded", "true");
|
|
864
|
+
historyCloseBtn.focus();
|
|
960
865
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (!res.ok) throw new Error("history failed");
|
|
966
|
-
return res.json();
|
|
967
|
-
}).then(function (json) {
|
|
968
|
-
if (!json) return;
|
|
969
|
-
var items = json.history || [];
|
|
866
|
+
historyClient.list({ collection: COLLECTION, id: ID }).then(function (result) {
|
|
867
|
+
if (result.kind === "unauthorized") { loginRedirect(); return; }
|
|
868
|
+
if (result.kind === "error") throw new Error("history failed");
|
|
869
|
+
var items = result.items;
|
|
970
870
|
if (items.length === 0) {
|
|
971
871
|
historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">No history yet. Edits create snapshots automatically.</p>';
|
|
972
872
|
return;
|
|
@@ -992,6 +892,12 @@
|
|
|
992
892
|
|
|
993
893
|
info.appendChild(actionSpan);
|
|
994
894
|
info.appendChild(timeSpan);
|
|
895
|
+
if (item.editor && typeof item.editor.id === "string") {
|
|
896
|
+
var editorSpan = document.createElement("span");
|
|
897
|
+
editorSpan.style.cssText = "font-size:0.7rem;color:var(--studio-text-dim);";
|
|
898
|
+
editorSpan.textContent = item.editor.name || item.editor.email || item.editor.id;
|
|
899
|
+
info.appendChild(editorSpan);
|
|
900
|
+
}
|
|
995
901
|
row.appendChild(info);
|
|
996
902
|
|
|
997
903
|
var restoreBtn = document.createElement("button");
|
|
@@ -1008,25 +914,28 @@
|
|
|
1008
914
|
});
|
|
1009
915
|
}
|
|
1010
916
|
|
|
917
|
+
function closeHistoryPanel() {
|
|
918
|
+
historyPanel.hidden = true;
|
|
919
|
+
historyBtn.setAttribute("aria-expanded", "false");
|
|
920
|
+
historyList.innerHTML = "";
|
|
921
|
+
if (historyBtn.isConnected) historyBtn.focus();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** @param {number} ts */
|
|
1011
925
|
function restoreSnapshot(ts) {
|
|
1012
926
|
if (!window.confirm("Restore this version? Current changes will be saved to history first.")) return;
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
credentials: "same-origin",
|
|
1017
|
-
body: JSON.stringify({ collection: COLLECTION, id: ID, ts: ts }),
|
|
1018
|
-
}).then(function (res) {
|
|
1019
|
-
if (handleAuth(res)) return null;
|
|
1020
|
-
if (!res.ok) throw new Error("restore failed");
|
|
1021
|
-
return res.json();
|
|
1022
|
-
}).then(function (json) {
|
|
1023
|
-
if (!json) return;
|
|
927
|
+
historyClient.restore({ collection: COLLECTION, id: ID, ts: ts }).then(function (result) {
|
|
928
|
+
if (result.kind === "unauthorized") { loginRedirect(); return; }
|
|
929
|
+
if (result.kind === "error") throw new Error("restore failed");
|
|
1024
930
|
var prevData = entryData;
|
|
1025
931
|
var prevJson = originalJson;
|
|
1026
932
|
try {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
933
|
+
if (!result.data || typeof result.data !== "object" || Array.isArray(result.data)) {
|
|
934
|
+
throw new Error("invalid restored entry data");
|
|
935
|
+
}
|
|
936
|
+
entryData = deepClone(/** @type {Record<string, unknown>} */ (result.data));
|
|
937
|
+
originalJson = JSON.stringify(result.data);
|
|
938
|
+
if (typeof result.revision === "number") entryRevision = result.revision;
|
|
1030
939
|
fieldsEl.innerHTML = "";
|
|
1031
940
|
renderFields(fieldsEl, entryData, entrySchema, "");
|
|
1032
941
|
updateSaveButton();
|
|
@@ -1039,7 +948,8 @@
|
|
|
1039
948
|
showStatus("error", "Restore failed — invalid data");
|
|
1040
949
|
return;
|
|
1041
950
|
}
|
|
1042
|
-
|
|
951
|
+
closeHistoryPanel();
|
|
952
|
+
studioSync.announceChange("cms:saved", entryData);
|
|
1043
953
|
showStatus("saved", "Restored");
|
|
1044
954
|
}).catch(function () {
|
|
1045
955
|
showStatus("error", "Restore failed");
|
|
@@ -1048,70 +958,113 @@
|
|
|
1048
958
|
|
|
1049
959
|
/* ─── Wire events ───────────────────────────────────────────────── */
|
|
1050
960
|
saveBtn.addEventListener("click", save);
|
|
1051
|
-
deleteBtn.addEventListener("click", deleteEntry);
|
|
961
|
+
if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
|
|
1052
962
|
historyBtn.addEventListener("click", loadHistory);
|
|
1053
|
-
historyCloseBtn.addEventListener("click",
|
|
1054
|
-
|
|
1055
|
-
historyList.innerHTML = "";
|
|
1056
|
-
});
|
|
1057
|
-
deleteCancelBtn.addEventListener("click", function () { deleteDialog.hidden = true; });
|
|
963
|
+
historyCloseBtn.addEventListener("click", closeHistoryPanel);
|
|
964
|
+
deleteCancelBtn.addEventListener("click", closeDeleteDialog);
|
|
1058
965
|
deleteConfirmBtn.addEventListener("click", confirmDelete);
|
|
1059
966
|
deleteDialog.addEventListener("click", function (e) {
|
|
1060
|
-
if (e.target === deleteDialog)
|
|
967
|
+
if (e.target === deleteDialog) closeDeleteDialog();
|
|
968
|
+
});
|
|
969
|
+
deleteDialog.addEventListener("keydown", function (e) {
|
|
970
|
+
if (deleteDialog.hidden) return;
|
|
971
|
+
if (e.key !== "Tab") return;
|
|
972
|
+
var controls = Array.from(deleteDialog.querySelectorAll("button:not([disabled])"));
|
|
973
|
+
var first = controls[0];
|
|
974
|
+
var last = controls[controls.length - 1];
|
|
975
|
+
if (!(first instanceof HTMLElement) || !(last instanceof HTMLElement)) return;
|
|
976
|
+
var active = document.activeElement;
|
|
977
|
+
if (e.shiftKey && (active === first || !deleteDialog.contains(active))) {
|
|
978
|
+
e.preventDefault();
|
|
979
|
+
last.focus();
|
|
980
|
+
} else if (!e.shiftKey && active === last) {
|
|
981
|
+
e.preventDefault();
|
|
982
|
+
first.focus();
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Focusing a Studio control locates its live-page counterpart without
|
|
987
|
+
// stealing focus from the editor. The temporary blue cue is distinct from
|
|
988
|
+
// green save confirmation.
|
|
989
|
+
fieldsEl.addEventListener("focusin", function (event) {
|
|
990
|
+
var control = event.target instanceof Element
|
|
991
|
+
? event.target.closest("input, select, textarea, button")
|
|
992
|
+
: null;
|
|
993
|
+
var group = control && control.closest("[data-field-path]");
|
|
994
|
+
var path = group instanceof HTMLElement ? group.dataset.fieldPath : undefined;
|
|
995
|
+
if (!path) return;
|
|
996
|
+
if (!group) return;
|
|
997
|
+
showLinkedSelection(group);
|
|
998
|
+
studioSync.announceFieldSelection(path);
|
|
1061
999
|
});
|
|
1062
1000
|
|
|
1063
1001
|
window.addEventListener("keydown", function (e) {
|
|
1002
|
+
if (e.key === "Escape" && (!deleteDialog.hidden || !historyPanel.hidden)) {
|
|
1003
|
+
e.preventDefault();
|
|
1004
|
+
e.stopPropagation();
|
|
1005
|
+
if (!deleteDialog.hidden) closeDeleteDialog();
|
|
1006
|
+
else closeHistoryPanel();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1064
1009
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
1065
1010
|
e.preventDefault();
|
|
1066
1011
|
if (isDirty()) save();
|
|
1067
1012
|
}
|
|
1068
|
-
|
|
1069
|
-
if (!deleteDialog.hidden) deleteDialog.hidden = true;
|
|
1070
|
-
if (!historyPanel.hidden) {
|
|
1071
|
-
historyPanel.hidden = true;
|
|
1072
|
-
historyList.innerHTML = "";
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1013
|
+
}, true);
|
|
1076
1014
|
|
|
1077
1015
|
window.addEventListener("beforeunload", function (e) {
|
|
1078
1016
|
if (isDirty()) { e.preventDefault(); e.returnValue = ""; }
|
|
1079
1017
|
});
|
|
1080
1018
|
|
|
1081
1019
|
/* ─── Load entry + schema ───────────────────────────────────────── */
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
var schemaPromise = schemaRes.ok ? schemaRes.json() : Promise.resolve(null);
|
|
1096
|
-
return schemaPromise.then(function (schemaJson) {
|
|
1097
|
-
if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
|
|
1098
|
-
|
|
1020
|
+
entryLoader.load({
|
|
1021
|
+
collection: COLLECTION,
|
|
1022
|
+
id: ID,
|
|
1023
|
+
isNew: IS_NEW,
|
|
1024
|
+
initializeIfMissing: INITIALIZE_IF_MISSING,
|
|
1025
|
+
}).then(function (result) {
|
|
1026
|
+
if (result.kind === "unauthorized") { loginRedirect(); return; }
|
|
1027
|
+
if (result.kind === "error") {
|
|
1028
|
+
throw new Error(msg("entry." + result.code, msg("entry.loadFailed", "Could not load this entry. Retry after checking its source or storage.")));
|
|
1029
|
+
}
|
|
1030
|
+
entrySchema = result.schema;
|
|
1031
|
+
publicationFieldName = result.publicationFieldName;
|
|
1099
1032
|
loadingEl.hidden = true;
|
|
1100
|
-
if (
|
|
1033
|
+
if (result.kind === "missing") {
|
|
1101
1034
|
notFoundEl.hidden = false;
|
|
1102
1035
|
return;
|
|
1103
1036
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1037
|
+
var initialData = result.data;
|
|
1038
|
+
entryData = deepClone(initialData);
|
|
1039
|
+
entryRevision = result.revision;
|
|
1040
|
+
originalJson = JSON.stringify(initialData);
|
|
1107
1041
|
titleEl.textContent = getTitle(entryData);
|
|
1108
1042
|
renderFields(fieldsEl, entryData, entrySchema, "");
|
|
1043
|
+
if (validationWarningEl && result.validationIssues.length > 0) {
|
|
1044
|
+
validationWarningEl.textContent = "This stored entry has invalid fields: " + result.validationIssues.map(function (issue) {
|
|
1045
|
+
return (issue.path || "entry") + " — " + issue.message;
|
|
1046
|
+
}).join("; ") + ". Correct them and Save to restore public delivery.";
|
|
1047
|
+
validationWarningEl.hidden = false;
|
|
1048
|
+
}
|
|
1109
1049
|
updateSaveButton();
|
|
1050
|
+
if (IS_NEW || result.initialized) {
|
|
1051
|
+
var guideTitle = document.getElementById("editor-guide-title");
|
|
1052
|
+
var guideCopy = document.getElementById("editor-guide-copy");
|
|
1053
|
+
if (guideTitle) guideTitle.textContent = msg("entry.newGuideTitle", "Entry created with safe defaults");
|
|
1054
|
+
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. When publication is enabled, new entries stay private until Published is turned on.");
|
|
1055
|
+
}
|
|
1110
1056
|
editorEl.hidden = false;
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1057
|
+
studioSync.announceReady();
|
|
1058
|
+
if (pendingRemoteSelection) {
|
|
1059
|
+
var pending = pendingRemoteSelection;
|
|
1060
|
+
pendingRemoteSelection = null;
|
|
1061
|
+
applyRemoteSelection(pending);
|
|
1062
|
+
}
|
|
1063
|
+
}).catch(function (error) {
|
|
1114
1064
|
loadingEl.hidden = true;
|
|
1115
|
-
notFoundEl.hidden =
|
|
1065
|
+
notFoundEl.hidden = true;
|
|
1066
|
+
/** @type {HTMLElement} */ (document.getElementById("load-error")).hidden = false;
|
|
1067
|
+
/** @type {HTMLElement} */ (document.getElementById("load-error-message")).textContent = error instanceof Error ? error.message : msg("entry.loadFailed", "Could not load this entry.");
|
|
1116
1068
|
});
|
|
1069
|
+
/** @type {HTMLElement} */ (document.getElementById("load-error-retry")).addEventListener("click", function () { window.location.reload(); });
|
|
1117
1070
|
})();
|