/**
* 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
* @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 {
self.browserStorage.remove( prefix );
}
} 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 );