PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/client/galaxy/scripts/mvc/ui.js

https://bitbucket.org/remy_d1/galaxy-central-manageapi
JavaScript | 1654 lines | 1039 code | 169 blank | 446 comment | 156 complexity | 8347dca09cf1ac74ac8eea44a929c55a MD5 | raw file
Possible License(s): CC-BY-3.0

Large files files are truncated, but you can click here to view the full file

  1. /**
  2. * functions for creating major ui elements
  3. */
  4. /**
  5. * backbone model for icon buttons
  6. */
  7. var IconButton = Backbone.Model.extend({
  8. defaults: {
  9. title : "",
  10. icon_class : "",
  11. on_click : null,
  12. menu_options : null,
  13. is_menu_button : true,
  14. id : null,
  15. href : null,
  16. target : null,
  17. enabled : true,
  18. visible : true,
  19. tooltip_config : {}
  20. }
  21. });
  22. /**
  23. * backbone view for icon buttons
  24. */
  25. var IconButtonView = Backbone.View.extend({
  26. initialize : function(){
  27. // better rendering this way
  28. this.model.attributes.tooltip_config = { placement : 'bottom' };
  29. this.model.bind( 'change', this.render, this );
  30. },
  31. render : function( ){
  32. // hide tooltip
  33. this.$el.tooltip( 'hide' );
  34. var new_elem = this.template( this.model.toJSON() );
  35. // configure tooltip
  36. new_elem.tooltip( this.model.get( 'tooltip_config' ));
  37. this.$el.replaceWith( new_elem );
  38. this.setElement( new_elem );
  39. return this;
  40. },
  41. events : {
  42. 'click' : 'click'
  43. },
  44. click : function( event ){
  45. // if on_click pass to that function
  46. if( _.isFunction( this.model.get( 'on_click' ) ) ){
  47. this.model.get( 'on_click' )( event );
  48. return false;
  49. }
  50. // otherwise, bubble up ( to href or whatever )
  51. return true;
  52. },
  53. // generate html element
  54. template: function( options ){
  55. var buffer = 'title="' + options.title + '" class="icon-button';
  56. if( options.is_menu_button ){
  57. buffer += ' menu-button';
  58. }
  59. buffer += ' ' + options.icon_class;
  60. if( !options.enabled ){
  61. buffer += '_disabled';
  62. }
  63. // close class tag
  64. buffer += '"';
  65. if( options.id ){
  66. buffer += ' id="' + options.id + '"';
  67. }
  68. buffer += ' href="' + options.href + '"';
  69. // add target for href
  70. if( options.target ){
  71. buffer += ' target="' + options.target + '"';
  72. }
  73. // set visibility
  74. if( !options.visible ){
  75. buffer += ' style="display: none;"';
  76. }
  77. // enabled/disabled
  78. if ( options.enabled ){
  79. buffer = '<a ' + buffer + '/>';
  80. } else {
  81. buffer = '<span ' + buffer + '/>';
  82. }
  83. // return element
  84. return $( buffer );
  85. }
  86. } );
  87. // define collection
  88. var IconButtonCollection = Backbone.Collection.extend({
  89. model: IconButton
  90. });
  91. /**
  92. * menu with multiple icon buttons
  93. * views are not needed nor used for individual buttons
  94. */
  95. var IconButtonMenuView = Backbone.View.extend({
  96. tagName: 'div',
  97. initialize: function(){
  98. this.render();
  99. },
  100. render: function(){
  101. // initialize icon buttons
  102. var self = this;
  103. this.collection.each(function(button){
  104. // create and add icon button to menu
  105. var elt = $('<a/>')
  106. .attr('href', 'javascript:void(0)')
  107. .attr('title', button.attributes.title)
  108. .addClass('icon-button menu-button')
  109. .addClass(button.attributes.icon_class)
  110. .appendTo(self.$el)
  111. .click(button.attributes.on_click);
  112. // configure tooltip
  113. if (button.attributes.tooltip_config){
  114. elt.tooltip(button.attributes.tooltip_config);
  115. }
  116. // add popup menu to icon
  117. var menu_options = button.get('options');
  118. if (menu_options){
  119. make_popupmenu(elt, menu_options);
  120. }
  121. });
  122. // return
  123. return this;
  124. }
  125. });
  126. /**
  127. * Returns an IconButtonMenuView for the provided configuration.
  128. * Configuration is a list of dictionaries where each dictionary
  129. * defines an icon button. Each dictionary must have the following
  130. * elements: icon_class, title, and on_click.
  131. */
  132. var create_icon_buttons_menu = function(config, global_config)
  133. {
  134. // initialize global configuration
  135. if (!global_config) global_config = {};
  136. // create and initialize menu
  137. var buttons = new IconButtonCollection(
  138. _.map(config, function(button_config){
  139. return new IconButton(_.extend(button_config, global_config));
  140. })
  141. );
  142. // return menu
  143. return new IconButtonMenuView( {collection: buttons} );
  144. };
  145. // =============================================================================
  146. /**
  147. *
  148. */
  149. var Grid = Backbone.Collection.extend({
  150. });
  151. /**
  152. *
  153. */
  154. var GridView = Backbone.View.extend({
  155. });
  156. // =============================================================================
  157. /**
  158. * view for a popup menu
  159. */
  160. var PopupMenu = Backbone.View.extend({
  161. //TODO: maybe better as singleton off the Galaxy obj
  162. /** Cache the desired button element and options, set up the button click handler
  163. * NOTE: attaches this view as HTML/jQ data on the button for later use.
  164. */
  165. initialize: function( $button, options ){
  166. // default settings
  167. this.$button = $button;
  168. if( !this.$button.size() ){
  169. this.$button = $( '<div/>' );
  170. }
  171. this.options = options || [];
  172. // set up button click -> open menu behavior
  173. var menu = this;
  174. this.$button.click( function( event ){
  175. // if there's already a menu open, remove it
  176. $( '.popmenu-wrapper' ).remove();
  177. menu._renderAndShow( event );
  178. return false;
  179. });
  180. },
  181. // render the menu, append to the page body at the click position, and set up the 'click-away' handlers, show
  182. _renderAndShow: function( clickEvent ){
  183. this.render();
  184. this.$el.appendTo( 'body' ).css( this._getShownPosition( clickEvent )).show();
  185. this._setUpCloseBehavior();
  186. },
  187. // render the menu
  188. // this menu doesn't attach itself to the DOM ( see _renderAndShow )
  189. render: function(){
  190. // render the menu body absolute and hidden, fill with template
  191. this.$el.addClass( 'popmenu-wrapper' ).hide()
  192. .css({ position : 'absolute' })
  193. .html( this.template( this.$button.attr( 'id' ), this.options ));
  194. // set up behavior on each link/anchor elem
  195. if( this.options.length ){
  196. var menu = this;
  197. //precondition: there should be one option per li
  198. this.$el.find( 'li' ).each( function( i, li ){
  199. var option = menu.options[i];
  200. // if the option has 'func', call that function when the anchor is clicked
  201. if( option.func ){
  202. $( this ).children( 'a.popupmenu-option' ).click( function( event ){
  203. option.func.call( menu, event, option );
  204. // bubble up so that an option click will call the close behavior
  205. //return false;
  206. });
  207. }
  208. });
  209. }
  210. return this;
  211. },
  212. template : function( id, options ){
  213. return [
  214. '<ul id="', id, '-menu" class="dropdown-menu">', this._templateOptions( options ), '</ul>'
  215. ].join( '' );
  216. },
  217. _templateOptions : function( options ){
  218. if( !options.length ){
  219. return '<li>(no options)</li>';
  220. }
  221. return _.map( options, function( option ){
  222. if( option.divider ){
  223. return '<li class="divider"></li>';
  224. } else if( option.header ){
  225. return [ '<li class="head"><a href="javascript:void(0);">', option.html, '</a></li>' ].join( '' );
  226. }
  227. var href = option.href || 'javascript:void(0);',
  228. target = ( option.target )?( ' target="' + option.target + '"' ):( '' ),
  229. check = ( option.checked )?( '<span class="fa fa-check"></span>' ):( '' );
  230. return [
  231. '<li><a class="popupmenu-option" href="', href, '"', target, '>',
  232. check, option.html,
  233. '</a></li>'
  234. ].join( '' );
  235. }).join( '' );
  236. },
  237. // get the absolute position/offset for the menu
  238. _getShownPosition : function( clickEvent ){
  239. // display menu horiz. centered on click...
  240. var menuWidth = this.$el.width();
  241. var x = clickEvent.pageX - menuWidth / 2 ;
  242. // adjust to handle horiz. scroll and window dimensions ( draw entirely on visible screen area )
  243. x = Math.min( x, $( document ).scrollLeft() + $( window ).width() - menuWidth - 5 );
  244. x = Math.max( x, $( document ).scrollLeft() + 5 );
  245. return {
  246. top: clickEvent.pageY,
  247. left: x
  248. };
  249. },
  250. // bind an event handler to all available frames so that when anything is clicked
  251. // the menu is removed from the DOM and the event handler unbinds itself
  252. _setUpCloseBehavior: function(){
  253. var menu = this;
  254. //TODO: alternately: focus hack, blocking overlay, jquery.blockui
  255. // function to close popup and unbind itself
  256. function closePopup( event ){
  257. $( document ).off( 'click.close_popup' );
  258. if( window.parent !== window ){
  259. try {
  260. $( window.parent.document ).off( "click.close_popup" );
  261. } catch( err ){}
  262. } else {
  263. try {
  264. $( 'iframe#galaxy_main' ).contents().off( "click.close_popup" );
  265. } catch( err ){}
  266. }
  267. menu.remove();
  268. }
  269. $( 'html' ).one( "click.close_popup", closePopup );
  270. if( window.parent !== window ){
  271. try {
  272. $( window.parent.document ).find( 'html' ).one( "click.close_popup", closePopup );
  273. } catch( err ){}
  274. } else {
  275. try {
  276. $( 'iframe#galaxy_main' ).contents().one( "click.close_popup", closePopup );
  277. } catch( err ){}
  278. }
  279. },
  280. // add a menu option/item at the given index
  281. addItem: function( item, index ){
  282. // append to end if no index
  283. index = ( index >= 0 ) ? index : this.options.length;
  284. this.options.splice( index, 0, item );
  285. return this;
  286. },
  287. // remove a menu option/item at the given index
  288. removeItem: function( index ){
  289. if( index >=0 ){
  290. this.options.splice( index, 1 );
  291. }
  292. return this;
  293. },
  294. // search for a menu option by its html
  295. findIndexByHtml: function( html ){
  296. for( var i = 0; i < this.options.length; i++ ){
  297. if( _.has( this.options[i], 'html' ) && ( this.options[i].html === html )){
  298. return i;
  299. }
  300. }
  301. return null;
  302. },
  303. // search for a menu option by its html
  304. findItemByHtml: function( html ){
  305. return this.options[( this.findIndexByHtml( html ))];
  306. },
  307. // string representation
  308. toString: function(){
  309. return 'PopupMenu';
  310. }
  311. });
  312. /** shortcut to new for when you don't need to preserve the ref */
  313. PopupMenu.create = function _create( $button, options ){
  314. return new PopupMenu( $button, options );
  315. };
  316. // -----------------------------------------------------------------------------
  317. // the following class functions are bridges from the original make_popupmenu and make_popup_menus
  318. // to the newer backbone.js PopupMenu
  319. /** Create a PopupMenu from simple map initial_options activated by clicking button_element.
  320. * Converts initial_options to object array used by PopupMenu.
  321. * @param {jQuery|DOMElement} button_element element which, when clicked, activates menu
  322. * @param {Object} initial_options map of key -> values, where
  323. * key is option text, value is fn to call when option is clicked
  324. * @returns {PopupMenu} the PopupMenu created
  325. */
  326. PopupMenu.make_popupmenu = function( button_element, initial_options ){
  327. var convertedOptions = [];
  328. _.each( initial_options, function( optionVal, optionKey ){
  329. var newOption = { html: optionKey };
  330. // keys with null values indicate: header
  331. if( optionVal === null ){ // !optionVal? (null only?)
  332. newOption.header = true;
  333. // keys with function values indicate: a menu option
  334. } else if( jQuery.type( optionVal ) === 'function' ){
  335. newOption.func = optionVal;
  336. }
  337. //TODO:?? any other special optionVals?
  338. // there was no divider option originally
  339. convertedOptions.push( newOption );
  340. });
  341. return new PopupMenu( $( button_element ), convertedOptions );
  342. };
  343. /** Find all anchors in $parent (using selector) and covert anchors into a PopupMenu options map.
  344. * @param {jQuery} $parent the element that contains the links to convert to options
  345. * @param {String} selector jq selector string to find links
  346. * @returns {Object[]} the options array to initialize a PopupMenu
  347. */
  348. //TODO: lose parent and selector, pass in array of links, use map to return options
  349. PopupMenu.convertLinksToOptions = function( $parent, selector ){
  350. $parent = $( $parent );
  351. selector = selector || 'a';
  352. var options = [];
  353. $parent.find( selector ).each( function( elem, i ){
  354. var option = {}, $link = $( elem );
  355. // convert link text to the option text (html) and the href into the option func
  356. option.html = $link.text();
  357. if( $link.attr( 'href' ) ){
  358. var linkHref = $link.attr( 'href' ),
  359. linkTarget = $link.attr( 'target' ),
  360. confirmText = $link.attr( 'confirm' );
  361. option.func = function(){
  362. // if there's a "confirm" attribute, throw up a confirmation dialog, and
  363. // if the user cancels - do nothing
  364. if( ( confirmText ) && ( !confirm( confirmText ) ) ){ return; }
  365. // if there's no confirm attribute, or the user accepted the confirm dialog:
  366. switch( linkTarget ){
  367. // relocate the center panel
  368. case '_parent':
  369. window.parent.location = linkHref;
  370. break;
  371. // relocate the entire window
  372. case '_top':
  373. window.top.location = linkHref;
  374. break;
  375. // relocate this panel
  376. default:
  377. window.location = linkHref;
  378. }
  379. };
  380. }
  381. options.push( option );
  382. });
  383. return options;
  384. };
  385. /** Create a single popupmenu from existing DOM button and anchor elements
  386. * @param {jQuery} $buttonElement the element that when clicked will open the menu
  387. * @param {jQuery} $menuElement the element that contains the anchors to convert into a menu
  388. * @param {String} menuElementLinkSelector jq selector string used to find anchors to be made into menu options
  389. * @returns {PopupMenu} the PopupMenu (Backbone View) that can render, control the menu
  390. */
  391. PopupMenu.fromExistingDom = function( $buttonElement, $menuElement, menuElementLinkSelector ){
  392. $buttonElement = $( $buttonElement );
  393. $menuElement = $( $menuElement );
  394. var options = PopupMenu.convertLinksToOptions( $menuElement, menuElementLinkSelector );
  395. // we're done with the menu (having converted it to an options map)
  396. $menuElement.remove();
  397. return new PopupMenu( $buttonElement, options );
  398. };
  399. /** Create all popupmenus within a document or a more specific element
  400. * @param {DOMElement} parent the DOM element in which to search for popupmenus to build (defaults to document)
  401. * @param {String} menuSelector jq selector string to find popupmenu menu elements (defaults to "div[popupmenu]")
  402. * @param {Function} buttonSelectorBuildFn the function to build the jq button selector.
  403. * Will be passed $menuElement, parent.
  404. * (Defaults to return '#' + $menuElement.attr( 'popupmenu' ); )
  405. * @returns {PopupMenu[]} array of popupmenus created
  406. */
  407. PopupMenu.make_popup_menus = function( parent, menuSelector, buttonSelectorBuildFn ){
  408. parent = parent || document;
  409. // orig. Glx popupmenu menus have a (non-std) attribute 'popupmenu'
  410. // which contains the id of the button that activates the menu
  411. menuSelector = menuSelector || 'div[popupmenu]';
  412. // default to (orig. Glx) matching button to menu by using the popupmenu attr of the menu as the id of the button
  413. buttonSelectorBuildFn = buttonSelectorBuildFn || function( $menuElement, parent ){
  414. return '#' + $menuElement.attr( 'popupmenu' );
  415. };
  416. // aggregate and return all PopupMenus
  417. var popupMenusCreated = [];
  418. $( parent ).find( menuSelector ).each( function(){
  419. var $menuElement = $( this ),
  420. $buttonElement = $( parent ).find( buttonSelectorBuildFn( $menuElement, parent ) );
  421. popupMenusCreated.push( PopupMenu.fromDom( $buttonElement, $menuElement ) );
  422. $buttonElement.addClass( 'popup' );
  423. });
  424. return popupMenusCreated;
  425. };
  426. //==============================================================================
  427. var faIconButton = function( options ){
  428. //TODO: move out of global
  429. options = options || {};
  430. options.tooltipConfig = options.tooltipConfig || { placement: 'bottom' };
  431. options.classes = [ 'icon-btn' ].concat( options.classes || [] );
  432. if( options.disabled ){
  433. options.classes.push( 'disabled' );
  434. }
  435. var html = [
  436. '<a class="', options.classes.join( ' ' ), '"',
  437. (( options.title )?( ' title="' + options.title + '"' ):( '' )),
  438. (( !options.disabled && options.target )? ( ' target="' + options.target + '"' ):( '' )),
  439. ' href="', (( !options.disabled && options.href )?( options.href ):( 'javascript:void(0);' )), '">',
  440. // could go with something less specific here - like 'html'
  441. '<span class="fa ', options.faIcon, '"></span>',
  442. '</a>'
  443. ].join( '' );
  444. var $button = $( html ).tooltip( options.tooltipConfig );
  445. if( _.isFunction( options.onclick ) ){
  446. $button.click( options.onclick );
  447. }
  448. return $button;
  449. };
  450. //==============================================================================
  451. function LoadingIndicator( $where, options ){
  452. //TODO: move out of global
  453. //TODO: too specific to history panel
  454. var self = this;
  455. // defaults
  456. options = jQuery.extend({
  457. cover : false
  458. }, options || {} );
  459. function render(){
  460. var html = [
  461. '<div class="loading-indicator">',
  462. '<div class="loading-indicator-text">',
  463. '<span class="fa fa-spinner fa-spin fa-lg"></span>',
  464. '<span class="loading-indicator-message">loading...</span>',
  465. '</div>',
  466. '</div>'
  467. ].join( '\n' );
  468. var $indicator = $( html ).hide().css( options.css || {
  469. position : 'fixed'
  470. }),
  471. $text = $indicator.children( '.loading-indicator-text' );
  472. if( options.cover ){
  473. $indicator.css({
  474. 'z-index' : 2,
  475. top : $where.css( 'top' ),
  476. bottom : $where.css( 'bottom' ),
  477. left : $where.css( 'left' ),
  478. right : $where.css( 'right' ),
  479. opacity : 0.5,
  480. 'background-color': 'white',
  481. 'text-align': 'center'
  482. });
  483. $text = $indicator.children( '.loading-indicator-text' ).css({
  484. 'margin-top' : '20px'
  485. });
  486. } else {
  487. $text = $indicator.children( '.loading-indicator-text' ).css({
  488. margin : '12px 0px 0px 10px',
  489. opacity : '0.85',
  490. color : 'grey'
  491. });
  492. $text.children( '.loading-indicator-message' ).css({
  493. margin : '0px 8px 0px 0px',
  494. 'font-style' : 'italic'
  495. });
  496. }
  497. return $indicator;
  498. }
  499. self.show = function( msg, speed, callback ){
  500. msg = msg || 'loading...';
  501. speed = speed || 'fast';
  502. // remove previous
  503. $where.parent().find( '.loading-indicator' ).remove();
  504. // since position is fixed - we insert as sibling
  505. self.$indicator = render().insertBefore( $where );
  506. self.message( msg );
  507. self.$indicator.fadeIn( speed, callback );
  508. return self;
  509. };
  510. self.message = function( msg ){
  511. self.$indicator.find( 'i' ).text( msg );
  512. };
  513. self.hide = function( speed, callback ){
  514. speed = speed || 'fast';
  515. if( self.$indicator && self.$indicator.size() ){
  516. self.$indicator.fadeOut( speed, function(){
  517. self.$indicator.remove();
  518. if( callback ){ callback(); }
  519. });
  520. } else {
  521. if( callback ){ callback(); }
  522. }
  523. return self;
  524. };
  525. return self;
  526. }
  527. //==============================================================================
  528. (function(){
  529. /** searchInput: (jQuery plugin)
  530. * Creates a search input, a clear button, and loading indicator
  531. * within the selected node.
  532. *
  533. * When the user either presses return or enters some minimal number
  534. * of characters, a callback is called. Pressing ESC when the input
  535. * is focused will clear the input and call a separate callback.
  536. */
  537. var _l = window._l || function( s ){ return s; };
  538. // contructor
  539. function searchInput( parentNode, options ){
  540. //TODO: consolidate with tool menu functionality, use there
  541. var KEYCODE_ESC = 27,
  542. KEYCODE_RETURN = 13,
  543. $parentNode = $( parentNode ),
  544. firstSearch = true,
  545. defaults = {
  546. initialVal : '',
  547. name : 'search',
  548. placeholder : 'search',
  549. classes : '',
  550. onclear : function(){},
  551. onfirstsearch : null,
  552. onsearch : function( inputVal ){},
  553. minSearchLen : 0,
  554. escWillClear : true,
  555. oninit : function(){}
  556. };
  557. // .................................................................... input rendering and events
  558. // visually clear the search, trigger an event, and call the callback
  559. function clearSearchInput( event ){
  560. var $input = $( this ).parent().children( 'input' );
  561. //console.debug( this, 'clear', $input );
  562. $input.focus().val( '' ).trigger( 'clear:searchInput' );
  563. options.onclear();
  564. }
  565. // search for searchTerms, trigger an event, call the appropo callback (based on whether this is the first)
  566. function search( event, searchTerms ){
  567. //console.debug( this, 'searching', searchTerms );
  568. $( this ).trigger( 'search:searchInput', searchTerms );
  569. if( typeof options.onfirstsearch === 'function' && firstSearch ){
  570. firstSearch = false;
  571. options.onfirstsearch( searchTerms );
  572. } else {
  573. options.onsearch( searchTerms );
  574. }
  575. }
  576. // .................................................................... input rendering and events
  577. function inputTemplate(){
  578. // class search-query is bootstrap 2.3 style that now lives in base.less
  579. return [ '<input type="text" name="', options.name, '" placeholder="', options.placeholder, '" ',
  580. 'class="search-query ', options.classes, '" ', '/>' ].join( '' );
  581. }
  582. // the search input that responds to keyboard events and displays the search value
  583. function $input(){
  584. return $( inputTemplate() )
  585. // select all text on a focus
  586. .focus( function( event ){
  587. $( this ).select();
  588. })
  589. // attach behaviors to esc, return if desired, search on some min len string
  590. .keyup( function( event ){
  591. event.preventDefault();
  592. event.stopPropagation();
  593. //TODO: doesn't work
  594. if( !$( this ).val() ){ $( this ).blur(); }
  595. // esc key will clear if desired
  596. if( event.which === KEYCODE_ESC && options.escWillClear ){
  597. clearSearchInput.call( this, event );
  598. } else {
  599. var searchTerms = $( this ).val();
  600. // return key or the search string len > minSearchLen (if not 0) triggers search
  601. if( ( event.which === KEYCODE_RETURN )
  602. || ( options.minSearchLen && searchTerms.length >= options.minSearchLen ) ){
  603. search.call( this, event, searchTerms );
  604. } else if( !searchTerms.length ){
  605. clearSearchInput.call( this, event );
  606. }
  607. }
  608. })
  609. .on( 'change', function( event ){
  610. search.call( this, event, $( this ).val() );
  611. })
  612. .val( options.initialVal );
  613. }
  614. // .................................................................... clear button rendering and events
  615. // a button for clearing the search bar, placed on the right hand side
  616. function $clearBtn(){
  617. return $([ '<span class="search-clear fa fa-times-circle" ',
  618. 'title="', _l( 'clear search (esc)' ), '"></span>' ].join('') )
  619. .tooltip({ placement: 'bottom' })
  620. .click( function( event ){
  621. clearSearchInput.call( this, event );
  622. });
  623. }
  624. // .................................................................... loadingIndicator rendering
  625. // a button for clearing the search bar, placed on the right hand side
  626. function $loadingIndicator(){
  627. return $([ '<span class="search-loading fa fa-spinner fa-spin" ',
  628. 'title="', _l( 'loading...' ), '"></span>' ].join('') )
  629. .hide().tooltip({ placement: 'bottom' });
  630. }
  631. // .................................................................... commands
  632. // visually swap the load, clear buttons
  633. function toggleLoadingIndicator(){
  634. $parentNode.find( '.search-loading' ).toggle();
  635. $parentNode.find( '.search-clear' ).toggle();
  636. }
  637. // .................................................................... init
  638. // string command (not constructor)
  639. if( jQuery.type( options ) === 'string' ){
  640. if( options === 'toggle-loading' ){
  641. toggleLoadingIndicator();
  642. }
  643. return $parentNode;
  644. }
  645. // initial render
  646. if( jQuery.type( options ) === 'object' ){
  647. options = jQuery.extend( true, {}, defaults, options );
  648. }
  649. //NOTE: prepended
  650. return $parentNode.addClass( 'search-input' ).prepend([ $input(), $clearBtn(), $loadingIndicator() ]);
  651. }
  652. // as jq plugin
  653. jQuery.fn.extend({
  654. searchInput : function $searchInput( options ){
  655. return this.each( function(){
  656. return searchInput( this, options );
  657. });
  658. }
  659. });
  660. }());
  661. //==============================================================================
  662. (function(){
  663. /** Multi 'mode' button (or any element really) that changes the html
  664. * contents of itself when clicked. Pass in an ordered list of
  665. * objects with 'html' and (optional) onclick functions.
  666. *
  667. * When clicked in a particular node, the onclick function will
  668. * be called (with the element as this) and the element will
  669. * switch to the next mode, replacing its html content with
  670. * that mode's html.
  671. *
  672. * If there is no next mode, the element will switch back to
  673. * the first mode.
  674. * @example:
  675. * $( '.myElement' ).modeButton({
  676. * modes : [
  677. * {
  678. * mode: 'bler',
  679. * html: '<h5>Bler</h5>',
  680. * onclick : function(){
  681. * $( 'body' ).css( 'background-color', 'red' );
  682. * }
  683. * },
  684. * {
  685. * mode: 'bloo',
  686. * html: '<h4>Bloo</h4>',
  687. * onclick : function(){
  688. * $( 'body' ).css( 'background-color', 'blue' );
  689. * }
  690. * },
  691. * {
  692. * mode: 'blah',
  693. * html: '<h3>Blah</h3>',
  694. * onclick : function(){
  695. * $( 'body' ).css( 'background-color', 'grey' );
  696. * }
  697. * },
  698. * ]
  699. * });
  700. * $( '.myElement' ).modeButton( 'callModeFn', 'bler' );
  701. */
  702. /** constructor */
  703. function ModeButton( element, options ){
  704. this.currModeIndex = 0;
  705. return this._init( element, options );
  706. }
  707. /** html5 data key to store this object inside an element */
  708. ModeButton.prototype.DATA_KEY = 'mode-button';
  709. /** default options */
  710. ModeButton.prototype.defaults = {
  711. switchModesOnClick : true
  712. };
  713. // ---- private interface
  714. /** set up options, intial mode, and the click handler */
  715. ModeButton.prototype._init = function _init( element, options ){
  716. //console.debug( 'ModeButton._init:', element, options );
  717. options = options || {};
  718. this.$element = $( element );
  719. this.options = jQuery.extend( true, {}, this.defaults, options );
  720. if( !options.modes ){
  721. throw new Error( 'ModeButton requires a "modes" array' );
  722. }
  723. var modeButton = this;
  724. this.$element.click( function _ModeButtonClick( event ){
  725. // call the curr mode fn
  726. modeButton.callModeFn();
  727. // inc the curr mode index
  728. if( modeButton.options.switchModesOnClick ){ modeButton._incModeIndex(); }
  729. // set the element html
  730. $( this ).html( modeButton.options.modes[ modeButton.currModeIndex ].html );
  731. });
  732. return this.reset();
  733. };
  734. /** increment the mode index to the next in the array, looping back to zero if at the last */
  735. ModeButton.prototype._incModeIndex = function _incModeIndex(){
  736. this.currModeIndex += 1;
  737. if( this.currModeIndex >= this.options.modes.length ){
  738. this.currModeIndex = 0;
  739. }
  740. return this;
  741. };
  742. /** get the mode index in the modes array for the given key (mode name) */
  743. ModeButton.prototype._getModeIndex = function _getModeIndex( modeKey ){
  744. for( var i=0; i<this.options.modes.length; i+=1 ){
  745. if( this.options.modes[ i ].mode === modeKey ){ return i; }
  746. }
  747. throw new Error( 'mode not found: ' + modeKey );
  748. };
  749. /** set the current mode to the one with the given index and set button html */
  750. ModeButton.prototype._setModeByIndex = function _setModeByIndex( index ){
  751. var newMode = this.options.modes[ index ];
  752. if( !newMode ){
  753. throw new Error( 'mode index not found: ' + index );
  754. }
  755. this.currModeIndex = index;
  756. if( newMode.html ){
  757. this.$element.html( newMode.html );
  758. }
  759. return this;
  760. };
  761. // ---- public interface
  762. /** get the current mode object (not just the mode name) */
  763. ModeButton.prototype.currentMode = function currentMode(){
  764. return this.options.modes[ this.currModeIndex ];
  765. };
  766. /** return the mode key of the current mode */
  767. ModeButton.prototype.current = function current(){
  768. // sugar for returning mode name
  769. return this.currentMode().mode;
  770. };
  771. /** get the mode with the given modeKey or the current mode if modeKey is undefined */
  772. ModeButton.prototype.getMode = function getMode( modeKey ){
  773. if( !modeKey ){ return this.currentMode(); }
  774. return this.options.modes[( this._getModeIndex( modeKey ) )];
  775. };
  776. /** T/F if the button has the given mode */
  777. ModeButton.prototype.hasMode = function hasMode( modeKey ){
  778. try {
  779. return !!this.getMode( modeKey );
  780. } catch( err ){}
  781. return false;
  782. };
  783. /** set the current mode to the mode with the given name */
  784. ModeButton.prototype.setMode = function setMode( modeKey ){
  785. return this._setModeByIndex( this._getModeIndex( modeKey ) );
  786. };
  787. /** reset to the initial mode */
  788. ModeButton.prototype.reset = function reset(){
  789. this.currModeIndex = 0;
  790. if( this.options.initialMode ){
  791. this.currModeIndex = this._getModeIndex( this.options.initialMode );
  792. }
  793. return this._setModeByIndex( this.currModeIndex );
  794. };
  795. /** manually call the click handler of the given mode */
  796. ModeButton.prototype.callModeFn = function callModeFn( modeKey ){
  797. var modeFn = this.getMode( modeKey ).onclick;
  798. if( modeFn && jQuery.type( modeFn === 'function' ) ){
  799. // call with the element as context (std jquery pattern)
  800. return modeFn.call( this.$element.get(0) );
  801. }
  802. return undefined;
  803. };
  804. // as jq plugin
  805. jQuery.fn.extend({
  806. modeButton : function $modeButton( options ){
  807. if( !this.size() ){ return this; }
  808. //TODO: does map still work with jq multi selection (i.e. $( '.class-for-many-btns' ).modeButton)?
  809. if( jQuery.type( options ) === 'object' ){
  810. return this.map( function(){
  811. var $this = $( this );
  812. $this.data( 'mode-button', new ModeButton( $this, options ) );
  813. return this;
  814. });
  815. }
  816. var $first = $( this[0] ),
  817. button = $first.data( 'mode-button' );
  818. if( !button ){
  819. throw new Error( 'modeButton needs an options object or string name of a function' );
  820. }
  821. if( button && jQuery.type( options ) === 'string' ){
  822. var fnName = options;
  823. if( button && jQuery.type( button[ fnName ] ) === 'function' ){
  824. return button[ fnName ].apply( button, jQuery.makeArray( arguments ).slice( 1 ) );
  825. }
  826. }
  827. return button;
  828. }
  829. });
  830. }());
  831. //==============================================================================
  832. /**
  833. * Template function that produces a bootstrap dropdown to replace the
  834. * vanilla HTML select input. Pass in an array of options and an initial selection:
  835. * $( '.my-div' ).append( dropDownSelect( [ 'option1', 'option2' ], 'option2' );
  836. *
  837. * When the user changes the selected option a 'change.dropdown-select' event will
  838. * fire with both the jq event and the new selection text as arguments.
  839. *
  840. * Get the currently selected choice using:
  841. * var userChoice = $( '.my-div .dropdown-select .dropdown-select-selected' ).text();
  842. *
  843. */
  844. function dropDownSelect( options, selected ){
  845. // replacement for vanilla select element using bootstrap dropdowns instead
  846. selected = selected || (( !_.isEmpty( options ) )?( options[0] ):( '' ));
  847. var $select = $([
  848. '<div class="dropdown-select btn-group">',
  849. '<button type="button" class="btn btn-default">',
  850. '<span class="dropdown-select-selected">' + selected + '</span>',
  851. '</button>',
  852. '</div>'
  853. ].join( '\n' ));
  854. // if there's only one option, do not style/create as buttons, dropdown - use simple span
  855. // otherwise, a dropdown displaying the current selection
  856. if( options && options.length > 1 ){
  857. $select.find( 'button' )
  858. .addClass( 'dropdown-toggle' ).attr( 'data-toggle', 'dropdown' )
  859. .append( ' <span class="caret"></span>' );
  860. $select.append([
  861. '<ul class="dropdown-menu" role="menu">',
  862. _.map( options, function( option ){
  863. return [
  864. '<li><a href="javascript:void(0)">', option, '</a></li>'
  865. ].join( '' );
  866. }).join( '\n' ),
  867. '</ul>'
  868. ].join( '\n' ));
  869. }
  870. // trigger 'change.dropdown-select' when a new selection is made using the dropdown
  871. function selectThis( event ){
  872. var $this = $( this ),
  873. $select = $this.parents( '.dropdown-select' ),
  874. newSelection = $this.text();
  875. $select.find( '.dropdown-select-selected' ).text( newSelection );
  876. $select.trigger( 'change.dropdown-select', newSelection );
  877. }
  878. $select.find( 'a' ).click( selectThis );
  879. return $select;
  880. }
  881. //==============================================================================
  882. (function(){
  883. /**
  884. * Creates a three part bootstrap button group (key, op, value) meant to
  885. * allow the user control of filters (e.g. { key: 'name', op: 'contains', value: 'my_history' })
  886. *
  887. * Each field uses a dropDownSelect (from ui.js) to allow selection
  888. * (with the 'value' field appearing as an input when set to do so).
  889. *
  890. * Any change or update in any of the fields will trigger a 'change.filter-control'
  891. * event which will be passed an object containing those fields (as the example above).
  892. *
  893. * Pass in an array of possible filter objects to control what the user can select.
  894. * Each filter object should have:
  895. * key : generally the attribute name on which to filter something
  896. * ops : an array of 1 or more filter operations (e.g. [ 'is', '<', 'contains', '!=' ])
  897. * values (optional) : an array of possible values for the filter (e.g. [ 'true', 'false' ])
  898. * @example:
  899. * $( '.my-div' ).filterControl({
  900. * filters : [
  901. * { key: 'name', ops: [ 'is exactly', 'contains' ] }
  902. * { key: 'deleted', ops: [ 'is' ], values: [ 'true', 'false' ] }
  903. * ]
  904. * });
  905. * // after initialization, you can prog. get the current value using:
  906. * $( '.my-div' ).filterControl( 'val' )
  907. *
  908. */
  909. function FilterControl( element, options ){
  910. return this.init( element, options );
  911. }
  912. /** the data key that this object will be stored under in the DOM element */
  913. FilterControl.prototype.DATA_KEY = 'filter-control';
  914. /** parses options, sets up instance vars, and does initial render */
  915. FilterControl.prototype.init = function _init( element, options ){
  916. options = options || { filters: [] };
  917. this.$element = $( element ).addClass( 'filter-control btn-group' );
  918. this.options = jQuery.extend( true, {}, this.defaults, options );
  919. this.currFilter = this.options.filters[0];
  920. return this.render();
  921. };
  922. /** render (or re-render) the controls on the element */
  923. FilterControl.prototype.render = function _render(){
  924. this.$element.empty()
  925. .append([ this._renderKeySelect(), this._renderOpSelect(), this._renderValueInput() ]);
  926. return this;
  927. };
  928. /** render the key dropDownSelect, bind a change event to it, and return it */
  929. FilterControl.prototype._renderKeySelect = function __renderKeySelect(){
  930. var filterControl = this;
  931. var keys = this.options.filters.map( function( filter ){
  932. return filter.key;
  933. });
  934. this.$keySelect = dropDownSelect( keys, this.currFilter.key )
  935. .addClass( 'filter-control-key' )
  936. .on( 'change.dropdown-select', function( event, selection ){
  937. filterControl.currFilter = _.findWhere( filterControl.options.filters, { key: selection });
  938. // when the filter/key changes, re-render the control entirely
  939. filterControl.render()._triggerChange();
  940. });
  941. return this.$keySelect;
  942. };
  943. /** render the op dropDownSelect, bind a change event to it, and return it */
  944. FilterControl.prototype._renderOpSelect = function __renderOpSelect(){
  945. var filterControl = this,
  946. ops = this.currFilter.ops;
  947. //TODO: search for currOp in avail. ops: use that for selected if there; otherwise: first op
  948. this.$opSelect = dropDownSelect( ops, ops[0] )
  949. .addClass( 'filter-control-op' )
  950. .on( 'change.dropdown-select', function( event, selection ){
  951. filterControl._triggerChange();
  952. });
  953. return this.$opSelect;
  954. };
  955. /** render the value control, bind a change event to it, and return it */
  956. FilterControl.prototype._renderValueInput = function __renderValueInput(){
  957. var filterControl = this;
  958. // if a values attribute is prov. on the filter - make this a dropdown; otherwise, use an input
  959. if( this.currFilter.values ){
  960. this.$valueSelect = dropDownSelect( this.currFilter.values, this.currFilter.values[0] )
  961. .on( 'change.dropdown-select', function( event, selection ){
  962. filterControl._triggerChange();
  963. });
  964. } else {
  965. //TODO: allow setting a value type (mainly for which html5 input to use: range, number, etc.)
  966. this.$valueSelect = $( '<input/>' ).addClass( 'form-control' )
  967. .on( 'change', function( event, value ){
  968. filterControl._triggerChange();
  969. });
  970. }
  971. this.$valueSelect.addClass( 'filter-control-value' );
  972. return this.$valueSelect;
  973. };
  974. /** return the current state/setting for the filter as a three key object: key, op, value */
  975. FilterControl.prototype.val = function _val(){
  976. var key = this.$element.find( '.filter-control-key .dropdown-select-selected' ).text(),
  977. op = this.$element.find( '.filter-control-op .dropdown-select-selected' ).text(),
  978. // handle either a dropdown or plain input
  979. $value = this.$element.find( '.filter-control-value' ),
  980. value = ( $value.hasClass( 'dropdown-select' ) )?( $value.find( '.dropdown-select-selected' ).text() )
  981. :( $value.val() );
  982. return { key: key, op: op, value: value };
  983. };
  984. // single point of change for change event
  985. FilterControl.prototype._triggerChange = function __triggerChange(){
  986. this.$element.trigger( 'change.filter-control', this.val() );
  987. };
  988. // as jq plugin
  989. jQuery.fn.extend({
  990. filterControl : function $filterControl( options ){
  991. var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
  992. return this.map( function(){
  993. var $this = $( this ),
  994. data = $this.data( FilterControl.prototype.DATA_KEY );
  995. if( jQuery.type( options ) === 'object' ){
  996. data = new FilterControl( $this, options );
  997. $this.data( FilterControl.prototype.DATA_KEY, data );
  998. }
  999. if( data && jQuery.type( options ) === 'string' ){
  1000. var fn = data[ options ];
  1001. if( jQuery.type( fn ) === 'function' ){
  1002. return fn.apply( data, nonOptionsArgs );
  1003. }
  1004. }
  1005. return this;
  1006. });
  1007. }
  1008. });
  1009. }());
  1010. //==============================================================================
  1011. (function(){
  1012. /** Builds (twitter bootstrap styled) pagination controls.
  1013. * If the totalDataSize is not null, a horizontal list of page buttons is displayed.
  1014. * If totalDataSize is null, two links ('Prev' and 'Next) are displayed.
  1015. * When pages are changed, a 'pagination.page-change' event is fired
  1016. * sending the event and the (0-based) page requested.
  1017. */
  1018. function Pagination( element, options ){
  1019. /** the total number of pages */
  1020. this.numPages = null;
  1021. /** the current, active page */
  1022. this.currPage = 0;
  1023. return this.init( element, options );
  1024. }
  1025. /** data key under which this object will be stored in the element */
  1026. Pagination.prototype.DATA_KEY = 'pagination';
  1027. /** default options */
  1028. Pagination.prototype.defaults = {
  1029. /** which page to begin at */
  1030. startingPage : 0,
  1031. /** number of data per page */
  1032. perPage : 20,
  1033. /** the total number of data (null == unknown) */
  1034. totalDataSize : null,
  1035. /** size of current data on current page */
  1036. currDataSize : null
  1037. };
  1038. /** init the control, calc numPages if possible, and render
  1039. * @param {jQuery} the element that will contain the pagination control
  1040. * @param {Object} options a map containing overrides to the pagination default options
  1041. */
  1042. Pagination.prototype.init = function _init( $element, options ){
  1043. options = options || {};
  1044. this.$element = $element;
  1045. this.options = jQuery.extend( true, {}, this.defaults, options );
  1046. this.currPage = this.options.startingPage;
  1047. if( this.options.totalDataSize !== null ){
  1048. this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage );
  1049. // limit currPage by numPages
  1050. if( this.currPage >= this.numPages ){
  1051. this.currPage = this.numPages - 1;
  1052. }
  1053. }
  1054. //console.debug( 'Pagination.prototype.init:', this.$element, this.currPage );
  1055. //console.debug( JSON.stringify( this.options ) );
  1056. // bind to data of element
  1057. this.$element.data( Pagination.prototype.DATA_KEY, this );
  1058. this._render();
  1059. return this;
  1060. };
  1061. /** helper to create a simple li + a combo */
  1062. function _make$Li( contents ){
  1063. return $([
  1064. '<li><a href="javascript:void(0);">', contents, '</a></li>'
  1065. ].join( '' ));
  1066. }
  1067. /** render previous and next pagination buttons */
  1068. Pagination.prototype._render = function __render(){
  1069. // no data - no pagination
  1070. if( this.options.totalDataSize === 0 ){ return this; }
  1071. // only one page
  1072. if( this.numPages === 1 ){ return this; }
  1073. // when the number of pages are known, render each page as a link
  1074. if( this.numPages > 0 ){
  1075. this._renderPages();
  1076. this._scrollToActivePage();
  1077. // when the number of pages is not known, render previous or next
  1078. } else {
  1079. this._renderPrevNext();
  1080. }
  1081. return this;
  1082. };
  1083. /** render previous and next pagination buttons */
  1084. Pagination.prototype._renderPrevNext = function __renderPrevNext(){
  1085. var pagination = this,
  1086. $prev = _make$Li( 'Prev' ),
  1087. $next = _make$Li( 'Next' ),
  1088. $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' );
  1089. // disable if it either end
  1090. if( this.currPage === 0 ){
  1091. $prev.addClass( 'disabled' );
  1092. } else {
  1093. $prev.click( function(){ pagination.prevPage(); });
  1094. }
  1095. if( ( this.numPages && this.currPage === ( this.numPages - 1 ) )
  1096. || ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){
  1097. $next.addClass( 'disabled' );
  1098. } else {
  1099. $next.click( function(){ pagination.nextPage(); });
  1100. }
  1101. this.$element.html( $paginationContainer.append([ $prev, $next ]) );
  1102. //console.debug( this.$element, this.$element.html() );
  1103. return this.$element;
  1104. };
  1105. /** render page links for each possible page (if we can) */
  1106. Pagination.prototype._renderPages = function __renderPages(){
  1107. // it's better to scroll the control and let the user see all pages
  1108. // than to force her/him to change pages in order to find the one they want (as traditional << >> does)
  1109. var pagination = this,
  1110. $scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ),
  1111. $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ),
  1112. page$LiClick = function( ev ){
  1113. pagination.goToPage( $( this ).data( 'page' ) );
  1114. };
  1115. for( var i=0; i<this.numPages; i+=1 ){
  1116. // add html5 data tag 'page' for later click event handler use
  1117. var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick );
  1118. // highlight the current page
  1119. if( i === this.currPage ){
  1120. $pageLi.addClass( 'active' );
  1121. }
  1122. //console.debug( '\t', $pageLi );
  1123. $paginationContainer.append( $pageLi );
  1124. }
  1125. return this.$element.html( $scrollingContainer.html( $paginationContainer ) );
  1126. };
  1127. /** scroll scroll-container (if any) to show the active page */
  1128. Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){
  1129. // scroll to show active page in center of scrollable area
  1130. var $container = this.$element.find( '.pagination-scroll-container' );
  1131. // no scroll container : don't scroll
  1132. if( !$container.size() ){ return this; }
  1133. var $activePage = this.$element.find( 'li.active' ),
  1134. midpoint = $container.width() / 2;
  1135. //console.debug( $container, $activePage, midpoint );
  1136. $container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint );
  1137. return this;
  1138. };
  1139. /** go to a certain page */
  1140. Pagination.prototype.goToPage = function goToPage( page ){
  1141. if( page <= 0 ){ page = 0; }
  1142. if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; }
  1143. if( page === this.currPage ){ return this; }
  1144. //console.debug( '\t going to page ' + page )
  1145. this.currPage = page;
  1146. this.$element.trigger( 'pagination.page-change', this.currPage );
  1147. //console.info( 'pagination:page-change', this.currPage );
  1148. this._render();
  1149. return this;
  1150. };
  1151. /** go to the previous page */
  1152. Pagination.prototype.prevPage = function prevPage(){
  1153. return this.goToPage( this.currPage - 1 );
  1154. };
  1155. /** go to the next page */
  1156. Pagination.p

Large files files are truncated, but you can click here to view the full file