PageRenderTime 65ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/mvc/ui.js

https://bitbucket.org/galaxy/galaxy-central
JavaScript | 478 lines | 261 code | 67 blank | 150 comment | 25 complexity | 678fbcd1e2b91efa551864ea2948fa32 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. /**
  2. * -- Functions for creating large UI elements. --
  3. */
  4. // =============================================================================
  5. /**
  6. * -- Utility models and views for Galaxy objects. --
  7. */
  8. /**
  9. * Clickable button represented as an icon.
  10. */
  11. var IconButton = Backbone.Model.extend({
  12. defaults: {
  13. title: "",
  14. icon_class: "",
  15. on_click: null,
  16. menu_options: null,
  17. tooltip_config: {},
  18. isMenuButton : true,
  19. id : null,
  20. href : null,
  21. target : null,
  22. enabled : true,
  23. visible : true
  24. }
  25. //validate : function( attributes ){
  26. //TODO: validate href or on_click
  27. //TODO: validate icon_class
  28. //}
  29. });
  30. /**
  31. *
  32. */
  33. var IconButtonView = Backbone.View.extend({
  34. initialize : function(){
  35. // better rendering this way (for me anyway)
  36. this.model.attributes.tooltip_config = { placement : 'bottom' };
  37. this.model.bind( 'change', this.render, this );
  38. },
  39. render : function(){
  40. //NOTE: not doing this hide will lead to disappearing buttons when they're both being hovered over & rendered
  41. this.$el.tooltip( 'hide' );
  42. // template in common-templates.html
  43. var newElem = $( Handlebars.partials.iconButton( this.model.toJSON() ) );
  44. newElem.tooltip( this.model.get( 'tooltip_config' ) );
  45. this.$el.replaceWith( newElem );
  46. this.setElement( newElem );
  47. return this;
  48. },
  49. events : {
  50. 'click' : 'click'
  51. },
  52. click : function( event ){
  53. // if on_click pass to that function
  54. if( this.model.attributes.on_click ){
  55. this.model.attributes.on_click( event );
  56. return false;
  57. }
  58. // otherwise, bubble up (to href or whatever)
  59. return true;
  60. }
  61. });
  62. //TODO: bc h.templates is gen. loaded AFTER ui, Handlebars.partials.iconButton === undefined
  63. IconButtonView.templates = {
  64. iconButton : Handlebars.partials.iconButton
  65. };
  66. var IconButtonCollection = Backbone.Collection.extend({
  67. model: IconButton
  68. });
  69. //------------------------------------------------------------------------------
  70. /**
  71. * Menu with multiple icon buttons. Views are not needed nor used for individual buttons.
  72. */
  73. var IconButtonMenuView = Backbone.View.extend({
  74. tagName: 'div',
  75. initialize: function() {
  76. this.render();
  77. },
  78. render: function() {
  79. var self = this;
  80. this.collection.each(function(button) {
  81. // Create and add icon button to menu.
  82. var elt =
  83. $('<a/>').attr('href', 'javascript:void(0)')
  84. .attr('title', button.attributes.title)
  85. .addClass('icon-button menu-button')
  86. .addClass(button.attributes.icon_class)
  87. .appendTo(self.$el)
  88. .click(button.attributes.on_click);
  89. if (button.attributes.tooltip_config) {
  90. elt.tooltip(button.attributes.tooltip_config);
  91. }
  92. // If there are options, add popup menu to icon.
  93. var menu_options = button.get('options');
  94. if (menu_options) {
  95. make_popupmenu(elt, menu_options);
  96. }
  97. });
  98. return this;
  99. }
  100. });
  101. /**
  102. * Returns an IconButtonMenuView for the provided configuration.
  103. * Configuration is a list of dictionaries where each dictionary
  104. * defines an icon button. Each dictionary must have the following
  105. * elements: icon_class, title, and on_click.
  106. */
  107. var create_icon_buttons_menu = function(config, global_config) {
  108. if (!global_config) { global_config = {}; }
  109. // Create and initialize menu.
  110. var buttons = new IconButtonCollection(
  111. _.map(config, function(button_config) {
  112. return new IconButton(_.extend(button_config, global_config));
  113. })
  114. );
  115. return new IconButtonMenuView( {collection: buttons} );
  116. };
  117. // =============================================================================
  118. /**
  119. *
  120. */
  121. var Grid = Backbone.Collection.extend({
  122. });
  123. /**
  124. *
  125. */
  126. var GridView = Backbone.View.extend({
  127. });
  128. // =============================================================================
  129. /**
  130. * Necessary Galaxy paths.
  131. */
  132. var GalaxyPaths = Backbone.Model.extend({
  133. defaults: {
  134. root_path: "",
  135. image_path: ""
  136. }
  137. });
  138. // =============================================================================
  139. /** @class View for a popup menu
  140. * @name PopupMenu
  141. *
  142. * @constructs
  143. */
  144. var PopupMenu = Backbone.View.extend(
  145. /** @lends PopupMenu.prototype */{
  146. /* TODO:
  147. add submenus
  148. add hrefs
  149. test various html keys
  150. add make_popupmenus style
  151. get template inside this file somehow
  152. */
  153. /** Cache the desired button element and options, set up the button click handler
  154. * NOTE: attaches this view as HTML/jQ data on the button for later use.
  155. */
  156. //TODO: include docs on special option keys (divider, checked, etc.)
  157. initialize : function( $button, options ){
  158. // default settings
  159. this.$button = $button || $( '<div/>' );
  160. this.options = options || [];
  161. // set up button click -> open menu behavior
  162. var menu = this;
  163. this.$button.click( function( event ){
  164. menu._renderAndShow( event );
  165. //event.stopPropagation();
  166. return false;
  167. });
  168. // attach this view as a data object on the button - for later access
  169. //TODO:?? memleak?
  170. this.$button.data( 'PopupMenu', this );
  171. // template loading is problematic - ui is loaded in base.mako
  172. // and the template (prev.) needed to be loaded before ui
  173. },
  174. /** Render the menu. NOTE: doesn't attach itself to the DOM.
  175. * @see PopupMenu#_renderAndShow
  176. */
  177. render : function(){
  178. var menu = this;
  179. // render the menu body
  180. this.$el.addClass( 'popmenu-wrapper' )
  181. .css({
  182. position: 'absolute',
  183. display: 'none'
  184. });
  185. //BUG: anchors within a.popupmenu-option render OUTSIDE the a.popupmenu-option!?
  186. this.$el.html( PopupMenu.templates.menu({
  187. options : this.options,
  188. // sets menu div id to '{{ id }}-menu'
  189. id : this.$button.attr( 'id' )
  190. }));
  191. // set up behavior on each link/anchor elem
  192. if( this.options.length ){
  193. this.$el.find( 'li' ).each( function( i, li ){
  194. var $li = $( li ),
  195. $anchor = $li.children( 'a.popupmenu-option' ),
  196. menuFunc = menu.options[ i ].func;
  197. if( $anchor.length && menuFunc ){
  198. $anchor.click( function( event ){
  199. menuFunc( event, menu.options[ i ] );
  200. });
  201. }
  202. // cache the anchor as a jq obj within the options obj
  203. menu.options[ i ].$li = $li;
  204. });
  205. }
  206. return this;
  207. },
  208. /** Get the absolute position/offset for the menu
  209. */
  210. _getShownPosition : function( clickEvent ){
  211. var menuWidth = this.$el.width(),
  212. // display menu horiz. centered on click...
  213. x = clickEvent.pageX - menuWidth / 2 ;
  214. // ...but adjust that to handle horiz. scroll and window dimensions (draw entirely on visible screen area)
  215. x = Math.min( x, $( document ).scrollLeft() + $( window ).width() - menuWidth - 5 );
  216. x = Math.max( x, $( document ).scrollLeft() + 5 );
  217. return {
  218. top: clickEvent.pageY,
  219. left: x
  220. };
  221. },
  222. /** Render the menu, append to the page body at the click position, and set up the 'click-away' handlers, show
  223. */
  224. _renderAndShow : function( clickEvent ){
  225. this.render();
  226. this.$el.appendTo( 'body' );
  227. this.$el.css( this._getShownPosition( clickEvent ) );
  228. this._setUpCloseBehavior();
  229. this.$el.show();
  230. },
  231. /** Bind an event handler to all available frames so that when anything is clicked
  232. * * the menu is removed from the DOM
  233. * * The event handler unbinds itself
  234. */
  235. _setUpCloseBehavior : function(){
  236. var menu = this,
  237. // function to close popup and unbind itself
  238. closePopupWhenClicked = function( $elClicked ){
  239. $elClicked.bind( "click.close_popup", function(){
  240. menu.remove();
  241. $elClicked.unbind( "click.close_popup" );
  242. });
  243. };
  244. // bind to current, parent, and sibling frames
  245. //TODO: (Assuming for now that this is the best way to do this...)
  246. closePopupWhenClicked( $( window.document ) );
  247. closePopupWhenClicked( $( window.top.document ) );
  248. _.each( window.top.frames, function( siblingFrame ){
  249. closePopupWhenClicked( $( siblingFrame.document ) );
  250. });
  251. },
  252. /** Add a menu option/item at the given index
  253. */
  254. addItem : function( item, index ){
  255. // append to end if no index
  256. index = ( index >= 0 )?( index ):( this.options.length );
  257. this.options.splice( index, 0, item );
  258. return this;
  259. },
  260. /** Remove a menu option/item at the given index
  261. */
  262. removeItem : function( index ){
  263. if( index >=0 ){
  264. this.options.splice( index, 1 );
  265. }
  266. return this;
  267. },
  268. /** Search for a menu option by it's html
  269. */
  270. findIndexByHtml : function( html ){
  271. for( var i=0; i<this.options.length; i++ ){
  272. if( ( _.has( this.options[i], 'html' ) )
  273. && ( this.options[i].html === html ) ){
  274. return i;
  275. }
  276. }
  277. return null;
  278. },
  279. /** Search for a menu option by it's html
  280. */
  281. findItemByHtml : function( html ){
  282. return this.options[( this.findIndexByHtml( html ) )];
  283. },
  284. /** String representation. */
  285. toString : function(){
  286. return 'PopupMenu';
  287. }
  288. });
  289. PopupMenu.templates = {
  290. menu : Handlebars.templates[ 'template-popupmenu-menu' ]
  291. };
  292. // -----------------------------------------------------------------------------
  293. // the following class functions are bridges from the original make_popupmenu and make_popup_menus
  294. // to the newer backbone.js PopupMenu
  295. /** Create a PopupMenu from simple map initial_options activated by clicking button_element.
  296. * Converts initial_options to object array used by PopupMenu.
  297. * @param {jQuery|DOMElement} button_element element which, when clicked, activates menu
  298. * @param {Object} initial_options map of key -> values, where
  299. * key is option text, value is fn to call when option is clicked
  300. * @returns {PopupMenu} the PopupMenu created
  301. */
  302. PopupMenu.make_popupmenu = function( button_element, initial_options ){
  303. var convertedOptions = [];
  304. _.each( initial_options, function( optionVal, optionKey ){
  305. var newOption = { html: optionKey };
  306. // keys with null values indicate: header
  307. if( optionVal === null ){ // !optionVal? (null only?)
  308. newOption.header = true;
  309. // keys with function values indicate: a menu option
  310. } else if( jQuery.type( optionVal ) === 'function' ){
  311. newOption.func = optionVal;
  312. }
  313. //TODO:?? any other special optionVals?
  314. // there was no divider option originally
  315. convertedOptions.push( newOption );
  316. });
  317. return new PopupMenu( $( button_element ), convertedOptions );
  318. };
  319. /** Find all anchors in $parent (using selector) and covert anchors into a PopupMenu options map.
  320. * @param {jQuery} $parent the element that contains the links to convert to options
  321. * @param {String} selector jq selector string to find links
  322. * @returns {Object[]} the options array to initialize a PopupMenu
  323. */
  324. //TODO: lose parent and selector, pass in array of links, use map to return options
  325. PopupMenu.convertLinksToOptions = function( $parent, selector ){
  326. $parent = $( $parent );
  327. selector = selector || 'a';
  328. var options = [];
  329. $parent.find( selector ).each( function( elem, i ){
  330. var option = {},
  331. $link = $( elem );
  332. // convert link text to the option text (html) and the href into the option func
  333. option.html = $link.text();
  334. if( linkHref ){
  335. var linkHref = $link.attr( 'href' ),
  336. linkTarget = $link.attr( 'target' ),
  337. confirmText = $link.attr( 'confirm' );
  338. option.func = function(){
  339. // if there's a "confirm" attribute, throw up a confirmation dialog, and
  340. // if the user cancels - do nothing
  341. if( ( confirmText ) && ( !confirm( confirmText ) ) ){ return; }
  342. // if there's no confirm attribute, or the user accepted the confirm dialog:
  343. var f;
  344. switch( linkTarget ){
  345. // relocate the center panel
  346. case '_parent':
  347. window.parent.location = linkHref;
  348. break;
  349. // relocate the entire window
  350. case '_top':
  351. window.top.location = linkHref;
  352. break;
  353. // Http request target is a window named demolocal on the local box
  354. //TODO: I still don't understand this option (where the hell does f get set? confirm?)
  355. case 'demo':
  356. if( f === undefined || f.closed ){
  357. f = window.open( linkHref, linkTarget );
  358. f.creator = self;
  359. }
  360. break;
  361. // relocate this panel
  362. default:
  363. window.location = linkHref;
  364. }
  365. };
  366. }
  367. options.push( option );
  368. });
  369. return options;
  370. };
  371. /** Create a single popupmenu from existing DOM button and anchor elements
  372. * @param {jQuery} $buttonElement the element that when clicked will open the menu
  373. * @param {jQuery} $menuElement the element that contains the anchors to convert into a menu
  374. * @param {String} menuElementLinkSelector jq selector string used to find anchors to be made into menu options
  375. * @returns {PopupMenu} the PopupMenu (Backbone View) that can render, control the menu
  376. */
  377. PopupMenu.fromExistingDom = function( $buttonElement, $menuElement, menuElementLinkSelector ){
  378. $buttonElement = $( $buttonElement );
  379. $menuElement = $( $menuElement );
  380. var options = PopupMenu.convertLinksToOptions( $menuElement, menuElementLinkSelector );
  381. // we're done with the menu (having converted it to an options map)
  382. $menuElement.remove();
  383. return new PopupMenu( $buttonElement, options );
  384. };
  385. /** Create all popupmenus within a document or a more specific element
  386. * @param {DOMElement} parent the DOM element in which to search for popupmenus to build (defaults to document)
  387. * @param {String} menuSelector jq selector string to find popupmenu menu elements (defaults to "div[popupmenu]")
  388. * @param {Function} buttonSelectorBuildFn the function to build the jq button selector.
  389. * Will be passed $menuElement, parent.
  390. * (Defaults to return '#' + $menuElement.attr( 'popupmenu' ); )
  391. * @returns {PopupMenu[]} array of popupmenus created
  392. */
  393. PopupMenu.make_popup_menus = function( parent, menuSelector, buttonSelectorBuildFn ){
  394. parent = parent || document;
  395. // orig. Glx popupmenu menus have a (non-std) attribute 'popupmenu'
  396. // which contains the id of the button that activates the menu
  397. menuSelector = menuSelector || 'div[popupmenu]';
  398. // default to (orig. Glx) matching button to menu by using the popupmenu attr of the menu as the id of the button
  399. buttonSelectorBuildFn = buttonSelectorBuildFn || function( $menuElement, parent ){
  400. return '#' + $menuElement.attr( 'popupmenu' );
  401. };
  402. // aggregate and return all PopupMenus
  403. var popupMenusCreated = [];
  404. $( parent ).find( menuSelector ).each( function(){
  405. var $menuElement = $( this ),
  406. $buttonElement = $( parent ).find( buttonSelectorBuildFn( $menuElement, parent ) );
  407. popupMenusCreated.push( PopupMenu.fromDom( $buttonElement, $menuElement ) );
  408. $buttonElement.addClass( 'popup' );
  409. });
  410. return popupMenusCreated;
  411. }