@caretcms/core 0.1.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/browser-runtime.d.ts +12 -0
- package/dist/browser-runtime.d.ts.map +1 -0
- package/dist/browser-runtime.js +298 -0
- package/dist/browser-runtime.js.map +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +343 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +61 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +80 -0
- package/dist/loader.js.map +1 -0
- package/dist/providers/storage/filesystem.d.ts +7 -0
- package/dist/providers/storage/filesystem.d.ts.map +1 -0
- package/dist/providers/storage/filesystem.js +6 -0
- package/dist/providers/storage/filesystem.js.map +1 -0
- package/dist/providers/uploads/local.d.ts +6 -0
- package/dist/providers/uploads/local.d.ts.map +1 -0
- package/dist/providers/uploads/local.js +6 -0
- package/dist/providers/uploads/local.js.map +1 -0
- package/dist/runtime/auth/cookie-utils.d.ts +3 -0
- package/dist/runtime/auth/cookie-utils.d.ts.map +1 -0
- package/dist/runtime/auth/cookie-utils.js +33 -0
- package/dist/runtime/auth/cookie-utils.js.map +1 -0
- package/dist/runtime/auth/demo-session.d.ts +24 -0
- package/dist/runtime/auth/demo-session.d.ts.map +1 -0
- package/dist/runtime/auth/demo-session.js +42 -0
- package/dist/runtime/auth/demo-session.js.map +1 -0
- package/dist/runtime/auth/rate-limiter.d.ts +26 -0
- package/dist/runtime/auth/rate-limiter.d.ts.map +1 -0
- package/dist/runtime/auth/rate-limiter.js +65 -0
- package/dist/runtime/auth/rate-limiter.js.map +1 -0
- package/dist/runtime/auth/session.d.ts +14 -0
- package/dist/runtime/auth/session.d.ts.map +1 -0
- package/dist/runtime/auth/session.js +128 -0
- package/dist/runtime/auth/session.js.map +1 -0
- package/dist/runtime/bind.d.ts +14 -0
- package/dist/runtime/bind.d.ts.map +1 -0
- package/dist/runtime/bind.js +19 -0
- package/dist/runtime/bind.js.map +1 -0
- package/dist/runtime/config.d.ts +20 -0
- package/dist/runtime/config.d.ts.map +1 -0
- package/dist/runtime/config.js +40 -0
- package/dist/runtime/config.js.map +1 -0
- package/dist/runtime/content.d.ts +28 -0
- package/dist/runtime/content.d.ts.map +1 -0
- package/dist/runtime/content.js +37 -0
- package/dist/runtime/content.js.map +1 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +16 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/middleware.d.ts +14 -0
- package/dist/runtime/middleware.d.ts.map +1 -0
- package/dist/runtime/middleware.js +80 -0
- package/dist/runtime/middleware.js.map +1 -0
- package/dist/runtime/mutations/contracts.d.ts +91 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -0
- package/dist/runtime/mutations/contracts.js +320 -0
- package/dist/runtime/mutations/contracts.js.map +1 -0
- package/dist/runtime/mutations/engine.d.ts +23 -0
- package/dist/runtime/mutations/engine.d.ts.map +1 -0
- package/dist/runtime/mutations/engine.js +255 -0
- package/dist/runtime/mutations/engine.js.map +1 -0
- package/dist/runtime/providers.d.ts +9 -0
- package/dist/runtime/providers.d.ts.map +1 -0
- package/dist/runtime/providers.js +44 -0
- package/dist/runtime/providers.js.map +1 -0
- package/dist/runtime/request-context.d.ts +11 -0
- package/dist/runtime/request-context.d.ts.map +1 -0
- package/dist/runtime/request-context.js +16 -0
- package/dist/runtime/request-context.js.map +1 -0
- package/dist/runtime/rewrite.d.ts +13 -0
- package/dist/runtime/rewrite.d.ts.map +1 -0
- package/dist/runtime/rewrite.js +253 -0
- package/dist/runtime/rewrite.js.map +1 -0
- package/dist/runtime/routes/_helpers.d.ts +17 -0
- package/dist/runtime/routes/_helpers.d.ts.map +1 -0
- package/dist/runtime/routes/_helpers.js +38 -0
- package/dist/runtime/routes/_helpers.js.map +1 -0
- package/dist/runtime/routes/admin-entry.d.ts +3 -0
- package/dist/runtime/routes/admin-entry.d.ts.map +1 -0
- package/dist/runtime/routes/admin-entry.js +170 -0
- package/dist/runtime/routes/admin-entry.js.map +1 -0
- package/dist/runtime/routes/admin.d.ts +3 -0
- package/dist/runtime/routes/admin.d.ts.map +1 -0
- package/dist/runtime/routes/admin.js +189 -0
- package/dist/runtime/routes/admin.js.map +1 -0
- package/dist/runtime/routes/auth-login.d.ts +4 -0
- package/dist/runtime/routes/auth-login.d.ts.map +1 -0
- package/dist/runtime/routes/auth-login.js +71 -0
- package/dist/runtime/routes/auth-login.js.map +1 -0
- package/dist/runtime/routes/auth-logout.d.ts +4 -0
- package/dist/runtime/routes/auth-logout.d.ts.map +1 -0
- package/dist/runtime/routes/auth-logout.js +47 -0
- package/dist/runtime/routes/auth-logout.js.map +1 -0
- package/dist/runtime/routes/auth-session.d.ts +3 -0
- package/dist/runtime/routes/auth-session.d.ts.map +1 -0
- package/dist/runtime/routes/auth-session.js +8 -0
- package/dist/runtime/routes/auth-session.js.map +1 -0
- package/dist/runtime/routes/collection-list.d.ts +3 -0
- package/dist/runtime/routes/collection-list.d.ts.map +1 -0
- package/dist/runtime/routes/collection-list.js +420 -0
- package/dist/runtime/routes/collection-list.js.map +1 -0
- package/dist/runtime/routes/collections-metadata.d.ts +3 -0
- package/dist/runtime/routes/collections-metadata.d.ts.map +1 -0
- package/dist/runtime/routes/collections-metadata.js +17 -0
- package/dist/runtime/routes/collections-metadata.js.map +1 -0
- package/dist/runtime/routes/editor-assets.d.ts +3 -0
- package/dist/runtime/routes/editor-assets.d.ts.map +1 -0
- package/dist/runtime/routes/editor-assets.js +73 -0
- package/dist/runtime/routes/editor-assets.js.map +1 -0
- package/dist/runtime/routes/entries.d.ts +3 -0
- package/dist/runtime/routes/entries.d.ts.map +1 -0
- package/dist/runtime/routes/entries.js +96 -0
- package/dist/runtime/routes/entries.js.map +1 -0
- package/dist/runtime/routes/history.d.ts +4 -0
- package/dist/runtime/routes/history.d.ts.map +1 -0
- package/dist/runtime/routes/history.js +54 -0
- package/dist/runtime/routes/history.js.map +1 -0
- package/dist/runtime/routes/mutate.d.ts +4 -0
- package/dist/runtime/routes/mutate.d.ts.map +1 -0
- package/dist/runtime/routes/mutate.js +38 -0
- package/dist/runtime/routes/mutate.js.map +1 -0
- package/dist/runtime/routes/schema.d.ts +3 -0
- package/dist/runtime/routes/schema.d.ts.map +1 -0
- package/dist/runtime/routes/schema.js +58 -0
- package/dist/runtime/routes/schema.js.map +1 -0
- package/dist/runtime/routes/studio-home.d.ts +3 -0
- package/dist/runtime/routes/studio-home.d.ts.map +1 -0
- package/dist/runtime/routes/studio-home.js +99 -0
- package/dist/runtime/routes/studio-home.js.map +1 -0
- package/dist/runtime/routes/theme-stylesheet.d.ts +3 -0
- package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -0
- package/dist/runtime/routes/theme-stylesheet.js +32 -0
- package/dist/runtime/routes/theme-stylesheet.js.map +1 -0
- package/dist/runtime/routes/upload.d.ts +3 -0
- package/dist/runtime/routes/upload.d.ts.map +1 -0
- package/dist/runtime/routes/upload.js +33 -0
- package/dist/runtime/routes/upload.js.map +1 -0
- package/dist/runtime/sanitize-html.d.ts +8 -0
- package/dist/runtime/sanitize-html.d.ts.map +1 -0
- package/dist/runtime/sanitize-html.js +86 -0
- package/dist/runtime/sanitize-html.js.map +1 -0
- package/dist/runtime/schema/template.d.ts +15 -0
- package/dist/runtime/schema/template.d.ts.map +1 -0
- package/dist/runtime/schema/template.js +38 -0
- package/dist/runtime/schema/template.js.map +1 -0
- package/dist/runtime/schema-registry.d.ts +17 -0
- package/dist/runtime/schema-registry.d.ts.map +1 -0
- package/dist/runtime/schema-registry.js +16 -0
- package/dist/runtime/schema-registry.js.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts +36 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.js +291 -0
- package/dist/runtime/storage/filesystem-adapter.js.map +1 -0
- package/dist/runtime/storage/filesystem-upload-handler.d.ts +11 -0
- package/dist/runtime/storage/filesystem-upload-handler.d.ts.map +1 -0
- package/dist/runtime/storage/filesystem-upload-handler.js +20 -0
- package/dist/runtime/storage/filesystem-upload-handler.js.map +1 -0
- package/dist/runtime/storage/image-validation.d.ts +28 -0
- package/dist/runtime/storage/image-validation.d.ts.map +1 -0
- package/dist/runtime/storage/image-validation.js +94 -0
- package/dist/runtime/storage/image-validation.js.map +1 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts +29 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -0
- package/dist/runtime/storage/in-memory-adapter.js +110 -0
- package/dist/runtime/storage/in-memory-adapter.js.map +1 -0
- package/dist/runtime/storage/quota-upload-handler.d.ts +27 -0
- package/dist/runtime/storage/quota-upload-handler.d.ts.map +1 -0
- package/dist/runtime/storage/quota-upload-handler.js +34 -0
- package/dist/runtime/storage/quota-upload-handler.js.map +1 -0
- package/dist/runtime/storage/session-overlay-adapter.d.ts +33 -0
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -0
- package/dist/runtime/storage/session-overlay-adapter.js +121 -0
- package/dist/runtime/storage/session-overlay-adapter.js.map +1 -0
- package/dist/runtime/themes/preset-tokens.d.ts +17 -0
- package/dist/runtime/themes/preset-tokens.d.ts.map +1 -0
- package/dist/runtime/themes/preset-tokens.js +47 -0
- package/dist/runtime/themes/preset-tokens.js.map +1 -0
- package/dist/runtime/utils.d.ts +6 -0
- package/dist/runtime/utils.d.ts.map +1 -0
- package/dist/runtime/utils.js +29 -0
- package/dist/runtime/utils.js.map +1 -0
- package/dist/runtime/views/studio-layout.d.ts +25 -0
- package/dist/runtime/views/studio-layout.d.ts.map +1 -0
- package/dist/runtime/views/studio-layout.js +108 -0
- package/dist/runtime/views/studio-layout.js.map +1 -0
- package/dist/schema-utils.d.ts +16 -0
- package/dist/schema-utils.d.ts.map +1 -0
- package/dist/schema-utils.js +62 -0
- package/dist/schema-utils.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/editor-runtime.d.ts +6 -0
- package/editor-runtime.js +2 -0
- package/package.json +88 -0
- package/static/cms/admin-entry.css +207 -0
- package/static/cms/admin-entry.js +1083 -0
- package/static/cms/editor/config.js +118 -0
- package/static/cms/editor/content-map.css +141 -0
- package/static/cms/editor/content-map.js +172 -0
- package/static/cms/editor/guards.js +72 -0
- package/static/cms/editor/helpers.js +51 -0
- package/static/cms/editor/image-edit.js +151 -0
- package/static/cms/editor/image-utils.js +84 -0
- package/static/cms/editor/image.css +133 -0
- package/static/cms/editor/link-follow.js +144 -0
- package/static/cms/editor/link-ui.css +199 -0
- package/static/cms/editor/linkify.js +55 -0
- package/static/cms/editor/panel.css +91 -0
- package/static/cms/editor/panel.js +70 -0
- package/static/cms/editor/rich-toolbar.css +67 -0
- package/static/cms/editor/rich-toolbar.js +200 -0
- package/static/cms/editor/sanitize.js +89 -0
- package/static/cms/editor/save-queue.js +166 -0
- package/static/cms/editor/section-controls/activation.js +10 -0
- package/static/cms/editor/section-controls/api.js +87 -0
- package/static/cms/editor/section-controls/constants.js +24 -0
- package/static/cms/editor/section-controls/index.js +622 -0
- package/static/cms/editor/section-controls/model.js +76 -0
- package/static/cms/editor/section-controls/mutations.js +112 -0
- package/static/cms/editor/section-controls/page-context.js +34 -0
- package/static/cms/editor/section-controls/pickers.js +196 -0
- package/static/cms/editor/section-controls/reorder.js +92 -0
- package/static/cms/editor/section-controls/selection.js +54 -0
- package/static/cms/editor/section-controls/spacing-drag.js +83 -0
- package/static/cms/editor/section-controls/ui-elements.js +54 -0
- package/static/cms/editor/section-controls/utils.js +35 -0
- package/static/cms/editor/section-controls.css +349 -0
- package/static/cms/editor/section-controls.js +1 -0
- package/static/cms/editor/security.js +48 -0
- package/static/cms/editor/sync.js +72 -0
- package/static/cms/editor/text-edit.js +151 -0
- package/static/cms/editor/text-link-interactions.js +254 -0
- package/static/cms/editor/toast.css +50 -0
- package/static/cms/editor/toast.js +29 -0
- package/static/cms/editor/tokens-and-text.css +93 -0
- package/static/cms/editor/toolbar.css +249 -0
- package/static/cms/editor/toolbar.js +117 -0
- package/static/cms/editor.css +12 -0
- package/static/cms/editor.js +116 -0
- package/static/cms/studio-login.css +300 -0
- package/static/cms/studio-tokens.css +85 -0
- package/static/cms/studio.css +437 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Runtime config injected by the integration bootstrap script. */
|
|
2
|
+
const defaults = {
|
|
3
|
+
mode: 'embedded',
|
|
4
|
+
mountPath: '/admin',
|
|
5
|
+
apiBasePath: '/api/cms',
|
|
6
|
+
cloud: null,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const cfg = (typeof window !== 'undefined' && window.__CARET__) || defaults;
|
|
10
|
+
|
|
11
|
+
function normalizePath(value, fallback) {
|
|
12
|
+
if (!value || typeof value !== 'string') return fallback;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (!trimmed) return fallback;
|
|
15
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
16
|
+
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/')
|
|
17
|
+
? withLeadingSlash.slice(0, -1)
|
|
18
|
+
: withLeadingSlash;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeEndpoint(value) {
|
|
22
|
+
if (!value || typeof value !== 'string') return '';
|
|
23
|
+
return value.trim().replace(/\/$/, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getCloudConfig() {
|
|
27
|
+
if (!cfg || cfg.mode !== 'cloud' || !cfg.cloud || typeof cfg.cloud !== 'object') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const endpoint = normalizeEndpoint(cfg.cloud.endpoint);
|
|
32
|
+
const projectId =
|
|
33
|
+
typeof cfg.cloud.projectId === 'string' ? cfg.cloud.projectId.trim() : '';
|
|
34
|
+
if (!endpoint || !projectId) return null;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
endpoint,
|
|
38
|
+
projectId,
|
|
39
|
+
contentPath: normalizePath(cfg.cloud.contentPath, '/content'),
|
|
40
|
+
publicToken:
|
|
41
|
+
typeof cfg.cloud.publicToken === 'string' && cfg.cloud.publicToken.trim()
|
|
42
|
+
? cfg.cloud.publicToken.trim()
|
|
43
|
+
: null,
|
|
44
|
+
environment:
|
|
45
|
+
typeof cfg.cloud.environment === 'string' && cfg.cloud.environment.trim()
|
|
46
|
+
? cfg.cloud.environment.trim()
|
|
47
|
+
: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getStorageKey(cloud) {
|
|
52
|
+
return `caretcms:cloud-session:${cloud.endpoint}:${cloud.projectId}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const CMS_CONFIG = cfg;
|
|
56
|
+
export const CMS_MODE = cfg.mode === 'cloud' || cfg.mode === 'hybrid' ? cfg.mode : 'embedded';
|
|
57
|
+
export const MOUNT_PATH = normalizePath(cfg.mountPath, defaults.mountPath);
|
|
58
|
+
export const API_BASE = normalizePath(cfg.apiBasePath, defaults.apiBasePath);
|
|
59
|
+
export const CLOUD = getCloudConfig();
|
|
60
|
+
export const STUDIO_PATH = CLOUD ? null : `${MOUNT_PATH}/cms`;
|
|
61
|
+
|
|
62
|
+
export function isCloudMode() {
|
|
63
|
+
return CMS_MODE === 'cloud' && CLOUD !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getCloudSessionToken() {
|
|
67
|
+
if (!isCloudMode() || typeof window === 'undefined') return null;
|
|
68
|
+
try {
|
|
69
|
+
const value = window.localStorage.getItem(getStorageKey(CLOUD));
|
|
70
|
+
return value && value.trim() ? value.trim() : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function clearCloudSessionToken() {
|
|
77
|
+
if (!isCloudMode() || typeof window === 'undefined') return;
|
|
78
|
+
try {
|
|
79
|
+
window.localStorage.removeItem(getStorageKey(CLOUD));
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore localStorage failures.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildCmsUrl(path, search) {
|
|
86
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
87
|
+
let url;
|
|
88
|
+
|
|
89
|
+
if (isCloudMode()) {
|
|
90
|
+
url = new URL(`${CLOUD.contentPath}${normalizedPath}`, `${CLOUD.endpoint}/`);
|
|
91
|
+
url.searchParams.set('projectId', CLOUD.projectId);
|
|
92
|
+
if (CLOUD.environment) {
|
|
93
|
+
url.searchParams.set('environment', CLOUD.environment);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
url = new URL(`${API_BASE}${normalizedPath}`, window.location.origin);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (search) {
|
|
100
|
+
const params = search instanceof URLSearchParams ? search : new URLSearchParams(search);
|
|
101
|
+
for (const [key, value] of params.entries()) {
|
|
102
|
+
if (value) url.searchParams.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return url.toString();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getEditorLoginUrl(redirectTo = window.location.href) {
|
|
110
|
+
if (isCloudMode()) {
|
|
111
|
+
const url = new URL(`${CLOUD.contentPath}/admin`, `${CLOUD.endpoint}/`);
|
|
112
|
+
url.searchParams.set('projectId', CLOUD.projectId);
|
|
113
|
+
url.searchParams.set('redirect', redirectTo);
|
|
114
|
+
return url.toString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `${MOUNT_PATH}?redirect=${encodeURIComponent(redirectTo)}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
.cms-content-map {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 48px;
|
|
4
|
+
right: -340px;
|
|
5
|
+
width: 320px;
|
|
6
|
+
max-height: calc(100vh - 60px);
|
|
7
|
+
background: #1a1a2e;
|
|
8
|
+
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
|
9
|
+
border-radius: 8px 0 0 8px;
|
|
10
|
+
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
|
11
|
+
z-index: 100001;
|
|
12
|
+
transition: right 0.25s ease;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
font-size: 12px;
|
|
17
|
+
color: #e0e0e0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.cms-content-map-open {
|
|
21
|
+
right: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.cms-content-map-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding: 10px 14px;
|
|
29
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cms-content-map-title {
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
font-size: 13px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cms-content-map-close {
|
|
38
|
+
background: none;
|
|
39
|
+
border: none;
|
|
40
|
+
color: #888;
|
|
41
|
+
font-size: 18px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
padding: 0 4px;
|
|
44
|
+
line-height: 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.cms-content-map-close:hover {
|
|
48
|
+
color: #fff;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cms-content-map-body {
|
|
52
|
+
overflow-y: auto;
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 8px 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.cms-content-map-loading,
|
|
58
|
+
.cms-content-map-empty {
|
|
59
|
+
padding: 20px 14px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
color: #888;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cms-content-map-group {
|
|
65
|
+
margin-bottom: 4px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.cms-content-map-group-title {
|
|
69
|
+
padding: 6px 14px 4px;
|
|
70
|
+
font-size: 10px;
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
letter-spacing: 0.05em;
|
|
74
|
+
color: #6c7a89;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cms-content-map-item {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
padding: 5px 14px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: background 0.15s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.cms-content-map-item:hover {
|
|
87
|
+
background: rgba(255, 255, 255, 0.05);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cms-content-map-field {
|
|
91
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
color: #c8d6e5;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cms-content-map-tags {
|
|
97
|
+
display: flex;
|
|
98
|
+
gap: 4px;
|
|
99
|
+
align-items: center;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.cms-map-status {
|
|
103
|
+
font-size: 10px;
|
|
104
|
+
padding: 1px 6px;
|
|
105
|
+
border-radius: 3px;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.cms-map-default {
|
|
110
|
+
background: rgba(255, 255, 255, 0.08);
|
|
111
|
+
color: #888;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.cms-map-saved {
|
|
115
|
+
background: rgba(46, 213, 115, 0.15);
|
|
116
|
+
color: #2ed573;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.cms-map-scoped {
|
|
120
|
+
font-size: 10px;
|
|
121
|
+
padding: 1px 6px;
|
|
122
|
+
border-radius: 3px;
|
|
123
|
+
background: rgba(116, 185, 255, 0.15);
|
|
124
|
+
color: #74b9ff;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cms-map-type {
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
padding: 1px 6px;
|
|
131
|
+
border-radius: 3px;
|
|
132
|
+
background: rgba(255, 165, 2, 0.15);
|
|
133
|
+
color: #ffa502;
|
|
134
|
+
font-weight: 500;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cms-map-highlight {
|
|
138
|
+
outline: 2px solid #74b9ff !important;
|
|
139
|
+
outline-offset: 2px;
|
|
140
|
+
transition: outline-color 0.3s;
|
|
141
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { buildCmsUrl } from './config.js';
|
|
2
|
+
import { readHeaders } from './security.js';
|
|
3
|
+
import { parseCaretAttr, resolveBinding, getResolvedKey } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Content Map dev panel.
|
|
7
|
+
* Lists all data-caret bindings on the current page with override status.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let panel = null;
|
|
11
|
+
let isOpen = false;
|
|
12
|
+
|
|
13
|
+
function createPanel() {
|
|
14
|
+
const el = document.createElement('div');
|
|
15
|
+
el.className = 'cms-content-map';
|
|
16
|
+
el.innerHTML = `
|
|
17
|
+
<div class="cms-content-map-header">
|
|
18
|
+
<span class="cms-content-map-title">Content Map</span>
|
|
19
|
+
<button type="button" class="cms-content-map-close">×</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="cms-content-map-body"></div>
|
|
22
|
+
`;
|
|
23
|
+
document.body.appendChild(el);
|
|
24
|
+
|
|
25
|
+
el.querySelector('.cms-content-map-close')?.addEventListener('click', () => {
|
|
26
|
+
toggle();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return el;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadOverrides(entryKeys) {
|
|
33
|
+
const overrides = new Map();
|
|
34
|
+
for (const key of entryKeys) {
|
|
35
|
+
const [collection, id] = key.split('::');
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(buildCmsUrl('/entries', { collection, id }), {
|
|
38
|
+
headers: readHeaders(),
|
|
39
|
+
});
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
if (data.entries && data.entries.length > 0) {
|
|
43
|
+
overrides.set(key, data.entries[0].data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Skip failed loads
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return overrides;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getNestedValue(data, path) {
|
|
54
|
+
const keys = path.split('.');
|
|
55
|
+
let current = data;
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
if (current === null || current === undefined) return undefined;
|
|
58
|
+
if (typeof current !== 'object') return undefined;
|
|
59
|
+
current = current[key];
|
|
60
|
+
}
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function refresh() {
|
|
65
|
+
if (!panel) return;
|
|
66
|
+
const body = panel.querySelector('.cms-content-map-body');
|
|
67
|
+
if (!body) return;
|
|
68
|
+
|
|
69
|
+
const elements = document.querySelectorAll('[data-caret]');
|
|
70
|
+
const bindings = [];
|
|
71
|
+
const entryKeys = new Set();
|
|
72
|
+
|
|
73
|
+
elements.forEach((el) => {
|
|
74
|
+
const attr = el.getAttribute('data-caret') || '';
|
|
75
|
+
const parsed = parseCaretAttr(attr);
|
|
76
|
+
const resolved = resolveBinding(el, parsed);
|
|
77
|
+
if (!resolved) return;
|
|
78
|
+
|
|
79
|
+
const key = `${resolved.collection}::${resolved.id}`;
|
|
80
|
+
entryKeys.add(key);
|
|
81
|
+
|
|
82
|
+
const isScoped = parsed && !parsed.collection;
|
|
83
|
+
bindings.push({
|
|
84
|
+
el,
|
|
85
|
+
resolved,
|
|
86
|
+
fullKey: `${resolved.collection}::${resolved.id}::${resolved.field}`,
|
|
87
|
+
isImg: el instanceof HTMLImageElement,
|
|
88
|
+
isScoped,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
body.innerHTML = `<div class="cms-content-map-loading">Loading...</div>`;
|
|
93
|
+
|
|
94
|
+
const overrides = await loadOverrides(entryKeys);
|
|
95
|
+
|
|
96
|
+
if (bindings.length === 0) {
|
|
97
|
+
body.innerHTML = `<div class="cms-content-map-empty">No data-caret bindings found on this page.</div>`;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Group by collection::id
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
for (const b of bindings) {
|
|
104
|
+
const key = `${b.resolved.collection}::${b.resolved.id}`;
|
|
105
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
106
|
+
groups.get(key).push(b);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let html = '';
|
|
110
|
+
for (const [entryKey, fields] of groups) {
|
|
111
|
+
const entryData = overrides.get(entryKey);
|
|
112
|
+
html += `<div class="cms-content-map-group">`;
|
|
113
|
+
html += `<div class="cms-content-map-group-title">${escapeHtml(entryKey)}</div>`;
|
|
114
|
+
|
|
115
|
+
for (const f of fields) {
|
|
116
|
+
const hasOverride = entryData
|
|
117
|
+
? getNestedValue(entryData, f.resolved.field) !== undefined
|
|
118
|
+
: false;
|
|
119
|
+
|
|
120
|
+
const statusClass = hasOverride ? 'cms-map-saved' : 'cms-map-default';
|
|
121
|
+
const statusLabel = hasOverride ? 'saved' : 'default';
|
|
122
|
+
const scopeTag = f.isScoped ? '<span class="cms-map-scoped">scoped</span>' : '';
|
|
123
|
+
const typeTag = f.isImg ? '<span class="cms-map-type">img</span>' : '';
|
|
124
|
+
|
|
125
|
+
html += `
|
|
126
|
+
<div class="cms-content-map-item" data-caret-map-key="${escapeAttr(f.fullKey)}">
|
|
127
|
+
<span class="cms-content-map-field">${escapeHtml(f.resolved.field)}</span>
|
|
128
|
+
<span class="cms-content-map-tags">
|
|
129
|
+
${typeTag}${scopeTag}
|
|
130
|
+
<span class="cms-map-status ${statusClass}">${statusLabel}</span>
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
html += `</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
body.innerHTML = html;
|
|
140
|
+
|
|
141
|
+
// Click to scroll to element
|
|
142
|
+
body.querySelectorAll('.cms-content-map-item').forEach((item) => {
|
|
143
|
+
item.addEventListener('click', () => {
|
|
144
|
+
const key = item.getAttribute('data-caret-map-key');
|
|
145
|
+
const target = bindings.find((b) => b.fullKey === key);
|
|
146
|
+
if (target?.el) {
|
|
147
|
+
target.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
148
|
+
target.el.classList.add('cms-map-highlight');
|
|
149
|
+
setTimeout(() => target.el.classList.remove('cms-map-highlight'), 2000);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toggle() {
|
|
156
|
+
if (!panel) panel = createPanel();
|
|
157
|
+
isOpen = !isOpen;
|
|
158
|
+
panel.classList.toggle('cms-content-map-open', isOpen);
|
|
159
|
+
if (isOpen) refresh();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function escapeHtml(str) {
|
|
163
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function escapeAttr(str) {
|
|
167
|
+
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function mountContentMap({ mapButton }) {
|
|
171
|
+
mapButton?.addEventListener('click', toggle);
|
|
172
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function mountEditorGuards(state) {
|
|
2
|
+
document.body.classList.add('cms-edit-mode');
|
|
3
|
+
|
|
4
|
+
window.addEventListener('beforeunload', (e) => {
|
|
5
|
+
if (state.dirtyEl) {
|
|
6
|
+
e.preventDefault();
|
|
7
|
+
e.returnValue = '';
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Smart navigation: only block clicks on CMS-editable elements.
|
|
12
|
+
document.addEventListener(
|
|
13
|
+
'click',
|
|
14
|
+
(e) => {
|
|
15
|
+
const target = e.target;
|
|
16
|
+
if (!(target instanceof Element)) return;
|
|
17
|
+
|
|
18
|
+
// Allow clicks inside the CMS toolbar
|
|
19
|
+
if (target.closest('.cms-toolbar')) return;
|
|
20
|
+
|
|
21
|
+
// Allow clicks on image overlay buttons
|
|
22
|
+
if (target.closest('.cms-img-overlay')) return;
|
|
23
|
+
|
|
24
|
+
// Allow clicks inside the link popover
|
|
25
|
+
if (target.closest('.cms-link-popover')) return;
|
|
26
|
+
|
|
27
|
+
// Allow clicks on the floating "Open page" affordance — it's a real
|
|
28
|
+
// anchor that lets visitors follow links instead of editing them.
|
|
29
|
+
if (target.closest('.cms-link-follow')) return;
|
|
30
|
+
|
|
31
|
+
const editable = target.closest('[data-caret]');
|
|
32
|
+
const link = target.closest('a');
|
|
33
|
+
if (link) {
|
|
34
|
+
// Link contains or is a CMS editable — block nav, focus editable
|
|
35
|
+
const cmsInLink = editable || link.querySelector('[data-caret]');
|
|
36
|
+
if (cmsInLink instanceof HTMLElement) {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
cmsInLink.focus();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Non-CMS link — let it navigate normally
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// For buttons: only intercept if they contain CMS content
|
|
47
|
+
const btn = target.closest('button');
|
|
48
|
+
if (btn && !btn.closest('.cms-toolbar') && !btn.closest('.cms-img-wrapper')) {
|
|
49
|
+
const cmsInBtn = editable || btn.querySelector('[data-caret]');
|
|
50
|
+
if (cmsInBtn instanceof HTMLElement) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
cmsInBtn.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
true,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Block form submissions (except CMS forms)
|
|
61
|
+
document.addEventListener(
|
|
62
|
+
'submit',
|
|
63
|
+
(e) => {
|
|
64
|
+
const target = e.target;
|
|
65
|
+
if (!(target instanceof Element)) return;
|
|
66
|
+
if (!target.closest('.cms-toolbar')) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function parseCaretAttr(attr) {
|
|
2
|
+
const parts = attr.split('::');
|
|
3
|
+
if (parts.length === 3) return { collection: parts[0], id: parts[1], field: parts[2] };
|
|
4
|
+
// Field-only (scoped) — return just the field, caller resolves scope
|
|
5
|
+
if (parts.length === 1 && !attr.includes('::')) return { collection: null, id: null, field: attr };
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a parsed binding by walking up the DOM to find data-caret-scope.
|
|
11
|
+
* If the binding already has collection+id, returns as-is.
|
|
12
|
+
* If field-only, walks up to nearest [data-caret-scope] ancestor.
|
|
13
|
+
* Returns null if scope cannot be resolved.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveBinding(el, parsed) {
|
|
16
|
+
if (!parsed) return null;
|
|
17
|
+
if (parsed.collection && parsed.id) return parsed;
|
|
18
|
+
|
|
19
|
+
// Walk up to find nearest data-caret-scope
|
|
20
|
+
let node = el.parentElement;
|
|
21
|
+
while (node) {
|
|
22
|
+
const scope = node.getAttribute('data-caret-scope');
|
|
23
|
+
if (scope) {
|
|
24
|
+
const parts = scope.split('::');
|
|
25
|
+
if (parts.length === 2) {
|
|
26
|
+
return { collection: parts[0], id: parts[1], field: parsed.field };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
node = node.parentElement;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the full data-caret key for an element, resolving scope if needed.
|
|
36
|
+
* Returns "collection::id::field" or null if unresolvable.
|
|
37
|
+
*/
|
|
38
|
+
export function getResolvedKey(el) {
|
|
39
|
+
const attr = el.getAttribute('data-caret');
|
|
40
|
+
if (!attr) return null;
|
|
41
|
+
const parsed = parseCaretAttr(attr);
|
|
42
|
+
const resolved = resolveBinding(el, parsed);
|
|
43
|
+
if (!resolved) return null;
|
|
44
|
+
return `${resolved.collection}::${resolved.id}::${resolved.field}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function flash(el, success) {
|
|
48
|
+
const cls = success ? 'cms-saved' : 'cms-error';
|
|
49
|
+
el.classList.add(cls);
|
|
50
|
+
setTimeout(() => el.classList.remove(cls), 2000);
|
|
51
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { buildCmsUrl } from './config.js';
|
|
2
|
+
import { uploadHeaders } from './security.js';
|
|
3
|
+
import { resolveBinding } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
export function mountImageEditors({
|
|
6
|
+
parseCaretAttr,
|
|
7
|
+
flash,
|
|
8
|
+
compressImage,
|
|
9
|
+
updateEmblaCarousel,
|
|
10
|
+
saveField,
|
|
11
|
+
setStatus,
|
|
12
|
+
showToast,
|
|
13
|
+
onUnauthorized,
|
|
14
|
+
}) {
|
|
15
|
+
const syncBoundImages = (marker, src) => {
|
|
16
|
+
document.querySelectorAll('[data-caret]').forEach((node) => {
|
|
17
|
+
if (!(node instanceof HTMLImageElement)) return;
|
|
18
|
+
if (node.getAttribute('data-caret') !== marker) return;
|
|
19
|
+
node.src = src;
|
|
20
|
+
updateEmblaCarousel(node, src);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
document.querySelectorAll('[data-caret]').forEach((el) => {
|
|
25
|
+
if (!(el instanceof HTMLImageElement)) return;
|
|
26
|
+
const img = el;
|
|
27
|
+
if (img.dataset.caretImgMounted === 'true') return;
|
|
28
|
+
img.dataset.caretImgMounted = 'true';
|
|
29
|
+
|
|
30
|
+
const existingWrapper = img.parentElement;
|
|
31
|
+
if (existingWrapper?.classList.contains('cms-img-wrapper')) {
|
|
32
|
+
existingWrapper.parentNode?.insertBefore(img, existingWrapper);
|
|
33
|
+
existingWrapper.remove();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const wrapper = document.createElement('div');
|
|
37
|
+
wrapper.className = 'cms-img-wrapper';
|
|
38
|
+
wrapper.style.position = 'relative';
|
|
39
|
+
wrapper.style.display = 'block';
|
|
40
|
+
wrapper.style.width = '100%';
|
|
41
|
+
wrapper.style.height = '100%';
|
|
42
|
+
|
|
43
|
+
img.parentNode?.insertBefore(wrapper, img);
|
|
44
|
+
wrapper.appendChild(img);
|
|
45
|
+
|
|
46
|
+
// Corner badge — persistent edit indicator
|
|
47
|
+
const badge = document.createElement('div');
|
|
48
|
+
badge.className = 'cms-img-badge';
|
|
49
|
+
badge.innerHTML =
|
|
50
|
+
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
|
51
|
+
wrapper.appendChild(badge);
|
|
52
|
+
|
|
53
|
+
// Full overlay with idle + uploading states
|
|
54
|
+
const overlay = document.createElement('button');
|
|
55
|
+
overlay.className = 'cms-img-overlay';
|
|
56
|
+
overlay.type = 'button';
|
|
57
|
+
overlay.innerHTML = `
|
|
58
|
+
<span class="cms-img-overlay-content cms-img-state-idle">
|
|
59
|
+
<svg class="cms-img-overlay-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
60
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/>
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/>
|
|
62
|
+
</svg>
|
|
63
|
+
<span class="cms-img-overlay-text">Replace Image</span>
|
|
64
|
+
</span>
|
|
65
|
+
<span class="cms-img-overlay-content cms-img-state-uploading">
|
|
66
|
+
<span class="cms-img-spinner"></span>
|
|
67
|
+
<span class="cms-img-overlay-text">Uploading...</span>
|
|
68
|
+
</span>
|
|
69
|
+
`;
|
|
70
|
+
wrapper.appendChild(overlay);
|
|
71
|
+
|
|
72
|
+
const input = document.createElement('input');
|
|
73
|
+
input.type = 'file';
|
|
74
|
+
input.accept = 'image/jpeg,image/png,image/webp,image/avif';
|
|
75
|
+
input.style.display = 'none';
|
|
76
|
+
wrapper.appendChild(input);
|
|
77
|
+
|
|
78
|
+
overlay.addEventListener('click', (e) => {
|
|
79
|
+
e.stopPropagation(); // prevent slide click (lightbox)
|
|
80
|
+
input.click();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
input.addEventListener('change', async () => {
|
|
84
|
+
const file = input.files?.[0];
|
|
85
|
+
if (!file) return;
|
|
86
|
+
|
|
87
|
+
const marker = img.getAttribute('data-caret') || '';
|
|
88
|
+
const parsed = parseCaretAttr(marker);
|
|
89
|
+
const resolved = resolveBinding(img, parsed);
|
|
90
|
+
if (!resolved) return;
|
|
91
|
+
|
|
92
|
+
// Optimistic preview
|
|
93
|
+
const previewUrl = URL.createObjectURL(file);
|
|
94
|
+
const originalSrc = img.src;
|
|
95
|
+
img.src = previewUrl;
|
|
96
|
+
|
|
97
|
+
overlay.classList.add('uploading');
|
|
98
|
+
badge.classList.add('cms-img-badge-hidden');
|
|
99
|
+
setStatus('saving', 'Uploading...');
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const compressed = await compressImage(file);
|
|
103
|
+
const formData = new FormData();
|
|
104
|
+
formData.append('file', compressed);
|
|
105
|
+
const headers = await uploadHeaders();
|
|
106
|
+
|
|
107
|
+
const uploadRes = await fetch(buildCmsUrl('/upload'), {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers,
|
|
110
|
+
body: formData,
|
|
111
|
+
});
|
|
112
|
+
if (uploadRes.status === 401) {
|
|
113
|
+
onUnauthorized();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!uploadRes.ok) throw new Error('Upload failed');
|
|
117
|
+
const { url } = await uploadRes.json();
|
|
118
|
+
|
|
119
|
+
const result = await saveField(resolved.collection, resolved.id, resolved.field, url);
|
|
120
|
+
if (result.ok) {
|
|
121
|
+
syncBoundImages(marker, url);
|
|
122
|
+
showToast('Image updated', 'success');
|
|
123
|
+
} else {
|
|
124
|
+
const rollbackSrc =
|
|
125
|
+
result.reason === 'conflict' && typeof result.latestValue === 'string'
|
|
126
|
+
? result.latestValue
|
|
127
|
+
: originalSrc;
|
|
128
|
+
syncBoundImages(marker, rollbackSrc);
|
|
129
|
+
if (result.reason === 'conflict') {
|
|
130
|
+
showToast('Image changed elsewhere. Loaded latest image.', 'error');
|
|
131
|
+
} else if (result.reason !== 'unauthorized') {
|
|
132
|
+
showToast('Failed to save image. Changes reverted.', 'error');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
flash(img, result.ok);
|
|
137
|
+
} catch {
|
|
138
|
+
img.src = originalSrc;
|
|
139
|
+
flash(img, false);
|
|
140
|
+
setStatus('error', 'Upload failed');
|
|
141
|
+
showToast('Upload failed', 'error');
|
|
142
|
+
} finally {
|
|
143
|
+
URL.revokeObjectURL(previewUrl);
|
|
144
|
+
overlay.classList.remove('uploading');
|
|
145
|
+
badge.classList.remove('cms-img-badge-hidden');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
input.value = '';
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|