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.