/** * Plugin developed to save html forms data to LocalStorage to restore them after browser crashes, tabs closings * and other disasters. * * @author Alexander Kaupanin */ //$.sisyphus().setOptions({timeout: 15}) ( function( $ ) { $.sisyphus = function() { return Sisyphus.getInstance(); }; $.fn.sisyphus = function( options ) { var sisyphus = Sisyphus.getInstance(); sisyphus.setOptions( options ); sisyphus.protect( this ); return sisyphus; }; var browserStorage = {}; /** * Check if local storage or other browser storage is available * * @return Boolean */ browserStorage.isAvailable = function() { if ( typeof $.jStorage === "object" ) { return true; } try { return localStorage.getItem; } catch ( e ) { return false; } }; /** * Set data to browser storage * * @param [String] key * @param [String] value * * @return Boolean */ browserStorage.set = function( key, value ) { if ( typeof $.jStorage === "object" ) { $.jStorage.set( key, value + "" ); } else { try { localStorage.setItem( key, value + "" ); } catch (e) { //QUOTA_EXCEEDED_ERR } } }; /** * Get data from browser storage by specified key * * @param [String] key * * @return string */ browserStorage.get = function( key ) { if ( typeof $.jStorage === "object" ) { var result = $.jStorage.get( key ); return result ? result.toString() : result; } else { return localStorage.getItem( key ); } }; /** * Delete data from browser storage by specified key * * @param [String] key * * @return void */ browserStorage.remove = function( key ) { if ( typeof $.jStorage === "object" ) { $.jStorage.deleteKey( key ); } else { localStorage.removeItem( key ); } }; Sisyphus = ( function() { var params = { instantiated: null, started: null }; function init () { return { /** * Set plugin initial options * * @param [Object] options * * @return void */ setInitialOptions: function ( options ) { var defaults = { excludeFields: [], customKeyPrefix: "", timeout: 0, autoRelease: true, name: null, onSave: function() {}, onBeforeRestore: function() {}, onRestore: function() {}, onRelease: function() {} }; this.options = this.options || $.extend( defaults, options ); this.browserStorage = browserStorage; }, /** * Set plugin options * * @param [Object] options * * @return void */ setOptions: function ( options ) { this.options = this.options || this.setInitialOptions( options ); this.options = $.extend( this.options, options ); }, /** * Protect specified forms, store it's fields data to local storage and restore them on page load * * @param [Object] targets forms object(s), result of jQuery selector * @param Object options plugin options * * @return void */ protect: function( targets ) { targets = targets || {}; var self = this; this.targets = this.targets || []; this.href = this.options.name || location.hostname + location.pathname + location.search + location.hash; this.targets = $.merge( this.targets, targets ); this.targets = $.unique( this.targets ); this.targets = $( this.targets ); if ( ! this.browserStorage.isAvailable() ) { return false; } self.restoreAllData(); if ( this.options.autoRelease ) { self.bindReleaseData(); } if ( ! params.started ) { self.bindSaveData(); params.started = true; } }, /** * Bind saving data * * @return void */ bindSaveData: function() { var self = this; if ( self.options.timeout ) { self.saveDataByTimeout(); } self.targets.each( function() { var targetFormId = $( this ).attr( "id" ); var fieldsToProtect = $( this ).find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ); fieldsToProtect.each( function() { if ( $.inArray( this, self.options.excludeFields ) !== -1 ) { // Returning non-false is the same as a continue statement in a for loop; it will skip immediately to the next iteration. return true; } var field = $( this ); var prefix = self.href + targetFormId + field.attr( "name" ) + self.options.customKeyPrefix; if ( field.is( ":text" ) || field.is( "textarea" ) ) { if ( ! self.options.timeout ) { self.bindSaveDataImmediately( field, prefix ); } } else { self.bindSaveDataOnChange( field, prefix ); } } ); } ); }, /** * Save all protected forms data to Local Storage. * Common method, necessary to not lead astray user firing 'data are saved' when select/checkbox/radio * is changed and saved, while textfield data are saved only by timeout * * @return void */ saveAllData: function() { var self = this; self.targets.each( function() { var targetFormId = $( this ).attr( "id" ); var fieldsToProtect = $( this ).find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ); fieldsToProtect.each( function() { var field = $( this ); if ( $.inArray( this, self.options.excludeFields ) !== -1 || field.attr( "name" ) === undefined ) { // Returning non-false is the same as a continue statement in a for loop; it will skip immediately to the next iteration. return true; } var prefix = self.href + targetFormId + field.attr( "name" ) + self.options.customKeyPrefix; var value = field.val(); if ( field.is(":checkbox") ) { if ( field.attr( "name" ).indexOf( "[" ) !== -1 ) { value = []; $( "[name='" + field.attr( "name" ) +"']:checked" ).each( function() { value.push( $( this ).val() ); } ); } else { value = field.is( ":checked" ); } self.saveToBrowserStorage( prefix, value, false ); } else if ( field.is( ":radio" ) ) { if ( field.is( ":checked" ) ) { value = field.val(); self.saveToBrowserStorage( prefix, value, false ); } } else { self.saveToBrowserStorage( prefix, value, false ); } } ); } ); if ( $.isFunction( self.options.onSave ) ) { self.options.onSave.call(); } }, /** * Restore forms data from Local Storage * * @return void */ restoreAllData: function() { var self = this; var restored = false; if ( $.isFunction( self.options.onBeforeRestore ) ) { self.options.onBeforeRestore.call(); } self.targets.each( function() { var target = $( this ); var targetFormId = target.attr( "id" ); var fieldsToProtect = target.find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ); fieldsToProtect.each( function() { if ( $.inArray( this, self.options.excludeFields ) !== -1 ) { // Returning non-false is the same as a continue statement in a for loop; it will skip immediately to the next iteration. return true; } var field = $( this ); var prefix = self.href + targetFormId + field.attr( "name" ) + self.options.customKeyPrefix; var resque = self.browserStorage.get( prefix ); if ( resque ) { self.restoreFieldsData( field, resque ); restored = true; } } ); } ); if ( restored && $.isFunction( self.options.onRestore ) ) { self.options.onRestore.call(); } }, /** * Restore form field data from local storage * * @param Object field jQuery form element object * @param String resque previously stored fields data * * @return void */ restoreFieldsData: function( field, resque ) { if ( field.attr( "name" ) === undefined ) { return false; } if ( field.is( ":checkbox" ) && resque !== "false" && field.attr( "name" ).indexOf( "[" ) === -1 ) { field.attr( "checked", "checked" ); } else if ( field.is( ":radio" ) ) { if ( field.val() === resque ) { field.attr( "checked", "checked" ); } } else if ( field.attr( "name" ).indexOf( "[" ) === -1 ) { field.val( resque ); } else { resque = resque.split( "," ); field.val( resque ); } }, /** * Bind immediate saving (on typing/checking/changing) field data to local storage when user fills it * * @param Object field jQuery form element object * @param String prefix prefix used as key to store data in local storage * * @return void */ bindSaveDataImmediately: function( field, prefix ) { var self = this; if ( typeof $.browser.msie === 'undefined' ) { field.get(0).oninput = function() { self.saveToBrowserStorage( prefix, field.val() ); }; } else { field.get(0).onpropertychange = function() { self.saveToBrowserStorage( prefix, field.val() ); }; } }, /** * Save data to Local Storage and fire callback if defined * * @param String key * @param String value * @param Boolean [true] fireCallback * * @return void */ saveToBrowserStorage: function( key, value, fireCallback ) { // if fireCallback is undefined it should be true fireCallback = fireCallback === null ? true : fireCallback; this.browserStorage.set( key, value ); if ( fireCallback && value !== "" && $.isFunction( this.options.onSave ) ) { this.options.onSave.call(); } }, /** * Bind saving field data on change * * @param Object field jQuery form element object * @param String prefix prefix used as key to store data in local storage * * @return void */ bindSaveDataOnChange: function( field, prefix ) { var self = this; field.change( function() { self.saveAllData(); } ); }, /** * Saving (by timeout) field data to local storage when user fills it * * @return void */ saveDataByTimeout: function() { var self = this; var targetForms = self.targets; setTimeout( ( function( targetForms ) { function timeout() { self.saveAllData(); setTimeout( timeout, self.options.timeout * 1000 ); } return timeout; } )( targetForms ), self.options.timeout * 1000 ); }, /** * Bind release form fields data from local storage on submit/reset form * * @return void */ bindReleaseData: function() { var self = this; self.targets.each( function( i ) { var target = $( this ); var fieldsToProtect = target.find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ); var formId = target.attr( "id" ); $( this ).bind( "submit reset", function() { self.releaseData( formId, fieldsToProtect ); } ); } ); }, /** * Manually release form fields * * @return void */ manuallyReleaseData: function() { var self = this; self.targets.each( function( i ) { var target = $( this ); var fieldsToProtect = target.find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ); var formId = target.attr( "id" ); self.releaseData( formId, fieldsToProtect ); } ); }, /** * Bind release form fields data from local storage on submit/resett form * * @param String targetFormId * @param Object fieldsToProtect jQuery object contains form fields to protect * * @return void */ releaseData: function( targetFormId, fieldsToProtect ) { var released = false; var self = this; fieldsToProtect.each( function() { if ( $.inArray( this, self.options.excludeFields ) !== -1 ) { // Returning non-false is the same as a continue statement in a for loop; it will skip immediately to the next iteration. return true; } var field = $( this ); var prefix = self.href + targetFormId + field.attr( "name" ) + self.options.customKeyPrefix; self.browserStorage.remove( prefix ); released = true; } ); if ( released && $.isFunction( self.options.onRelease ) ) { self.options.onRelease.call(); } } }; } return { getInstance: function() { if ( ! params.instantiated ) { params.instantiated = init(); params.instantiated.setInitialOptions(); } return params.instantiated; }, free: function() { params = {}; return null; } }; } )(); } )( jQuery );