@crombolo/backoffice-host 0.11.0 → 0.13.1

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The form designer's layout operations: pages, groups and fields moved, added
3
+ * and removed. Pure — each takes a form and answers a new one, leaving the
4
+ * argument untouched — so the designer's element only has to render, and each
5
+ * operation is tested on its own (tests/forms-layout.test.ts).
6
+ */
7
+
8
+ const copy = (form) => structuredClone(form)
9
+
10
+ /** `list` with the item at `from` moved to `to`, clamped to the list. */
11
+ export function moved(list, from, to) {
12
+ const out = [...list]
13
+ if (from < 0 || from >= out.length) return out
14
+ const [item] = out.splice(from, 1)
15
+ out.splice(Math.max(0, Math.min(to, out.length)), 0, item)
16
+ return out
17
+ }
18
+
19
+ export const emptyGroup = () => ({ columns: 1, fields: [] })
20
+ export const emptyPage = () => ({ groups: [emptyGroup()] })
21
+
22
+ /** Every page with at least one group, which is what the designer edits. */
23
+ export function withLayout(form) {
24
+ const next = copy(form)
25
+ next.pages = next.pages?.length ? next.pages : [emptyPage()]
26
+ for (const page of next.pages) page.groups = page.groups?.length ? page.groups : [emptyGroup()]
27
+ return next
28
+ }
29
+
30
+ export function addPage(form) {
31
+ const next = copy(form)
32
+ next.pages.push(emptyPage())
33
+ return next
34
+ }
35
+
36
+ export function movePage(form, index, to) {
37
+ const next = copy(form)
38
+ next.pages = moved(next.pages, index, to)
39
+ return next
40
+ }
41
+
42
+ /** A form keeps one page at least: the last one cannot be removed. */
43
+ export function removePage(form, index) {
44
+ if (form.pages.length <= 1) return form
45
+ const next = copy(form)
46
+ next.pages.splice(index, 1)
47
+ return next
48
+ }
49
+
50
+ export function setPageCaption(form, index, caption) {
51
+ const next = copy(form)
52
+ if (caption.trim()) next.pages[index].caption = caption
53
+ else delete next.pages[index].caption
54
+ return next
55
+ }
56
+
57
+ export function addGroup(form, page) {
58
+ const next = copy(form)
59
+ next.pages[page].groups.push(emptyGroup())
60
+ return next
61
+ }
62
+
63
+ export function moveGroup(form, page, index, to) {
64
+ const next = copy(form)
65
+ next.pages[page].groups = moved(next.pages[page].groups, index, to)
66
+ return next
67
+ }
68
+
69
+ /** A page keeps one group at least. */
70
+ export function removeGroup(form, page, index) {
71
+ if (form.pages[page].groups.length <= 1) return form
72
+ const next = copy(form)
73
+ next.pages[page].groups.splice(index, 1)
74
+ return next
75
+ }
76
+
77
+ export function setGroupCaption(form, page, index, caption) {
78
+ const next = copy(form)
79
+ if (caption.trim()) next.pages[page].groups[index].caption = caption
80
+ else delete next.pages[page].groups[index].caption
81
+ return next
82
+ }
83
+
84
+ /** Between 1 and 4, as the file allows. */
85
+ export function setColumns(form, page, index, columns) {
86
+ const next = copy(form)
87
+ next.pages[page].groups[index].columns = Math.max(1, Math.min(4, Math.round(columns) || 1))
88
+ return next
89
+ }
90
+
91
+ /**
92
+ * Moves a field to a place, `{ page, group, index }`, in its own group or
93
+ * another. Within one group the index is where it ends up.
94
+ */
95
+ export function moveField(form, from, to) {
96
+ const next = copy(form)
97
+ const source = next.pages[from.page]?.groups[from.group]?.fields
98
+ const target = next.pages[to.page]?.groups[to.group]?.fields
99
+ if (!source || !target || from.index < 0 || from.index >= source.length) return form
100
+ const [field] = source.splice(from.index, 1)
101
+ target.splice(Math.max(0, Math.min(to.index, target.length)), 0, field)
102
+ return next
103
+ }
104
+
105
+ /** One step up or down within a group, or into the neighbouring group at its edge. */
106
+ export function nudgeField(form, at, step) {
107
+ const groups = form.pages.flatMap((page, p) => page.groups.map((_, g) => ({ page: p, group: g })))
108
+ const here = groups.findIndex((place) => place.page === at.page && place.group === at.group)
109
+ const fields = form.pages[at.page].groups[at.group].fields
110
+ const index = at.index + step
111
+ if (index >= 0 && index < fields.length) return moveField(form, at, { ...at, index })
112
+ const neighbour = groups[here + step]
113
+ if (!neighbour) return form
114
+ const length = form.pages[neighbour.page].groups[neighbour.group].fields.length
115
+ return moveField(form, at, { ...neighbour, index: step < 0 ? length : 0 })
116
+ }
117
+
118
+ /** Whether removing it would take fields with it, so the designer asks first. */
119
+ export const pageHasFields = (page) => page.groups.some((group) => group.fields.length > 0)
120
+ export const groupHasFields = (group) => group.fields.length > 0
121
+
122
+ /** The group's fields laid out in rows of `columns`, as the rendered form arranges them. */
123
+ export function layoutRows(group) {
124
+ const rows = []
125
+ for (let at = 0; at < group.fields.length; at += group.columns)
126
+ rows.push(group.fields.slice(at, at + group.columns))
127
+ return rows
128
+ }