PluginProbe
The Innovative Form Builder – IvyForms / 0.8.2
The Innovative Form Builder – IvyForms v0.8.2
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 / useUnsavedChangesStore.ts

useUnsavedChangesStore.ts in The Innovative Form Builder – IvyForms 0.8.2, at frontend/src/stores/useUnsavedChangesStore.ts

211 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ref, computed } from 'vue'
2 import type { ComputedRef } from 'vue'
3 import { defineStore } from 'pinia'
4 import { useActionEntityStore } from '@/stores/actionEntityStore'
5
6 /**
7 * Entity types that can track unsaved changes
8 */
9 export type UnsavedEntityKey = 'formBuilder' | 'confirmationBuilder' | 'notificationBuilder'
10
11 /**
12 * Design principles:
13 * 1. Uses a dirty flag approach
14 * 2. Each entity store is responsible for calling markDirty() when changes occur
15 * 3. Changes are marked clean after successful save operations
16 *
17 * How to use in entity stores:
18 * 1. Call startEditing() when loading data or starting to edit
19 * 2. Call markDirty() whenever user makes any change
20 * 3. Call markClean() after successful save
21 * 4. Call stopEditing() when leaving the editor
22 */
23 export const useUnsavedChangesStore = defineStore('unsavedChanges', () => {
24 // Track dirty state for each entity type
25 const dirtyFlags = ref<Record<UnsavedEntityKey, boolean>>({
26 formBuilder: false,
27 confirmationBuilder: false,
28 notificationBuilder: false,
29 })
30
31 // Track which entities are currently being edited (loaded from server)
32 const editingFlags = ref<Record<UnsavedEntityKey, boolean>>({
33 formBuilder: false,
34 confirmationBuilder: false,
35 notificationBuilder: false,
36 })
37
38 // Action entity store for showing the dialog
39 const actionEntityStore = useActionEntityStore()
40
41 /**
42 * Mark an entity as dirty (has unsaved changes)
43 * Call this whenever user makes a change to form fields, settings, etc.
44 */
45 const markDirty = (entityKey: UnsavedEntityKey) => {
46 // Only mark dirty if the entity is being edited
47 if (editingFlags.value[entityKey]) {
48 dirtyFlags.value[entityKey] = true
49 }
50 }
51
52 /**
53 * Mark an entity as clean (no unsaved changes)
54 * Call this after successful save operations
55 */
56 const markClean = (entityKey: UnsavedEntityKey) => {
57 dirtyFlags.value[entityKey] = false
58 }
59
60 /**
61 * Mark an entity as being edited (loaded from server or new form started)
62 * This enables dirty tracking for the entity
63 */
64 const startEditing = (entityKey: UnsavedEntityKey) => {
65 editingFlags.value[entityKey] = true
66 dirtyFlags.value[entityKey] = false
67 }
68
69 /**
70 * Mark an entity as no longer being edited
71 * This disables dirty tracking and clears the dirty flag
72 */
73 const stopEditing = (entityKey: UnsavedEntityKey) => {
74 editingFlags.value[entityKey] = false
75 dirtyFlags.value[entityKey] = false
76 }
77
78 /**
79 * Check if a specific entity has unsaved changes
80 */
81 const isDirty = (entityKey: UnsavedEntityKey): boolean => {
82 return dirtyFlags.value[entityKey]
83 }
84
85 /**
86 * Get a computed ref for dirty state (useful for reactive bindings)
87 */
88 const isDirtyRef = (entityKey: UnsavedEntityKey): ComputedRef<boolean> => {
89 return computed(() => dirtyFlags.value[entityKey])
90 }
91
92 /**
93 * Check if a specific entity is being edited
94 */
95 const isEntityEditing = (entityKey: UnsavedEntityKey): boolean => {
96 return editingFlags.value[entityKey]
97 }
98
99 /**
100 * Check if any of the specified entities have unsaved changes
101 * @param entities - Array of entity keys to check (defaults to all)
102 */
103 const hasAnyUnsavedChanges = (
104 entities: UnsavedEntityKey[] = ['formBuilder', 'confirmationBuilder', 'notificationBuilder'],
105 ): boolean => {
106 return entities.some((entity) => dirtyFlags.value[entity])
107 }
108
109 /**
110 * Get a computed ref for any unsaved changes (useful for reactive bindings)
111 */
112 const hasAnyUnsavedChangesRef = (
113 entities: UnsavedEntityKey[] = ['formBuilder', 'confirmationBuilder', 'notificationBuilder'],
114 ): ComputedRef<boolean> => {
115 return computed(() => entities.some((entity) => dirtyFlags.value[entity]))
116 }
117
118 /**
119 * Clear all dirty flags
120 * Use when navigating away after user confirms, or after successful save of all entities
121 */
122 const clearAllDirtyFlags = () => {
123 Object.keys(dirtyFlags.value).forEach((key) => {
124 dirtyFlags.value[key as UnsavedEntityKey] = false
125 })
126 }
127
128 /**
129 * Reset all state (dirty flags and editing flags)
130 * Use when completely leaving the form builder context
131 */
132 const resetAllState = () => {
133 Object.keys(dirtyFlags.value).forEach((key) => {
134 dirtyFlags.value[key as UnsavedEntityKey] = false
135 editingFlags.value[key as UnsavedEntityKey] = false
136 })
137 }
138
139 /**
140 * Show confirmation dialog if there are unsaved changes, then proceed
141 * @param onProceed - Callback to execute if user confirms or no changes exist
142 * @param entities - Which entities to check (defaults to all)
143 */
144 const confirmIfDirty = (
145 onProceed: () => void,
146 entities: UnsavedEntityKey[] = ['formBuilder', 'confirmationBuilder', 'notificationBuilder'],
147 ) => {
148 if (!hasAnyUnsavedChanges(entities)) {
149 onProceed()
150 return
151 }
152
153 actionEntityStore.showUnsavedChangesDialog(() => {
154 // Clear dirty flags before proceeding since user confirmed they want to leave
155 entities.forEach((entity) => markClean(entity))
156 onProceed()
157 })
158 }
159
160 /**
161 * Handle admin menu click with unsaved changes check
162 * Used to intercept WordPress admin menu clicks
163 * @param event - Click event
164 * @param entities - Which entities to check
165 */
166 const handleAdminMenuClick = (
167 event: Event,
168 entities: UnsavedEntityKey[] = ['formBuilder', 'confirmationBuilder', 'notificationBuilder'],
169 ) => {
170 const mouseEvent = event as MouseEvent
171 const target = mouseEvent.target as HTMLElement
172 const link = target.closest('a') as HTMLAnchorElement | null
173
174 if (!link) return
175
176 // Prevent default navigation first, before checking for unsaved changes
177 mouseEvent.preventDefault()
178 mouseEvent.stopPropagation()
179
180 // If no unsaved changes, proceed with navigation immediately
181 if (!hasAnyUnsavedChanges(entities)) {
182 window.location.href = link.href
183 return
184 }
185
186 confirmIfDirty(() => {
187 window.location.href = link.href
188 }, entities)
189 }
190 return {
191 // State tracking
192 markDirty,
193 markClean,
194 startEditing,
195 stopEditing,
196 isDirty,
197 isDirtyRef,
198 isEntityEditing,
199
200 // Aggregated state checks
201 hasAnyUnsavedChanges,
202 hasAnyUnsavedChangesRef,
203 clearAllDirtyFlags,
204 resetAllState,
205
206 // Navigation helpers
207 confirmIfDirty,
208 handleAdminMenuClick,
209 }
210 })
211