PluginProbe
The Innovative Form Builder – IvyForms / 1.1.1
The Innovative Form Builder – IvyForms v1.1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / stores / useMultiPageStore.ts

useMultiPageStore.ts in The Innovative Form Builder – IvyForms 1.1.1, at frontend/src/stores/useMultiPageStore.ts

372 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { defineStore } from 'pinia'
2 import { ref } from 'vue'
3 import { nanoid } from 'nanoid'
4 import { useLabels } from '@/composables/useLabels'
5 import type { Field } from '@/types/field'
6 import type { FormPage } from '@/types/form-page'
7 import { duplicateFieldForStore } from '@/utils/fieldUtils'
8
9 export type { FormPage } from '@/types/form-page'
10
11 export type PageNavigationSettings = {
12 previousButton: {
13 label: string
14 cssClasses: string
15 priority: 'primary' | 'secondary' | 'tertiary' | 'white'
16 type: 'fill' | 'border' | 'border-light' | 'ghost'
17 showIcon: boolean
18 }
19 nextButton: {
20 label: string
21 cssClasses: string
22 priority: 'primary' | 'secondary' | 'tertiary' | 'white'
23 type: 'fill' | 'border' | 'border-light' | 'ghost'
24 showIcon: boolean
25 }
26 submitButton: {
27 label: string
28 cssClasses: string
29 priority: 'primary' | 'secondary' | 'tertiary' | 'white'
30 type: 'fill' | 'border' | 'border-light' | 'ghost'
31 showIcon: boolean
32 }
33 buttonPosition: 'left' | 'center' | 'right' | 'separated'
34 isGrouped: boolean
35 }
36
37 export const useMultiPageStore = defineStore('multiPageData', () => {
38 const { getLabel } = useLabels()
39
40 type ProgressIndicatorSettings = {
41 type: 'progress-bar' | 'rootline' | 'none'
42 showPageTitles: boolean
43 hidePageNumbers: boolean
44 hideConnectingLines: boolean
45 }
46
47 // Pages state
48 const pages = ref<FormPage[]>([
49 {
50 id: nanoid(),
51 label: 'Page 1',
52 closable: false,
53 },
54 ])
55 const currentPageId = ref<string>(pages.value[0].id)
56
57 // Progress indicator settings
58 const progressIndicator = ref<ProgressIndicatorSettings>({
59 type: 'progress-bar',
60 showPageTitles: true,
61 hidePageNumbers: false,
62 hideConnectingLines: false,
63 })
64
65 // Page navigation settings (per page)
66 const pageNavigationSettings = ref<Record<string, PageNavigationSettings>>({})
67
68 // Selection state for multipage UI elements
69 const isProgressIndicatorSelected = ref(false)
70 const isPageNavigationSelected = ref(false)
71 const isNavigationButtonSelected = ref(false)
72
73 // ----- Selection actions -----
74 const selectProgressIndicator = () => {
75 isProgressIndicatorSelected.value = true
76 isPageNavigationSelected.value = false
77 isNavigationButtonSelected.value = false
78 }
79
80 const deselectProgressIndicator = () => {
81 isProgressIndicatorSelected.value = false
82 }
83
84 const selectPageNavigation = () => {
85 isProgressIndicatorSelected.value = false
86 isPageNavigationSelected.value = true
87 isNavigationButtonSelected.value = false
88 }
89
90 const deselectPageNavigation = () => {
91 isPageNavigationSelected.value = false
92 }
93
94 const selectNavigationButton = () => {
95 isProgressIndicatorSelected.value = false
96 isPageNavigationSelected.value = true
97 isNavigationButtonSelected.value = true
98 }
99
100 const deselectNavigationButton = () => {
101 isNavigationButtonSelected.value = false
102 isPageNavigationSelected.value = false
103 }
104
105 const deselectAll = () => {
106 isProgressIndicatorSelected.value = false
107 isPageNavigationSelected.value = false
108 isNavigationButtonSelected.value = false
109 }
110
111 // ----- Progress indicator -----
112 const updateProgressIndicator = (settings: ProgressIndicatorSettings) => {
113 progressIndicator.value = { ...progressIndicator.value, ...settings }
114 }
115
116 const setPages = (value: FormPage[]) => {
117 pages.value = value
118 if (!pages.value.length) {
119 const firstPageId = nanoid()
120 pages.value = [{ id: firstPageId, label: 'Page 1', closable: false }]
121 }
122
123 if (!pages.value.some((page) => page.id === currentPageId.value)) {
124 currentPageId.value = pages.value[0].id
125 }
126 }
127
128 const setCurrentPageId = (pageId: string) => {
129 currentPageId.value = pageId
130 }
131
132 const setProgressIndicator = (value: ProgressIndicatorSettings) => {
133 progressIndicator.value = value
134 }
135
136 const setPageNavigationSettings = (value: Record<string, PageNavigationSettings>) => {
137 // Backend may return an empty PHP array serialized as JSON [] instead of {}.
138 // Arrays are unusable as keyed maps – normalize to empty object.
139 if (Array.isArray(value) || value === null || value === undefined) {
140 pageNavigationSettings.value = {}
141 return
142 }
143 pageNavigationSettings.value = value
144 }
145
146 const ensurePageNavigationSettings = () => {
147 pages.value.forEach((page) => {
148 if (!pageNavigationSettings.value[page.id]) {
149 getPageNavigationSettings(page.id)
150 }
151 })
152 }
153
154 const hydrate = (payload: {
155 pages?: FormPage[]
156 progressIndicator?: ProgressIndicatorSettings
157 pageNavigationSettings?: Record<string, PageNavigationSettings>
158 }) => {
159 if (payload.pages && payload.pages.length) {
160 setPages(payload.pages)
161 }
162
163 if (payload.progressIndicator) {
164 setProgressIndicator(payload.progressIndicator)
165 }
166
167 if (payload.pageNavigationSettings) {
168 setPageNavigationSettings(payload.pageNavigationSettings)
169 }
170
171 ensurePageNavigationSettings()
172 }
173
174 // ----- Page navigation settings -----
175 const getPageNavigationSettings = (pageId: string): PageNavigationSettings => {
176 if (!pageNavigationSettings.value[pageId]) {
177 pageNavigationSettings.value[pageId] = {
178 previousButton: {
179 label: getLabel('previous') || 'Previous',
180 cssClasses: '',
181 priority: 'tertiary',
182 type: 'border',
183 showIcon: true,
184 },
185 nextButton: {
186 label: getLabel('next') || 'Next',
187 cssClasses: '',
188 priority: 'tertiary',
189 type: 'border',
190 showIcon: true,
191 },
192 submitButton: {
193 label: getLabel('submit') || 'Submit',
194 cssClasses: '',
195 priority: 'tertiary',
196 type: 'border',
197 showIcon: false,
198 },
199 buttonPosition: 'separated',
200 isGrouped: false,
201 }
202 }
203 return pageNavigationSettings.value[pageId]
204 }
205
206 const updatePageNavigationSettings = (
207 pageId: string,
208 settings: Partial<PageNavigationSettings>,
209 ) => {
210 const current = getPageNavigationSettings(pageId)
211 pageNavigationSettings.value[pageId] = { ...current, ...settings }
212 }
213
214 // ----- Pages CRUD -----
215 const addPage = () => {
216 const newPageId = nanoid()
217 const index = pages.value.length + 1
218 pages.value.push({ id: newPageId, label: `Page ${index}`, closable: true })
219 getPageNavigationSettings(newPageId)
220 }
221
222 const addPageAfter = (pageIndex: number) => {
223 const newPageId = nanoid()
224 const insertIndex = pageIndex + 1
225 const pageNumbers = pages.value.map((p) => {
226 const match = p.label.match(/Page (\d+)/)
227 return match ? parseInt(match[1], 10) : 0
228 })
229 const nextNumber = pageNumbers.length > 0 ? Math.max(...pageNumbers) + 1 : 1
230 pages.value.splice(insertIndex, 0, {
231 id: newPageId,
232 label: `Page ${nextNumber}`,
233 closable: true,
234 })
235 getPageNavigationSettings(newPageId)
236 }
237
238 /**
239 * Delete a page. Caller is responsible for removing associated fields and
240 * clearing selectedField if it belonged to the deleted page.
241 */
242 const deletePage = (
243 pageId: string,
244 onDeleteFields: (pageId: string) => void,
245 clearSelectedFieldIfPage: (pageId: string) => void,
246 ) => {
247 if (pages.value.length <= 1) return
248 const idx = pages.value.findIndex((p) => p.id === pageId)
249 if (idx === -1) return
250
251 pages.value.splice(idx, 1)
252
253 if (currentPageId.value === pageId) {
254 const nextIndex = Math.min(idx, pages.value.length - 1)
255 currentPageId.value = pages.value[nextIndex].id
256 }
257
258 clearSelectedFieldIfPage(pageId)
259 onDeleteFields(pageId)
260 delete pageNavigationSettings.value[pageId]
261 }
262
263 /**
264 * Duplicate a page. Caller provides increaseCounter and fields array for
265 * duplicating fields onto the new page.
266 */
267 const duplicatePage = (
268 pageId: string,
269 fields: Field[],
270 increaseCounter: () => number,
271 pushField: (field: Field) => void,
272 ) => {
273 const pageIndex = pages.value.findIndex((p) => p.id === pageId)
274 if (pageIndex === -1) return
275
276 const originalPage = pages.value[pageIndex]
277 const newPageId = nanoid()
278
279 pages.value.splice(pageIndex + 1, 0, {
280 id: newPageId,
281 label: `${originalPage.label} (Copy)`,
282 closable: true,
283 })
284
285 const pageFields = fields.filter((f) => f.pageId === pageId)
286 pageFields.forEach((field) => {
287 const newIndex = increaseCounter()
288 const duplicatedField = duplicateFieldForStore(field, newIndex, getLabel)
289 duplicatedField.pageId = newPageId
290 pushField(duplicatedField)
291 })
292
293 const originalSettings = getPageNavigationSettings(pageId)
294 pageNavigationSettings.value[newPageId] = JSON.parse(JSON.stringify(originalSettings))
295
296 currentPageId.value = newPageId
297 }
298
299 const renamePage = (pageId: string, newName: string) => {
300 const page = pages.value.find((p) => p.id === pageId)
301 if (page) page.label = newName
302 }
303
304 const updatePage = (pageId: string, updates: { label?: string; closable?: boolean }) => {
305 const page = pages.value.find((p) => p.id === pageId)
306 if (page) {
307 if (updates.label !== undefined) page.label = updates.label
308 if (updates.closable !== undefined) page.closable = updates.closable
309 }
310 }
311
312 const reorderPages = (orderedIds: string[]) => {
313 const newOrder = orderedIds
314 .map((id) => pages.value.find((p) => p.id === id))
315 .filter(Boolean) as typeof pages.value
316 if (newOrder.length === pages.value.length) {
317 pages.value = newOrder
318 }
319 }
320
321 const reset = () => {
322 const firstPageId = nanoid()
323 pages.value = [{ id: firstPageId, label: 'Page 1', closable: false }]
324 currentPageId.value = firstPageId
325 progressIndicator.value = {
326 type: 'progress-bar',
327 showPageTitles: true,
328 hidePageNumbers: false,
329 hideConnectingLines: false,
330 }
331 pageNavigationSettings.value = {}
332 isProgressIndicatorSelected.value = false
333 isPageNavigationSelected.value = false
334 isNavigationButtonSelected.value = false
335 }
336
337 return {
338 pages,
339 currentPageId,
340 progressIndicator,
341 pageNavigationSettings,
342 setPages,
343 setCurrentPageId,
344 setProgressIndicator,
345 setPageNavigationSettings,
346 ensurePageNavigationSettings,
347 hydrate,
348 isProgressIndicatorSelected,
349 isPageNavigationSelected,
350 isNavigationButtonSelected,
351 selectProgressIndicator,
352 deselectProgressIndicator,
353 selectPageNavigation,
354 deselectPageNavigation,
355 selectNavigationButton,
356 deselectNavigationButton,
357 deselectAll,
358 updateProgressIndicator,
359 getPageNavigationSettings,
360 updatePageNavigationSettings,
361 addPage,
362 addPageAfter,
363 deletePage,
364 duplicatePage,
365 copyPage: duplicatePage,
366 renamePage,
367 updatePage,
368 reorderPages,
369 reset,
370 }
371 })
372