@caretcms/core 0.3.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 +116 -11
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -5
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +8 -0
- 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 +3 -1
- 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/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/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 +13 -2
- package/dist/runtime/i18n.d.ts.map +1 -1
- package/dist/runtime/i18n.js +26 -4
- package/dist/runtime/i18n.js.map +1 -1
- 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 +21 -2
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +9 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +45 -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 +115 -63
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +3 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +12 -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 +104 -67
- 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 +3 -1
- package/dist/runtime/request-context.d.ts.map +1 -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 +24 -12
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +6 -0
- 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 +164 -40
- 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 +74 -39
- 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 +37 -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 +4 -0
- 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 +3 -1
- 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.map +1 -1
- package/dist/runtime/schema-registry.js +2 -0
- 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/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +3 -4
- package/dist/runtime/utils.js.map +1 -1
- package/dist/types.d.ts +123 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -3
- package/static/cms/admin-entry.css +9 -0
- package/static/cms/admin-entry.js +251 -687
- 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 +43 -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 +43 -15
- 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 +208 -10
- 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/toolbar.css +5 -0
- package/static/cms/editor/toolbar.js +91 -101
- package/static/cms/editor.js +2 -1
- 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
|
@@ -23,6 +23,22 @@ import { createSpacingDragController } from './spacing-drag.js';
|
|
|
23
23
|
import { bindDragSourceHandlers, bindDropTargetHandlers } from './reorder.js';
|
|
24
24
|
import { bindSectionActivation } from './activation.js';
|
|
25
25
|
|
|
26
|
+
/** @typedef {import('./model.js').Section} Section */
|
|
27
|
+
/** @typedef {import('./page-context.js').PageContext} PageContext */
|
|
28
|
+
/** @typedef {{ collection: string | null, id: string | null, field: string }} ParsedCaretBinding */
|
|
29
|
+
/** @typedef {ReturnType<typeof createSpacingDragController>} SpacingDragController */
|
|
30
|
+
/** @typedef {'saving' | 'idle' | 'error'} ToolbarStatusState */
|
|
31
|
+
/** @typedef {'success' | 'error'} ToastType */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{
|
|
35
|
+
* parseCaretAttr: (value: string) => ParsedCaretBinding | null,
|
|
36
|
+
* setStatus: (type: ToolbarStatusState, message: string) => void,
|
|
37
|
+
* showToast: (message: string, type?: ToastType) => void,
|
|
38
|
+
* onUnauthorized: () => void,
|
|
39
|
+
* onCanvasStructureChanged?: () => void,
|
|
40
|
+
* }} options
|
|
41
|
+
*/
|
|
26
42
|
export function mountSectionControls({
|
|
27
43
|
parseCaretAttr,
|
|
28
44
|
setStatus,
|
|
@@ -30,33 +46,46 @@ export function mountSectionControls({
|
|
|
30
46
|
onUnauthorized,
|
|
31
47
|
onCanvasStructureChanged,
|
|
32
48
|
}) {
|
|
49
|
+
/** @type {HTMLElement[]} */
|
|
33
50
|
let sectionNodes = Array.from(
|
|
34
51
|
document.querySelectorAll('[data-caret-section][data-caret-section-id]'),
|
|
35
|
-
);
|
|
52
|
+
).filter((node) => node instanceof HTMLElement);
|
|
36
53
|
if (!sectionNodes.length) return;
|
|
37
54
|
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
55
|
+
const detectedPageContext = getPageContext(parseCaretAttr, sectionNodes);
|
|
56
|
+
if (!detectedPageContext) return;
|
|
57
|
+
/** @type {PageContext} */
|
|
58
|
+
const pageContext = detectedPageContext;
|
|
40
59
|
|
|
60
|
+
/** @type {Section[]} */
|
|
41
61
|
let sections = sectionsFromDom(sectionNodes);
|
|
62
|
+
/** @type {Record<string, unknown>} */
|
|
42
63
|
let entryData = {};
|
|
43
64
|
let revision = 0;
|
|
44
65
|
let busy = false;
|
|
66
|
+
/** @type {string | null} */
|
|
45
67
|
let dragSourceId = null;
|
|
46
68
|
let refreshRequired = false;
|
|
47
69
|
|
|
70
|
+
/** @type {Map<string, HTMLElement>} */
|
|
48
71
|
const sectionNodeById = new Map();
|
|
72
|
+
/** @type {Map<string, HTMLButtonElement>} */
|
|
49
73
|
const toggleButtonsById = new Map();
|
|
74
|
+
/** @type {Map<string, HTMLButtonElement>} */
|
|
50
75
|
const spacingButtonsById = new Map();
|
|
76
|
+
/** @type {Map<string, HTMLButtonElement>} */
|
|
51
77
|
const gapHandlesById = new Map();
|
|
78
|
+
/** @type {Map<string, HTMLElement>} */
|
|
52
79
|
const sectionTemplateByKey = new Map();
|
|
80
|
+
/** @type {SpacingDragController | null} */
|
|
53
81
|
let spacingDragController = null;
|
|
54
82
|
|
|
55
83
|
sectionNodes.forEach((sectionNode) => {
|
|
56
84
|
if (!(sectionNode instanceof HTMLElement)) return;
|
|
57
85
|
const sectionKey = sectionNode.getAttribute('data-caret-section') || 'home.hero';
|
|
58
86
|
if (sectionTemplateByKey.has(sectionKey)) return;
|
|
59
|
-
|
|
87
|
+
const cloned = sectionNode.cloneNode(true);
|
|
88
|
+
if (cloned instanceof HTMLElement) sectionTemplateByKey.set(sectionKey, cloned);
|
|
60
89
|
});
|
|
61
90
|
|
|
62
91
|
document
|
|
@@ -70,7 +99,8 @@ export function mountSectionControls({
|
|
|
70
99
|
'[data-caret-section][data-caret-section-id]',
|
|
71
100
|
);
|
|
72
101
|
if (!(contentNode instanceof HTMLElement)) return;
|
|
73
|
-
|
|
102
|
+
const cloned = contentNode.cloneNode(true);
|
|
103
|
+
if (cloned instanceof HTMLElement) sectionTemplateByKey.set(sectionKey, cloned);
|
|
74
104
|
});
|
|
75
105
|
|
|
76
106
|
const addPicker = createAddPicker({
|
|
@@ -97,6 +127,7 @@ export function mountSectionControls({
|
|
|
97
127
|
},
|
|
98
128
|
});
|
|
99
129
|
|
|
130
|
+
/** @param {Section[]} sectionList @returns {Section[]} */
|
|
100
131
|
function serializeSectionsForLayout(sectionList) {
|
|
101
132
|
return sectionList.map((section) => ({
|
|
102
133
|
id: section.id,
|
|
@@ -130,16 +161,17 @@ export function mountSectionControls({
|
|
|
130
161
|
document.body.classList.remove('cms-spacing-dragging');
|
|
131
162
|
}
|
|
132
163
|
|
|
164
|
+
/** @param {boolean} disabled */
|
|
133
165
|
function setControlsDisabled(disabled) {
|
|
134
166
|
const shouldDisable = disabled || refreshRequired;
|
|
135
167
|
document.querySelectorAll('.cms-section-btn').forEach((button) => {
|
|
136
|
-
button.disabled = shouldDisable;
|
|
168
|
+
if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
|
|
137
169
|
});
|
|
138
170
|
document.querySelectorAll('.cms-section-insert-handle').forEach((button) => {
|
|
139
|
-
button.disabled = shouldDisable;
|
|
171
|
+
if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
|
|
140
172
|
});
|
|
141
173
|
document.querySelectorAll('.cms-section-gap-handle').forEach((button) => {
|
|
142
|
-
button.disabled = shouldDisable;
|
|
174
|
+
if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
|
|
143
175
|
});
|
|
144
176
|
addPicker.setDisabled(shouldDisable);
|
|
145
177
|
spacingPicker.setDisabled(shouldDisable);
|
|
@@ -162,6 +194,7 @@ export function mountSectionControls({
|
|
|
162
194
|
busy || refreshRequired || Boolean(dragSourceId) || Boolean(spacingDragController?.isDragging()),
|
|
163
195
|
});
|
|
164
196
|
|
|
197
|
+
/** @param {string} sectionId @param {string | undefined} spacingY */
|
|
165
198
|
function setNodeSpacingPreview(sectionId, spacingY) {
|
|
166
199
|
const node = sectionNodeById.get(sectionId);
|
|
167
200
|
if (!(node instanceof HTMLElement)) return;
|
|
@@ -172,6 +205,7 @@ export function mountSectionControls({
|
|
|
172
205
|
}
|
|
173
206
|
}
|
|
174
207
|
|
|
208
|
+
/** @param {string} sectionId */
|
|
175
209
|
function updateSpacingUi(sectionId) {
|
|
176
210
|
const section = sections.find((item) => item.id === sectionId);
|
|
177
211
|
if (!section) return;
|
|
@@ -191,6 +225,7 @@ export function mountSectionControls({
|
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
|
|
228
|
+
/** @param {string} sectionId */
|
|
194
229
|
function updateToggleUi(sectionId) {
|
|
195
230
|
const section = sections.find((item) => item.id === sectionId);
|
|
196
231
|
if (!section) return;
|
|
@@ -200,6 +235,7 @@ export function mountSectionControls({
|
|
|
200
235
|
}
|
|
201
236
|
}
|
|
202
237
|
|
|
238
|
+
/** @param {string} sectionId @param {boolean} enabled */
|
|
203
239
|
function setSectionEnabledPreview(sectionId, enabled) {
|
|
204
240
|
const node = sectionNodeById.get(sectionId);
|
|
205
241
|
if (!(node instanceof HTMLElement)) return;
|
|
@@ -207,6 +243,7 @@ export function mountSectionControls({
|
|
|
207
243
|
node.setAttribute('data-caret-section-enabled', enabled === false ? 'false' : 'true');
|
|
208
244
|
}
|
|
209
245
|
|
|
246
|
+
/** @param {string} sectionId @param {string} sectionKey @returns {Section} */
|
|
210
247
|
function getSectionSnapshot(sectionId, sectionKey) {
|
|
211
248
|
return sections.find((item) => item.id === sectionId) || {
|
|
212
249
|
id: sectionId,
|
|
@@ -215,6 +252,7 @@ export function mountSectionControls({
|
|
|
215
252
|
};
|
|
216
253
|
}
|
|
217
254
|
|
|
255
|
+
/** @param {string} sectionId @param {number} order @param {string} sectionKey */
|
|
218
256
|
function updateSectionChip(sectionId, order, sectionKey) {
|
|
219
257
|
const node = sectionNodeById.get(sectionId);
|
|
220
258
|
if (!(node instanceof HTMLElement)) return;
|
|
@@ -226,6 +264,7 @@ export function mountSectionControls({
|
|
|
226
264
|
}
|
|
227
265
|
}
|
|
228
266
|
|
|
267
|
+
/** @param {HTMLElement} sectionNode @param {number} sectionIndex */
|
|
229
268
|
function mountSectionNode(sectionNode, sectionIndex) {
|
|
230
269
|
if (!(sectionNode instanceof HTMLElement)) return;
|
|
231
270
|
|
|
@@ -362,6 +401,7 @@ export function mountSectionControls({
|
|
|
362
401
|
updateSectionChip(sectionId, sectionIndex + 1, sectionKey);
|
|
363
402
|
}
|
|
364
403
|
|
|
404
|
+
/** @param {Section[]} nextSections @returns {boolean} */
|
|
365
405
|
function applySectionsToCanvas(nextSections) {
|
|
366
406
|
if (!Array.isArray(nextSections) || !nextSections.length) return false;
|
|
367
407
|
|
|
@@ -376,6 +416,7 @@ export function mountSectionControls({
|
|
|
376
416
|
existingById.set(id, node);
|
|
377
417
|
});
|
|
378
418
|
|
|
419
|
+
/** @type {HTMLElement[]} */
|
|
379
420
|
const nextNodes = [];
|
|
380
421
|
let structureChanged = false;
|
|
381
422
|
|
|
@@ -441,6 +482,9 @@ export function mountSectionControls({
|
|
|
441
482
|
return true;
|
|
442
483
|
}
|
|
443
484
|
|
|
485
|
+
/**
|
|
486
|
+
* @param {{ statusMessage: string, toastMessage: string, statusType?: ToolbarStatusState, toastType?: ToastType }} options
|
|
487
|
+
*/
|
|
444
488
|
function markRefreshRequired({
|
|
445
489
|
statusMessage,
|
|
446
490
|
toastMessage,
|
|
@@ -480,6 +524,7 @@ export function mountSectionControls({
|
|
|
480
524
|
showToast('Layout conflict resolved with latest content.', 'success');
|
|
481
525
|
}
|
|
482
526
|
|
|
527
|
+
/** @param {string} sectionId @param {unknown} spacingValue @returns {boolean} */
|
|
483
528
|
function applySectionSpacing(sectionId, spacingValue) {
|
|
484
529
|
const index = sections.findIndex((section) => section.id === sectionId);
|
|
485
530
|
if (index === -1) return false;
|
|
@@ -493,6 +538,11 @@ export function mountSectionControls({
|
|
|
493
538
|
return true;
|
|
494
539
|
}
|
|
495
540
|
|
|
541
|
+
/**
|
|
542
|
+
* @param {string} action
|
|
543
|
+
* @param {string} sectionId
|
|
544
|
+
* @param {{ sectionKey?: unknown, spacingY?: unknown }} [payload]
|
|
545
|
+
*/
|
|
496
546
|
function mutateSections(action, sectionId, payload) {
|
|
497
547
|
const result = mutateSectionsModel({
|
|
498
548
|
sections,
|
|
@@ -523,6 +573,7 @@ export function mountSectionControls({
|
|
|
523
573
|
},
|
|
524
574
|
});
|
|
525
575
|
|
|
576
|
+
/** @param {string} successMessage */
|
|
526
577
|
async function persistWithReload(successMessage) {
|
|
527
578
|
if (!canEditLayout()) return;
|
|
528
579
|
if (busy) return;
|
|
@@ -535,6 +586,7 @@ export function mountSectionControls({
|
|
|
535
586
|
|
|
536
587
|
try {
|
|
537
588
|
const result = await savePageLayout({
|
|
589
|
+
collection: pageContext.collection,
|
|
538
590
|
id: pageContext.id,
|
|
539
591
|
sections,
|
|
540
592
|
expectedRevision: revision,
|
|
@@ -1,33 +1,60 @@
|
|
|
1
1
|
import { HOME_SECTION_KEYS } from './constants.js';
|
|
2
2
|
import { normalizeSpacingY, isRecord } from './utils.js';
|
|
3
3
|
|
|
4
|
+
/** @typedef {{ id: string, key: string, enabled: boolean, spacing_y?: string }} Section */
|
|
5
|
+
|
|
6
|
+
/** @type {Record<string, string>} */
|
|
4
7
|
const LEGACY_SECTION_KEY_MAP = {
|
|
5
8
|
'home.trust_badges': 'home.logo_bar',
|
|
6
9
|
'home.featured': 'home.features',
|
|
7
10
|
};
|
|
8
11
|
|
|
12
|
+
/** @param {unknown} rawKey @returns {string | null} */
|
|
9
13
|
function normalizeSectionKey(rawKey) {
|
|
10
14
|
if (typeof rawKey !== 'string') return null;
|
|
11
15
|
const mapped = LEGACY_SECTION_KEY_MAP[rawKey] || rawKey;
|
|
12
16
|
return HOME_SECTION_KEYS.includes(mapped) ? mapped : null;
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} preferred
|
|
21
|
+
* @param {string} key
|
|
22
|
+
* @param {number} index
|
|
23
|
+
* @param {Set<string>} usedIds
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function uniqueSectionId(preferred, key, index, usedIds) {
|
|
27
|
+
const base = preferred.trim() || `sec-${key.replace(/\./g, '-')}-${index + 1}`;
|
|
28
|
+
let id = base;
|
|
29
|
+
let suffix = 2;
|
|
30
|
+
while (usedIds.has(id)) {
|
|
31
|
+
id = `${base}-${suffix}`;
|
|
32
|
+
suffix++;
|
|
33
|
+
}
|
|
34
|
+
usedIds.add(id);
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {unknown} entryData @param {Section[]} fallbackFromDom @returns {Section[]} */
|
|
15
39
|
export function normalizeSectionsFromData(entryData, fallbackFromDom) {
|
|
40
|
+
if (!isRecord(entryData)) return fallbackFromDom;
|
|
16
41
|
const layout = isRecord(entryData.layout) ? entryData.layout : {};
|
|
17
42
|
const raw = Array.isArray(layout.sections) ? layout.sections : null;
|
|
18
43
|
|
|
19
44
|
if (!raw) return fallbackFromDom;
|
|
20
45
|
|
|
46
|
+
const usedIds = new Set();
|
|
21
47
|
const normalized = raw
|
|
22
48
|
.map((item, index) => {
|
|
23
49
|
if (!isRecord(item)) return null;
|
|
24
50
|
const key = normalizeSectionKey(item.key);
|
|
25
51
|
if (!key) return null;
|
|
26
52
|
|
|
27
|
-
const
|
|
53
|
+
const preferredId =
|
|
28
54
|
typeof item.id === 'string' && item.id.trim()
|
|
29
55
|
? item.id.trim()
|
|
30
56
|
: `sec-${key.replace(/\./g, '-')}-${index + 1}`;
|
|
57
|
+
const id = uniqueSectionId(preferredId, key, index, usedIds);
|
|
31
58
|
|
|
32
59
|
return {
|
|
33
60
|
id,
|
|
@@ -36,13 +63,15 @@ export function normalizeSectionsFromData(entryData, fallbackFromDom) {
|
|
|
36
63
|
spacing_y: normalizeSpacingY(item.spacing_y),
|
|
37
64
|
};
|
|
38
65
|
})
|
|
39
|
-
.filter(
|
|
66
|
+
.filter((section) => section !== null);
|
|
40
67
|
|
|
41
68
|
if (!normalized.length) return fallbackFromDom;
|
|
42
69
|
return normalized;
|
|
43
70
|
}
|
|
44
71
|
|
|
72
|
+
/** @param {Element[]} sectionNodes @returns {Section[]} */
|
|
45
73
|
export function sectionsFromDom(sectionNodes) {
|
|
74
|
+
const usedIds = new Set();
|
|
46
75
|
return sectionNodes
|
|
47
76
|
.map((el, index) => {
|
|
48
77
|
const rawKey = el.getAttribute('data-caret-section') || 'home.hero';
|
|
@@ -50,16 +79,21 @@ export function sectionsFromDom(sectionNodes) {
|
|
|
50
79
|
if (!key) return null;
|
|
51
80
|
|
|
52
81
|
const fallbackId = `sec-${key.replace(/\./g, '-')}-${index + 1}`;
|
|
82
|
+
const rawId = el.getAttribute('data-caret-section-id') || fallbackId;
|
|
53
83
|
return {
|
|
54
|
-
id:
|
|
84
|
+
id: uniqueSectionId(rawId, key, index, usedIds),
|
|
55
85
|
key,
|
|
56
86
|
enabled: true,
|
|
57
87
|
spacing_y: normalizeSpacingY(el.getAttribute('data-caret-spacing-y')),
|
|
58
88
|
};
|
|
59
89
|
})
|
|
60
|
-
.filter(
|
|
90
|
+
.filter((section) => section !== null);
|
|
61
91
|
}
|
|
62
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {{ sections: Section[], sourceId: string, targetId: string, placeAfter: boolean }} options
|
|
95
|
+
* @returns {Section[]}
|
|
96
|
+
*/
|
|
63
97
|
export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
|
|
64
98
|
if (sourceId === targetId) return sections;
|
|
65
99
|
|
|
@@ -69,6 +103,7 @@ export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
|
|
|
69
103
|
|
|
70
104
|
const next = [...sections];
|
|
71
105
|
const [source] = next.splice(sourceIndex, 1);
|
|
106
|
+
if (!source) return sections;
|
|
72
107
|
const currentTargetIndex = next.findIndex((section) => section.id === targetId);
|
|
73
108
|
const insertIndex = currentTargetIndex + (placeAfter ? 1 : 0);
|
|
74
109
|
next.splice(insertIndex, 0, source);
|
|
@@ -5,6 +5,19 @@ import {
|
|
|
5
5
|
normalizeSpacingY,
|
|
6
6
|
} from './utils.js';
|
|
7
7
|
|
|
8
|
+
/** @typedef {import('./model.js').Section} Section */
|
|
9
|
+
/** @typedef {{ sectionKey?: unknown, spacingY?: unknown }} MutationPayload */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{
|
|
13
|
+
* sections: Section[],
|
|
14
|
+
* action: string,
|
|
15
|
+
* sectionId: string,
|
|
16
|
+
* payload?: MutationPayload,
|
|
17
|
+
* applySpacing: (sectionId: string, spacingY: string | undefined) => boolean,
|
|
18
|
+
* }} options
|
|
19
|
+
* @returns {{ ok: true, message: string, sections: Section[] } | { ok: false, message: string }}
|
|
20
|
+
*/
|
|
8
21
|
export function mutateSections({
|
|
9
22
|
sections,
|
|
10
23
|
action,
|
|
@@ -33,6 +46,7 @@ export function mutateSections({
|
|
|
33
46
|
|
|
34
47
|
if (action === 'duplicate') {
|
|
35
48
|
const source = sections[index];
|
|
49
|
+
if (!source) return { ok: false, message: 'Section not found' };
|
|
36
50
|
const duplicate = {
|
|
37
51
|
...source,
|
|
38
52
|
id: `${source.id}-copy-${Math.random().toString(36).slice(2, 5)}`,
|
|
@@ -55,6 +69,7 @@ export function mutateSections({
|
|
|
55
69
|
|
|
56
70
|
if (action === 'toggle') {
|
|
57
71
|
const section = sections[index];
|
|
72
|
+
if (!section) return { ok: false, message: 'Section not found' };
|
|
58
73
|
const enabledCount = sections.filter((item) => item.enabled).length;
|
|
59
74
|
if (section.enabled && enabledCount <= 1) {
|
|
60
75
|
return { ok: false, message: 'At least one section must remain visible' };
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** @typedef {{ collection: string, id: string }} PageContext */
|
|
2
|
+
/** @typedef {{ collection: string | null, id: string | null, field: string }} ParsedCaretBinding */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {(value: string) => ParsedCaretBinding | null} parseCaretAttr
|
|
6
|
+
* @param {Element[]} sectionNodes
|
|
7
|
+
* @returns {PageContext | null}
|
|
8
|
+
*/
|
|
1
9
|
export function getPageContext(parseCaretAttr, sectionNodes) {
|
|
2
10
|
for (const sectionNode of sectionNodes) {
|
|
3
11
|
if (!(sectionNode instanceof Element)) continue;
|
|
@@ -19,6 +27,7 @@ export function getPageContext(parseCaretAttr, sectionNodes) {
|
|
|
19
27
|
const parsed = parseCaretAttr(attr);
|
|
20
28
|
if (!parsed) continue;
|
|
21
29
|
if (parsed.collection !== 'pages') continue;
|
|
30
|
+
if (!parsed.id) continue;
|
|
22
31
|
|
|
23
32
|
return {
|
|
24
33
|
collection: parsed.collection,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HOME_SECTION_KEYS, SPACING_Y_OPTIONS } from './constants.js';
|
|
2
2
|
import { humanizeSectionKey } from './utils.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {{ picker: HTMLElement, getAnchor: () => HTMLButtonElement | null }} options
|
|
6
|
+
* @returns {() => void}
|
|
7
|
+
*/
|
|
4
8
|
function attachGlobalPositioning({ picker, getAnchor }) {
|
|
5
9
|
function position() {
|
|
6
10
|
const anchorButton = getAnchor();
|
|
@@ -25,6 +29,9 @@ function attachGlobalPositioning({ picker, getAnchor }) {
|
|
|
25
29
|
return position;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ picker: HTMLElement, getAnchor: () => HTMLButtonElement | null, close: () => void }} options
|
|
34
|
+
*/
|
|
28
35
|
function attachOutsideClose({ picker, getAnchor, close }) {
|
|
29
36
|
document.addEventListener('click', (event) => {
|
|
30
37
|
if (picker.hidden) return;
|
|
@@ -38,9 +45,13 @@ function attachOutsideClose({ picker, getAnchor, close }) {
|
|
|
38
45
|
});
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
/** @param {{ onPick: (sectionId: string, key: string) => void }} options */
|
|
41
49
|
export function createAddPicker({ onPick }) {
|
|
42
50
|
const picker = document.createElement('div');
|
|
51
|
+
picker.id = 'cms-section-add-picker';
|
|
43
52
|
picker.className = 'cms-section-add-picker';
|
|
53
|
+
picker.setAttribute('role', 'dialog');
|
|
54
|
+
picker.setAttribute('aria-label', 'Insert section');
|
|
44
55
|
picker.hidden = true;
|
|
45
56
|
picker.innerHTML = `
|
|
46
57
|
<div class="cms-section-add-picker-head">Insert Section</div>
|
|
@@ -59,10 +70,13 @@ export function createAddPicker({ onPick }) {
|
|
|
59
70
|
|
|
60
71
|
document.body.appendChild(picker);
|
|
61
72
|
|
|
73
|
+
/** @type {string | null} */
|
|
62
74
|
let openForSectionId = null;
|
|
75
|
+
/** @type {HTMLButtonElement | null} */
|
|
63
76
|
let anchorButton = null;
|
|
64
77
|
|
|
65
78
|
function close() {
|
|
79
|
+
anchorButton?.setAttribute('aria-expanded', 'false');
|
|
66
80
|
picker.hidden = true;
|
|
67
81
|
picker.classList.remove('open');
|
|
68
82
|
openForSectionId = null;
|
|
@@ -74,14 +88,28 @@ export function createAddPicker({ onPick }) {
|
|
|
74
88
|
getAnchor: () => anchorButton,
|
|
75
89
|
});
|
|
76
90
|
|
|
91
|
+
/** @param {HTMLButtonElement} button @param {string} sectionId */
|
|
77
92
|
function open(button, sectionId) {
|
|
78
93
|
anchorButton = button;
|
|
79
94
|
openForSectionId = sectionId;
|
|
95
|
+
button.setAttribute('aria-haspopup', 'dialog');
|
|
96
|
+
button.setAttribute('aria-controls', picker.id);
|
|
97
|
+
button.setAttribute('aria-expanded', 'true');
|
|
80
98
|
picker.hidden = false;
|
|
81
99
|
picker.classList.add('open');
|
|
82
100
|
position();
|
|
101
|
+
const firstButton = picker.querySelector('button');
|
|
102
|
+
if (firstButton instanceof HTMLButtonElement) firstButton.focus();
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
picker.addEventListener('keydown', (event) => {
|
|
106
|
+
if (event.key !== 'Escape') return;
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
const previousAnchor = anchorButton;
|
|
109
|
+
close();
|
|
110
|
+
previousAnchor?.focus();
|
|
111
|
+
});
|
|
112
|
+
|
|
85
113
|
picker.addEventListener('click', (event) => {
|
|
86
114
|
const target = event.target;
|
|
87
115
|
if (!(target instanceof Element)) return;
|
|
@@ -104,17 +132,22 @@ export function createAddPicker({ onPick }) {
|
|
|
104
132
|
return {
|
|
105
133
|
open,
|
|
106
134
|
close,
|
|
135
|
+
/** @param {boolean} disabled */
|
|
107
136
|
setDisabled(disabled) {
|
|
108
137
|
picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
|
|
109
|
-
button.disabled = disabled;
|
|
138
|
+
if (button instanceof HTMLButtonElement) button.disabled = disabled;
|
|
110
139
|
});
|
|
111
140
|
},
|
|
112
141
|
};
|
|
113
142
|
}
|
|
114
143
|
|
|
144
|
+
/** @param {{ onPick: (sectionId: string, value: string) => void }} options */
|
|
115
145
|
export function createSpacingPicker({ onPick }) {
|
|
116
146
|
const picker = document.createElement('div');
|
|
147
|
+
picker.id = 'cms-section-spacing-picker';
|
|
117
148
|
picker.className = 'cms-section-add-picker cms-section-spacing-picker';
|
|
149
|
+
picker.setAttribute('role', 'dialog');
|
|
150
|
+
picker.setAttribute('aria-label', 'Vertical spacing');
|
|
118
151
|
picker.hidden = true;
|
|
119
152
|
picker.innerHTML = `
|
|
120
153
|
<div class="cms-section-add-picker-head">Vertical Spacing</div>
|
|
@@ -133,10 +166,13 @@ export function createSpacingPicker({ onPick }) {
|
|
|
133
166
|
|
|
134
167
|
document.body.appendChild(picker);
|
|
135
168
|
|
|
169
|
+
/** @type {string | null} */
|
|
136
170
|
let openForSectionId = null;
|
|
171
|
+
/** @type {HTMLButtonElement | null} */
|
|
137
172
|
let anchorButton = null;
|
|
138
173
|
|
|
139
174
|
function close() {
|
|
175
|
+
anchorButton?.setAttribute('aria-expanded', 'false');
|
|
140
176
|
picker.hidden = true;
|
|
141
177
|
picker.classList.remove('open');
|
|
142
178
|
openForSectionId = null;
|
|
@@ -148,9 +184,13 @@ export function createSpacingPicker({ onPick }) {
|
|
|
148
184
|
getAnchor: () => anchorButton,
|
|
149
185
|
});
|
|
150
186
|
|
|
187
|
+
/** @param {HTMLButtonElement} button @param {string} sectionId @param {unknown} currentValue */
|
|
151
188
|
function open(button, sectionId, currentValue) {
|
|
152
189
|
anchorButton = button;
|
|
153
190
|
openForSectionId = sectionId;
|
|
191
|
+
button.setAttribute('aria-haspopup', 'dialog');
|
|
192
|
+
button.setAttribute('aria-controls', picker.id);
|
|
193
|
+
button.setAttribute('aria-expanded', 'true');
|
|
154
194
|
picker.hidden = false;
|
|
155
195
|
picker.classList.add('open');
|
|
156
196
|
|
|
@@ -163,8 +203,18 @@ export function createSpacingPicker({ onPick }) {
|
|
|
163
203
|
});
|
|
164
204
|
|
|
165
205
|
position();
|
|
206
|
+
const selectedButton = picker.querySelector('button.active, button');
|
|
207
|
+
if (selectedButton instanceof HTMLButtonElement) selectedButton.focus();
|
|
166
208
|
}
|
|
167
209
|
|
|
210
|
+
picker.addEventListener('keydown', (event) => {
|
|
211
|
+
if (event.key !== 'Escape') return;
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
const previousAnchor = anchorButton;
|
|
214
|
+
close();
|
|
215
|
+
previousAnchor?.focus();
|
|
216
|
+
});
|
|
217
|
+
|
|
168
218
|
picker.addEventListener('click', (event) => {
|
|
169
219
|
const target = event.target;
|
|
170
220
|
if (!(target instanceof Element)) return;
|
|
@@ -187,9 +237,10 @@ export function createSpacingPicker({ onPick }) {
|
|
|
187
237
|
return {
|
|
188
238
|
open,
|
|
189
239
|
close,
|
|
240
|
+
/** @param {boolean} disabled */
|
|
190
241
|
setDisabled(disabled) {
|
|
191
242
|
picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
|
|
192
|
-
button.disabled = disabled;
|
|
243
|
+
if (button instanceof HTMLButtonElement) button.disabled = disabled;
|
|
193
244
|
});
|
|
194
245
|
},
|
|
195
246
|
};
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { reorderByDrag } from './model.js';
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('./model.js').Section} Section */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{
|
|
7
|
+
* controls: HTMLElement,
|
|
8
|
+
* sectionId: string,
|
|
9
|
+
* sectionNode: HTMLElement,
|
|
10
|
+
* setDragSourceId: (sectionId: string | null) => void,
|
|
11
|
+
* clearDropIndicators: () => void,
|
|
12
|
+
* }} options
|
|
13
|
+
*/
|
|
3
14
|
export function bindDragSourceHandlers({
|
|
4
15
|
controls,
|
|
5
16
|
sectionId,
|
|
@@ -29,6 +40,18 @@ export function bindDragSourceHandlers({
|
|
|
29
40
|
});
|
|
30
41
|
}
|
|
31
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {{
|
|
45
|
+
* sectionNode: HTMLElement,
|
|
46
|
+
* sectionId: string,
|
|
47
|
+
* isBusy: () => boolean,
|
|
48
|
+
* getDragSourceId: () => string | null,
|
|
49
|
+
* getSections: () => Section[],
|
|
50
|
+
* setSections: (sections: Section[]) => void,
|
|
51
|
+
* clearDropIndicators: () => void,
|
|
52
|
+
* onReordered: () => void,
|
|
53
|
+
* }} options
|
|
54
|
+
*/
|
|
32
55
|
export function bindDropTargetHandlers({
|
|
33
56
|
sectionNode,
|
|
34
57
|
sectionId,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ sectionNodeById: Map<string, HTMLElement>, isSelectionLocked: () => boolean }} options
|
|
3
|
+
*/
|
|
1
4
|
export function createSectionSelectionController({
|
|
2
5
|
sectionNodeById,
|
|
3
6
|
isSelectionLocked,
|
|
4
7
|
}) {
|
|
8
|
+
/** @type {string | null} */
|
|
5
9
|
let activeSectionId = null;
|
|
10
|
+
/** @type {IntersectionObserver | null} */
|
|
6
11
|
let observer = null;
|
|
7
12
|
|
|
13
|
+
/** @param {string} sectionId */
|
|
8
14
|
function setActiveSection(sectionId) {
|
|
9
15
|
if (!sectionId || activeSectionId === sectionId) return;
|
|
10
16
|
activeSectionId = sectionId;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { SPACING_Y_VALUES } from './constants.js';
|
|
2
2
|
import { spacingIndex } from './utils.js';
|
|
3
3
|
|
|
4
|
+
/** @typedef {{ sectionId: string, startY: number, startIndex: number, currentIndex: number, changed: boolean, handle: HTMLButtonElement }} SpacingDragState */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{
|
|
8
|
+
* isBusy: () => boolean,
|
|
9
|
+
* isReordering: () => boolean,
|
|
10
|
+
* closePickers: () => void,
|
|
11
|
+
* getSectionSpacing: (sectionId: string) => string | undefined,
|
|
12
|
+
* updateSpacingByToken: (sectionId: string, token: string) => boolean,
|
|
13
|
+
* setActiveSection: (sectionId: string) => void,
|
|
14
|
+
* onCommitted: (sectionId: string) => void,
|
|
15
|
+
* }} options
|
|
16
|
+
*/
|
|
4
17
|
export function createSpacingDragController({
|
|
5
18
|
isBusy,
|
|
6
19
|
isReordering,
|
|
@@ -10,6 +23,7 @@ export function createSpacingDragController({
|
|
|
10
23
|
setActiveSection,
|
|
11
24
|
onCommitted,
|
|
12
25
|
}) {
|
|
26
|
+
/** @type {SpacingDragState | null} */
|
|
13
27
|
let dragState = null;
|
|
14
28
|
|
|
15
29
|
function stop() {
|
|
@@ -22,6 +36,7 @@ export function createSpacingDragController({
|
|
|
22
36
|
dragState = null;
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
/** @param {PointerEvent} event @param {string} sectionId @param {HTMLButtonElement} handle */
|
|
25
40
|
function start(event, sectionId, handle) {
|
|
26
41
|
if (isBusy()) return;
|
|
27
42
|
|
|
@@ -47,6 +62,7 @@ export function createSpacingDragController({
|
|
|
47
62
|
window.addEventListener('pointercancel', onEnd, { once: true });
|
|
48
63
|
}
|
|
49
64
|
|
|
65
|
+
/** @param {PointerEvent} event */
|
|
50
66
|
function onMove(event) {
|
|
51
67
|
if (!dragState || isBusy() || isReordering()) return;
|
|
52
68
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spacingToken, humanizeSectionKey } from './utils.js';
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('./model.js').Section} Section */
|
|
4
|
+
|
|
5
|
+
/** @returns {HTMLDivElement} */
|
|
3
6
|
export function createSectionControlsElement() {
|
|
4
7
|
const controls = document.createElement('div');
|
|
5
8
|
controls.className = 'cms-section-controls';
|
|
@@ -17,6 +20,7 @@ export function createSectionControlsElement() {
|
|
|
17
20
|
return controls;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** @param {Section} section @param {number} order @returns {HTMLDivElement} */
|
|
20
24
|
export function createSectionBadge(section, order) {
|
|
21
25
|
const badge = document.createElement('div');
|
|
22
26
|
badge.className = 'cms-section-chip';
|
|
@@ -24,11 +28,13 @@ export function createSectionBadge(section, order) {
|
|
|
24
28
|
return badge;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/** @param {Section} section @returns {HTMLButtonElement} */
|
|
27
32
|
export function createSpacingDragHandle(section) {
|
|
28
33
|
const handle = document.createElement('button');
|
|
29
34
|
handle.type = 'button';
|
|
30
35
|
handle.className = 'cms-section-gap-handle';
|
|
31
36
|
handle.title = 'Drag vertically to resize section spacing';
|
|
37
|
+
handle.setAttribute('aria-label', 'Resize vertical section spacing');
|
|
32
38
|
handle.dataset.sectionId = section.id;
|
|
33
39
|
handle.innerHTML = `
|
|
34
40
|
<span class="cms-section-gap-grip"></span>
|
|
@@ -41,6 +47,7 @@ export function createSpacingDragHandle(section) {
|
|
|
41
47
|
return handle;
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/** @returns {HTMLButtonElement} */
|
|
44
51
|
export function createInsertHandle() {
|
|
45
52
|
const handle = document.createElement('button');
|
|
46
53
|
handle.type = 'button';
|