mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
1655 lines
65 KiB
JavaScript
1655 lines
65 KiB
JavaScript
/**
|
|
* functions for creating major ui elements
|
|
*/
|
|
|
|
/**
|
|
* backbone model for icon buttons
|
|
*/
|
|
var IconButton = Backbone.Model.extend({
|
|
defaults: {
|
|
title : "",
|
|
icon_class : "",
|
|
on_click : null,
|
|
menu_options : null,
|
|
is_menu_button : true,
|
|
id : null,
|
|
href : null,
|
|
target : null,
|
|
enabled : true,
|
|
visible : true,
|
|
tooltip_config : {}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* backbone view for icon buttons
|
|
*/
|
|
var IconButtonView = Backbone.View.extend({
|
|
|
|
initialize : function(){
|
|
// better rendering this way
|
|
this.model.attributes.tooltip_config = { placement : 'bottom' };
|
|
this.model.bind( 'change', this.render, this );
|
|
},
|
|
|
|
render : function( ){
|
|
// hide tooltip
|
|
this.$el.tooltip( 'hide' );
|
|
|
|
var new_elem = this.template( this.model.toJSON() );
|
|
// configure tooltip
|
|
new_elem.tooltip( this.model.get( 'tooltip_config' ));
|
|
this.$el.replaceWith( new_elem );
|
|
this.setElement( new_elem );
|
|
return this;
|
|
},
|
|
|
|
events : {
|
|
'click' : 'click'
|
|
},
|
|
|
|
click : function( event ){
|
|
// if on_click pass to that function
|
|
if( _.isFunction( this.model.get( 'on_click' ) ) ){
|
|
this.model.get( 'on_click' )( event );
|
|
return false;
|
|
}
|
|
// otherwise, bubble up ( to href or whatever )
|
|
return true;
|
|
},
|
|
|
|
// generate html element
|
|
template: function( options ){
|
|
var buffer = 'title="' + options.title + '" class="icon-button';
|
|
|
|
if( options.is_menu_button ){
|
|
buffer += ' menu-button';
|
|
}
|
|
|
|
buffer += ' ' + options.icon_class;
|
|
|
|
if( !options.enabled ){
|
|
buffer += '_disabled';
|
|
}
|
|
|
|
// close class tag
|
|
buffer += '"';
|
|
|
|
if( options.id ){
|
|
buffer += ' id="' + options.id + '"';
|
|
}
|
|
|
|
buffer += ' href="' + options.href + '"';
|
|
// add target for href
|
|
if( options.target ){
|
|
buffer += ' target="' + options.target + '"';
|
|
}
|
|
// set visibility
|
|
if( !options.visible ){
|
|
buffer += ' style="display: none;"';
|
|
}
|
|
|
|
// enabled/disabled
|
|
if ( options.enabled ){
|
|
buffer = '<a ' + buffer + '/>';
|
|
} else {
|
|
buffer = '<span ' + buffer + '/>';
|
|
}
|
|
|
|
// return element
|
|
return $( buffer );
|
|
}
|
|
} );
|
|
|
|
// define collection
|
|
var IconButtonCollection = Backbone.Collection.extend({
|
|
model: IconButton
|
|
});
|
|
|
|
/**
|
|
* menu with multiple icon buttons
|
|
* views are not needed nor used for individual buttons
|
|
*/
|
|
var IconButtonMenuView = Backbone.View.extend({
|
|
|
|
tagName: 'div',
|
|
|
|
initialize: function(){
|
|
this.render();
|
|
},
|
|
|
|
render: function(){
|
|
// initialize icon buttons
|
|
var self = this;
|
|
this.collection.each(function(button){
|
|
// create and add icon button to menu
|
|
var elt = $('<a/>')
|
|
.attr('href', 'javascript:void(0)')
|
|
.attr('title', button.attributes.title)
|
|
.addClass('icon-button menu-button')
|
|
.addClass(button.attributes.icon_class)
|
|
.appendTo(self.$el)
|
|
.click(button.attributes.on_click);
|
|
|
|
// configure tooltip
|
|
if (button.attributes.tooltip_config){
|
|
elt.tooltip(button.attributes.tooltip_config);
|
|
}
|
|
|
|
// add popup menu to icon
|
|
var menu_options = button.get('options');
|
|
if (menu_options){
|
|
make_popupmenu(elt, menu_options);
|
|
}
|
|
});
|
|
|
|
// return
|
|
return this;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns an IconButtonMenuView for the provided configuration.
|
|
* Configuration is a list of dictionaries where each dictionary
|
|
* defines an icon button. Each dictionary must have the following
|
|
* elements: icon_class, title, and on_click.
|
|
*/
|
|
var create_icon_buttons_menu = function(config, global_config)
|
|
{
|
|
// initialize global configuration
|
|
if (!global_config) global_config = {};
|
|
|
|
// create and initialize menu
|
|
var buttons = new IconButtonCollection(
|
|
_.map(config, function(button_config){
|
|
return new IconButton(_.extend(button_config, global_config));
|
|
})
|
|
);
|
|
|
|
// return menu
|
|
return new IconButtonMenuView( {collection: buttons} );
|
|
};
|
|
|
|
// =============================================================================
|
|
/**
|
|
*
|
|
*/
|
|
var Grid = Backbone.Collection.extend({
|
|
|
|
});
|
|
|
|
/**
|
|
*
|
|
*/
|
|
var GridView = Backbone.View.extend({
|
|
|
|
});
|
|
|
|
// =============================================================================
|
|
/**
|
|
* view for a popup menu
|
|
*/
|
|
var PopupMenu = Backbone.View.extend({
|
|
//TODO: maybe better as singleton off the Galaxy obj
|
|
/** Cache the desired button element and options, set up the button click handler
|
|
* NOTE: attaches this view as HTML/jQ data on the button for later use.
|
|
*/
|
|
initialize: function( $button, options ){
|
|
// default settings
|
|
this.$button = $button;
|
|
if( !this.$button.size() ){
|
|
this.$button = $( '<div/>' );
|
|
}
|
|
this.options = options || [];
|
|
|
|
// set up button click -> open menu behavior
|
|
var menu = this;
|
|
this.$button.click( function( event ){
|
|
// if there's already a menu open, remove it
|
|
$( '.popmenu-wrapper' ).remove();
|
|
menu._renderAndShow( event );
|
|
return false;
|
|
});
|
|
},
|
|
|
|
// render the menu, append to the page body at the click position, and set up the 'click-away' handlers, show
|
|
_renderAndShow: function( clickEvent ){
|
|
this.render();
|
|
this.$el.appendTo( 'body' ).css( this._getShownPosition( clickEvent )).show();
|
|
this._setUpCloseBehavior();
|
|
},
|
|
|
|
// render the menu
|
|
// this menu doesn't attach itself to the DOM ( see _renderAndShow )
|
|
render: function(){
|
|
// render the menu body absolute and hidden, fill with template
|
|
this.$el.addClass( 'popmenu-wrapper' ).hide()
|
|
.css({ position : 'absolute' })
|
|
.html( this.template( this.$button.attr( 'id' ), this.options ));
|
|
|
|
// set up behavior on each link/anchor elem
|
|
if( this.options.length ){
|
|
var menu = this;
|
|
//precondition: there should be one option per li
|
|
this.$el.find( 'li' ).each( function( i, li ){
|
|
var option = menu.options[i];
|
|
|
|
// if the option has 'func', call that function when the anchor is clicked
|
|
if( option.func ){
|
|
$( this ).children( 'a.popupmenu-option' ).click( function( event ){
|
|
option.func.call( menu, event, option );
|
|
// bubble up so that an option click will call the close behavior
|
|
//return false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
return this;
|
|
},
|
|
|
|
template : function( id, options ){
|
|
return [
|
|
'<ul id="', id, '-menu" class="dropdown-menu">', this._templateOptions( options ), '</ul>'
|
|
].join( '' );
|
|
},
|
|
|
|
_templateOptions : function( options ){
|
|
if( !options.length ){
|
|
return '<li>(no options)</li>';
|
|
}
|
|
return _.map( options, function( option ){
|
|
if( option.divider ){
|
|
return '<li class="divider"></li>';
|
|
} else if( option.header ){
|
|
return [ '<li class="head"><a href="javascript:void(0);">', option.html, '</a></li>' ].join( '' );
|
|
}
|
|
var href = option.href || 'javascript:void(0);',
|
|
target = ( option.target )?( ' target="' + option.target + '"' ):( '' ),
|
|
check = ( option.checked )?( '<span class="fa fa-check"></span>' ):( '' );
|
|
return [
|
|
'<li><a class="popupmenu-option" href="', href, '"', target, '>',
|
|
check, option.html,
|
|
'</a></li>'
|
|
].join( '' );
|
|
}).join( '' );
|
|
},
|
|
|
|
// get the absolute position/offset for the menu
|
|
_getShownPosition : function( clickEvent ){
|
|
|
|
// display menu horiz. centered on click...
|
|
var menuWidth = this.$el.width();
|
|
var x = clickEvent.pageX - menuWidth / 2 ;
|
|
|
|
// adjust to handle horiz. scroll and window dimensions ( draw entirely on visible screen area )
|
|
x = Math.min( x, $( document ).scrollLeft() + $( window ).width() - menuWidth - 5 );
|
|
x = Math.max( x, $( document ).scrollLeft() + 5 );
|
|
return {
|
|
top: clickEvent.pageY,
|
|
left: x
|
|
};
|
|
},
|
|
|
|
// bind an event handler to all available frames so that when anything is clicked
|
|
// the menu is removed from the DOM and the event handler unbinds itself
|
|
_setUpCloseBehavior: function(){
|
|
var menu = this;
|
|
//TODO: alternately: focus hack, blocking overlay, jquery.blockui
|
|
|
|
// function to close popup and unbind itself
|
|
function closePopup( event ){
|
|
$( document ).off( 'click.close_popup' );
|
|
if( window.parent !== window ){
|
|
try {
|
|
$( window.parent.document ).off( "click.close_popup" );
|
|
} catch( err ){}
|
|
} else {
|
|
try {
|
|
$( 'iframe#galaxy_main' ).contents().off( "click.close_popup" );
|
|
} catch( err ){}
|
|
}
|
|
menu.remove();
|
|
}
|
|
|
|
$( 'html' ).one( "click.close_popup", closePopup );
|
|
if( window.parent !== window ){
|
|
try {
|
|
$( window.parent.document ).find( 'html' ).one( "click.close_popup", closePopup );
|
|
} catch( err ){}
|
|
} else {
|
|
try {
|
|
$( 'iframe#galaxy_main' ).contents().one( "click.close_popup", closePopup );
|
|
} catch( err ){}
|
|
}
|
|
},
|
|
|
|
// add a menu option/item at the given index
|
|
addItem: function( item, index ){
|
|
// append to end if no index
|
|
index = ( index >= 0 ) ? index : this.options.length;
|
|
this.options.splice( index, 0, item );
|
|
return this;
|
|
},
|
|
|
|
// remove a menu option/item at the given index
|
|
removeItem: function( index ){
|
|
if( index >=0 ){
|
|
this.options.splice( index, 1 );
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// search for a menu option by its html
|
|
findIndexByHtml: function( html ){
|
|
for( var i = 0; i < this.options.length; i++ ){
|
|
if( _.has( this.options[i], 'html' ) && ( this.options[i].html === html )){
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// search for a menu option by its html
|
|
findItemByHtml: function( html ){
|
|
return this.options[( this.findIndexByHtml( html ))];
|
|
},
|
|
|
|
// string representation
|
|
toString: function(){
|
|
return 'PopupMenu';
|
|
}
|
|
});
|
|
/** shortcut to new for when you don't need to preserve the ref */
|
|
PopupMenu.create = function _create( $button, options ){
|
|
return new PopupMenu( $button, options );
|
|
};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// the following class functions are bridges from the original make_popupmenu and make_popup_menus
|
|
// to the newer backbone.js PopupMenu
|
|
|
|
/** Create a PopupMenu from simple map initial_options activated by clicking button_element.
|
|
* Converts initial_options to object array used by PopupMenu.
|
|
* @param {jQuery|DOMElement} button_element element which, when clicked, activates menu
|
|
* @param {Object} initial_options map of key -> values, where
|
|
* key is option text, value is fn to call when option is clicked
|
|
* @returns {PopupMenu} the PopupMenu created
|
|
*/
|
|
PopupMenu.make_popupmenu = function( button_element, initial_options ){
|
|
var convertedOptions = [];
|
|
_.each( initial_options, function( optionVal, optionKey ){
|
|
var newOption = { html: optionKey };
|
|
|
|
// keys with null values indicate: header
|
|
if( optionVal === null ){ // !optionVal? (null only?)
|
|
newOption.header = true;
|
|
|
|
// keys with function values indicate: a menu option
|
|
} else if( jQuery.type( optionVal ) === 'function' ){
|
|
newOption.func = optionVal;
|
|
}
|
|
//TODO:?? any other special optionVals?
|
|
// there was no divider option originally
|
|
convertedOptions.push( newOption );
|
|
});
|
|
return new PopupMenu( $( button_element ), convertedOptions );
|
|
};
|
|
|
|
/** Find all anchors in $parent (using selector) and covert anchors into a PopupMenu options map.
|
|
* @param {jQuery} $parent the element that contains the links to convert to options
|
|
* @param {String} selector jq selector string to find links
|
|
* @returns {Object[]} the options array to initialize a PopupMenu
|
|
*/
|
|
//TODO: lose parent and selector, pass in array of links, use map to return options
|
|
PopupMenu.convertLinksToOptions = function( $parent, selector ){
|
|
$parent = $( $parent );
|
|
selector = selector || 'a';
|
|
var options = [];
|
|
$parent.find( selector ).each( function( elem, i ){
|
|
var option = {}, $link = $( elem );
|
|
|
|
// convert link text to the option text (html) and the href into the option func
|
|
option.html = $link.text();
|
|
if( $link.attr( 'href' ) ){
|
|
var linkHref = $link.attr( 'href' ),
|
|
linkTarget = $link.attr( 'target' ),
|
|
confirmText = $link.attr( 'confirm' );
|
|
|
|
option.func = function(){
|
|
// if there's a "confirm" attribute, throw up a confirmation dialog, and
|
|
// if the user cancels - do nothing
|
|
if( ( confirmText ) && ( !confirm( confirmText ) ) ){ return; }
|
|
|
|
// if there's no confirm attribute, or the user accepted the confirm dialog:
|
|
switch( linkTarget ){
|
|
// relocate the center panel
|
|
case '_parent':
|
|
window.parent.location = linkHref;
|
|
break;
|
|
|
|
// relocate the entire window
|
|
case '_top':
|
|
window.top.location = linkHref;
|
|
break;
|
|
|
|
// relocate this panel
|
|
default:
|
|
window.location = linkHref;
|
|
}
|
|
};
|
|
}
|
|
options.push( option );
|
|
});
|
|
return options;
|
|
};
|
|
|
|
/** Create a single popupmenu from existing DOM button and anchor elements
|
|
* @param {jQuery} $buttonElement the element that when clicked will open the menu
|
|
* @param {jQuery} $menuElement the element that contains the anchors to convert into a menu
|
|
* @param {String} menuElementLinkSelector jq selector string used to find anchors to be made into menu options
|
|
* @returns {PopupMenu} the PopupMenu (Backbone View) that can render, control the menu
|
|
*/
|
|
PopupMenu.fromExistingDom = function( $buttonElement, $menuElement, menuElementLinkSelector ){
|
|
$buttonElement = $( $buttonElement );
|
|
$menuElement = $( $menuElement );
|
|
var options = PopupMenu.convertLinksToOptions( $menuElement, menuElementLinkSelector );
|
|
// we're done with the menu (having converted it to an options map)
|
|
$menuElement.remove();
|
|
return new PopupMenu( $buttonElement, options );
|
|
};
|
|
|
|
/** Create all popupmenus within a document or a more specific element
|
|
* @param {DOMElement} parent the DOM element in which to search for popupmenus to build (defaults to document)
|
|
* @param {String} menuSelector jq selector string to find popupmenu menu elements (defaults to "div[popupmenu]")
|
|
* @param {Function} buttonSelectorBuildFn the function to build the jq button selector.
|
|
* Will be passed $menuElement, parent.
|
|
* (Defaults to return '#' + $menuElement.attr( 'popupmenu' ); )
|
|
* @returns {PopupMenu[]} array of popupmenus created
|
|
*/
|
|
PopupMenu.make_popup_menus = function( parent, menuSelector, buttonSelectorBuildFn ){
|
|
parent = parent || document;
|
|
// orig. Glx popupmenu menus have a (non-std) attribute 'popupmenu'
|
|
// which contains the id of the button that activates the menu
|
|
menuSelector = menuSelector || 'div[popupmenu]';
|
|
// default to (orig. Glx) matching button to menu by using the popupmenu attr of the menu as the id of the button
|
|
buttonSelectorBuildFn = buttonSelectorBuildFn || function( $menuElement, parent ){
|
|
return '#' + $menuElement.attr( 'popupmenu' );
|
|
};
|
|
|
|
// aggregate and return all PopupMenus
|
|
var popupMenusCreated = [];
|
|
$( parent ).find( menuSelector ).each( function(){
|
|
var $menuElement = $( this ),
|
|
$buttonElement = $( parent ).find( buttonSelectorBuildFn( $menuElement, parent ) );
|
|
popupMenusCreated.push( PopupMenu.fromDom( $buttonElement, $menuElement ) );
|
|
$buttonElement.addClass( 'popup' );
|
|
});
|
|
return popupMenusCreated;
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
var faIconButton = function( options ){
|
|
//TODO: move out of global
|
|
options = options || {};
|
|
options.tooltipConfig = options.tooltipConfig || { placement: 'bottom' };
|
|
|
|
options.classes = [ 'icon-btn' ].concat( options.classes || [] );
|
|
if( options.disabled ){
|
|
options.classes.push( 'disabled' );
|
|
}
|
|
|
|
var html = [
|
|
'<a class="', options.classes.join( ' ' ), '"',
|
|
(( options.title )?( ' title="' + options.title + '"' ):( '' )),
|
|
(( !options.disabled && options.target )? ( ' target="' + options.target + '"' ):( '' )),
|
|
' href="', (( !options.disabled && options.href )?( options.href ):( 'javascript:void(0);' )), '">',
|
|
// could go with something less specific here - like 'html'
|
|
'<span class="fa ', options.faIcon, '"></span>',
|
|
'</a>'
|
|
].join( '' );
|
|
var $button = $( html ).tooltip( options.tooltipConfig );
|
|
if( _.isFunction( options.onclick ) ){
|
|
$button.click( options.onclick );
|
|
}
|
|
return $button;
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
function LoadingIndicator( $where, options ){
|
|
//TODO: move out of global
|
|
//TODO: too specific to history panel
|
|
|
|
var self = this;
|
|
// defaults
|
|
options = jQuery.extend({
|
|
cover : false
|
|
}, options || {} );
|
|
|
|
function render(){
|
|
var html = [
|
|
'<div class="loading-indicator">',
|
|
'<div class="loading-indicator-text">',
|
|
'<span class="fa fa-spinner fa-spin fa-lg"></span>',
|
|
'<span class="loading-indicator-message">loading...</span>',
|
|
'</div>',
|
|
'</div>'
|
|
].join( '\n' );
|
|
|
|
var $indicator = $( html ).hide().css( options.css || {
|
|
position : 'fixed'
|
|
}),
|
|
$text = $indicator.children( '.loading-indicator-text' );
|
|
|
|
if( options.cover ){
|
|
$indicator.css({
|
|
'z-index' : 2,
|
|
top : $where.css( 'top' ),
|
|
bottom : $where.css( 'bottom' ),
|
|
left : $where.css( 'left' ),
|
|
right : $where.css( 'right' ),
|
|
opacity : 0.5,
|
|
'background-color': 'white',
|
|
'text-align': 'center'
|
|
});
|
|
$text = $indicator.children( '.loading-indicator-text' ).css({
|
|
'margin-top' : '20px'
|
|
});
|
|
|
|
} else {
|
|
$text = $indicator.children( '.loading-indicator-text' ).css({
|
|
margin : '12px 0px 0px 10px',
|
|
opacity : '0.85',
|
|
color : 'grey'
|
|
});
|
|
$text.children( '.loading-indicator-message' ).css({
|
|
margin : '0px 8px 0px 0px',
|
|
'font-style' : 'italic'
|
|
});
|
|
}
|
|
return $indicator;
|
|
}
|
|
|
|
self.show = function( msg, speed, callback ){
|
|
msg = msg || 'loading...';
|
|
speed = speed || 'fast';
|
|
// remove previous
|
|
$where.parent().find( '.loading-indicator' ).remove();
|
|
// since position is fixed - we insert as sibling
|
|
self.$indicator = render().insertBefore( $where );
|
|
self.message( msg );
|
|
self.$indicator.fadeIn( speed, callback );
|
|
return self;
|
|
};
|
|
|
|
self.message = function( msg ){
|
|
self.$indicator.find( 'i' ).text( msg );
|
|
};
|
|
|
|
self.hide = function( speed, callback ){
|
|
speed = speed || 'fast';
|
|
if( self.$indicator && self.$indicator.size() ){
|
|
self.$indicator.fadeOut( speed, function(){
|
|
self.$indicator.remove();
|
|
if( callback ){ callback(); }
|
|
});
|
|
} else {
|
|
if( callback ){ callback(); }
|
|
}
|
|
return self;
|
|
};
|
|
return self;
|
|
}
|
|
|
|
//==============================================================================
|
|
(function(){
|
|
/** searchInput: (jQuery plugin)
|
|
* Creates a search input, a clear button, and loading indicator
|
|
* within the selected node.
|
|
*
|
|
* When the user either presses return or enters some minimal number
|
|
* of characters, a callback is called. Pressing ESC when the input
|
|
* is focused will clear the input and call a separate callback.
|
|
*/
|
|
var _l = window._l || function( s ){ return s; };
|
|
|
|
// contructor
|
|
function searchInput( parentNode, options ){
|
|
//TODO: consolidate with tool menu functionality, use there
|
|
var KEYCODE_ESC = 27,
|
|
KEYCODE_RETURN = 13,
|
|
$parentNode = $( parentNode ),
|
|
firstSearch = true,
|
|
defaults = {
|
|
initialVal : '',
|
|
name : 'search',
|
|
placeholder : 'search',
|
|
classes : '',
|
|
onclear : function(){},
|
|
onfirstsearch : null,
|
|
onsearch : function( inputVal ){},
|
|
minSearchLen : 0,
|
|
escWillClear : true,
|
|
oninit : function(){}
|
|
};
|
|
|
|
// .................................................................... input rendering and events
|
|
// visually clear the search, trigger an event, and call the callback
|
|
function clearSearchInput( event ){
|
|
var $input = $( this ).parent().children( 'input' );
|
|
//console.debug( this, 'clear', $input );
|
|
$input.focus().val( '' ).trigger( 'clear:searchInput' );
|
|
options.onclear();
|
|
}
|
|
|
|
// search for searchTerms, trigger an event, call the appropo callback (based on whether this is the first)
|
|
function search( event, searchTerms ){
|
|
//console.debug( this, 'searching', searchTerms );
|
|
$( this ).trigger( 'search:searchInput', searchTerms );
|
|
if( typeof options.onfirstsearch === 'function' && firstSearch ){
|
|
firstSearch = false;
|
|
options.onfirstsearch( searchTerms );
|
|
} else {
|
|
options.onsearch( searchTerms );
|
|
}
|
|
}
|
|
|
|
// .................................................................... input rendering and events
|
|
function inputTemplate(){
|
|
// class search-query is bootstrap 2.3 style that now lives in base.less
|
|
return [ '<input type="text" name="', options.name, '" placeholder="', options.placeholder, '" ',
|
|
'class="search-query ', options.classes, '" ', '/>' ].join( '' );
|
|
}
|
|
|
|
// the search input that responds to keyboard events and displays the search value
|
|
function $input(){
|
|
return $( inputTemplate() )
|
|
// select all text on a focus
|
|
.focus( function( event ){
|
|
$( this ).select();
|
|
})
|
|
// attach behaviors to esc, return if desired, search on some min len string
|
|
.keyup( function( event ){
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
//TODO: doesn't work
|
|
if( !$( this ).val() ){ $( this ).blur(); }
|
|
|
|
// esc key will clear if desired
|
|
if( event.which === KEYCODE_ESC && options.escWillClear ){
|
|
clearSearchInput.call( this, event );
|
|
|
|
} else {
|
|
var searchTerms = $( this ).val();
|
|
// return key or the search string len > minSearchLen (if not 0) triggers search
|
|
if( ( event.which === KEYCODE_RETURN )
|
|
|| ( options.minSearchLen && searchTerms.length >= options.minSearchLen ) ){
|
|
search.call( this, event, searchTerms );
|
|
} else if( !searchTerms.length ){
|
|
clearSearchInput.call( this, event );
|
|
}
|
|
}
|
|
})
|
|
.on( 'change', function( event ){
|
|
search.call( this, event, $( this ).val() );
|
|
})
|
|
.val( options.initialVal );
|
|
}
|
|
|
|
// .................................................................... clear button rendering and events
|
|
// a button for clearing the search bar, placed on the right hand side
|
|
function $clearBtn(){
|
|
return $([ '<span class="search-clear fa fa-times-circle" ',
|
|
'title="', _l( 'clear search (esc)' ), '"></span>' ].join('') )
|
|
.tooltip({ placement: 'bottom' })
|
|
.click( function( event ){
|
|
clearSearchInput.call( this, event );
|
|
});
|
|
}
|
|
|
|
// .................................................................... loadingIndicator rendering
|
|
// a button for clearing the search bar, placed on the right hand side
|
|
function $loadingIndicator(){
|
|
return $([ '<span class="search-loading fa fa-spinner fa-spin" ',
|
|
'title="', _l( 'loading...' ), '"></span>' ].join('') )
|
|
.hide().tooltip({ placement: 'bottom' });
|
|
}
|
|
|
|
// .................................................................... commands
|
|
// visually swap the load, clear buttons
|
|
function toggleLoadingIndicator(){
|
|
$parentNode.find( '.search-loading' ).toggle();
|
|
$parentNode.find( '.search-clear' ).toggle();
|
|
}
|
|
|
|
// .................................................................... init
|
|
// string command (not constructor)
|
|
if( jQuery.type( options ) === 'string' ){
|
|
if( options === 'toggle-loading' ){
|
|
toggleLoadingIndicator();
|
|
}
|
|
return $parentNode;
|
|
}
|
|
|
|
// initial render
|
|
if( jQuery.type( options ) === 'object' ){
|
|
options = jQuery.extend( true, {}, defaults, options );
|
|
}
|
|
//NOTE: prepended
|
|
return $parentNode.addClass( 'search-input' ).prepend([ $input(), $clearBtn(), $loadingIndicator() ]);
|
|
}
|
|
|
|
// as jq plugin
|
|
jQuery.fn.extend({
|
|
searchInput : function $searchInput( options ){
|
|
return this.each( function(){
|
|
return searchInput( this, options );
|
|
});
|
|
}
|
|
});
|
|
}());
|
|
|
|
|
|
//==============================================================================
|
|
(function(){
|
|
/** Multi 'mode' button (or any element really) that changes the html
|
|
* contents of itself when clicked. Pass in an ordered list of
|
|
* objects with 'html' and (optional) onclick functions.
|
|
*
|
|
* When clicked in a particular node, the onclick function will
|
|
* be called (with the element as this) and the element will
|
|
* switch to the next mode, replacing its html content with
|
|
* that mode's html.
|
|
*
|
|
* If there is no next mode, the element will switch back to
|
|
* the first mode.
|
|
* @example:
|
|
* $( '.myElement' ).modeButton({
|
|
* modes : [
|
|
* {
|
|
* mode: 'bler',
|
|
* html: '<h5>Bler</h5>',
|
|
* onclick : function(){
|
|
* $( 'body' ).css( 'background-color', 'red' );
|
|
* }
|
|
* },
|
|
* {
|
|
* mode: 'bloo',
|
|
* html: '<h4>Bloo</h4>',
|
|
* onclick : function(){
|
|
* $( 'body' ).css( 'background-color', 'blue' );
|
|
* }
|
|
* },
|
|
* {
|
|
* mode: 'blah',
|
|
* html: '<h3>Blah</h3>',
|
|
* onclick : function(){
|
|
* $( 'body' ).css( 'background-color', 'grey' );
|
|
* }
|
|
* },
|
|
* ]
|
|
* });
|
|
* $( '.myElement' ).modeButton( 'callModeFn', 'bler' );
|
|
*/
|
|
/** constructor */
|
|
function ModeButton( element, options ){
|
|
this.currModeIndex = 0;
|
|
return this._init( element, options );
|
|
}
|
|
|
|
/** html5 data key to store this object inside an element */
|
|
ModeButton.prototype.DATA_KEY = 'mode-button';
|
|
/** default options */
|
|
ModeButton.prototype.defaults = {
|
|
switchModesOnClick : true
|
|
};
|
|
|
|
// ---- private interface
|
|
/** set up options, intial mode, and the click handler */
|
|
ModeButton.prototype._init = function _init( element, options ){
|
|
//console.debug( 'ModeButton._init:', element, options );
|
|
options = options || {};
|
|
this.$element = $( element );
|
|
this.options = jQuery.extend( true, {}, this.defaults, options );
|
|
if( !options.modes ){
|
|
throw new Error( 'ModeButton requires a "modes" array' );
|
|
}
|
|
|
|
var modeButton = this;
|
|
this.$element.click( function _ModeButtonClick( event ){
|
|
// call the curr mode fn
|
|
modeButton.callModeFn();
|
|
// inc the curr mode index
|
|
if( modeButton.options.switchModesOnClick ){ modeButton._incModeIndex(); }
|
|
// set the element html
|
|
$( this ).html( modeButton.options.modes[ modeButton.currModeIndex ].html );
|
|
});
|
|
return this.reset();
|
|
};
|
|
/** increment the mode index to the next in the array, looping back to zero if at the last */
|
|
ModeButton.prototype._incModeIndex = function _incModeIndex(){
|
|
this.currModeIndex += 1;
|
|
if( this.currModeIndex >= this.options.modes.length ){
|
|
this.currModeIndex = 0;
|
|
}
|
|
return this;
|
|
};
|
|
/** get the mode index in the modes array for the given key (mode name) */
|
|
ModeButton.prototype._getModeIndex = function _getModeIndex( modeKey ){
|
|
for( var i=0; i<this.options.modes.length; i+=1 ){
|
|
if( this.options.modes[ i ].mode === modeKey ){ return i; }
|
|
}
|
|
throw new Error( 'mode not found: ' + modeKey );
|
|
};
|
|
/** set the current mode to the one with the given index and set button html */
|
|
ModeButton.prototype._setModeByIndex = function _setModeByIndex( index ){
|
|
var newMode = this.options.modes[ index ];
|
|
if( !newMode ){
|
|
throw new Error( 'mode index not found: ' + index );
|
|
}
|
|
this.currModeIndex = index;
|
|
if( newMode.html ){
|
|
this.$element.html( newMode.html );
|
|
}
|
|
return this;
|
|
};
|
|
|
|
// ---- public interface
|
|
/** get the current mode object (not just the mode name) */
|
|
ModeButton.prototype.currentMode = function currentMode(){
|
|
return this.options.modes[ this.currModeIndex ];
|
|
};
|
|
/** return the mode key of the current mode */
|
|
ModeButton.prototype.current = function current(){
|
|
// sugar for returning mode name
|
|
return this.currentMode().mode;
|
|
};
|
|
/** get the mode with the given modeKey or the current mode if modeKey is undefined */
|
|
ModeButton.prototype.getMode = function getMode( modeKey ){
|
|
if( !modeKey ){ return this.currentMode(); }
|
|
return this.options.modes[( this._getModeIndex( modeKey ) )];
|
|
};
|
|
/** T/F if the button has the given mode */
|
|
ModeButton.prototype.hasMode = function hasMode( modeKey ){
|
|
try {
|
|
return !!this.getMode( modeKey );
|
|
} catch( err ){}
|
|
return false;
|
|
};
|
|
/** set the current mode to the mode with the given name */
|
|
ModeButton.prototype.setMode = function setMode( modeKey ){
|
|
return this._setModeByIndex( this._getModeIndex( modeKey ) );
|
|
};
|
|
/** reset to the initial mode */
|
|
ModeButton.prototype.reset = function reset(){
|
|
this.currModeIndex = 0;
|
|
if( this.options.initialMode ){
|
|
this.currModeIndex = this._getModeIndex( this.options.initialMode );
|
|
}
|
|
return this._setModeByIndex( this.currModeIndex );
|
|
};
|
|
/** manually call the click handler of the given mode */
|
|
ModeButton.prototype.callModeFn = function callModeFn( modeKey ){
|
|
var modeFn = this.getMode( modeKey ).onclick;
|
|
if( modeFn && jQuery.type( modeFn === 'function' ) ){
|
|
// call with the element as context (std jquery pattern)
|
|
return modeFn.call( this.$element.get(0) );
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// as jq plugin
|
|
jQuery.fn.extend({
|
|
modeButton : function $modeButton( options ){
|
|
if( !this.size() ){ return this; }
|
|
|
|
//TODO: does map still work with jq multi selection (i.e. $( '.class-for-many-btns' ).modeButton)?
|
|
if( jQuery.type( options ) === 'object' ){
|
|
return this.map( function(){
|
|
var $this = $( this );
|
|
$this.data( 'mode-button', new ModeButton( $this, options ) );
|
|
return this;
|
|
});
|
|
}
|
|
|
|
var $first = $( this[0] ),
|
|
button = $first.data( 'mode-button' );
|
|
|
|
if( !button ){
|
|
throw new Error( 'modeButton needs an options object or string name of a function' );
|
|
}
|
|
|
|
if( button && jQuery.type( options ) === 'string' ){
|
|
var fnName = options;
|
|
if( button && jQuery.type( button[ fnName ] ) === 'function' ){
|
|
return button[ fnName ].apply( button, jQuery.makeArray( arguments ).slice( 1 ) );
|
|
}
|
|
}
|
|
return button;
|
|
}
|
|
});
|
|
}());
|
|
|
|
|
|
//==============================================================================
|
|
/**
|
|
* Template function that produces a bootstrap dropdown to replace the
|
|
* vanilla HTML select input. Pass in an array of options and an initial selection:
|
|
* $( '.my-div' ).append( dropDownSelect( [ 'option1', 'option2' ], 'option2' );
|
|
*
|
|
* When the user changes the selected option a 'change.dropdown-select' event will
|
|
* fire with both the jq event and the new selection text as arguments.
|
|
*
|
|
* Get the currently selected choice using:
|
|
* var userChoice = $( '.my-div .dropdown-select .dropdown-select-selected' ).text();
|
|
*
|
|
*/
|
|
function dropDownSelect( options, selected ){
|
|
// replacement for vanilla select element using bootstrap dropdowns instead
|
|
selected = selected || (( !_.isEmpty( options ) )?( options[0] ):( '' ));
|
|
var $select = $([
|
|
'<div class="dropdown-select btn-group">',
|
|
'<button type="button" class="btn btn-default">',
|
|
'<span class="dropdown-select-selected">' + selected + '</span>',
|
|
'</button>',
|
|
'</div>'
|
|
].join( '\n' ));
|
|
|
|
// if there's only one option, do not style/create as buttons, dropdown - use simple span
|
|
// otherwise, a dropdown displaying the current selection
|
|
if( options && options.length > 1 ){
|
|
$select.find( 'button' )
|
|
.addClass( 'dropdown-toggle' ).attr( 'data-toggle', 'dropdown' )
|
|
.append( ' <span class="caret"></span>' );
|
|
$select.append([
|
|
'<ul class="dropdown-menu" role="menu">',
|
|
_.map( options, function( option ){
|
|
return [
|
|
'<li><a href="javascript:void(0)">', option, '</a></li>'
|
|
].join( '' );
|
|
}).join( '\n' ),
|
|
'</ul>'
|
|
].join( '\n' ));
|
|
}
|
|
|
|
// trigger 'change.dropdown-select' when a new selection is made using the dropdown
|
|
function selectThis( event ){
|
|
var $this = $( this ),
|
|
$select = $this.parents( '.dropdown-select' ),
|
|
newSelection = $this.text();
|
|
$select.find( '.dropdown-select-selected' ).text( newSelection );
|
|
$select.trigger( 'change.dropdown-select', newSelection );
|
|
}
|
|
|
|
$select.find( 'a' ).click( selectThis );
|
|
return $select;
|
|
}
|
|
|
|
|
|
//==============================================================================
|
|
(function(){
|
|
/**
|
|
* Creates a three part bootstrap button group (key, op, value) meant to
|
|
* allow the user control of filters (e.g. { key: 'name', op: 'contains', value: 'my_history' })
|
|
*
|
|
* Each field uses a dropDownSelect (from ui.js) to allow selection
|
|
* (with the 'value' field appearing as an input when set to do so).
|
|
*
|
|
* Any change or update in any of the fields will trigger a 'change.filter-control'
|
|
* event which will be passed an object containing those fields (as the example above).
|
|
*
|
|
* Pass in an array of possible filter objects to control what the user can select.
|
|
* Each filter object should have:
|
|
* key : generally the attribute name on which to filter something
|
|
* ops : an array of 1 or more filter operations (e.g. [ 'is', '<', 'contains', '!=' ])
|
|
* values (optional) : an array of possible values for the filter (e.g. [ 'true', 'false' ])
|
|
* @example:
|
|
* $( '.my-div' ).filterControl({
|
|
* filters : [
|
|
* { key: 'name', ops: [ 'is exactly', 'contains' ] }
|
|
* { key: 'deleted', ops: [ 'is' ], values: [ 'true', 'false' ] }
|
|
* ]
|
|
* });
|
|
* // after initialization, you can prog. get the current value using:
|
|
* $( '.my-div' ).filterControl( 'val' )
|
|
*
|
|
*/
|
|
function FilterControl( element, options ){
|
|
return this.init( element, options );
|
|
}
|
|
/** the data key that this object will be stored under in the DOM element */
|
|
FilterControl.prototype.DATA_KEY = 'filter-control';
|
|
|
|
/** parses options, sets up instance vars, and does initial render */
|
|
FilterControl.prototype.init = function _init( element, options ){
|
|
options = options || { filters: [] };
|
|
this.$element = $( element ).addClass( 'filter-control btn-group' );
|
|
this.options = jQuery.extend( true, {}, this.defaults, options );
|
|
|
|
this.currFilter = this.options.filters[0];
|
|
return this.render();
|
|
};
|
|
|
|
/** render (or re-render) the controls on the element */
|
|
FilterControl.prototype.render = function _render(){
|
|
this.$element.empty()
|
|
.append([ this._renderKeySelect(), this._renderOpSelect(), this._renderValueInput() ]);
|
|
return this;
|
|
};
|
|
|
|
/** render the key dropDownSelect, bind a change event to it, and return it */
|
|
FilterControl.prototype._renderKeySelect = function __renderKeySelect(){
|
|
var filterControl = this;
|
|
var keys = this.options.filters.map( function( filter ){
|
|
return filter.key;
|
|
});
|
|
this.$keySelect = dropDownSelect( keys, this.currFilter.key )
|
|
.addClass( 'filter-control-key' )
|
|
.on( 'change.dropdown-select', function( event, selection ){
|
|
filterControl.currFilter = _.findWhere( filterControl.options.filters, { key: selection });
|
|
// when the filter/key changes, re-render the control entirely
|
|
filterControl.render()._triggerChange();
|
|
});
|
|
return this.$keySelect;
|
|
};
|
|
|
|
/** render the op dropDownSelect, bind a change event to it, and return it */
|
|
FilterControl.prototype._renderOpSelect = function __renderOpSelect(){
|
|
var filterControl = this,
|
|
ops = this.currFilter.ops;
|
|
//TODO: search for currOp in avail. ops: use that for selected if there; otherwise: first op
|
|
this.$opSelect = dropDownSelect( ops, ops[0] )
|
|
.addClass( 'filter-control-op' )
|
|
.on( 'change.dropdown-select', function( event, selection ){
|
|
filterControl._triggerChange();
|
|
});
|
|
return this.$opSelect;
|
|
};
|
|
|
|
/** render the value control, bind a change event to it, and return it */
|
|
FilterControl.prototype._renderValueInput = function __renderValueInput(){
|
|
var filterControl = this;
|
|
// if a values attribute is prov. on the filter - make this a dropdown; otherwise, use an input
|
|
if( this.currFilter.values ){
|
|
this.$valueSelect = dropDownSelect( this.currFilter.values, this.currFilter.values[0] )
|
|
.on( 'change.dropdown-select', function( event, selection ){
|
|
filterControl._triggerChange();
|
|
});
|
|
} else {
|
|
//TODO: allow setting a value type (mainly for which html5 input to use: range, number, etc.)
|
|
this.$valueSelect = $( '<input/>' ).addClass( 'form-control' )
|
|
.on( 'change', function( event, value ){
|
|
filterControl._triggerChange();
|
|
});
|
|
}
|
|
this.$valueSelect.addClass( 'filter-control-value' );
|
|
return this.$valueSelect;
|
|
};
|
|
|
|
/** return the current state/setting for the filter as a three key object: key, op, value */
|
|
FilterControl.prototype.val = function _val(){
|
|
var key = this.$element.find( '.filter-control-key .dropdown-select-selected' ).text(),
|
|
op = this.$element.find( '.filter-control-op .dropdown-select-selected' ).text(),
|
|
// handle either a dropdown or plain input
|
|
$value = this.$element.find( '.filter-control-value' ),
|
|
value = ( $value.hasClass( 'dropdown-select' ) )?( $value.find( '.dropdown-select-selected' ).text() )
|
|
:( $value.val() );
|
|
return { key: key, op: op, value: value };
|
|
};
|
|
|
|
// single point of change for change event
|
|
FilterControl.prototype._triggerChange = function __triggerChange(){
|
|
this.$element.trigger( 'change.filter-control', this.val() );
|
|
};
|
|
|
|
|
|
// as jq plugin
|
|
jQuery.fn.extend({
|
|
filterControl : function $filterControl( options ){
|
|
var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
|
|
return this.map( function(){
|
|
var $this = $( this ),
|
|
data = $this.data( FilterControl.prototype.DATA_KEY );
|
|
|
|
if( jQuery.type( options ) === 'object' ){
|
|
data = new FilterControl( $this, options );
|
|
$this.data( FilterControl.prototype.DATA_KEY, data );
|
|
}
|
|
if( data && jQuery.type( options ) === 'string' ){
|
|
var fn = data[ options ];
|
|
if( jQuery.type( fn ) === 'function' ){
|
|
return fn.apply( data, nonOptionsArgs );
|
|
}
|
|
}
|
|
return this;
|
|
});
|
|
}
|
|
});
|
|
}());
|
|
|
|
|
|
//==============================================================================
|
|
(function(){
|
|
/** Builds (twitter bootstrap styled) pagination controls.
|
|
* If the totalDataSize is not null, a horizontal list of page buttons is displayed.
|
|
* If totalDataSize is null, two links ('Prev' and 'Next) are displayed.
|
|
* When pages are changed, a 'pagination.page-change' event is fired
|
|
* sending the event and the (0-based) page requested.
|
|
*/
|
|
function Pagination( element, options ){
|
|
/** the total number of pages */
|
|
this.numPages = null;
|
|
/** the current, active page */
|
|
this.currPage = 0;
|
|
return this.init( element, options );
|
|
}
|
|
|
|
/** data key under which this object will be stored in the element */
|
|
Pagination.prototype.DATA_KEY = 'pagination';
|
|
/** default options */
|
|
Pagination.prototype.defaults = {
|
|
/** which page to begin at */
|
|
startingPage : 0,
|
|
/** number of data per page */
|
|
perPage : 20,
|
|
/** the total number of data (null == unknown) */
|
|
totalDataSize : null,
|
|
/** size of current data on current page */
|
|
currDataSize : null
|
|
};
|
|
|
|
/** init the control, calc numPages if possible, and render
|
|
* @param {jQuery} the element that will contain the pagination control
|
|
* @param {Object} options a map containing overrides to the pagination default options
|
|
*/
|
|
Pagination.prototype.init = function _init( $element, options ){
|
|
options = options || {};
|
|
this.$element = $element;
|
|
this.options = jQuery.extend( true, {}, this.defaults, options );
|
|
|
|
this.currPage = this.options.startingPage;
|
|
if( this.options.totalDataSize !== null ){
|
|
this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage );
|
|
// limit currPage by numPages
|
|
if( this.currPage >= this.numPages ){
|
|
this.currPage = this.numPages - 1;
|
|
}
|
|
}
|
|
//console.debug( 'Pagination.prototype.init:', this.$element, this.currPage );
|
|
//console.debug( JSON.stringify( this.options ) );
|
|
|
|
// bind to data of element
|
|
this.$element.data( Pagination.prototype.DATA_KEY, this );
|
|
|
|
this._render();
|
|
return this;
|
|
};
|
|
|
|
/** helper to create a simple li + a combo */
|
|
function _make$Li( contents ){
|
|
return $([
|
|
'<li><a href="javascript:void(0);">', contents, '</a></li>'
|
|
].join( '' ));
|
|
}
|
|
|
|
/** render previous and next pagination buttons */
|
|
Pagination.prototype._render = function __render(){
|
|
// no data - no pagination
|
|
if( this.options.totalDataSize === 0 ){ return this; }
|
|
// only one page
|
|
if( this.numPages === 1 ){ return this; }
|
|
|
|
// when the number of pages are known, render each page as a link
|
|
if( this.numPages > 0 ){
|
|
this._renderPages();
|
|
this._scrollToActivePage();
|
|
|
|
// when the number of pages is not known, render previous or next
|
|
} else {
|
|
this._renderPrevNext();
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/** render previous and next pagination buttons */
|
|
Pagination.prototype._renderPrevNext = function __renderPrevNext(){
|
|
var pagination = this,
|
|
$prev = _make$Li( 'Prev' ),
|
|
$next = _make$Li( 'Next' ),
|
|
$paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' );
|
|
|
|
// disable if it either end
|
|
if( this.currPage === 0 ){
|
|
$prev.addClass( 'disabled' );
|
|
} else {
|
|
$prev.click( function(){ pagination.prevPage(); });
|
|
}
|
|
if( ( this.numPages && this.currPage === ( this.numPages - 1 ) )
|
|
|| ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){
|
|
$next.addClass( 'disabled' );
|
|
} else {
|
|
$next.click( function(){ pagination.nextPage(); });
|
|
}
|
|
|
|
this.$element.html( $paginationContainer.append([ $prev, $next ]) );
|
|
//console.debug( this.$element, this.$element.html() );
|
|
return this.$element;
|
|
};
|
|
|
|
/** render page links for each possible page (if we can) */
|
|
Pagination.prototype._renderPages = function __renderPages(){
|
|
// it's better to scroll the control and let the user see all pages
|
|
// than to force her/him to change pages in order to find the one they want (as traditional << >> does)
|
|
var pagination = this,
|
|
$scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ),
|
|
$paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ),
|
|
page$LiClick = function( ev ){
|
|
pagination.goToPage( $( this ).data( 'page' ) );
|
|
};
|
|
|
|
for( var i=0; i<this.numPages; i+=1 ){
|
|
// add html5 data tag 'page' for later click event handler use
|
|
var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick );
|
|
// highlight the current page
|
|
if( i === this.currPage ){
|
|
$pageLi.addClass( 'active' );
|
|
}
|
|
//console.debug( '\t', $pageLi );
|
|
$paginationContainer.append( $pageLi );
|
|
}
|
|
return this.$element.html( $scrollingContainer.html( $paginationContainer ) );
|
|
};
|
|
|
|
/** scroll scroll-container (if any) to show the active page */
|
|
Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){
|
|
// scroll to show active page in center of scrollable area
|
|
var $container = this.$element.find( '.pagination-scroll-container' );
|
|
// no scroll container : don't scroll
|
|
if( !$container.size() ){ return this; }
|
|
|
|
var $activePage = this.$element.find( 'li.active' ),
|
|
midpoint = $container.width() / 2;
|
|
//console.debug( $container, $activePage, midpoint );
|
|
$container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint );
|
|
return this;
|
|
};
|
|
|
|
/** go to a certain page */
|
|
Pagination.prototype.goToPage = function goToPage( page ){
|
|
if( page <= 0 ){ page = 0; }
|
|
if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; }
|
|
if( page === this.currPage ){ return this; }
|
|
|
|
//console.debug( '\t going to page ' + page )
|
|
this.currPage = page;
|
|
this.$element.trigger( 'pagination.page-change', this.currPage );
|
|
//console.info( 'pagination:page-change', this.currPage );
|
|
this._render();
|
|
return this;
|
|
};
|
|
|
|
/** go to the previous page */
|
|
Pagination.prototype.prevPage = function prevPage(){
|
|
return this.goToPage( this.currPage - 1 );
|
|
};
|
|
|
|
/** go to the next page */
|
|
Pagination.prototype.nextPage = function nextPage(){
|
|
return this.goToPage( this.currPage + 1 );
|
|
};
|
|
|
|
/** return the current page */
|
|
Pagination.prototype.page = function page(){
|
|
return this.currPage;
|
|
};
|
|
|
|
// alternate constructor invocation
|
|
Pagination.create = function _create( $element, options ){
|
|
return new Pagination( $element, options );
|
|
};
|
|
|
|
// as jq plugin
|
|
jQuery.fn.extend({
|
|
pagination : function $pagination( options ){
|
|
var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
|
|
|
|
// if passed an object - use that as an options map to create pagination for each selected
|
|
if( jQuery.type( options ) === 'object' ){
|
|
return this.map( function(){
|
|
Pagination.create( $( this ), options );
|
|
return this;
|
|
});
|
|
}
|
|
|
|
// (other invocations only work on the first element in selected)
|
|
var $firstElement = $( this[0] ),
|
|
previousControl = $firstElement.data( Pagination.prototype.DATA_KEY );
|
|
// if a pagination control was found for this element, either...
|
|
if( previousControl ){
|
|
// invoke a function on the pagination object if passed a string (the function name)
|
|
if( jQuery.type( options ) === 'string' ){
|
|
var fn = previousControl[ options ];
|
|
if( jQuery.type( fn ) === 'function' ){
|
|
return fn.apply( previousControl, nonOptionsArgs );
|
|
}
|
|
|
|
// if passed nothing, return the previously set control
|
|
} else {
|
|
return previousControl;
|
|
}
|
|
}
|
|
// if there is no control already set, return undefined
|
|
return undefined;
|
|
}
|
|
});
|
|
}());
|
|
|
|
|
|
//==============================================================================
|
|
/** Column selection using the peek display as the control.
|
|
* Adds rows to the bottom of the peek with clickable areas in each cell
|
|
* to allow the user to select columns.
|
|
* Column selection can be limited to a single column or multiple.
|
|
* (Optionally) adds a left hand column of column selection prompts.
|
|
* (Optionally) allows the column headers to be clicked/renamed
|
|
* and set to some initial value.
|
|
* (Optionally) hides comment rows.
|
|
* (Optionally) allows pre-selecting and disabling certain columns for
|
|
* each row control.
|
|
*
|
|
* Construct by selecting a peek table to be used with jQuery and
|
|
* calling 'peekControl' with options.
|
|
* Options must include a 'controls' array and can include other options
|
|
* listed below.
|
|
* @example:
|
|
* $( 'pre.peek' ).peekControl({
|
|
* columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ],
|
|
* controls : [
|
|
* { label: 'X Column', id: 'xColumn' },
|
|
* { label: 'Y Column', id: 'yColumn', selected: 2 },
|
|
* { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] },
|
|
* { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true,
|
|
* selectedText: 'Included', unselectedText: 'Excluded' }
|
|
* ],
|
|
* renameColumns : true,
|
|
* hideCommentRows : true,
|
|
* includePrompts : true,
|
|
* topLeftContent : 'Data sample:'
|
|
* }).on( 'peek-control.change', function( ev, selection ){
|
|
* console.info( 'new selection:', selection );
|
|
* //{ yColumn: 2 }
|
|
* }).on( 'peek-control.rename', function( ev, names ){
|
|
* console.info( 'column names', names );
|
|
* //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ]
|
|
* });
|
|
*
|
|
* An event is fired when column selection is changed and the event
|
|
* is passed an object in the form: { the row id : the new selection value }.
|
|
* An event is also fired when the table headers are re-named and
|
|
* is passed the new array of column names.
|
|
*/
|
|
(function(){
|
|
|
|
/** option defaults */
|
|
var defaults = {
|
|
/** does this control allow renaming headers? */
|
|
renameColumns : false,
|
|
/** does this control allow renaming headers? */
|
|
columnNames : [],
|
|
/** the comment character used by the peek's datatype */
|
|
commentChar : '#',
|
|
/** should comment rows be shown or hidden in the peek */
|
|
hideCommentRows : false,
|
|
/** should a column of row control prompts be used */
|
|
includePrompts : true,
|
|
/** what is the content of the top left cell (often a title) */
|
|
topLeftContent : 'Columns:'
|
|
},
|
|
/** the string of the event fired when a control row changes */
|
|
CHANGE_EVENT = 'peek-control.change',
|
|
/** the string of the event fired when a column is renamed */
|
|
RENAME_EVENT = 'peek-control.rename',
|
|
/** class added to the pre.peek element (to allow css on just the control) */
|
|
PEEKCONTROL_CLASS = 'peek-control',
|
|
/** class added to the control rows */
|
|
ROW_CLASS = 'control',
|
|
/** class added to the left-hand cells that serve as row prompts */
|
|
PROMPT_CLASS = 'control-prompt',
|
|
/** class added to selected _cells_/tds */
|
|
SELECTED_CLASS = 'selected',
|
|
/** class added to disabled/un-clickable cells/tds */
|
|
DISABLED_CLASS = 'disabled',
|
|
/** class added to the clickable surface within a cell to select it */
|
|
BUTTON_CLASS = 'button',
|
|
/** class added to peek table header (th) cells to indicate they can be clicked and are renamable */
|
|
RENAMABLE_HEADER_CLASS = 'renamable-header',
|
|
/** the data key used for each cell to store the column index ('data-...') */
|
|
COLUMN_INDEX_DATA_KEY = 'column-index',
|
|
/** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */
|
|
COLUMN_NAME_DATA_KEY = 'column-name';
|
|
|
|
//TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.)
|
|
//TODO: needs clean up, move handlers to outer scope
|
|
|
|
// ........................................................................
|
|
/** validate the control data sent in for each row */
|
|
function validateControl( control ){
|
|
if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){
|
|
throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) );
|
|
}
|
|
if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){
|
|
throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) );
|
|
}
|
|
if( !control.label || !control.id ){
|
|
throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) );
|
|
}
|
|
if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){
|
|
throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) );
|
|
}
|
|
return control;
|
|
}
|
|
|
|
/** build the inner control surface (i.e. button-like) */
|
|
function buildButton( control, columnIndex ){
|
|
return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label );
|
|
}
|
|
|
|
/** build the basic (shared) cell structure */
|
|
function buildControlCell( control, columnIndex ){
|
|
var $td = $( '<td/>' )
|
|
.html( buildButton( control, columnIndex ) )
|
|
.attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex );
|
|
|
|
// disable if index in disabled array
|
|
if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){
|
|
$td.addClass( DISABLED_CLASS );
|
|
}
|
|
return $td;
|
|
}
|
|
|
|
/** set the text of the control based on selected/un */
|
|
function setSelectedText( $cell, control, columnIndex ){
|
|
var $button = $cell.children( '.' + BUTTON_CLASS );
|
|
if( $cell.hasClass( SELECTED_CLASS ) ){
|
|
$button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) );
|
|
} else {
|
|
$button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) );
|
|
}
|
|
}
|
|
|
|
/** build a cell for a row that only allows one selection */
|
|
function buildSingleSelectCell( control, columnIndex ){
|
|
// only one selection - selected is single index
|
|
var $cell = buildControlCell( control, columnIndex );
|
|
if( control.selected === columnIndex ){
|
|
$cell.addClass( SELECTED_CLASS );
|
|
}
|
|
setSelectedText( $cell, control, columnIndex );
|
|
|
|
// only add the handler to non-disabled controls
|
|
if( !$cell.hasClass( DISABLED_CLASS ) ){
|
|
$cell.click( function selectClick( ev ){
|
|
var $cell = $( this );
|
|
// don't re-select or fire event if already selected
|
|
if( !$cell.hasClass( SELECTED_CLASS ) ){
|
|
// only one can be selected - remove selected on all others, add it here
|
|
var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS );
|
|
$otherSelected.each( function(){
|
|
setSelectedText( $( this ), control, columnIndex );
|
|
});
|
|
|
|
$cell.addClass( SELECTED_CLASS );
|
|
setSelectedText( $cell, control, columnIndex );
|
|
|
|
// fire the event from the table itself, passing the id and index of selected
|
|
var eventData = {},
|
|
key = $cell.parent().attr( 'id' ),
|
|
val = $cell.data( COLUMN_INDEX_DATA_KEY );
|
|
eventData[ key ] = val;
|
|
$cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData );
|
|
}
|
|
});
|
|
}
|
|
return $cell;
|
|
}
|
|
|
|
/** build a cell for a row that allows multiple selections */
|
|
function buildMultiSelectCell( control, columnIndex ){
|
|
var $cell = buildControlCell( control, columnIndex );
|
|
// multiple selection - selected is an array
|
|
if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){
|
|
$cell.addClass( SELECTED_CLASS );
|
|
}
|
|
setSelectedText( $cell, control, columnIndex );
|
|
|
|
// only add the handler to non-disabled controls
|
|
if( !$cell.hasClass( DISABLED_CLASS ) ){
|
|
$cell.click( function multiselectClick( ev ){
|
|
var $cell = $( this );
|
|
// can be more than one selected - toggle selected on this cell
|
|
$cell.toggleClass( SELECTED_CLASS );
|
|
setSelectedText( $cell, control, columnIndex );
|
|
var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){
|
|
return $( e ).data( COLUMN_INDEX_DATA_KEY );
|
|
});
|
|
// fire the event from the table itself, passing the id and index of selected
|
|
var eventData = {},
|
|
key = $cell.parent().attr( 'id' ),
|
|
val = jQuery.makeArray( selectedColumnIndeces );
|
|
eventData[ key ] = val;
|
|
$cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData );
|
|
});
|
|
}
|
|
return $cell;
|
|
}
|
|
|
|
/** iterate over columns in peek and create a control for each */
|
|
function buildControlCells( count, control ){
|
|
var $cells = [];
|
|
// build a control for each column - using a build fn based on control
|
|
for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){
|
|
$cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex )
|
|
: buildSingleSelectCell( control, columnIndex ) );
|
|
}
|
|
return $cells;
|
|
}
|
|
|
|
/** build a row of controls for the peek */
|
|
function buildControlRow( cellCount, control, includePrompts ){
|
|
var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS );
|
|
if( includePrompts ){
|
|
var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' );
|
|
$controlRow.append( $promptCell );
|
|
}
|
|
$controlRow.append( buildControlCells( cellCount, control ) );
|
|
return $controlRow;
|
|
}
|
|
|
|
// ........................................................................
|
|
/** add to the peek, using options for configuration, return the peek */
|
|
function peekControl( options ){
|
|
options = jQuery.extend( true, {}, defaults, options );
|
|
|
|
var $peek = $( this ).addClass( PEEKCONTROL_CLASS ),
|
|
$peektable = $peek.find( 'table' ),
|
|
// get the size of the tables - width and height, number of comment rows
|
|
columnCount = $peektable.find( 'th' ).size(),
|
|
rowCount = $peektable.find( 'tr' ).size(),
|
|
// get the rows containing text starting with the comment char (also make them grey)
|
|
$commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){
|
|
var $this = $( this );
|
|
if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){
|
|
return $( this ).css( 'color', 'grey' ).parent().get(0);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
// should comment rows in the peek be hidden?
|
|
if( options.hideCommentRows ){
|
|
$commentRows.hide();
|
|
rowCount -= $commentRows.size();
|
|
}
|
|
//console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows );
|
|
|
|
// should a first column of control prompts be added?
|
|
if( options.includePrompts ){
|
|
var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent )
|
|
.attr( 'rowspan', rowCount );
|
|
$peektable.find( 'tr' ).first().prepend( $topLeft );
|
|
}
|
|
|
|
// save either the options column name or the parsed text of each column header in html5 data attr and text
|
|
var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){
|
|
var $this = $( this ),
|
|
// can be '1.name' or '1'
|
|
text = $this.text().replace( /^\d+\.*/, '' ),
|
|
name = options.columnNames[ i ] || text;
|
|
$this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name )
|
|
.text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) );
|
|
});
|
|
|
|
// allow renaming of columns when the header is clicked
|
|
if( options.renameColumns ){
|
|
$headers.addClass( RENAMABLE_HEADER_CLASS )
|
|
.click( function renameColumn(){
|
|
// prompt for new name
|
|
var $this = $( this ),
|
|
index = $this.index() + ( options.includePrompts? 0: 1 ),
|
|
prevName = $this.data( COLUMN_NAME_DATA_KEY ),
|
|
newColumnName = prompt( 'New column name:', prevName );
|
|
if( newColumnName !== null && newColumnName !== prevName ){
|
|
// set the new text and data
|
|
$this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) )
|
|
.data( COLUMN_NAME_DATA_KEY, newColumnName )
|
|
.attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName );
|
|
// fire event for new column names
|
|
var columnNames = jQuery.makeArray(
|
|
$this.parent().children( 'th:not(.top-left)' ).map( function(){
|
|
return $( this ).data( COLUMN_NAME_DATA_KEY );
|
|
}));
|
|
$this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames );
|
|
}
|
|
});
|
|
}
|
|
|
|
// build a row for each control
|
|
options.controls.forEach( function( control, i ){
|
|
validateControl( control );
|
|
var $controlRow = buildControlRow( columnCount, control, options.includePrompts );
|
|
$peektable.find( 'tbody' ).append( $controlRow );
|
|
});
|
|
return this;
|
|
}
|
|
|
|
// ........................................................................
|
|
// as jq plugin
|
|
jQuery.fn.extend({
|
|
peekControl : function $peekControl( options ){
|
|
return this.map( function(){
|
|
return peekControl.call( this, options );
|
|
});
|
|
}
|
|
});
|
|
}());
|