@astro-live-cms/core 0.2.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/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.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 +77 -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 +11 -0
- package/dist/runtime/bind.js.map +1 -0
- package/dist/runtime/config.d.ts +6 -0
- package/dist/runtime/config.d.ts.map +1 -0
- package/dist/runtime/config.js +18 -0
- package/dist/runtime/config.js.map +1 -0
- package/dist/runtime/content.d.ts +30 -0
- package/dist/runtime/content.d.ts.map +1 -0
- package/dist/runtime/content.js +48 -0
- package/dist/runtime/content.js.map +1 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/markers.d.ts +17 -0
- package/dist/runtime/markers.d.ts.map +1 -0
- package/dist/runtime/markers.js +17 -0
- package/dist/runtime/markers.js.map +1 -0
- package/dist/runtime/middleware.d.ts +12 -0
- package/dist/runtime/middleware.d.ts.map +1 -0
- package/dist/runtime/middleware.js +37 -0
- package/dist/runtime/middleware.js.map +1 -0
- package/dist/runtime/mutations/contracts.d.ts +57 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -0
- package/dist/runtime/mutations/contracts.js +242 -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 +161 -0
- package/dist/runtime/mutations/engine.js.map +1 -0
- package/dist/runtime/routes/_helpers.d.ts +6 -0
- package/dist/runtime/routes/_helpers.d.ts.map +1 -0
- package/dist/runtime/routes/_helpers.js +23 -0
- package/dist/runtime/routes/_helpers.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 +110 -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 +66 -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 +51 -0
- package/dist/runtime/routes/auth-logout.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 +47 -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 +89 -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 +56 -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 +35 -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 +27 -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 +174 -0
- package/dist/runtime/routes/studio-home.js.map +1 -0
- package/dist/runtime/routes/theme-bootstrap.d.ts +4 -0
- package/dist/runtime/routes/theme-bootstrap.d.ts.map +1 -0
- package/dist/runtime/routes/theme-bootstrap.js +65 -0
- package/dist/runtime/routes/theme-bootstrap.js.map +1 -0
- package/dist/runtime/routes/theme-factory.d.ts +3 -0
- package/dist/runtime/routes/theme-factory.d.ts.map +1 -0
- package/dist/runtime/routes/theme-factory.js +142 -0
- package/dist/runtime/routes/theme-factory.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 +29 -0
- package/dist/runtime/routes/upload.js.map +1 -0
- package/dist/runtime/schema/infer-json-schema.d.ts +12 -0
- package/dist/runtime/schema/infer-json-schema.d.ts.map +1 -0
- package/dist/runtime/schema/infer-json-schema.js +75 -0
- package/dist/runtime/schema/infer-json-schema.js.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts +29 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.js +182 -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 +37 -0
- package/dist/runtime/storage/filesystem-upload-handler.js.map +1 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts +24 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -0
- package/dist/runtime/storage/in-memory-adapter.js +78 -0
- package/dist/runtime/storage/in-memory-adapter.js.map +1 -0
- package/dist/runtime/themes/preset-catalog.d.ts +9 -0
- package/dist/runtime/themes/preset-catalog.d.ts.map +1 -0
- package/dist/runtime/themes/preset-catalog.js +47 -0
- package/dist/runtime/themes/preset-catalog.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/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite/virtual-config-plugin.d.ts +7 -0
- package/dist/vite/virtual-config-plugin.d.ts.map +1 -0
- package/dist/vite/virtual-config-plugin.js +25 -0
- package/dist/vite/virtual-config-plugin.js.map +1 -0
- package/package.json +35 -0
- package/static/cms/editor/config.js +6 -0
- package/static/cms/editor/guards.js +68 -0
- package/static/cms/editor/helpers.js +16 -0
- package/static/cms/editor/image-edit.js +148 -0
- package/static/cms/editor/image-utils.js +84 -0
- package/static/cms/editor/image.css +133 -0
- package/static/cms/editor/link-ui.css +143 -0
- package/static/cms/editor/linkify.js +55 -0
- package/static/cms/editor/panel.css +91 -0
- package/static/cms/editor/panel.js +64 -0
- package/static/cms/editor/save-queue.js +167 -0
- package/static/cms/editor/section-controls/activation.js +10 -0
- package/static/cms/editor/section-controls/api.js +88 -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 +23 -0
- package/static/cms/editor/sync.js +64 -0
- package/static/cms/editor/text-edit.js +129 -0
- package/static/cms/editor/text-link-interactions.js +191 -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 +94 -0
- package/static/cms/editor/toolbar.css +261 -0
- package/static/cms/editor/toolbar.js +110 -0
- package/static/cms/editor.css +10 -0
- package/static/cms/editor.js +101 -0
- package/static/cms/studio.css +312 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeSpacingY,
|
|
3
|
+
spacingButtonLabel,
|
|
4
|
+
spacingToken,
|
|
5
|
+
isRecord,
|
|
6
|
+
} from './utils.js';
|
|
7
|
+
import {
|
|
8
|
+
normalizeSectionsFromData,
|
|
9
|
+
sectionsFromDom,
|
|
10
|
+
} from './model.js';
|
|
11
|
+
import { getPageContext } from './page-context.js';
|
|
12
|
+
import { fetchPageEntry, savePageLayout } from './api.js';
|
|
13
|
+
import {
|
|
14
|
+
createSectionControlsElement,
|
|
15
|
+
createSectionBadge,
|
|
16
|
+
createSpacingDragHandle,
|
|
17
|
+
createInsertHandle,
|
|
18
|
+
} from './ui-elements.js';
|
|
19
|
+
import { createAddPicker, createSpacingPicker } from './pickers.js';
|
|
20
|
+
import { mutateSections as mutateSectionsModel } from './mutations.js';
|
|
21
|
+
import { createSectionSelectionController } from './selection.js';
|
|
22
|
+
import { createSpacingDragController } from './spacing-drag.js';
|
|
23
|
+
import { bindDragSourceHandlers, bindDropTargetHandlers } from './reorder.js';
|
|
24
|
+
import { bindSectionActivation } from './activation.js';
|
|
25
|
+
|
|
26
|
+
export function mountSectionControls({
|
|
27
|
+
parseCmsAttr,
|
|
28
|
+
setStatus,
|
|
29
|
+
showToast,
|
|
30
|
+
onUnauthorized,
|
|
31
|
+
onCanvasStructureChanged,
|
|
32
|
+
}) {
|
|
33
|
+
let sectionNodes = Array.from(
|
|
34
|
+
document.querySelectorAll('[data-cms-section][data-cms-section-id]'),
|
|
35
|
+
);
|
|
36
|
+
if (!sectionNodes.length) return;
|
|
37
|
+
|
|
38
|
+
const pageContext = getPageContext(parseCmsAttr, sectionNodes);
|
|
39
|
+
if (!pageContext) return;
|
|
40
|
+
|
|
41
|
+
let sections = sectionsFromDom(sectionNodes);
|
|
42
|
+
let entryData = {};
|
|
43
|
+
let revision = 0;
|
|
44
|
+
let busy = false;
|
|
45
|
+
let dragSourceId = null;
|
|
46
|
+
let refreshRequired = false;
|
|
47
|
+
|
|
48
|
+
const sectionNodeById = new Map();
|
|
49
|
+
const toggleButtonsById = new Map();
|
|
50
|
+
const spacingButtonsById = new Map();
|
|
51
|
+
const gapHandlesById = new Map();
|
|
52
|
+
const sectionTemplateByKey = new Map();
|
|
53
|
+
let spacingDragController = null;
|
|
54
|
+
|
|
55
|
+
sectionNodes.forEach((sectionNode) => {
|
|
56
|
+
if (!(sectionNode instanceof HTMLElement)) return;
|
|
57
|
+
const sectionKey = sectionNode.getAttribute('data-cms-section') || 'home.hero';
|
|
58
|
+
if (sectionTemplateByKey.has(sectionKey)) return;
|
|
59
|
+
sectionTemplateByKey.set(sectionKey, sectionNode.cloneNode(true));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
document
|
|
63
|
+
.querySelectorAll('template[data-cms-section-template]')
|
|
64
|
+
.forEach((templateNode) => {
|
|
65
|
+
if (!(templateNode instanceof HTMLTemplateElement)) return;
|
|
66
|
+
const sectionKey = templateNode.getAttribute('data-cms-section-template');
|
|
67
|
+
if (!sectionKey || sectionTemplateByKey.has(sectionKey)) return;
|
|
68
|
+
|
|
69
|
+
const contentNode = templateNode.content.querySelector(
|
|
70
|
+
'[data-cms-section][data-cms-section-id]',
|
|
71
|
+
);
|
|
72
|
+
if (!(contentNode instanceof HTMLElement)) return;
|
|
73
|
+
sectionTemplateByKey.set(sectionKey, contentNode.cloneNode(true));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const addPicker = createAddPicker({
|
|
77
|
+
onPick: (sectionId, key) => {
|
|
78
|
+
if (!canEditLayout()) return;
|
|
79
|
+
const result = mutateSections('add', sectionId, { sectionKey: key });
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
showToast(result.message, 'error');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
persistWithReload(result.message);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const spacingPicker = createSpacingPicker({
|
|
89
|
+
onPick: (sectionId, spacingValue) => {
|
|
90
|
+
if (!canEditLayout()) return;
|
|
91
|
+
const result = mutateSections('spacing', sectionId, { spacingY: spacingValue });
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
showToast(result.message, 'error');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
persistWithReload(result.message);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function serializeSectionsForLayout(sectionList) {
|
|
101
|
+
return sectionList.map((section) => ({
|
|
102
|
+
id: section.id,
|
|
103
|
+
key: section.key,
|
|
104
|
+
enabled: section.enabled,
|
|
105
|
+
spacing_y: section.spacing_y,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function syncEntryDataSections() {
|
|
110
|
+
if (!isRecord(entryData)) entryData = {};
|
|
111
|
+
const layout = isRecord(entryData.layout) ? { ...entryData.layout } : {};
|
|
112
|
+
layout.sections = serializeSectionsForLayout(sections);
|
|
113
|
+
entryData.layout = layout;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
syncEntryDataSections();
|
|
117
|
+
|
|
118
|
+
function clearDropIndicators() {
|
|
119
|
+
sectionNodes.forEach((node) => {
|
|
120
|
+
if (!(node instanceof HTMLElement)) return;
|
|
121
|
+
node.classList.remove(
|
|
122
|
+
'cms-section-drop-target',
|
|
123
|
+
'cms-section-drop-before',
|
|
124
|
+
'cms-section-drop-after',
|
|
125
|
+
'cms-section-drag-source',
|
|
126
|
+
);
|
|
127
|
+
delete node.dataset.dropPosition;
|
|
128
|
+
});
|
|
129
|
+
document.body.classList.remove('cms-section-dragging');
|
|
130
|
+
document.body.classList.remove('cms-spacing-dragging');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setControlsDisabled(disabled) {
|
|
134
|
+
const shouldDisable = disabled || refreshRequired;
|
|
135
|
+
document.querySelectorAll('.cms-section-btn').forEach((button) => {
|
|
136
|
+
button.disabled = shouldDisable;
|
|
137
|
+
});
|
|
138
|
+
document.querySelectorAll('.cms-section-insert-handle').forEach((button) => {
|
|
139
|
+
button.disabled = shouldDisable;
|
|
140
|
+
});
|
|
141
|
+
document.querySelectorAll('.cms-section-gap-handle').forEach((button) => {
|
|
142
|
+
button.disabled = shouldDisable;
|
|
143
|
+
});
|
|
144
|
+
addPicker.setDisabled(shouldDisable);
|
|
145
|
+
spacingPicker.setDisabled(shouldDisable);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function closePickers() {
|
|
149
|
+
addPicker.close();
|
|
150
|
+
spacingPicker.close();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function canEditLayout() {
|
|
154
|
+
if (!refreshRequired) return true;
|
|
155
|
+
showToast('Refresh page to continue editing section structure.', 'error');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const selectionController = createSectionSelectionController({
|
|
160
|
+
sectionNodeById,
|
|
161
|
+
isSelectionLocked: () =>
|
|
162
|
+
busy || refreshRequired || Boolean(dragSourceId) || Boolean(spacingDragController?.isDragging()),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function setNodeSpacingPreview(sectionId, spacingY) {
|
|
166
|
+
const node = sectionNodeById.get(sectionId);
|
|
167
|
+
if (!(node instanceof HTMLElement)) return;
|
|
168
|
+
if (spacingY) {
|
|
169
|
+
node.setAttribute('data-cms-spacing-y', spacingY);
|
|
170
|
+
} else {
|
|
171
|
+
node.removeAttribute('data-cms-spacing-y');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function updateSpacingUi(sectionId) {
|
|
176
|
+
const section = sections.find((item) => item.id === sectionId);
|
|
177
|
+
if (!section) return;
|
|
178
|
+
|
|
179
|
+
const token = spacingToken(section.spacing_y);
|
|
180
|
+
const spacingBtn = spacingButtonsById.get(sectionId);
|
|
181
|
+
if (spacingBtn instanceof HTMLButtonElement) {
|
|
182
|
+
spacingBtn.textContent = spacingButtonLabel(section.spacing_y);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const gapHandle = gapHandlesById.get(sectionId);
|
|
186
|
+
if (gapHandle instanceof HTMLButtonElement) {
|
|
187
|
+
const label = gapHandle.querySelector('.cms-section-gap-label');
|
|
188
|
+
if (label) {
|
|
189
|
+
label.textContent = token === 'default' ? 'Y' : `Y:${token}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function updateToggleUi(sectionId) {
|
|
195
|
+
const section = sections.find((item) => item.id === sectionId);
|
|
196
|
+
if (!section) return;
|
|
197
|
+
const toggleBtn = toggleButtonsById.get(sectionId);
|
|
198
|
+
if (toggleBtn instanceof HTMLButtonElement) {
|
|
199
|
+
toggleBtn.textContent = section.enabled ? 'Hide' : 'Show';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setSectionEnabledPreview(sectionId, enabled) {
|
|
204
|
+
const node = sectionNodeById.get(sectionId);
|
|
205
|
+
if (!(node instanceof HTMLElement)) return;
|
|
206
|
+
node.classList.toggle('cms-section-disabled-preview', enabled === false);
|
|
207
|
+
node.setAttribute('data-cms-section-enabled', enabled === false ? 'false' : 'true');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getSectionSnapshot(sectionId, sectionKey) {
|
|
211
|
+
return sections.find((item) => item.id === sectionId) || {
|
|
212
|
+
id: sectionId,
|
|
213
|
+
key: sectionKey,
|
|
214
|
+
enabled: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function updateSectionChip(sectionId, order, sectionKey) {
|
|
219
|
+
const node = sectionNodeById.get(sectionId);
|
|
220
|
+
if (!(node instanceof HTMLElement)) return;
|
|
221
|
+
const chip = node.querySelector('.cms-section-chip');
|
|
222
|
+
if (chip instanceof HTMLElement) {
|
|
223
|
+
const snapshot = getSectionSnapshot(sectionId, sectionKey);
|
|
224
|
+
const nextChip = createSectionBadge(snapshot, order);
|
|
225
|
+
chip.textContent = nextChip.textContent;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function mountSectionNode(sectionNode, sectionIndex) {
|
|
230
|
+
if (!(sectionNode instanceof HTMLElement)) return;
|
|
231
|
+
|
|
232
|
+
const sectionId = sectionNode.getAttribute('data-cms-section-id');
|
|
233
|
+
if (!sectionId) return;
|
|
234
|
+
const sectionKey = sectionNode.getAttribute('data-cms-section') || 'home.hero';
|
|
235
|
+
const sectionSnapshot = getSectionSnapshot(sectionId, sectionKey);
|
|
236
|
+
|
|
237
|
+
sectionNodeById.set(sectionId, sectionNode);
|
|
238
|
+
sectionNode.setAttribute('data-cms-collection', pageContext.collection);
|
|
239
|
+
sectionNode.setAttribute('data-cms-entry-id', pageContext.id);
|
|
240
|
+
|
|
241
|
+
if (getComputedStyle(sectionNode).position === 'static') {
|
|
242
|
+
sectionNode.style.position = 'relative';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (sectionNode.dataset.cmsSectionControlsMounted !== 'true') {
|
|
246
|
+
const badge = createSectionBadge(sectionSnapshot, sectionIndex + 1);
|
|
247
|
+
const controls = createSectionControlsElement();
|
|
248
|
+
|
|
249
|
+
controls.addEventListener('click', (event) => {
|
|
250
|
+
const target = event.target;
|
|
251
|
+
if (!(target instanceof Element)) return;
|
|
252
|
+
const button = target.closest('.cms-section-btn');
|
|
253
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
254
|
+
if (!canEditLayout()) return;
|
|
255
|
+
if (busy) return;
|
|
256
|
+
|
|
257
|
+
const currentSectionId = sectionNode.getAttribute('data-cms-section-id');
|
|
258
|
+
if (!currentSectionId) return;
|
|
259
|
+
|
|
260
|
+
selectionController.setActiveSection(currentSectionId);
|
|
261
|
+
const action = button.dataset.action;
|
|
262
|
+
if (!action || action === 'drag') return;
|
|
263
|
+
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
|
|
267
|
+
if (action === 'spacing') {
|
|
268
|
+
closePickers();
|
|
269
|
+
const currentSection = sections.find((item) => item.id === currentSectionId);
|
|
270
|
+
spacingPicker.open(button, currentSectionId, currentSection?.spacing_y);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = mutateSections(action, currentSectionId);
|
|
275
|
+
if (!result.ok) {
|
|
276
|
+
showToast(result.message, 'error');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
persistWithReload(result.message);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
bindDragSourceHandlers({
|
|
283
|
+
controls,
|
|
284
|
+
sectionId,
|
|
285
|
+
sectionNode,
|
|
286
|
+
setDragSourceId: (value) => {
|
|
287
|
+
dragSourceId = value;
|
|
288
|
+
},
|
|
289
|
+
clearDropIndicators,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
bindSectionActivation({
|
|
293
|
+
sectionNode,
|
|
294
|
+
sectionId,
|
|
295
|
+
setActiveSection: (id) => selectionController.setActiveSection(id),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
bindDropTargetHandlers({
|
|
299
|
+
sectionNode,
|
|
300
|
+
sectionId,
|
|
301
|
+
isBusy: () => busy || refreshRequired,
|
|
302
|
+
getDragSourceId: () => dragSourceId,
|
|
303
|
+
getSections: () => sections,
|
|
304
|
+
setSections: (nextSections) => {
|
|
305
|
+
sections = nextSections;
|
|
306
|
+
},
|
|
307
|
+
clearDropIndicators,
|
|
308
|
+
onReordered: () => persistWithReload('Section reordered'),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
sectionNode.classList.add('cms-section-editable');
|
|
312
|
+
sectionNode.appendChild(badge);
|
|
313
|
+
sectionNode.appendChild(controls);
|
|
314
|
+
|
|
315
|
+
const boundaryActions = document.createElement('div');
|
|
316
|
+
boundaryActions.className = 'cms-section-boundary-actions';
|
|
317
|
+
|
|
318
|
+
const insertHandle = createInsertHandle();
|
|
319
|
+
insertHandle.addEventListener('click', (event) => {
|
|
320
|
+
if (!canEditLayout()) return;
|
|
321
|
+
if (busy) return;
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
event.stopPropagation();
|
|
324
|
+
const currentSectionId = sectionNode.getAttribute('data-cms-section-id');
|
|
325
|
+
if (!currentSectionId) return;
|
|
326
|
+
selectionController.setActiveSection(currentSectionId);
|
|
327
|
+
closePickers();
|
|
328
|
+
addPicker.open(insertHandle, currentSectionId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const gapHandle = createSpacingDragHandle(sectionSnapshot);
|
|
332
|
+
gapHandle.addEventListener('pointerdown', (event) => {
|
|
333
|
+
if (!canEditLayout()) return;
|
|
334
|
+
const currentSectionId = sectionNode.getAttribute('data-cms-section-id');
|
|
335
|
+
if (!currentSectionId) return;
|
|
336
|
+
spacingDragController?.start(event, currentSectionId, gapHandle);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
boundaryActions.appendChild(insertHandle);
|
|
340
|
+
boundaryActions.appendChild(gapHandle);
|
|
341
|
+
sectionNode.appendChild(boundaryActions);
|
|
342
|
+
sectionNode.dataset.cmsSectionControlsMounted = 'true';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const controls = sectionNode.querySelector('.cms-section-controls');
|
|
346
|
+
const toggleBtn = controls?.querySelector('[data-action="toggle"]');
|
|
347
|
+
if (toggleBtn instanceof HTMLButtonElement) {
|
|
348
|
+
toggleButtonsById.set(sectionId, toggleBtn);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const spacingBtn = controls?.querySelector('[data-action="spacing"]');
|
|
352
|
+
if (spacingBtn instanceof HTMLButtonElement) {
|
|
353
|
+
spacingButtonsById.set(sectionId, spacingBtn);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const gapHandle = sectionNode.querySelector('.cms-section-gap-handle');
|
|
357
|
+
if (gapHandle instanceof HTMLButtonElement) {
|
|
358
|
+
gapHandlesById.set(sectionId, gapHandle);
|
|
359
|
+
gapHandle.dataset.sectionId = sectionId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
updateSectionChip(sectionId, sectionIndex + 1, sectionKey);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function applySectionsToCanvas(nextSections) {
|
|
366
|
+
if (!Array.isArray(nextSections) || !nextSections.length) return false;
|
|
367
|
+
|
|
368
|
+
const parent = sectionNodes[0]?.parentNode;
|
|
369
|
+
if (!parent) return false;
|
|
370
|
+
|
|
371
|
+
const existingById = new Map();
|
|
372
|
+
sectionNodes.forEach((node) => {
|
|
373
|
+
if (!(node instanceof HTMLElement)) return;
|
|
374
|
+
const id = node.getAttribute('data-cms-section-id');
|
|
375
|
+
if (!id) return;
|
|
376
|
+
existingById.set(id, node);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const nextNodes = [];
|
|
380
|
+
let structureChanged = false;
|
|
381
|
+
|
|
382
|
+
for (const section of nextSections) {
|
|
383
|
+
const existingNode = existingById.get(section.id);
|
|
384
|
+
if (existingNode instanceof HTMLElement) {
|
|
385
|
+
nextNodes.push(existingNode);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const template = sectionTemplateByKey.get(section.key);
|
|
390
|
+
if (!(template instanceof HTMLElement)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const cloned = template.cloneNode(true);
|
|
395
|
+
if (!(cloned instanceof HTMLElement)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
cloned.setAttribute('data-cms-section', section.key);
|
|
400
|
+
cloned.setAttribute('data-cms-section-id', section.id);
|
|
401
|
+
structureChanged = true;
|
|
402
|
+
nextNodes.push(cloned);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const nextIdSet = new Set(nextSections.map((section) => section.id));
|
|
406
|
+
sectionNodes.forEach((node) => {
|
|
407
|
+
if (!(node instanceof HTMLElement)) return;
|
|
408
|
+
const id = node.getAttribute('data-cms-section-id');
|
|
409
|
+
if (!id || nextIdSet.has(id)) return;
|
|
410
|
+
structureChanged = true;
|
|
411
|
+
sectionNodeById.delete(id);
|
|
412
|
+
toggleButtonsById.delete(id);
|
|
413
|
+
spacingButtonsById.delete(id);
|
|
414
|
+
gapHandlesById.delete(id);
|
|
415
|
+
node.remove();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
nextNodes.forEach((node, index) => {
|
|
419
|
+
parent.appendChild(node);
|
|
420
|
+
mountSectionNode(node, index);
|
|
421
|
+
});
|
|
422
|
+
sectionNodes = nextNodes;
|
|
423
|
+
|
|
424
|
+
nextSections.forEach((section) => {
|
|
425
|
+
setNodeSpacingPreview(section.id, section.spacing_y);
|
|
426
|
+
updateSpacingUi(section.id);
|
|
427
|
+
updateToggleUi(section.id);
|
|
428
|
+
setSectionEnabledPreview(section.id, section.enabled);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const firstSectionId = nextSections[0]?.id;
|
|
432
|
+
if (firstSectionId) {
|
|
433
|
+
selectionController.setActiveSection(firstSectionId);
|
|
434
|
+
}
|
|
435
|
+
selectionController.startObserver();
|
|
436
|
+
|
|
437
|
+
if (structureChanged && typeof onCanvasStructureChanged === 'function') {
|
|
438
|
+
onCanvasStructureChanged();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function markRefreshRequired({
|
|
445
|
+
statusMessage,
|
|
446
|
+
toastMessage,
|
|
447
|
+
statusType = 'error',
|
|
448
|
+
toastType = 'error',
|
|
449
|
+
}) {
|
|
450
|
+
refreshRequired = true;
|
|
451
|
+
closePickers();
|
|
452
|
+
setControlsDisabled(true);
|
|
453
|
+
setStatus(statusType, statusMessage);
|
|
454
|
+
showToast(toastMessage, toastType);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function syncLatestLayoutAfterConflict() {
|
|
458
|
+
const loaded = await fetchPageEntry({
|
|
459
|
+
collection: pageContext.collection,
|
|
460
|
+
id: pageContext.id,
|
|
461
|
+
onUnauthorized,
|
|
462
|
+
});
|
|
463
|
+
if (!loaded) return;
|
|
464
|
+
|
|
465
|
+
entryData = loaded.data;
|
|
466
|
+
revision = loaded.revision;
|
|
467
|
+
|
|
468
|
+
const latestSections = normalizeSectionsFromData(entryData, sectionsFromDom(sectionNodes));
|
|
469
|
+
if (!applySectionsToCanvas(latestSections)) {
|
|
470
|
+
sections = latestSections;
|
|
471
|
+
markRefreshRequired({
|
|
472
|
+
statusMessage: 'Layout changed',
|
|
473
|
+
toastMessage: 'Layout changed structurally. Refresh page to see latest sections.',
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
sections = latestSections;
|
|
479
|
+
setStatus('idle', 'Layout synced');
|
|
480
|
+
showToast('Layout conflict resolved with latest content.', 'success');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function applySectionSpacing(sectionId, spacingValue) {
|
|
484
|
+
const index = sections.findIndex((section) => section.id === sectionId);
|
|
485
|
+
if (index === -1) return false;
|
|
486
|
+
const spacingY = normalizeSpacingY(spacingValue);
|
|
487
|
+
sections[index] = {
|
|
488
|
+
...sections[index],
|
|
489
|
+
spacing_y: spacingY,
|
|
490
|
+
};
|
|
491
|
+
setNodeSpacingPreview(sectionId, spacingY);
|
|
492
|
+
updateSpacingUi(sectionId);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function mutateSections(action, sectionId, payload) {
|
|
497
|
+
const result = mutateSectionsModel({
|
|
498
|
+
sections,
|
|
499
|
+
action,
|
|
500
|
+
sectionId,
|
|
501
|
+
payload,
|
|
502
|
+
applySpacing: (id, spacingY) => applySectionSpacing(id, spacingY),
|
|
503
|
+
});
|
|
504
|
+
if (result.ok && Array.isArray(result.sections)) {
|
|
505
|
+
sections = result.sections;
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
spacingDragController = createSpacingDragController({
|
|
511
|
+
isBusy: () => busy || refreshRequired,
|
|
512
|
+
isReordering: () => Boolean(dragSourceId),
|
|
513
|
+
closePickers,
|
|
514
|
+
getSectionSpacing: (sectionId) =>
|
|
515
|
+
sections.find((item) => item.id === sectionId)?.spacing_y,
|
|
516
|
+
updateSpacingByToken: (sectionId, token) => applySectionSpacing(sectionId, token),
|
|
517
|
+
setActiveSection: (sectionId) => selectionController.setActiveSection(sectionId),
|
|
518
|
+
onCommitted: (sectionId) => {
|
|
519
|
+
if (!canEditLayout()) return;
|
|
520
|
+
const section = sections.find((item) => item.id === sectionId);
|
|
521
|
+
showToast(`Spacing: ${section?.spacing_y ? section.spacing_y : 'default'}`, 'success');
|
|
522
|
+
persistWithReload('Section spacing updated');
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
async function persistWithReload(successMessage) {
|
|
527
|
+
if (!canEditLayout()) return;
|
|
528
|
+
if (busy) return;
|
|
529
|
+
|
|
530
|
+
spacingDragController?.stop();
|
|
531
|
+
busy = true;
|
|
532
|
+
setControlsDisabled(true);
|
|
533
|
+
closePickers();
|
|
534
|
+
setStatus('saving', 'Saving layout...');
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const result = await savePageLayout({
|
|
538
|
+
id: pageContext.id,
|
|
539
|
+
sections,
|
|
540
|
+
expectedRevision: revision,
|
|
541
|
+
onUnauthorized,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (!result.ok) {
|
|
545
|
+
if (result.reason === 'conflict') {
|
|
546
|
+
await syncLatestLayoutAfterConflict();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (result.reason !== 'unauthorized') {
|
|
550
|
+
const errorMessage =
|
|
551
|
+
typeof result.error === 'string' && result.error.trim()
|
|
552
|
+
? result.error
|
|
553
|
+
: 'Failed to save layout';
|
|
554
|
+
setStatus('error', 'Layout save failed');
|
|
555
|
+
showToast(errorMessage, 'error');
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
revision = result.revision;
|
|
561
|
+
syncEntryDataSections();
|
|
562
|
+
|
|
563
|
+
if (!applySectionsToCanvas(sections)) {
|
|
564
|
+
markRefreshRequired({
|
|
565
|
+
statusType: 'idle',
|
|
566
|
+
statusMessage: 'Layout saved (refresh required)',
|
|
567
|
+
toastType: 'success',
|
|
568
|
+
toastMessage: `${successMessage}. Refresh page to render structural changes.`,
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
setStatus('idle', 'Layout saved');
|
|
574
|
+
showToast(successMessage, 'success');
|
|
575
|
+
} catch {
|
|
576
|
+
setStatus('error', 'Layout save failed');
|
|
577
|
+
showToast('Failed to save layout', 'error');
|
|
578
|
+
} finally {
|
|
579
|
+
busy = false;
|
|
580
|
+
setControlsDisabled(false);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function mountControls() {
|
|
585
|
+
sectionNodes.forEach((sectionNode, sectionIndex) => {
|
|
586
|
+
mountSectionNode(sectionNode, sectionIndex);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
sections.forEach((section) => {
|
|
590
|
+
setNodeSpacingPreview(section.id, section.spacing_y);
|
|
591
|
+
updateSpacingUi(section.id);
|
|
592
|
+
updateToggleUi(section.id);
|
|
593
|
+
setSectionEnabledPreview(section.id, section.enabled);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const firstSectionId = sections[0]?.id;
|
|
597
|
+
if (firstSectionId) selectionController.setActiveSection(firstSectionId);
|
|
598
|
+
selectionController.startObserver();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
mountControls();
|
|
602
|
+
|
|
603
|
+
(async () => {
|
|
604
|
+
try {
|
|
605
|
+
const loaded = await fetchPageEntry({
|
|
606
|
+
collection: pageContext.collection,
|
|
607
|
+
id: pageContext.id,
|
|
608
|
+
onUnauthorized,
|
|
609
|
+
});
|
|
610
|
+
if (!loaded) return;
|
|
611
|
+
|
|
612
|
+
entryData = loaded.data;
|
|
613
|
+
revision = loaded.revision;
|
|
614
|
+
sections = normalizeSectionsFromData(entryData, sections);
|
|
615
|
+
mountControls();
|
|
616
|
+
syncEntryDataSections();
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error('[cms] Failed to load section controls data', error);
|
|
619
|
+
syncEntryDataSections();
|
|
620
|
+
}
|
|
621
|
+
})();
|
|
622
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { HOME_SECTION_KEYS } from './constants.js';
|
|
2
|
+
import { normalizeSpacingY, isRecord } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const LEGACY_SECTION_KEY_MAP = {
|
|
5
|
+
'home.trust_badges': 'home.logo_bar',
|
|
6
|
+
'home.featured': 'home.features',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function normalizeSectionKey(rawKey) {
|
|
10
|
+
if (typeof rawKey !== 'string') return null;
|
|
11
|
+
const mapped = LEGACY_SECTION_KEY_MAP[rawKey] || rawKey;
|
|
12
|
+
return HOME_SECTION_KEYS.includes(mapped) ? mapped : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeSectionsFromData(entryData, fallbackFromDom) {
|
|
16
|
+
const layout = isRecord(entryData.layout) ? entryData.layout : {};
|
|
17
|
+
const raw = Array.isArray(layout.sections) ? layout.sections : null;
|
|
18
|
+
|
|
19
|
+
if (!raw) return fallbackFromDom;
|
|
20
|
+
|
|
21
|
+
const normalized = raw
|
|
22
|
+
.map((item, index) => {
|
|
23
|
+
if (!isRecord(item)) return null;
|
|
24
|
+
const key = normalizeSectionKey(item.key);
|
|
25
|
+
if (!key) return null;
|
|
26
|
+
|
|
27
|
+
const id =
|
|
28
|
+
typeof item.id === 'string' && item.id.trim()
|
|
29
|
+
? item.id.trim()
|
|
30
|
+
: `sec-${key.replace(/\./g, '-')}-${index + 1}`;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
key,
|
|
35
|
+
enabled: item.enabled !== false,
|
|
36
|
+
spacing_y: normalizeSpacingY(item.spacing_y),
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
if (!normalized.length) return fallbackFromDom;
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sectionsFromDom(sectionNodes) {
|
|
46
|
+
return sectionNodes
|
|
47
|
+
.map((el, index) => {
|
|
48
|
+
const rawKey = el.getAttribute('data-cms-section') || 'home.hero';
|
|
49
|
+
const key = normalizeSectionKey(rawKey);
|
|
50
|
+
if (!key) return null;
|
|
51
|
+
|
|
52
|
+
const fallbackId = `sec-${key.replace(/\./g, '-')}-${index + 1}`;
|
|
53
|
+
return {
|
|
54
|
+
id: el.getAttribute('data-cms-section-id') || fallbackId,
|
|
55
|
+
key,
|
|
56
|
+
enabled: true,
|
|
57
|
+
spacing_y: normalizeSpacingY(el.getAttribute('data-cms-spacing-y')),
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
|
|
64
|
+
if (sourceId === targetId) return sections;
|
|
65
|
+
|
|
66
|
+
const sourceIndex = sections.findIndex((section) => section.id === sourceId);
|
|
67
|
+
const targetIndex = sections.findIndex((section) => section.id === targetId);
|
|
68
|
+
if (sourceIndex === -1 || targetIndex === -1) return sections;
|
|
69
|
+
|
|
70
|
+
const next = [...sections];
|
|
71
|
+
const [source] = next.splice(sourceIndex, 1);
|
|
72
|
+
const currentTargetIndex = next.findIndex((section) => section.id === targetId);
|
|
73
|
+
const insertIndex = currentTargetIndex + (placeAfter ? 1 : 0);
|
|
74
|
+
next.splice(insertIndex, 0, source);
|
|
75
|
+
return next;
|
|
76
|
+
}
|