PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/ui.js

https://bitbucket.org/kellrott/galaxy-central
JavaScript | 1645 lines | 1032 code | 168 blank | 445 comment | 154 complexity | ee406b47260e011834e2a26c36d0fb7a 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. // since position is fixed - we insert as sibling
  503. self.$indicator = render().insertBefore( $where );
  504. self.message( msg );
  505. self.$indicator.fadeIn( speed, callback );
  506. return self;
  507. };
  508. self.message = function( msg ){
  509. self.$indicator.find( 'i' ).text( msg );
  510. };
  511. self.hide = function( speed, callback ){
  512. speed = speed || 'fast';
  513. if( self.$indicator && self.$indicator.size() ){
  514. self.$indicator.fadeOut( speed, function(){
  515. self.$indicator.remove();
  516. if( callback ){ callback(); }
  517. });
  518. } else {
  519. if( callback ){ callback(); }
  520. }
  521. return self;
  522. };
  523. return self;
  524. }
  525. //==============================================================================
  526. (function(){
  527. /** searchInput: (jQuery plugin)
  528. * Creates a search input, a clear button, and loading indicator
  529. * within the selected node.
  530. *
  531. * When the user either presses return or enters some minimal number
  532. * of characters, a callback is called. Pressing ESC when the input
  533. * is focused will clear the input and call a separate callback.
  534. */
  535. // contructor
  536. function searchInput( parentNode, options ){
  537. //TODO: consolidate with tool menu functionality, use there
  538. var KEYCODE_ESC = 27,
  539. KEYCODE_RETURN = 13,
  540. $parentNode = $( parentNode ),
  541. firstSearch = true,
  542. defaults = {
  543. initialVal : '',
  544. name : 'search',
  545. placeholder : 'search',
  546. classes : '',
  547. onclear : function(){},
  548. onfirstsearch : null,
  549. onsearch : function( inputVal ){},
  550. minSearchLen : 0,
  551. escWillClear : true,
  552. oninit : function(){}
  553. };
  554. // .................................................................... input rendering and events
  555. // visually clear the search, trigger an event, and call the callback
  556. function clearSearchInput( event ){
  557. //console.debug( this, 'clear' );
  558. var $input = $( this ).parent().children( 'input' );
  559. //console.debug( 'input', $input );
  560. $input.val( '' );
  561. $input.trigger( 'clear:searchInput' );
  562. options.onclear();
  563. }
  564. // search for searchTerms, trigger an event, call the appropo callback (based on whether this is the first)
  565. function search( event, searchTerms ){
  566. //console.debug( this, 'searching', searchTerms );
  567. $( this ).trigger( 'search:searchInput', searchTerms );
  568. if( typeof options.onfirstsearch === 'function' && firstSearch ){
  569. firstSearch = false;
  570. options.onfirstsearch( searchTerms );
  571. } else {
  572. options.onsearch( searchTerms );
  573. }
  574. }
  575. // .................................................................... input rendering and events
  576. function inputTemplate(){
  577. // class search-query is bootstrap 2.3 style that now lives in base.less
  578. return [ '<input type="text" name="', options.name, '" placeholder="', options.placeholder, '" ',
  579. 'class="search-query ', options.classes, '" ', '/>' ].join( '' );
  580. }
  581. // the search input that responds to keyboard events and displays the search value
  582. function $input(){
  583. return $( inputTemplate() )
  584. // select all text on a focus
  585. .focus( function( event ){
  586. $( this ).select();
  587. })
  588. // attach behaviors to esc, return if desired, search on some min len string
  589. .keyup( function( event ){
  590. // esc key will clear if desired
  591. if( event.which === KEYCODE_ESC && options.escWillClear ){
  592. clearSearchInput.call( this, event );
  593. } else {
  594. var searchTerms = $( this ).val();
  595. // return key or the search string len > minSearchLen (if not 0) triggers search
  596. if( ( event.which === KEYCODE_RETURN )
  597. || ( options.minSearchLen && searchTerms.length >= options.minSearchLen ) ){
  598. search.call( this, event, searchTerms );
  599. } else if( !searchTerms.length ){
  600. clearSearchInput.call( this, event );
  601. }
  602. }
  603. })
  604. .val( options.initialVal );
  605. }
  606. // .................................................................... clear button rendering and events
  607. // a button for clearing the search bar, placed on the right hand side
  608. function $clearBtn(){
  609. return $([ '<span class="search-clear fa fa-times-circle" ',
  610. 'title="', _l( 'clear search (esc)' ), '"></span>' ].join('') )
  611. .tooltip({ placement: 'bottom' })
  612. .click( function( event ){
  613. clearSearchInput.call( this, event );
  614. });
  615. }
  616. // .................................................................... loadingIndicator rendering
  617. // a button for clearing the search bar, placed on the right hand side
  618. function $loadingIndicator(){
  619. return $([ '<span class="search-loading fa fa-spinner fa-spin" ',
  620. 'title="', _l( 'loading...' ), '"></span>' ].join('') )
  621. .hide().tooltip({ placement: 'bottom' });
  622. }
  623. // .................................................................... commands
  624. // visually swap the load, clear buttons
  625. function toggleLoadingIndicator(){
  626. $parentNode.find( '.search-loading' ).toggle();
  627. $parentNode.find( '.search-clear' ).toggle();
  628. }
  629. // .................................................................... init
  630. // string command (not constructor)
  631. if( jQuery.type( options ) === 'string' ){
  632. if( options === 'toggle-loading' ){
  633. toggleLoadingIndicator();
  634. }
  635. return $parentNode;
  636. }
  637. // initial render
  638. if( jQuery.type( options ) === 'object' ){
  639. options = jQuery.extend( true, {}, defaults, options );
  640. }
  641. //NOTE: prepended
  642. return $parentNode.addClass( 'search-input' ).prepend([ $input(), $clearBtn(), $loadingIndicator() ]);
  643. }
  644. // as jq plugin
  645. jQuery.fn.extend({
  646. searchInput : function $searchInput( options ){
  647. return this.each( function(){
  648. return searchInput( this, options );
  649. });
  650. }
  651. });
  652. }());
  653. //==============================================================================
  654. (function(){
  655. /** Multi 'mode' button (or any element really) that changes the html
  656. * contents of itself when clicked. Pass in an ordered list of
  657. * objects with 'html' and (optional) onclick functions.
  658. *
  659. * When clicked in a particular node, the onclick function will
  660. * be called (with the element as this) and the element will
  661. * switch to the next mode, replacing its html content with
  662. * that mode's html.
  663. *
  664. * If there is no next mode, the element will switch back to
  665. * the first mode.
  666. * @example:
  667. * $( '.myElement' ).modeButton({
  668. * modes : [
  669. * {
  670. * mode: 'bler',
  671. * html: '<h5>Bler</h5>',
  672. * onclick : function(){
  673. * $( 'body' ).css( 'background-color', 'red' );
  674. * }
  675. * },
  676. * {
  677. * mode: 'bloo',
  678. * html: '<h4>Bloo</h4>',
  679. * onclick : function(){
  680. * $( 'body' ).css( 'background-color', 'blue' );
  681. * }
  682. * },
  683. * {
  684. * mode: 'blah',
  685. * html: '<h3>Blah</h3>',
  686. * onclick : function(){
  687. * $( 'body' ).css( 'background-color', 'grey' );
  688. * }
  689. * },
  690. * ]
  691. * });
  692. * $( '.myElement' ).modeButton( 'callModeFn', 'bler' );
  693. */
  694. /** constructor */
  695. function ModeButton( element, options ){
  696. this.currModeIndex = 0;
  697. return this._init( element, options );
  698. }
  699. /** html5 data key to store this object inside an element */
  700. ModeButton.prototype.DATA_KEY = 'mode-button';
  701. /** default options */
  702. ModeButton.prototype.defaults = {
  703. switchModesOnClick : true
  704. };
  705. // ---- private interface
  706. /** set up options, intial mode, and the click handler */
  707. ModeButton.prototype._init = function _init( element, options ){
  708. //console.debug( 'ModeButton._init:', element, options );
  709. options = options || {};
  710. this.$element = $( element );
  711. this.options = jQuery.extend( true, {}, this.defaults, options );
  712. if( !options.modes ){
  713. throw new Error( 'ModeButton requires a "modes" array' );
  714. }
  715. var modeButton = this;
  716. this.$element.click( function _ModeButtonClick( event ){
  717. // call the curr mode fn
  718. modeButton.callModeFn();
  719. // inc the curr mode index
  720. if( modeButton.options.switchModesOnClick ){ modeButton._incModeIndex(); }
  721. // set the element html
  722. $( this ).html( modeButton.options.modes[ modeButton.currModeIndex ].html );
  723. });
  724. return this.reset();
  725. };
  726. /** increment the mode index to the next in the array, looping back to zero if at the last */
  727. ModeButton.prototype._incModeIndex = function _incModeIndex(){
  728. this.currModeIndex += 1;
  729. if( this.currModeIndex >= this.options.modes.length ){
  730. this.currModeIndex = 0;
  731. }
  732. return this;
  733. };
  734. /** get the mode index in the modes array for the given key (mode name) */
  735. ModeButton.prototype._getModeIndex = function _getModeIndex( modeKey ){
  736. for( var i=0; i<this.options.modes.length; i+=1 ){
  737. if( this.options.modes[ i ].mode === modeKey ){ return i; }
  738. }
  739. throw new Error( 'mode not found: ' + modeKey );
  740. };
  741. /** set the current mode to the one with the given index and set button html */
  742. ModeButton.prototype._setModeByIndex = function _setModeByIndex( index ){
  743. var newMode = this.options.modes[ index ];
  744. if( !newMode ){
  745. throw new Error( 'mode index not found: ' + index );
  746. }
  747. this.currModeIndex = index;
  748. if( newMode.html ){
  749. this.$element.html( newMode.html );
  750. }
  751. return this;
  752. };
  753. // ---- public interface
  754. /** get the current mode object (not just the mode name) */
  755. ModeButton.prototype.currentMode = function currentMode(){
  756. return this.options.modes[ this.currModeIndex ];
  757. };
  758. /** return the mode key of the current mode */
  759. ModeButton.prototype.current = function current(){
  760. // sugar for returning mode name
  761. return this.currentMode().mode;
  762. };
  763. /** get the mode with the given modeKey or the current mode if modeKey is undefined */
  764. ModeButton.prototype.getMode = function getMode( modeKey ){
  765. if( !modeKey ){ return this.currentMode(); }
  766. return this.options.modes[( this._getModeIndex( modeKey ) )];
  767. };
  768. /** T/F if the button has the given mode */
  769. ModeButton.prototype.hasMode = function hasMode( modeKey ){
  770. try {
  771. return !!this.getMode( modeKey );
  772. } catch( err ){}
  773. return false;
  774. };
  775. /** set the current mode to the mode with the given name */
  776. ModeButton.prototype.setMode = function setMode( modeKey ){
  777. return this._setModeByIndex( this._getModeIndex( modeKey ) );
  778. };
  779. /** reset to the initial mode */
  780. ModeButton.prototype.reset = function reset(){
  781. this.currModeIndex = 0;
  782. if( this.options.initialMode ){
  783. this.currModeIndex = this._getModeIndex( this.options.initialMode );
  784. }
  785. return this._setModeByIndex( this.currModeIndex );
  786. };
  787. /** manually call the click handler of the given mode */
  788. ModeButton.prototype.callModeFn = function callModeFn( modeKey ){
  789. var modeFn = this.getMode( modeKey ).onclick;
  790. if( modeFn && jQuery.type( modeFn === 'function' ) ){
  791. // call with the element as context (std jquery pattern)
  792. return modeFn.call( this.$element.get(0) );
  793. }
  794. return undefined;
  795. };
  796. // as jq plugin
  797. jQuery.fn.extend({
  798. modeButton : function $modeButton( options ){
  799. if( !this.size() ){ return this; }
  800. //TODO: does map still work with jq multi selection (i.e. $( '.class-for-many-btns' ).modeButton)?
  801. if( jQuery.type( options ) === 'object' ){
  802. return this.map( function(){
  803. var $this = $( this );
  804. $this.data( 'mode-button', new ModeButton( $this, options ) );
  805. return this;
  806. });
  807. }
  808. var $first = $( this[0] ),
  809. button = $first.data( 'mode-button' );
  810. if( !button ){
  811. throw new Error( 'modeButton needs an options object or string name of a function' );
  812. }
  813. if( button && jQuery.type( options ) === 'string' ){
  814. var fnName = options;
  815. if( button && jQuery.type( button[ fnName ] ) === 'function' ){
  816. return button[ fnName ].apply( button, jQuery.makeArray( arguments ).slice( 1 ) );
  817. }
  818. }
  819. return button;
  820. }
  821. });
  822. }());
  823. //==============================================================================
  824. /**
  825. * Template function that produces a bootstrap dropdown to replace the
  826. * vanilla HTML select input. Pass in an array of options and an initial selection:
  827. * $( '.my-div' ).append( dropDownSelect( [ 'option1', 'option2' ], 'option2' );
  828. *
  829. * When the user changes the selected option a 'change.dropdown-select' event will
  830. * fire with both the jq event and the new selection text as arguments.
  831. *
  832. * Get the currently selected choice using:
  833. * var userChoice = $( '.my-div .dropdown-select .dropdown-select-selected' ).text();
  834. *
  835. */
  836. function dropDownSelect( options, selected ){
  837. // replacement for vanilla select element using bootstrap dropdowns instead
  838. selected = selected || (( !_.isEmpty( options ) )?( options[0] ):( '' ));
  839. var $select = $([
  840. '<div class="dropdown-select btn-group">',
  841. '<button type="button" class="btn btn-default">',
  842. '<span class="dropdown-select-selected">' + selected + '</span>',
  843. '</button>',
  844. '</div>'
  845. ].join( '\n' ));
  846. // if there's only one option, do not style/create as buttons, dropdown - use simple span
  847. // otherwise, a dropdown displaying the current selection
  848. if( options && options.length > 1 ){
  849. $select.find( 'button' )
  850. .addClass( 'dropdown-toggle' ).attr( 'data-toggle', 'dropdown' )
  851. .append( ' <span class="caret"></span>' );
  852. $select.append([
  853. '<ul class="dropdown-menu" role="menu">',
  854. _.map( options, function( option ){
  855. return [
  856. '<li><a href="javascript:void(0)">', option, '</a></li>'
  857. ].join( '' );
  858. }).join( '\n' ),
  859. '</ul>'
  860. ].join( '\n' ));
  861. }
  862. // trigger 'change.dropdown-select' when a new selection is made using the dropdown
  863. function selectThis( event ){
  864. var $this = $( this ),
  865. $select = $this.parents( '.dropdown-select' ),
  866. newSelection = $this.text();
  867. $select.find( '.dropdown-select-selected' ).text( newSelection );
  868. $select.trigger( 'change.dropdown-select', newSelection );
  869. }
  870. $select.find( 'a' ).click( selectThis );
  871. return $select;
  872. }
  873. //==============================================================================
  874. (function(){
  875. /**
  876. * Creates a three part bootstrap button group (key, op, value) meant to
  877. * allow the user control of filters (e.g. { key: 'name', op: 'contains', value: 'my_history' })
  878. *
  879. * Each field uses a dropDownSelect (from ui.js) to allow selection
  880. * (with the 'value' field appearing as an input when set to do so).
  881. *
  882. * Any change or update in any of the fields will trigger a 'change.filter-control'
  883. * event which will be passed an object containing those fields (as the example above).
  884. *
  885. * Pass in an array of possible filter objects to control what the user can select.
  886. * Each filter object should have:
  887. * key : generally the attribute name on which to filter something
  888. * ops : an array of 1 or more filter operations (e.g. [ 'is', '<', 'contains', '!=' ])
  889. * values (optional) : an array of possible values for the filter (e.g. [ 'true', 'false' ])
  890. * @example:
  891. * $( '.my-div' ).filterControl({
  892. * filters : [
  893. * { key: 'name', ops: [ 'is exactly', 'contains' ] }
  894. * { key: 'deleted', ops: [ 'is' ], values: [ 'true', 'false' ] }
  895. * ]
  896. * });
  897. * // after initialization, you can prog. get the current value using:
  898. * $( '.my-div' ).filterControl( 'val' )
  899. *
  900. */
  901. function FilterControl( element, options ){
  902. return this.init( element, options );
  903. }
  904. /** the data key that this object will be stored under in the DOM element */
  905. FilterControl.prototype.DATA_KEY = 'filter-control';
  906. /** parses options, sets up instance vars, and does initial render */
  907. FilterControl.prototype.init = function _init( element, options ){
  908. options = options || { filters: [] };
  909. this.$element = $( element ).addClass( 'filter-control btn-group' );
  910. this.options = jQuery.extend( true, {}, this.defaults, options );
  911. this.currFilter = this.options.filters[0];
  912. return this.render();
  913. };
  914. /** render (or re-render) the controls on the element */
  915. FilterControl.prototype.render = function _render(){
  916. this.$element.empty()
  917. .append([ this._renderKeySelect(), this._renderOpSelect(), this._renderValueInput() ]);
  918. return this;
  919. };
  920. /** render the key dropDownSelect, bind a change event to it, and return it */
  921. FilterControl.prototype._renderKeySelect = function __renderKeySelect(){
  922. var filterControl = this;
  923. var keys = this.options.filters.map( function( filter ){
  924. return filter.key;
  925. });
  926. this.$keySelect = dropDownSelect( keys, this.currFilter.key )
  927. .addClass( 'filter-control-key' )
  928. .on( 'change.dropdown-select', function( event, selection ){
  929. filterControl.currFilter = _.findWhere( filterControl.options.filters, { key: selection });
  930. // when the filter/key changes, re-render the control entirely
  931. filterControl.render()._triggerChange();
  932. });
  933. return this.$keySelect;
  934. };
  935. /** render the op dropDownSelect, bind a change event to it, and return it */
  936. FilterControl.prototype._renderOpSelect = function __renderOpSelect(){
  937. var filterControl = this,
  938. ops = this.currFilter.ops;
  939. //TODO: search for currOp in avail. ops: use that for selected if there; otherwise: first op
  940. this.$opSelect = dropDownSelect( ops, ops[0] )
  941. .addClass( 'filter-control-op' )
  942. .on( 'change.dropdown-select', function( event, selection ){
  943. filterControl._triggerChange();
  944. });
  945. return this.$opSelect;
  946. };
  947. /** render the value control, bind a change event to it, and return it */
  948. FilterControl.prototype._renderValueInput = function __renderValueInput(){
  949. var filterControl = this;
  950. // if a values attribute is prov. on the filter - make this a dropdown; otherwise, use an input
  951. if( this.currFilter.values ){
  952. this.$valueSelect = dropDownSelect( this.currFilter.values, this.currFilter.values[0] )
  953. .on( 'change.dropdown-select', function( event, selection ){
  954. filterControl._triggerChange();
  955. });
  956. } else {
  957. //TODO: allow setting a value type (mainly for which html5 input to use: range, number, etc.)
  958. this.$valueSelect = $( '<input/>' ).addClass( 'form-control' )
  959. .on( 'change', function( event, value ){
  960. filterControl._triggerChange();
  961. });
  962. }
  963. this.$valueSelect.addClass( 'filter-control-value' );
  964. return this.$valueSelect;
  965. };
  966. /** return the current state/setting for the filter as a three key object: key, op, value */
  967. FilterControl.prototype.val = function _val(){
  968. var key = this.$element.find( '.filter-control-key .dropdown-select-selected' ).text(),
  969. op = this.$element.find( '.filter-control-op .dropdown-select-selected' ).text(),
  970. // handle either a dropdown or plain input
  971. $value = this.$element.find( '.filter-control-value' ),
  972. value = ( $value.hasClass( 'dropdown-select' ) )?( $value.find( '.dropdown-select-selected' ).text() )
  973. :( $value.val() );
  974. return { key: key, op: op, value: value };
  975. };
  976. // single point of change for change event
  977. FilterControl.prototype._triggerChange = function __triggerChange(){
  978. this.$element.trigger( 'change.filter-control', this.val() );
  979. };
  980. // as jq plugin
  981. jQuery.fn.extend({
  982. filterControl : function $filterControl( options ){
  983. var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
  984. return this.map( function(){
  985. var $this = $( this ),
  986. data = $this.data( FilterControl.prototype.DATA_KEY );
  987. if( jQuery.type( options ) === 'object' ){
  988. data = new FilterControl( $this, options );
  989. $this.data( FilterControl.prototype.DATA_KEY, data );
  990. }
  991. if( data && jQuery.type( options ) === 'string' ){
  992. var fn = data[ options ];
  993. if( jQuery.type( fn ) === 'function' ){
  994. return fn.apply( data, nonOptionsArgs );
  995. }
  996. }
  997. return this;
  998. });
  999. }
  1000. });
  1001. }());
  1002. //==============================================================================
  1003. (function(){
  1004. /** Builds (twitter bootstrap styled) pagination controls.
  1005. * If the totalDataSize is not null, a horizontal list of page buttons is displayed.
  1006. * If totalDataSize is null, two links ('Prev' and 'Next) are displayed.
  1007. * When pages are changed, a 'pagination.page-change' event is fired
  1008. * sending the event and the (0-based) page requested.
  1009. */
  1010. function Pagination( element, options ){
  1011. /** the total number of pages */
  1012. this.numPages = null;
  1013. /** the current, active page */
  1014. this.currPage = 0;
  1015. return this.init( element, options );
  1016. }
  1017. /** data key under which this object will be stored in the element */
  1018. Pagination.prototype.DATA_KEY = 'pagination';
  1019. /** default options */
  1020. Pagination.prototype.defaults = {
  1021. /** which page to begin at */
  1022. startingPage : 0,
  1023. /** number of data per page */
  1024. perPage : 20,
  1025. /** the total number of data (null == unknown) */
  1026. totalDataSize : null,
  1027. /** size of current data on current page */
  1028. currDataSize : null
  1029. };
  1030. /** init the control, calc numPages if possible, and render
  1031. * @param {jQuery} the element that will contain the pagination control
  1032. * @param {Object} options a map containing overrides to the pagination default options
  1033. */
  1034. Pagination.prototype.init = function _init( $element, options ){
  1035. options = options || {};
  1036. this.$element = $element;
  1037. this.options = jQuery.extend( true, {}, this.defaults, options );
  1038. this.currPage = this.options.startingPage;
  1039. if( this.options.totalDataSize !== null ){
  1040. this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage );
  1041. // limit currPage by numPages
  1042. if( this.currPage >= this.numPages ){
  1043. this.currPage = this.numPages - 1;
  1044. }
  1045. }
  1046. //console.debug( 'Pagination.prototype.init:', this.$element, this.currPage );
  1047. //console.debug( JSON.stringify( this.options ) );
  1048. // bind to data of element
  1049. this.$element.data( Pagination.prototype.DATA_KEY, this );
  1050. this._render();
  1051. return this;
  1052. };
  1053. /** helper to create a simple li + a combo */
  1054. function _make$Li( contents ){
  1055. return $([
  1056. '<li><a href="javascript:void(0);">', contents, '</a></li>'
  1057. ].join( '' ));
  1058. }
  1059. /** render previous and next pagination buttons */
  1060. Pagination.prototype._render = function __render(){
  1061. // no data - no pagination
  1062. if( this.options.totalDataSize === 0 ){ return this; }
  1063. // only one page
  1064. if( this.numPages === 1 ){ return this; }
  1065. // when the number of pages are known, render each page as a link
  1066. if( this.numPages > 0 ){
  1067. this._renderPages();
  1068. this._scrollToActivePage();
  1069. // when the number of pages is not known, render previous or next
  1070. } else {
  1071. this._renderPrevNext();
  1072. }
  1073. return this;
  1074. };
  1075. /** render previous and next pagination buttons */
  1076. Pagination.prototype._renderPrevNext = function __renderPrevNext(){
  1077. var pagination = this,
  1078. $prev = _make$Li( 'Prev' ),
  1079. $next = _make$Li( 'Next' ),
  1080. $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' );
  1081. // disable if it either end
  1082. if( this.currPage === 0 ){
  1083. $prev.addClass( 'disabled' );
  1084. } else {
  1085. $prev.click( function(){ pagination.prevPage(); });
  1086. }
  1087. if( ( this.numPages && this.currPage === ( this.numPages - 1 ) )
  1088. || ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){
  1089. $next.addClass( 'disabled' );
  1090. } else {
  1091. $next.click( function(){ pagination.nextPage(); });
  1092. }
  1093. this.$element.html( $paginationContainer.append([ $prev, $next ]) );
  1094. //console.debug( this.$element, this.$element.html() );
  1095. return this.$element;
  1096. };
  1097. /** render page links for each possible page (if we can) */
  1098. Pagination.prototype._renderPages = function __renderPages(){
  1099. // it's better to scroll the control and let the user see all pages
  1100. // than to force her/him to change pages in order to find the one they want (as traditional << >> does)
  1101. var pagination = this,
  1102. $scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ),
  1103. $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ),
  1104. page$LiClick = function( ev ){
  1105. pagination.goToPage( $( this ).data( 'page' ) );
  1106. };
  1107. for( var i=0; i<this.numPages; i+=1 ){
  1108. // add html5 data tag 'page' for later click event handler use
  1109. var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick );
  1110. // highlight the current page
  1111. if( i === this.currPage ){
  1112. $pageLi.addClass( 'active' );
  1113. }
  1114. //console.debug( '\t', $pageLi );
  1115. $paginationContainer.append( $pageLi );
  1116. }
  1117. return this.$element.html( $scrollingContainer.html( $paginationContainer ) );
  1118. };
  1119. /** scroll scroll-container (if any) to show the active page */
  1120. Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){
  1121. // scroll to show active page in center of scrollable area
  1122. var $container = this.$element.find( '.pagination-scroll-container' );
  1123. // no scroll container : don't scroll
  1124. if( !$container.size() ){ return this; }
  1125. var $activePage = this.$element.find( 'li.active' ),
  1126. midpoint = $container.width() / 2;
  1127. //console.debug( $container, $activePage, midpoint );
  1128. $container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint );
  1129. return this;
  1130. };
  1131. /** go to a certain page */
  1132. Pagination.prototype.goToPage = function goToPage( page ){
  1133. if( page <= 0 ){ page = 0; }
  1134. if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; }
  1135. if( page === this.currPage ){ return this; }
  1136. //console.debug( '\t going to page ' + page )
  1137. this.currPage = page;
  1138. this.$element.trigger( 'pagination.page-change', this.currPage );
  1139. //console.info( 'pagination:page-change', this.currPage );
  1140. this._render();
  1141. return this;
  1142. };
  1143. /** go to the previous page */
  1144. Pagination.prototype.prevPage = function prevPage(){
  1145. return this.goToPage( this.currPage - 1 );
  1146. };
  1147. /** go to the next page */
  1148. Pagination.prototype.nextPage = function nextPage(){
  1149. return this.goToPage( this.currPage + 1 );
  1150. };
  1151. /** return the current page */
  1152. Pagination.prototype.page = function page(){
  1153. return this.currPage;
  1154. };
  1155. // alternate constructor invocation
  1156. Pagination.create = function _create( $element, options ){
  1157. return new Pagination( $element, options );
  1158. };
  1159. // as jq plu…

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