UNPKG

sisyphus.js

Version:

Gmail-like client-side drafts and bit more. Plugin developed to save html forms data to LocalStorage to restore them after browser crashes, tabs closings and other disasters.

550 lines (501 loc) 16.1 kB
/** * Plugin developed to save html forms data to LocalStorage to restore them after browser crashes, tabs closings * and other disasters. * * https://github.com/simsalabim/sisyphus * * @author Alexander Kaupanin <kaupanin@gmail.com> * @license MIT - see https://github.com/simsalabim/sisyphus/blob/master/MIT-LICENSE */ ( function( $ ) { function getElementIdentifier(el) { return '[id=' + el.attr( "id" ) + '][name=' + el.attr( "name" ) + ']'; } $.fn.sisyphus = function( options ) { var identifier = $.map( this, function( obj ) { return getElementIdentifier( $( obj ) ); }).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: [] }; var CKEDITOR = window.CKEDITOR; function init () { 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, onBeforeSave: function() {}, 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 || []; if ( self.options.name ) { this.href = self.options.name; } else { 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"; }, findFieldsToProtect: function( target ) { return target.find( ":input" ).not( ":submit" ).not( ":reset" ).not( ":button" ).not( ":file" ).not( ":password" ).not( ":disabled" ).not( "[readonly]" ); }, /** * Bind saving data * * @return void */ bindSaveData: function() { var self = this; if ( self.options.timeout ) { self.saveDataByTimeout(); } self.targets.each( function() { var targetFormIdAndName = getElementIdentifier( $( this ) ); self.findFieldsToProtect( $( this ) ).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 + getElementIdentifier( field ) + self.options.customKeySuffix; if ( field.is( ":text" ) || field.is( "textarea" ) ) { if ( ! self.options.timeout ) { self.bindSaveDataImmediately( field, prefix ); } } self.bindSaveDataOnChange( field ); } ); } ); }, /** * 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 text field data is saved only by timeout * * @return void */ saveAllData: function() { var self = this; self.targets.each( function() { var targetFormIdAndName = getElementIdentifier( $( this ) ); var multiCheckboxCache = {}; self.findFieldsToProtect( $( this) ).each( function() { var field = $( this ); if ( $.inArray( this, self.options.excludeFields ) !== -1 || ( field.attr( "name" ) === undefined && field.attr( "id" ) === 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 + getElementIdentifier( field ) + self.options.customKeySuffix; var value = field.val(); if ( field.is(":checkbox") ) { var name = field.attr( "name" ); if ( name !== undefined && name.indexOf( "[" ) !== -1 ) { if ( multiCheckboxCache[ name ] === true ) { return; } value = []; $( "[name='" + name +"']:checked" ).each( function() { value.push( $( this ).val() ); } ); multiCheckboxCache[ name ] = true; } 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 = CKEDITOR.instances[ field.attr("name") ] || CKEDITOR.instances[ field.attr("id") ]; if ( editor ) { 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 = getElementIdentifier( $( this ) ); self.findFieldsToProtect( target ).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 + getElementIdentifier( field ) + 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 && field.attr( "id" ) === undefined ) { return false; } var name = field.attr( "name" ); if ( field.is( ":checkbox" ) && resque !== "false" && ( name === undefined || name.indexOf( "[" ) === -1 ) ) { // If we aren't named by name (e.g. id) or we aren't in a multiple element field field.prop( "checked", true ); } else if( field.is( ":checkbox" ) && resque === "false" && ( name === undefined || name.indexOf( "[" ) === -1 ) ) { // If we aren't named by name (e.g. id) or we aren't in a multiple element field field.prop( "checked", false ); } else if ( field.is( ":radio" ) ) { if ( field.val() === resque ) { field.prop( "checked", true ); } } else if ( name === undefined || name.indexOf( "[" ) === -1 ) { // If we aren't named by name (e.g. id) or we aren't in a multiple element field 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 = CKEDITOR.instances[ field.attr("name") ] || CKEDITOR.instances[ field.attr("id") ]; if ( editor ) { editor.document.on( 'keyup', function() { 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 ) { var self = this; var callback_result = self.options.onBeforeSave.call( self ); if ( callback_result !== undefined && callback_result === false ) { return; } // 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 * * @return void */ bindSaveDataOnChange: function( field ) { 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() { 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() { var target = $( this ); var formIdAndName = getElementIdentifier( target ); $( this ).bind( "submit reset", function() { self.releaseData( formIdAndName, self.findFieldsToProtect( target ) ); } ); } ); }, /** * Manually release form fields * * @return void */ manuallyReleaseData: function() { var self = this; self.targets.each( function() { var target = $( this ); var formIdAndName = getElementIdentifier( target ); self.releaseData( formIdAndName, self.findFieldsToProtect( target ) ); } ); }, /** * 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; // Released form, are not started anymore. Fix for ajax loaded forms. params.started[ self.getInstanceIdentifier() ] = false; 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 + getElementIdentifier( field ) + 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 = { instantiated: [], started: [] }; return null; }, version: '1.1.3' }; } )(); } )( jQuery );