PageSourceSearch

https://mcgaw.io/wp-content/themes/genesis-mcgaw/js/formpersistence.js?ver=20260924.1774902981

js mcgaw.io collected 2026-09-24 11:16:51 UTC 17,355 bytes, 397 lines download raw bytes

1/**
2 * Copyright (c) 2020 Finn Thompson, licensed under the MIT License.
3 * 
4 * This module implements form persistence across sessions via local storage.
5 * * Register a form for persistence with `FormPersistence#persist(form[, options])`.
6 * * Save a form to local storage with `FormPersistence#save(form[, options])`.
7 * * Load a saved form (e.g. at window load time) with `FormPersistence#load(form[, options])`.
8 * * Clear saved form data with `FormPersistence#clearStorage(form[, options])`.
9 * * Serialize form data to an object with `FormPersistence#serialize(form[, options])`.
10 * * Deserialize a data object into a form with `FormPersistence#deserialize(form, data[, options])`.
11 * 
12 * See https://github.com/FThompson/FormPersistence.js
13 */
14 const FormPersistence = (function () {
15    /**
16     * Registers the given form for persistence by saving its data to local or session storage.
17     * Saved form data will be stored upon page refresh and cleared upon form submission.
18     * Saved form data will be loaded upon calling this function, typically on page load.
19     * 
20     * @param {HTMLFormElement} form    The form to make persistent.
21     * @param {Object}          options Options object containing any of the following:
22     *  * uuid - A unique identifier for this form's storage key.
23     *           Required if using a form without an id. If unspecified, form id will be used.
24     *  * useSessionStorage - Use session storage if `true`, local storage if `false`. Default `false`.
25     *  * saveOnSubmit - Save form data upon submit if `true`. Default `false`.
26     *  * valueFunctions - Special value functions to apply, like `name: fn(form, value)`.
27     *  * include - Define a whitelist array of data names to include.
28     *  * exclude - Define a blacklist array of data names to exclude.
29     *  * includeFilter - Define a whitelist filter function that inputs an element and outputs a boolean. The element
30     *                    is included if the function returns true.
31     *  * excludeFilter - Define a blacklist filter function that inputs an element and outputs a boolean. The element
32     *                    is excluded if the function returns true.
33     */
34    function persist(form, options) {
35        //console.log('persist debug');
36        //console.log(form);
37        let defaults = {
38            saveOnSubmit: false
39        }
40        let config = Object.assign({}, defaults, options)
41        load(form, config)
42        // Some devices like ios safari do not support beforeunload events.
43        // Unload event does not work in some situations, so we use both unload/beforeunload
44        // and remove the unload event if the beforeunload event fires successfully.
45        // If problems persist, we can add listeners on the pagehide event as well.
46        let saveForm = () => save(form, config)
47        let saveFormBeforeUnload = () => {
48            window.removeEventListener('unload', saveForm)
49            saveForm()
50        }
51        window.addEventListener('beforeunload', saveFormBeforeUnload)
52        window.addEventListener('unload', saveForm)
53        if (!config.saveOnSubmit) {
54            form.addEventListener('submit', () => {
55                window.removeEventListener('beforeunload', saveFormBeforeUnload)
56                window.removeEventListener('unload', saveForm)
57                clearStorage(form, config)
58            })
59        }
60    }
61
62    /**
63     * Serializes the given form into an object, excluding password and file inputs.
64     * 
65     * @param {HTMLFormElement} form    The form to serialize.
66     * @param {Object}          options Options object containing any of the following:
67     *  * include - Define a whitelist array of data names to include.
68     *  * exclude - Define a blacklist array of data names to exclude.
69     *  * includeFilter - Define a whitelist filter function that inputs an element and outputs a boolean. The element
70     *                    is included if the function returns true.
71     *  * excludeFilter - Define a blacklist filter function that inputs an element and outputs a boolean. The element
72     *                    is excluded if the function returns true.
73     * 
74     * @return {Object}
74 The serialized data object.
75     */
76    function serialize(form, options) {
77        //console.log('persist debug -- serialize');
78        let defaults = {
79            include: [],
80            exclude: [],
81            includeFilter: null,
82            excludeFilter: null
83        }
84        let config = Object.assign({}, defaults, options)
85        let data = {}
86        let formElements = form.elements;
87
88        if (formElements.length == 0) {
89            //try to get form elements 
90            let formElement = document.querySelector('#'+form.getAttribute('id'));
91            formElements = formElement.querySelectorAll('input,select,textarea');
92        }
93        //console.log(formElements);
94        for (let element of formElements) {
95            let tag = element.tagName
96            let type = element.type
97            if (tag === 'INPUT' && (type === 'password' || type === 'file')) {
98                continue // do not serialize passwords or files
99            }
100            if (isNameFiltered(element.name, config.include, config.exclude)
101                    || isElementFiltered(element, config.includeFilter, config.excludeFilter)) {
102                continue
103            }
104            if (tag === 'INPUT') {
105                let type = element.type
106                if (type === 'radio') {
107                    if (element.checked) {
108                        pushToArray(data, element.name, element.value)
109                    }
110                } else if (type === 'checkbox') {
111                    pushToArray(data, element.name, element.checked)
112                } else {
113                    pushToArray(data, element.name, element.value)
114                }
115            } else if (tag === 'TEXTAREA') {
116                pushToArray(data, element.name, element.value)
117            } else if (tag === 'SELECT') {
118                if (element.multiple) {
119                    for (let option of element.options) {
120                        if (option.selected) {
121                            pushToArray(data, element.name, option.value)
122                        }
123                    }
124                } else {
125                    pushToArray(data, element.name, element.value)
126                }
127            }
128        }
129        return data
130    }
131
132    /**
133     * Add a value to an object, creating an array to place it in if needed.
134     */
135    function pushToArray(dict, key, value) {
136        if (!(key in dict)) {
137            dict[key] = []
138        }
139        dict[key].push(value)
140    }
141
142    /**
143     * Checks if the given name should be filtered out.
144     */
145    function isNameFiltered(name, include, exclude) {
146        if (!name) {
147            return true
148        }
149        if (exclude.includes(name)) {
150            return true
151        }
152        if (include.length > 0 && !include.includes(name)) {
153            return true
154        }
155        return false
156    }
157
158    /**
159     * Checks if the given element should be filtered out, either by name or by predicate.
160     */
161    function isElementFiltered(element, includeFilter, excludeFilter) {
162        if (excludeFilter && excludeFilter(element)) {
163            return true
164        }
165        if (includeFilter && !includeFilter(element)) {
166            return true
167        }
168        return false
169    }
170
171    /**
172     * Saves the given form to local or session storage.
173     * 
174     * @param {HTMLFormElement} form    The form to serialize to local storage.
175     * @param {Object}          options Options object containing any of the following:
176     *  * uuid - A unique identifier for this form's storage key.
177     *           Required if using a form without an id. If unspecified, form id will be used.
178     *  * useSessionStorage - Use session storage if `true`, local storage if `false`. Default `false`.
179     *  * include - Define a whitelist array of data names to include.
180     *  * exclude - Define a blacklist array of data names to exclude.
181     *  * includeFilter - Define a whitelist filter function that inputs an element and outputs a boolean. The element
182     *                    is included if the function returns true.
183     *  * excludeFilter - Define a blacklist filter function that inputs an element and outputs a boolean. The element
184     *                    is excluded if the function returns true.
185     */
186    function save(form, options) {
187        //console.log('persist debug -- save');
188        //console.log(form,options);
189        let defaults = {
190            uuid: null,
191            useSessionStorage: false
192        }
193        let config = Object.assign({}, defaults, options)
194        let data = serialize(form, config)
195        //console.log('data');
196        //console.log(data);
197        let storage = config.useSessionStorage ? sessionStorage : localStorage
198        storage.setItem(getStorageKey(form, config.uuid), JSON.stringify(data))
199    }
200
201    /**
202     * Loads a given form by deserializing given data, optionally with given special value handling functions.
203     * 
204     * @param {HTMLFormElement}
204 form    The form to deserialize data into.
205     * @param {Object}          data    The data object to deserialize into the form.
206     * @param {Object}          options Options object containing any of the following:
207     *  * valueFunctions - Special value functions to apply, like `name: fn(form, value)`.
208     *  * include - Define a whitelist array of data names to include.
209     *  * exclude - Define a blacklist array of data names to exclude.
210     *  * includeFilter - Define a whitelist filter function that inputs an element and outputs a boolean. The element
211     *                    is included if the function returns true.
212     *  * excludeFilter - Define a blacklist filter function that inputs an element and outputs a boolean. The element
213     *                    is excluded if the function returns true.
214     */
215    function deserialize(form, data, options) {
216        //console.log('persist debug -- deserialize');
217        let defaults = {
218            valueFunctions: null,
219            include: [],
220            exclude: [],
221            includeFilter: null,
222            excludeFilter: null
223        }
224        let config = Object.assign({}, defaults, options)
225        // apply given value functions first
226        let speciallyHandled = []
227        if (config.valueFunctions !== null) {
228            speciallyHandled = applySpecialHandlers(data, form, config)
229        }
230        // fill remaining values normally
231        for (let name in data) {
232            if (isNameFiltered(name, config.include, config.exclude)) {
233                continue
234            }
235            if (!speciallyHandled.includes(name)) {
236                let inputs = [...form.elements].filter(element => element.name === name
237                        && !isElementFiltered(element, config.includeFilter, config.excludeFilter))
238                inputs.forEach((input, i) => {
239                    applyValues(input, data[name], i)
240                })
241            }
242        }
243    }
244
245    /**
246     * Loads a given form from local or session storage, optionally with given special value handling functions.
247     * Does nothing if no saved values are found.
248     * 
249     * @param {HTMLFormElement} form    The form to load saved values into.
250     * @param {Object}          options Options object containing any of the following:
251     *  * uuid - A unique identifier for this form's storage key.
252     *           Required if using a form without an id. If unspecified, form id will be used.
253     *  * useSessionStorage - Use session storage if `true`, local storage if `false`. Default `false`.
254     *  * valueFunctions - Special value functions to apply, like `name: fn(form, value)`.
255     *  * include - Define a whitelist array of data names to include.
256     *  * exclude - Define a blacklist array of data names to exclude.
257     *  * includeFilter - Define a whitelist filter function that inputs an element and outputs a boolean. The element
258     *                    is included if the function returns true.
259     *  * excludeFilter - Define a blacklist filter function that inputs an element and outputs a boolean. The element
260     *                    is excluded if the function returns true.
261     */
262    function load(form, options) {
263        //console.log('persist debug -- load');
264        let defaults = {
265            uuid: null,
266            useSessionStorage: false
267        }
268        let config = Object.assign({}, defaults, options)
269        let storage = config.useSessionStorage ? sessionStorage : localStorage
270        let json = storage.getItem(getStorageKey(form, config.uuid))
271        if (json) {
272            let data = JSON.parse(json)
273            deserialize(form, data, options)
274        }
275    }
276
277    /**
278     * Clears a given form's data from local or session storage.
279     * 
280     * @param {HTMLFormElement} form              The form to clear stored data for.
281     * @param {Object}          options Options object containing any of the following:
282     *  * uuid - A unique identifier for this form's storage key.
283     *           Required if using a form without an id. If unspecified, form id will be used.
284     *  * useSessionStorage - Use session storage if `true`, local storage if `false`. Default `false`.
285     */
286    function clearStorage(form, options) {
287        let defaults = {
288            uuid: null,
289            useSessionStorage: false
290        }
291        let config = Object.assign({}, defaults, options)
292        let storage = config.useSessionStorage ? sessionStorage : localStorage
293        storage.removeItem(getStorageKey(form, config.uuid))
294    }
295
296    /**
297     * Applies the given values to the given element.
298     * Adds any checkbox elements checked to the given array.
299     * 
300     * @param {HTMLElement} element The element to apply values to.
301     * @param {Array} values        The array of values. Some element types use the first element instead of the index.
302     * @param {Number} index        The index of the value array to apply if applicable.
303     * @param {Array} checkedBoxes  The array of checkboxes to add any clicked checkboxes to.
304     */
305    function applyValues(element, values, index) {
306        //console.log('persist debug -- applyValues');
307        let tag = element.tagName;
308        if (element.value !== "" && element.value !== null && element.value !== undefined ) {
309            //we already have a value set, skip
310            return false;
311        }
312        if (tag === 'INPUT') {
313            let type = element.type
314            if (type === 'radio') {
315                element.checked = (element.value === values[0])
316            } else if (type === 'checkbox') {
317                element.checked = values[index]
318            } else {
319                element.value = values[index]
320            }
321        } else if (tag === 'TEXTAREA') {
322            element.value = values[index]
323        } else if (tag === 'SELECT') {
324            if (element.multiple) {
325                for (let option of element.options) {
326                    option.selected = values.includes(option.value)
327                }
328            } else {
329                element.value = values[index]
330            }
331        }
332    }
333
334    /**
335     * Runs given value handling functions in place of basic value insertion.
336     * 
337     * Note that inclusion and exclusion filter functions do not apply here.
338     * 
339     * @param {Object}          data           The form data being loaded.
340     * @param {HTMLFormElement} form           The HTML form being loaded.
341     * @param {Object}          valueFunctions The special value functions, like `name: fn(form, value)`.
342     * 
343     * @return {Array} An array containing the data entry names that were handled.
344     */
345    function applySpecialHandlers(data, form, options) {
346        let speciallyHandled = []
347        for (let fnName in options.valueFunctions) {
348            if (fnName in data) {
349                if (isNameFiltered(fnName, options.include, options.exclude)) {
350                    continue
351                }
352                for (let value of data[fnName]) {
353                    options.valueFunctions[fnName](form, value)
354                }
355                speciallyHandled.push(fnName)
356            }
357        }
358        return speciallyHandled
359    }
360
361    /**
362     * Creates a local storage key for the given form.
363     * 
364     * @param {HTMLFormElement} form The form to create a storage key for.
365     * 
366     * @return {String} The unique form storage key.
367     * @throws {Error} If given a form without an id or uuid.
368     */
369    function getStorageKey(form, uuid) {
370        if (!uuid && !form.id) {
371            throw Error('form persistence requires a form id or uuid')
372        }
373        return 'form#' + (uuid ? uuid : form.id)
374    }
375
376    /**
377     * Return the public interface of FormPersistence.
378     */
379    return {
380        persist: persist,
381        load: load,
382        save: save,
383        clearStorage: clearStorage,
384        serialize: serialize,
385        deserialize: deserialize
386    }
387})();
388
389/**
390 * Export the module if applicable.
391 */
392(function () {
393    // istanbul ignore else
394    if (typeof module !== 'undefined' && module.exports) {
395        module.exports = exports = FormPersistence
396    }
397})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.