From e8cfc01f5bd4a483f1a5b4518f0dbc6785d71b33 Mon Sep 17 00:00:00 2001 From: Devon Noonan Date: Tue, 2 Oct 2012 16:52:07 -0400 Subject: [PATCH] Added excludeFields for built in fields (like authenticity token). Added requirements for jquery rather than dependencies. Added non-minified version of sisyphus with fix for object === null check when looking to see if browser is msie. --- lib/sisyphus-rails/form_tag_helper.rb | 4 +- sisyphus-rails.gemspec | 7 +- vendor/assets/javascripts/sisyphus.js | 460 +++++++++++++++++++++++++- 3 files changed, 466 insertions(+), 5 deletions(-) diff --git a/lib/sisyphus-rails/form_tag_helper.rb b/lib/sisyphus-rails/form_tag_helper.rb index 866f498..5a98156 100644 --- a/lib/sisyphus-rails/form_tag_helper.rb +++ b/lib/sisyphus-rails/form_tag_helper.rb @@ -6,7 +6,7 @@ module ActionView buf = ActiveSupport::SafeBuffer.new if options.has_key?(:id) && Sisyphus::process - buf.safe_concat("") + buf.safe_concat("") end buf << form_tag_without_sisyphus(url_for_options, options, &block) @@ -15,4 +15,4 @@ module ActionView alias_method_chain :form_tag, :sisyphus end end -end \ No newline at end of file +end diff --git a/sisyphus-rails.gemspec b/sisyphus-rails.gemspec index a40d7da..e700930 100644 --- a/sisyphus-rails.gemspec +++ b/sisyphus-rails.gemspec @@ -16,6 +16,9 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] - gem.add_dependency "railties", "~> 3.1" - gem.add_dependency "jquery-rails" + gem.requirements << 'requires jQuery 1.4+ to be included before Sisyphus' + gem.requirements << 'requires jQuery 1.8+ to be included if you require jStorage' + + gem.add_dependency "rails", ">= 3.1.0" end + diff --git a/vendor/assets/javascripts/sisyphus.js b/vendor/assets/javascripts/sisyphus.js index 1c5b652..8509bc5 100644 --- a/vendor/assets/javascripts/sisyphus.js +++ b/vendor/assets/javascripts/sisyphus.js @@ -1 +1,459 @@ -(function(a){a.sisyphus=function(){return Sisyphus.getInstance()},a.fn.sisyphus=function(a){var b=Sisyphus.getInstance();return b.setOptions(a),b.protect(this),b};var b={};b.isAvailable=function(){if(typeof a.jStorage=="object")return!0;try{return localStorage.getItem}catch(b){return!1}},b.set=function(b,c){if(typeof a.jStorage=="object")a.jStorage.set(b,c+"");else try{localStorage.setItem(b,c+"")}catch(d){}},b.get=function(b){if(typeof a.jStorage=="object"){var c=a.jStorage.get(b);return c?c.toString():c}return localStorage.getItem(b)},b.remove=function(b){typeof a.jStorage=="object"?a.jStorage.deleteKey(b):localStorage.removeItem(b)},Sisyphus=function(){function d(){return{setInitialOptions:function(c){var d={excludeFields:[],customKeyPrefix:"",timeout:0,autoRelease:!0,name:null,onSave:function(){},onBeforeRestore:function(){},onRestore:function(){},onRelease:function(){}};this.options=this.options||a.extend(d,c),this.browserStorage=b},setOptions:function(b){this.options=this.options||this.setInitialOptions(b),this.options=a.extend(this.options,b)},protect:function(b){b=b||{};var d=this;this.targets=this.targets||[],this.href=this.options.name||location.hostname+location.pathname+location.search+location.hash,this.targets=a.merge(this.targets,b),this.targets=a.unique(this.targets),this.targets=a(this.targets);if(!this.browserStorage.isAvailable())return!1;d.restoreAllData(),this.options.autoRelease&&d.bindReleaseData(),c.started||(d.bindSaveData(),c.started=!0)},bindSaveData:function(){var b=this;b.options.timeout&&b.saveDataByTimeout(),b.targets.each(function(){var c=a(this).attr("id"),d=a(this).find(":input").not(":submit").not(":reset").not(":button").not(":file");d.each(function(){if(a.inArray(this,b.options.excludeFields)!==-1)return!0;var d=a(this),e=b.href+c+d.attr("name")+b.options.customKeyPrefix;d.is(":text")||d.is("textarea")?b.options.timeout||b.bindSaveDataImmediately(d,e):b.bindSaveDataOnChange(d,e)})})},saveAllData:function(){var b=this;b.targets.each(function(){var c=a(this).attr("id"),d=a(this).find(":input").not(":submit").not(":reset").not(":button").not(":file");d.each(function(){var d=a(this);if(a.inArray(this,b.options.excludeFields)!==-1||d.attr("name")===undefined)return!0;var e=b.href+c+d.attr("name")+b.options.customKeyPrefix,f=d.val();d.is(":checkbox")?(d.attr("name").indexOf("[")!==-1?(f=[],a("[name='"+d.attr("name")+"']:checked").each(function(){f.push(a(this).val())})):f=d.is(":checked"),b.saveToBrowserStorage(e,f,!1)):d.is(":radio")?d.is(":checked")&&(f=d.val(),b.saveToBrowserStorage(e,f,!1)):b.saveToBrowserStorage(e,f,!1)})}),a.isFunction(b.options.onSave)&&b.options.onSave.call()},restoreAllData:function(){var b=this,c=!1;a.isFunction(b.options.onBeforeRestore)&&b.options.onBeforeRestore.call(),b.targets.each(function(){var d=a(this),e=d.attr("id"),f=d.find(":input").not(":submit").not(":reset").not(":button").not(":file");f.each(function(){if(a.inArray(this,b.options.excludeFields)!==-1)return!0;var d=a(this),f=b.href+e+d.attr("name")+b.options.customKeyPrefix,g=b.browserStorage.get(f);g&&(b.restoreFieldsData(d,g),c=!0)})}),c&&a.isFunction(b.options.onRestore)&&b.options.onRestore.call()},restoreFieldsData:function(a,b){if(a.attr("name")===undefined)return!1;a.is(":checkbox")&&b!=="false"&&a.attr("name").indexOf("[")===-1?a.attr("checked","checked"):a.is(":radio")?a.val()===b&&a.attr("checked","checked"):a.attr("name").indexOf("[")===-1?a.val(b):(b=b.split(","),a.val(b))},bindSaveDataImmediately:function(b,c){var d=this;a.browser.msie===null?b.get(0).oninput=function(){d.saveToBrowserStorage(c,b.val())}:b.get(0).onpropertychange=function(){d.saveToBrowserStorage(c,b.val())}},saveToBrowserStorage:function(b,c,d){d=d===null?!0:d,this.browserStorage.set(b,c),d&&c!==""&&a.isFunction(this.options.onSave)&&this.options.onSave.call()},bindSaveDataOnChange:function(a,b){var c=this;a.change(function(){c.saveAllData()})},saveDataByTimeout:function(){var a=this,b=a.targets;setTimeout(function(b){function c(){a.saveAllData(),setTimeout(c,a.options.timeout*1e3)}return c}(b),a.options.timeout*1e3)},bindReleaseData:function(){var b=this;b.targets.each(function(c){var d=a(this),e=d.find(":input").not(":submit").not(":reset").not(":button").not(":file"),f=d.attr("id");a(this).bind("submit reset",function(){b.releaseData(f,e)})})},manuallyReleaseData:function(){var b=this;b.targets.each(function(c){var d=a(this),e=d.find(":input").not(":submit").not(":reset").not(":button").not(":file"),f=d.attr("id");b.releaseData(f,e)})},releaseData:function(b,c){var d=!1,e=this;c.each(function(){if(a.inArray(this,e.options.excludeFields)!==-1)return!0;var c=a(this),f=e.href+b+c.attr("name")+e.options.customKeyPrefix;e.browserStorage.remove(f),d=!0}),d&&a.isFunction(e.options.onRelease)&&e.options.onRelease.call()}}}var c={instantiated:null,started:null};return{getInstance:function(){return c.instantiated||(c.instantiated=d(),c.instantiated.setInitialOptions()),c.instantiated},free:function(){return c={},null}}}()})(jQuery) \ No newline at end of file +/** + * 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 ); \ No newline at end of file