/** * Plugin developed to save html forms data to LocalStorage to restore them after browser crashes, tabs closings * and other disasters. * * @author Alexander Kaupanin <kaupanin@gmail.com> * @version 1.1.103 */ ( function( $ ) { $.fn.sisyphus = function( options ) { var identifier = $.map( this, function( obj, i ) { return $( obj ).attr( "id" ) + $( obj ).attr( "name" ) }).join(); var sisyphus = Sisyphus.getInstance( identifier ); sisyphus.protect( this, options ); 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: [], started: [] }; function init ( identifier ) { return { setInstanceIdentifier: function( identifier ) { this.identifier = identifier }, getInstanceIdentifier: function() { return this.identifier; }, /** * Set plugin initial options * * @param [Object] options * * @return void */ setInitialOptions: function ( options ) { var defaults = { excludeFields: [], customKeySuffix: "", locationBased: false, timeout: 0, autoRelease: true, 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, options ) { this.setOptions( options ); targets = targets || {}; var self = this; this.targets = this.targets || []; this.href = 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; } var callback_result = self.options.onBeforeRestore.call( self ); if ( callback_result === undefined || callback_result ) { self.restoreAllData(); } if ( this.options.autoRelease ) { self.bindReleaseData(); } if ( ! params.started[ this.getInstanceIdentifier() ] ) { if ( self.isCKEditorPresent() ) { var intervalId = setInterval( function() { if (CKEDITOR.isLoaded) { clearInterval(intervalId); self.bindSaveData(); params.started[ self.getInstanceIdentifier() ] = true; } }, 100); } else { self.bindSaveData(); params.started[ self.getInstanceIdentifier() ] = true; } } }, isCKEditorPresent: function() { if ( this.isCKEditorExists() ) { CKEDITOR.isLoaded = false; CKEDITOR.on('instanceReady', function() { CKEDITOR.isLoaded = true; } ); return true; } else { return false; } }, isCKEditorExists: function() { return typeof CKEDITOR != "undefined"; }, /** * Bind saving data * * @return void */ bindSaveData: function() { var self = this; if ( self.options.timeout ) { self.saveDataByTimeout(); } self.targets.each( function() { var targetFormIdAndName = $( this ).attr( "id" ) + $( this ).attr( "name" ); var fieldsToProtect = $( this ).find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ).not( ":password" ); 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.options.locationBased ? self.href : "") + targetFormIdAndName + field.attr( "name" ) + self.options.customKeySuffix; if ( field.is( ":text" ) || field.is( "textarea" ) ) { if ( ! self.options.timeout ) { self.bindSaveDataImmediately( field, prefix ); } } self.bindSaveDataOnChange( field, prefix ); } ); } ); }, /** * Save all protected forms data to Local Storage. * Common method, necessary to not lead astray user firing 'data is saved' when select/checkbox/radio * is changed and saved, while textfield data is saved only by timeout * * @return void */ saveAllData: function() { var self = this; self.targets.each( function() { var targetFormIdAndName = $( this ).attr( "id" ) + $( this ).attr( "name" ); var fieldsToProtect = $( this ).find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file").not( ":password" ); 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.options.locationBased ? self.href : "") + targetFormIdAndName + field.attr( "name" ) + self.options.customKeySuffix; 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 { if ( self.isCKEditorExists() ) { var editor; if ( editor = CKEDITOR.instances[ field.attr("name") ] || CKEDITOR.instances[ field.attr("id") ] ) { editor.updateElement(); self.saveToBrowserStorage( prefix, field.val(), false); } else { self.saveToBrowserStorage( prefix, value, false ); } } else { self.saveToBrowserStorage( prefix, value, false ); } } } ); } ); self.options.onSave.call( self ); }, /** * Restore forms data from Local Storage * * @return void */ restoreAllData: function() { var self = this; var restored = false; self.targets.each( function() { var target = $( this ); var targetFormIdAndName = $( this ).attr( "id" ) + $( this ).attr( "name" ); var fieldsToProtect = target.find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ).not( ":password" ); 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.options.locationBased ? self.href : "") + targetFormIdAndName + field.attr( "name" ) + self.options.customKeySuffix; var resque = self.browserStorage.get( prefix ); if ( resque !== null ) { self.restoreFieldsData( field, resque ); restored = true; } } ); } ); if ( restored ) { self.options.onRestore.call( self ); } }, /** * 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( ":checkbox" ) && resque === "false" && field.attr( "name" ).indexOf( "[" ) === -1 ) { field.removeAttr( "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 ( 'onpropertychange' in field ) { field.get(0).onpropertychange = function() { self.saveToBrowserStorage( prefix, field.val() ); }; } else { field.get(0).oninput = function() { self.saveToBrowserStorage( prefix, field.val() ); }; } if ( this.isCKEditorExists() ) { var editor; if ( editor = CKEDITOR.instances[ field.attr("name") ] || CKEDITOR.instances[ field.attr("id") ] ) { editor.document.on( 'keyup', function( event ) { editor.updateElement(); 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 === undefined ? true : fireCallback; this.browserStorage.set( key, value ); if ( fireCallback && value !== "" ) { this.options.onSave.call( this ); } }, /** * 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" ).not( ":password" ); var formIdAndName = target.attr( "id" ) + target.attr( "name" ); $( this ).bind( "submit reset", function() { self.releaseData( formIdAndName, 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" ).not( ":password" ); var formIdAndName = target.attr( "id" ) + target.attr( "name" ); self.releaseData( formIdAndName, fieldsToProtect ); } ); }, /** * Bind release form fields data from local storage on submit/resett form * * @param String targetFormIdAndName a form identifier consists of its id and name glued * @param Object fieldsToProtect jQuery object contains form fields to protect * * @return void */ releaseData: function( targetFormIdAndName, 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.options.locationBased ? self.href : "") + targetFormIdAndName + field.attr( "name" ) + self.options.customKeySuffix; self.browserStorage.remove( prefix ); released = true; } ); if ( released ) { self.options.onRelease.call( self ); } } }; } return { getInstance: function( identifier ) { if ( ! params.instantiated[ identifier ] ) { params.instantiated[ identifier ] = init(); params.instantiated[ identifier ].setInstanceIdentifier( identifier ); params.instantiated[ identifier ].setInitialOptions(); } if ( identifier ) { return params.instantiated[ identifier ]; } return params.instantiated[ identifier ]; }, free: function() { params = {}; return null; }, version: '1.1.103' }; } )(); } )( jQuery );