History panel: use SessionStorageModel, remove PersistentStorage

This commit is contained in:
Carl Eberhard
2013-11-25 14:24:14 -05:00
parent b006e1c330
commit ba57dd7adc
5 changed files with 95 additions and 183 deletions
+38 -138
View File
@@ -92,18 +92,30 @@ var LoggableMixin = /** @lends LoggableMixin# */{
/** Backbone model that syncs to the browser's sessionStorage API.
*/
var SessionStorageModel = Backbone.Model.extend({
initialize : function( hash, x, y, z ){
if( !hash || !hash.hasOwnProperty( 'id' ) ){
throw new Error( 'SessionStorageModel needs an id on init' );
}
// immed. save the passed in model and save on any change to it
this.save();
initialize : function( initialAttrs ){
// create unique id if none provided
initialAttrs.id = ( !_.isString( initialAttrs.id ) )?( _.uniqueId() ):( initialAttrs.id );
this.id = initialAttrs.id;
// load existing from storage (if any), clear any attrs set by bbone before init is called,
// layer initial over existing and defaults, and save
var existing = ( !this.isNew() )?( this._read( this ) ):( {} );
this.clear({ silent: true });
this.save( _.extend( {}, this.defaults, existing, initialAttrs ), { silent: true });
// save on any change to it immediately
this.on( 'change', function(){
this.save();
});
},
/** override of bbone sync to save to sessionStorage rather than REST
* bbone options (success, errror, etc.) should still apply
*/
sync : function( method, model, options ){
model.trigger('request', model, {}, options);
if( !options.silent ){
model.trigger( 'request', model, {}, options );
}
var returned;
switch( method ){
case 'create' : returned = this._create( model ); break;
@@ -118,159 +130,47 @@ var SessionStorageModel = Backbone.Model.extend({
}
return returned;
},
/** set storage to the stringified item */
_create : function( model ){
var json = model.toJSON(),
set = sessionStorage.setItem( model.id, JSON.stringify( json ) );
return ( set === null )?( set ):( json );
},
/** read and parse json from storage */
_read : function( model ){
return JSON.parse( sessionStorage.getItem( model.id, JSON.stringify( model.toJSON() ) ) );
return JSON.parse( sessionStorage.getItem( model.id ) );
},
/** set storage to the item (alias to create) */
_update : function( model ){
return model._create( model );
},
/** remove the item from storage */
_delete : function( model ){
return sessionStorage.removeItem( model.id );
},
/** T/F whether sessionStorage contains the model's id (data is present) */
isNew : function(){
return !sessionStorage.hasOwnProperty( this.id );
},
_log : function(){
return JSON.stringify( this.toJSON(), null, ' ' );
},
toString : function(){
return 'SessionStorageModel(' + this.id + ')';
}
});
(function(){
SessionStorageModel.prototype = _.omit( SessionStorageModel.prototype, 'url', 'urlRoot' );
}());
//==============================================================================
/**
* @class persistent storage adapter.
* Provides an easy interface to object based storage using method chaining.
* Allows easy change of the storage engine used (h5's local storage?).
* @augments StorageRecursionHelper
*
* @param {String} storageKey : the key the storage engine will place the storage object under
* @param {Object} storageDefaults : [optional] initial object to set up storage with
*
* @example
* // example of construction and use
* HistoryPanel.storage = new PersistanStorage( HistoryPanel.toString(), { visibleItems, {} });
* itemView.bind( 'toggleBodyVisibility', function( id, visible ){
* if( visible ){
* HistoryPanel.storage.get( 'visibleItems' ).set( id, true );
* } else {
* HistoryPanel.storage.get( 'visibleItems' ).deleteKey( id );
* }
* });
* @constructor
*/
var PersistentStorage = function( storageKey, storageDefaults ){
if( !storageKey ){
throw( "PersistentStorage needs storageKey argument" );
}
storageDefaults = storageDefaults || {};
// ~constants for the current engine
var STORAGE_ENGINE = sessionStorage,
STORAGE_ENGINE_GETTER = function sessionStorageGet( key ){
var item = this.getItem( key );
return ( item !== null )?( JSON.parse( this.getItem( key ) ) ):( null );
},
STORAGE_ENGINE_SETTER = function sessionStorageSet( key, val ){
return this.setItem( key, JSON.stringify( val ) );
},
STORAGE_ENGINE_KEY_DELETER = function sessionStorageDel( key ){ return this.removeItem( key ); };
/** Inner, recursive, private class for method chaining access.
* @name StorageRecursionHelper
* @constructor
*/
function StorageRecursionHelper( data, parent ){
//console.debug( 'new StorageRecursionHelper. data:', data );
data = data || {};
parent = parent || null;
return /** @lends StorageRecursionHelper.prototype */{
/** get a value from the storage obj named 'key',
* if it's an object - return a new StorageRecursionHelper wrapped around it
* if it's something simpler - return the value
* if this isn't passed a key - return the data at this level of recursion
*/
get : function( key ){
//console.debug( this + '.get', key );
if( key === undefined ){
return data;
} else if( data.hasOwnProperty( key ) ){
return ( jQuery.type( data[ key ] ) === 'object' )?
( new StorageRecursionHelper( data[ key ], this ) )
:( data[ key ] );
}
return undefined;
},
/** get the underlying data based on this key */
// set a value on the current data - then pass up to top to save current entire object in storage
set : function( key, value ){
//TODO: add parameterless variation setting the data somehow
// ??: difficult bc of obj by ref, closure
//console.debug( this + '.set', key, value );
data[ key ] = value;
this._save();
return this;
},
// remove a key at this level - then save entire (as 'set' above)
deleteKey : function( key ){
//console.debug( this + '.deleteKey', key );
delete data[ key ];
this._save();
return this;
},
// pass up the recursion chain (see below for base case)
_save : function(){
//console.debug( this + '.save', parent );
return parent._save();
},
toString : function(){
return ( 'StorageRecursionHelper(' + data + ')' );
}
};
}
//??: more readable to make another class?
var returnedStorage = {},
// attempt to get starting data from engine...
data = STORAGE_ENGINE_GETTER.call( STORAGE_ENGINE, storageKey );
// ...if that fails, use the defaults (and store them)
if( data === null || data === undefined ){
data = jQuery.extend( true, {}, storageDefaults );
STORAGE_ENGINE_SETTER.call( STORAGE_ENGINE, storageKey, data );
}
// the object returned by this constructor will be a modified StorageRecursionHelper
returnedStorage = new StorageRecursionHelper( data );
jQuery.extend( returnedStorage, /** @lends PersistentStorage.prototype */{
/** The base case for save()'s upward recursion - save everything to storage.
* @private
* @param {Any} newData data object to save to storage
*/
_save : function( newData ){
//console.debug( returnedStorage, '._save:', JSON.stringify( returnedStorage.get() ) );
return STORAGE_ENGINE_SETTER.call( STORAGE_ENGINE, storageKey, returnedStorage.get() );
},
/** Delete function to remove the entire base data object from the storageEngine.
*/
destroy : function(){
//console.debug( returnedStorage, '.destroy:' );
return STORAGE_ENGINE_KEY_DELETER.call( STORAGE_ENGINE, storageKey );
},
/** String representation.
*/
toString : function(){ return 'PersistentStorage(' + storageKey + ')'; }
});
return returnedStorage;
};
//==============================================================================
var HiddenUntilActivatedViewMixin = /** @lends hiddenUntilActivatedMixin# */{
+1 -1
View File
@@ -93,8 +93,8 @@ var HDABaseView = Backbone.View.extend( LoggableMixin ).extend(
var $newRender = $( HDABaseView.templates.skeleton( this.model.toJSON() ) );
$newRender.find( '.dataset-primary-actions' ).append( this._render_titleButtons() );
$newRender.children( '.dataset-body' ).replaceWith( this._render_body() );
//this._renderSelectable( $newRender );
this._setUpBehaviors( $newRender );
//this._renderSelectable( $newRender );
// fade the old render out (if desired)
if( fade ){
+54 -42
View File
@@ -3,6 +3,44 @@ define([
"mvc/dataset/hda-base",
"mvc/dataset/hda-edit"
], function( historyModel, hdaBase, hdaEdit ){
// ============================================================================
/** session storage for individual history preferences
*/
var HistoryPanelPrefs = SessionStorageModel.extend({
defaults : {
//TODO:?? expandedHdas to array?
expandedHdas : {},
//TODO:?? move to user?
show_deleted : false,
show_hidden : false
//TODO: add scroll position?
},
/** add an hda id to the hash of expanded hdas */
addExpandedHda : function( id ){
this.save( 'expandedHdas', _.extend( this.get( 'expandedHdas' ), _.object([ id ], [ true ]) ) );
},
/** remove an hda id from the hash of expanded hdas */
removeExpandedHda : function( id ){
this.save( 'expandedHdas', _.omit( this.get( 'expandedHdas' ), id ) );
},
toString : function(){
return 'HistoryPanelPrefs(' + this.id + ')';
}
});
/** key string to store each histories settings under */
HistoryPanelPrefs.historyStorageKey = function historyStorageKey( historyId ){
// class lvl for access w/o instantiation
if( !historyId ){
throw new Error( 'HistoryPanelPrefs.historyStorageKey needs valid id: ' + historyId );
}
// single point of change
return ( 'history:' + historyId );
};
/* =============================================================================
Backbone.js implementation of history panel
@@ -216,8 +254,8 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
// - hdas w/o details will have summary data only (name, hid, deleted, visible, state, etc.))
/** (re-)loads the user's current history & hdas w/ details */
// implemented as a 'fresh start' or for when there is no model (intial panel render)
loadCurrentHistory : function( attributes ){
// implemented as a 'fresh start' or for when there is no model (intial panel render)
var panel = this;
return this.loadHistoryWithHDADetails( 'current', attributes )
.then(function( historyData, hdaData ){
@@ -334,7 +372,7 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
return this;
},
// alias to the model. Updates the hda list only (not the history)
/** alias to the model. Updates the hda list only (not the history) */
refreshHdas : function( detailIds, options ){
if( this.model ){
return this.model.refresh( detailIds, options );
@@ -344,8 +382,6 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
},
// ------------------------------------------------------------------------ browser stored prefs
//TODO: simplify
/** Set up client side storage. Currently PersistanStorage keyed under 'HistoryPanel.<id>'
* @param {Object} initiallyExpanded
* @param {Boolean} show_deleted whether to show deleted HDAs (overrides stored)
@@ -353,55 +389,31 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
* @see PersistentStorage
*/
_setUpWebStorage : function( initiallyExpanded, show_deleted, show_hidden ){
//this.log( '_setUpWebStorage, initiallyExpanded:', initiallyExpanded,
// 'show_deleted:', show_deleted, 'show_hidden', show_hidden );
//TODO: add scroll position?
// data that needs to be persistent over page refreshes
// (note the key function which uses the history id as well)
this.storage = new PersistentStorage( this._getStorageKey( this.model.get( 'id' ) ), {
expandedHdas : {},
show_deleted : false,
show_hidden : false
//console.debug( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
this.storage = new HistoryPanelPrefs({
id: HistoryPanelPrefs.historyStorageKey( this.model.get( 'id' ) )
});
this.log( this + ' (prev) storage:', JSON.stringify( this.storage.get(), null, 2 ) );
// expanded Hdas is a map of hda.ids -> a boolean rep'ing whether this hda's body is expanded
// expanded Hdas is a map of hda.ids -> a boolean repr'ing whether this hda's body is already expanded
// store any pre-expanded ids passed in
if( initiallyExpanded ){
if( _.isObject( initiallyExpanded ) ){
this.storage.set( 'exandedHdas', initiallyExpanded );
}
//TODO: should be global pref? (not specific to each history)
// get the show_deleted/hidden settings giving priority to values passed in,
// using web storage otherwise
// get the show_deleted/hidden settings giving priority to values passed in, using web storage otherwise
// if the page has specifically requested show_deleted/hidden, these will be either true or false
// (as opposed to undefined, null) - and we give priority to that setting
if( ( show_deleted === true ) || ( show_deleted === false ) ){
// save them to web storage
if( _.isBoolean( show_deleted ) ){
this.storage.set( 'show_deleted', show_deleted );
}
if( ( show_hidden === true ) || ( show_hidden === false ) ){
if( _.isBoolean( show_hidden ) ){
this.storage.set( 'show_hidden', show_hidden );
}
// if the page hasn't specified whether to show_deleted/hidden, pull show_deleted/hidden from the web storage
this.show_deleted = this.storage.get( 'show_deleted' );
this.show_hidden = this.storage.get( 'show_hidden' );
//this.log( 'this.show_deleted:', this.show_deleted, 'show_hidden', this.show_hidden );
this.trigger( 'new-storage', this.storage, this );
this.log( this + ' (init\'d) storage:', this.storage.get() );
},
/** key string to store each histories settings under */
_getStorageKey : function( id ){
if( !id ){
throw new Error( '_getStorageKey needs valid id: ' + id );
}
return ( 'history:' + id );
},
/** clear all stored history panel data */
clearWebStorage : function(){
for( var key in sessionStorage ){
@@ -417,11 +429,11 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
return ( this.storage )?( this.storage.get() ):( {} );
}
//TODO: make storage engine generic
var item = sessionStorage.getItem( this._getStorageKey( historyId ) );
var item = sessionStorage.getItem( HistoryPanelPrefs.historyStorageKey( historyId ) );
return ( item === null )?( {} ):( JSON.parse( item ) );
},
/** get all the map of expaneded hda ids for the given history id */
/** get an array of expanded hda ids for the given history id */
getExpandedHdaIds : function( historyId ){
var expandedHdas = this.getStoredOptions( historyId ).expandedHdas;
return (( _.isEmpty( expandedHdas ) )?( [] ):( _.keys( expandedHdas ) ));
@@ -509,7 +521,7 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
*/
createHdaView : function( hda ){
var hdaId = hda.get( 'id' ),
expanded = this.storage.get( 'expandedHdas' ).get( hdaId ),
expanded = this.storage.get( 'expandedHdas' )[ hdaId ],
hdaView = new this.HDAView({
model : hda,
expanded : expanded,
@@ -529,10 +541,10 @@ var HistoryPanel = Backbone.View.extend( LoggableMixin ).extend(
var historyView = this;
// maintain a list of hdas whose bodies are expanded
hdaView.on( 'body-expanded', function( id ){
historyView.storage.get( 'expandedHdas' ).set( id, true );
historyView.storage.addExpandedHda( id );
});
hdaView.on( 'body-collapsed', function( id ){
historyView.storage.get( 'expandedHdas' ).deleteKey( id );
historyView.storage.removeExpandedHda( id );
});
//TODO: remove?
hdaView.on( 'error', function( model, xhr, options, msg ){
+1 -1
View File
@@ -1 +1 @@
var BaseModel=Backbone.RelationalModel.extend({defaults:{name:null,hidden:false},show:function(){this.set("hidden",false)},hide:function(){this.set("hidden",true)},is_visible:function(){return !this.attributes.hidden}});var BaseView=Backbone.View.extend({initialize:function(){this.model.on("change:hidden",this.update_visible,this);this.update_visible()},update_visible:function(){if(this.model.attributes.hidden){this.$el.hide()}else{this.$el.show()}}});var LoggableMixin={logger:null,log:function(){if(this.logger){var a=this.logger.log;if(typeof this.logger.log==="object"){a=Function.prototype.bind.call(this.logger.log,this.logger)}return a.apply(this.logger,arguments)}return undefined}};var SessionStorageModel=Backbone.Model.extend({initialize:function(b,a,d,c){if(!b||!b.hasOwnProperty("id")){throw new Error("SessionStorageModel needs an id on init")}this.save();this.on("change",function(){this.save()})},sync:function(d,b,a){b.trigger("request",b,{},a);var c;switch(d){case"create":c=this._create(b);break;case"read":c=this._read(b);break;case"update":c=this._update(b);break;case"delete":c=this._delete(b);break}if(c!==undefined||c!==null){if(a.success){a.success()}}else{if(a.error){a.error()}}return c},_create:function(a){var b=a.toJSON(),c=sessionStorage.setItem(a.id,JSON.stringify(b));return(c===null)?(c):(b)},_read:function(a){return JSON.parse(sessionStorage.getItem(a.id,JSON.stringify(a.toJSON())))},_update:function(a){return a._create(a)},_delete:function(a){return sessionStorage.removeItem(a.id)},isNew:function(){return !sessionStorage.hasOwnProperty(this.id)}});(function(){SessionStorageModel.prototype=_.omit(SessionStorageModel.prototype,"url","urlRoot")}());var PersistentStorage=function(k,g){if(!k){throw ("PersistentStorage needs storageKey argument")}g=g||{};var i=sessionStorage,c=function j(m){var n=this.getItem(m);return(n!==null)?(JSON.parse(this.getItem(m))):(null)},b=function e(m,n){return this.setItem(m,JSON.stringify(n))},d=function f(m){return this.removeItem(m)};function a(n,m){n=n||{};m=m||null;return{get:function(o){if(o===undefined){return n}else{if(n.hasOwnProperty(o)){return(jQuery.type(n[o])==="object")?(new a(n[o],this)):(n[o])}}return undefined},set:function(o,p){n[o]=p;this._save();return this},deleteKey:function(o){delete n[o];this._save();return this},_save:function(){return m._save()},toString:function(){return("StorageRecursionHelper("+n+")")}}}var l={},h=c.call(i,k);if(h===null||h===undefined){h=jQuery.extend(true,{},g);b.call(i,k,h)}l=new a(h);jQuery.extend(l,{_save:function(m){return b.call(i,k,l.get())},destroy:function(){return d.call(i,k)},toString:function(){return"PersistentStorage("+k+")"}});return l};var HiddenUntilActivatedViewMixin={hiddenUntilActivated:function(a,c){c=c||{};this.HUAVOptions={$elementShown:this.$el,showFn:jQuery.prototype.toggle,showSpeed:"fast"};_.extend(this.HUAVOptions,c||{});this.HUAVOptions.hasBeenShown=this.HUAVOptions.$elementShown.is(":visible");if(a){var b=this;a.on("click",function(d){b.toggle(b.HUAVOptions.showSpeed)})}},toggle:function(){if(this.HUAVOptions.$elementShown.is(":hidden")){if(!this.HUAVOptions.hasBeenShown){if(_.isFunction(this.HUAVOptions.onshowFirstTime)){this.HUAVOptions.hasBeenShown=true;this.HUAVOptions.onshowFirstTime.call(this)}}else{if(_.isFunction(this.HUAVOptions.onshow)){this.HUAVOptions.onshow.call(this)}}}return this.HUAVOptions.showFn.apply(this.HUAVOptions.$elementShown,arguments)}};
var BaseModel=Backbone.RelationalModel.extend({defaults:{name:null,hidden:false},show:function(){this.set("hidden",false)},hide:function(){this.set("hidden",true)},is_visible:function(){return !this.attributes.hidden}});var BaseView=Backbone.View.extend({initialize:function(){this.model.on("change:hidden",this.update_visible,this);this.update_visible()},update_visible:function(){if(this.model.attributes.hidden){this.$el.hide()}else{this.$el.show()}}});var LoggableMixin={logger:null,log:function(){if(this.logger){var a=this.logger.log;if(typeof this.logger.log==="object"){a=Function.prototype.bind.call(this.logger.log,this.logger)}return a.apply(this.logger,arguments)}return undefined}};var SessionStorageModel=Backbone.Model.extend({initialize:function(b){b.id=(!_.isString(b.id))?(_.uniqueId()):(b.id);this.id=b.id;var a=(!this.isNew())?(this._read(this)):({});this.clear({silent:true});this.save(_.extend({},this.defaults,a,b),{silent:true});this.on("change",function(){this.save()})},sync:function(d,b,a){if(!a.silent){b.trigger("request",b,{},a)}var c;switch(d){case"create":c=this._create(b);break;case"read":c=this._read(b);break;case"update":c=this._update(b);break;case"delete":c=this._delete(b);break}if(c!==undefined||c!==null){if(a.success){a.success()}}else{if(a.error){a.error()}}return c},_create:function(a){var b=a.toJSON(),c=sessionStorage.setItem(a.id,JSON.stringify(b));return(c===null)?(c):(b)},_read:function(a){return JSON.parse(sessionStorage.getItem(a.id))},_update:function(a){return a._create(a)},_delete:function(a){return sessionStorage.removeItem(a.id)},isNew:function(){return !sessionStorage.hasOwnProperty(this.id)},_log:function(){return JSON.stringify(this.toJSON(),null," ")},toString:function(){return"SessionStorageModel("+this.id+")"}});(function(){SessionStorageModel.prototype=_.omit(SessionStorageModel.prototype,"url","urlRoot")}());var HiddenUntilActivatedViewMixin={hiddenUntilActivated:function(a,c){c=c||{};this.HUAVOptions={$elementShown:this.$el,showFn:jQuery.prototype.toggle,showSpeed:"fast"};_.extend(this.HUAVOptions,c||{});this.HUAVOptions.hasBeenShown=this.HUAVOptions.$elementShown.is(":visible");if(a){var b=this;a.on("click",function(d){b.toggle(b.HUAVOptions.showSpeed)})}},toggle:function(){if(this.HUAVOptions.$elementShown.is(":hidden")){if(!this.HUAVOptions.hasBeenShown){if(_.isFunction(this.HUAVOptions.onshowFirstTime)){this.HUAVOptions.hasBeenShown=true;this.HUAVOptions.onshowFirstTime.call(this)}}else{if(_.isFunction(this.HUAVOptions.onshow)){this.HUAVOptions.onshow.call(this)}}}return this.HUAVOptions.showFn.apply(this.HUAVOptions.$elementShown,arguments)}};
File diff suppressed because one or more lines are too long