PageRenderTime 63ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/client/galaxy/scripts/mvc/list/list-panel.js

https://bitbucket.org/afgane/galaxy-central
JavaScript | 949 lines | 593 code | 121 blank | 235 comment | 63 complexity | 32aef7f961d725818271e6a658aa7b90 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/list/list-item",
  3. "ui/loading-indicator",
  4. "mvc/base-mvc",
  5. "utils/localization",
  6. "ui/search-input"
  7. ], function( LIST_ITEM, LoadingIndicator, BASE_MVC, _l ){
  8. /* ============================================================================
  9. TODO:
  10. ============================================================================ */
  11. /** @class View for a list/collection of models and the sub-views of those models.
  12. * Sub-views must (at least have the interface if not) inherit from ListItemView.
  13. * (For a list panel that also includes some 'container' model (History->HistoryContents)
  14. * use ModelWithListPanel)
  15. *
  16. * Allows for:
  17. * searching collection/sub-views
  18. * selecting/multi-selecting sub-views
  19. *
  20. * Currently used:
  21. * for dataset/dataset-choice
  22. * as superclass of ModelListPanel
  23. */
  24. var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
  25. /** @lends ReadOnlyHistoryPanel.prototype */{
  26. /** logger used to record this.log messages, commonly set to console */
  27. //logger : console,
  28. /** class to use for constructing the sub-views */
  29. viewClass : LIST_ITEM.ListItemView,
  30. /** class to used for constructing collection of sub-view models */
  31. collectionClass : Backbone.Collection,
  32. tagName : 'div',
  33. className : 'list-panel',
  34. /** (in ms) that jquery effects will use */
  35. fxSpeed : 'fast',
  36. /** string to display when the collection has no contents */
  37. emptyMsg : _l( 'This list is empty' ),
  38. /** displayed when no items match the search terms */
  39. noneFoundMsg : _l( 'No matching items found' ),
  40. /** string used for search placeholder */
  41. searchPlaceholder : _l( 'search' ),
  42. /** actions available for multiselected items */
  43. multiselectActions : [],
  44. // ......................................................................... SET UP
  45. /** Set up the view, set up storage, bind listeners to HistoryContents events
  46. * @param {Object} attributes optional settings for the list
  47. */
  48. initialize : function( attributes, options ){
  49. attributes = attributes || {};
  50. // set the logger if requested
  51. if( attributes.logger ){
  52. this.logger = attributes.logger;
  53. }
  54. this.log( this + '.initialize:', attributes );
  55. // ---- instance vars
  56. /** how quickly should jquery fx run? */
  57. this.fxSpeed = _.has( attributes, 'fxSpeed' )?( attributes.fxSpeed ):( this.fxSpeed );
  58. /** filters for displaying subviews */
  59. this.filters = [];
  60. /** current search terms */
  61. this.searchFor = attributes.searchFor || '';
  62. /** loading indicator */
  63. this.indicator = new LoadingIndicator( this.$el );
  64. /** currently showing selectors on items? */
  65. this.selecting = ( attributes.selecting !== undefined )? attributes.selecting : true;
  66. //this.selecting = false;
  67. /** cached selected item.model.ids to persist btwn renders */
  68. this.selected = attributes.selected || [];
  69. /** the last selected item.model.id */
  70. this.lastSelected = null;
  71. /** are sub-views draggable */
  72. this.dragItems = attributes.dragItems || false;
  73. /** list item view class (when passed models) */
  74. this.viewClass = attributes.viewClass || this.viewClass;
  75. /** list item views */
  76. this.views = [];
  77. /** list item models */
  78. this.collection = attributes.collection || ( new this.collectionClass([]) );
  79. /** filter fns run over collection items to see if they should show in the list */
  80. this.filters = attributes.filters || [];
  81. /** override $scrollContainer fn via attributes - fn should return jq for elem to call scrollTo on */
  82. this.$scrollContainer = attributes.$scrollContainer || this.$scrollContainer;
  83. //TODO: remove
  84. this.title = attributes.title || '';
  85. this.subtitle = attributes.subtitle || '';
  86. // allow override of multiselectActions through attributes
  87. this.multiselectActions = attributes.multiselectActions || this.multiselectActions;
  88. /** the popup displayed when 'for all selected...' is clicked */
  89. this.actionsPopup = null;
  90. this._setUpListeners();
  91. },
  92. /** free any sub-views the list has */
  93. freeViews : function(){
  94. //TODO: stopListening? remove?
  95. _.each( this.views, function( view ){
  96. view.off();
  97. });
  98. this.views = [];
  99. return this;
  100. },
  101. // ------------------------------------------------------------------------ listeners
  102. /** create any event listeners for the list
  103. */
  104. _setUpListeners : function(){
  105. this.off();
  106. this.on( 'error', function( model, xhr, options, msg, details ){
  107. //this.errorHandler( model, xhr, options, msg, details );
  108. console.error( model, xhr, options, msg, details );
  109. }, this );
  110. // show hide the loading indicator
  111. this.on( 'loading', function(){
  112. this._showLoadingIndicator( 'loading...', 40 );
  113. }, this );
  114. this.on( 'loading-done', function(){
  115. this._hideLoadingIndicator( 40 );
  116. }, this );
  117. // throw the first render up as a diff namespace using once (for outside consumption)
  118. this.once( 'rendered', function(){
  119. this.trigger( 'rendered:initial', this );
  120. }, this );
  121. // debugging
  122. if( this.logger ){
  123. this.on( 'all', function( event ){
  124. this.log( this + '', arguments );
  125. }, this );
  126. }
  127. this._setUpCollectionListeners();
  128. this._setUpViewListeners();
  129. return this;
  130. },
  131. /** listening for collection events */
  132. _setUpCollectionListeners : function(){
  133. this.log( this + '._setUpCollectionListeners', this.collection );
  134. this.collection.off();
  135. // bubble up error events
  136. this.collection.on( 'error', function( model, xhr, options, msg, details ){
  137. this.trigger( 'error', model, xhr, options, msg, details );
  138. }, this );
  139. this.collection.on( 'reset', function(){
  140. this.renderItems();
  141. }, this );
  142. this.collection.on( 'add', this.addItemView, this );
  143. this.collection.on( 'remove', this.removeItemView, this );
  144. // debugging
  145. if( this.logger ){
  146. this.collection.on( 'all', function( event ){
  147. this.info( this + '(collection)', arguments );
  148. }, this );
  149. }
  150. return this;
  151. },
  152. /** listening for sub-view events that bubble up with the 'view:' prefix */
  153. _setUpViewListeners : function(){
  154. this.log( this + '._setUpViewListeners' );
  155. // shift to select a range
  156. this.on( 'view:selected', function( view, ev ){
  157. if( ev && ev.shiftKey && this.lastSelected ){
  158. var lastSelectedView = this.viewFromModelId( this.lastSelected );
  159. if( lastSelectedView ){
  160. this.selectRange( view, lastSelectedView );
  161. }
  162. } else if( ev && ev.altKey && !this.selecting ){
  163. this.showSelectors();
  164. }
  165. this.selected.push( view.model.id );
  166. this.lastSelected = view.model.id;
  167. }, this );
  168. this.on( 'view:de-selected', function( view, ev ){
  169. this.selected = _.without( this.selected, view.model.id );
  170. //this.lastSelected = view.model.id;
  171. }, this );
  172. },
  173. // ------------------------------------------------------------------------ rendering
  174. /** Render this content, set up ui.
  175. * @param {Number or String} speed the speed of the render
  176. */
  177. render : function( speed ){
  178. this.log( this + '.render', speed );
  179. var $newRender = this._buildNewRender();
  180. this._setUpBehaviors( $newRender );
  181. this._queueNewRender( $newRender, speed );
  182. return this;
  183. },
  184. /** Build a temp div containing the new children for the view's $el.
  185. */
  186. _buildNewRender : function(){
  187. this.debug( this + '(ListPanel)._buildNewRender' );
  188. var $newRender = $( this.templates.el( {}, this ) );
  189. this._renderControls( $newRender );
  190. this._renderTitle( $newRender );
  191. this._renderSubtitle( $newRender );
  192. this._renderSearch( $newRender );
  193. this.renderItems( $newRender );
  194. return $newRender;
  195. },
  196. /** Build a temp div containing the new children for the view's $el.
  197. */
  198. _renderControls : function( $newRender ){
  199. this.debug( this + '(ListPanel)._renderControls' );
  200. var $controls = $( this.templates.controls( {}, this ) );
  201. $newRender.find( '.controls' ).replaceWith( $controls );
  202. return $controls;
  203. },
  204. /**
  205. */
  206. _renderTitle : function( $where ){
  207. //$where = $where || this.$el;
  208. //$where.find( '.title' ).replaceWith( ... )
  209. },
  210. /**
  211. */
  212. _renderSubtitle : function( $where ){
  213. //$where = $where || this.$el;
  214. //$where.find( '.title' ).replaceWith( ... )
  215. },
  216. /** Fade out the old el, swap in the new contents, then fade in.
  217. * @param {Number or String} speed jq speed to use for rendering effects
  218. * @fires rendered when rendered
  219. */
  220. _queueNewRender : function( $newRender, speed ) {
  221. speed = ( speed === undefined )?( this.fxSpeed ):( speed );
  222. var panel = this;
  223. panel.log( '_queueNewRender:', $newRender, speed );
  224. $( panel ).queue( 'fx', [
  225. function( next ){ this.$el.fadeOut( speed, next ); },
  226. function( next ){
  227. panel._swapNewRender( $newRender );
  228. next();
  229. },
  230. function( next ){ this.$el.fadeIn( speed, next ); },
  231. function( next ){
  232. panel.trigger( 'rendered', panel );
  233. next();
  234. }
  235. ]);
  236. },
  237. /** empty out the current el, move the $newRender's children in */
  238. _swapNewRender : function( $newRender ){
  239. this.$el.empty().attr( 'class', this.className ).append( $newRender.children() );
  240. if( this.selecting ){ this.showSelectors( 0 ); }
  241. return this;
  242. },
  243. /** */
  244. _setUpBehaviors : function( $where ){
  245. $where = $where || this.$el;
  246. $where.find( '.controls [title]' ).tooltip({ placement: 'bottom' });
  247. return this;
  248. },
  249. // ------------------------------------------------------------------------ sub-$element shortcuts
  250. /** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
  251. $scrollContainer : function(){
  252. // override or set via attributes.$scrollContainer
  253. return this.$el.parent().parent();
  254. },
  255. /** */
  256. $list : function( $where ){
  257. return ( $where || this.$el ).find( '> .list-items' );
  258. },
  259. /** container where list messages are attached */
  260. $messages : function( $where ){
  261. return ( $where || this.$el ).find( '> .controls .messages' );
  262. },
  263. /** the message displayed when no views can be shown (no views, none matching search) */
  264. $emptyMessage : function( $where ){
  265. return ( $where || this.$el ).find( '> .empty-message' );
  266. },
  267. // ------------------------------------------------------------------------ hda sub-views
  268. /**
  269. * @param {jQuery} $whereTo what dom element to prepend the sub-views to
  270. * @returns the visible item views
  271. */
  272. renderItems : function( $whereTo ){
  273. $whereTo = $whereTo || this.$el;
  274. var panel = this;
  275. panel.log( this + '.renderItems', $whereTo );
  276. var $list = panel.$list( $whereTo );
  277. //TODO: free prev. views?
  278. panel.views = panel._filterCollection().map( function( itemModel ){
  279. //TODO: creates views each time - not neccessarily good
  280. //TODO: pass speed here
  281. return panel._createItemView( itemModel ).render( 0 );
  282. });
  283. //panel.debug( item$els );
  284. //panel.debug( newViews );
  285. $list.empty();
  286. if( panel.views.length ){
  287. panel._attachItems( $whereTo );
  288. panel.$emptyMessage( $whereTo ).hide();
  289. } else {
  290. panel._renderEmptyMessage( $whereTo ).show();
  291. }
  292. return panel.views;
  293. },
  294. /** Filter the collection to only those models that should be currently viewed */
  295. _filterCollection : function(){
  296. // override this
  297. var panel = this;
  298. return panel.collection.filter( _.bind( panel._filterItem, panel ) );
  299. },
  300. /** Should the model be viewable in the current state?
  301. * Checks against this.filters and this.searchFor
  302. */
  303. _filterItem : function( model ){
  304. // override this
  305. var panel = this;
  306. return ( _.every( panel.filters.map( function( fn ){ return fn.call( model ); }) ) )
  307. && ( !panel.searchFor || model.matchesAll( panel.searchFor ) );
  308. },
  309. /** Create a view for a model and set up it's listeners */
  310. _createItemView : function( model ){
  311. var ViewClass = this._getItemViewClass( model ),
  312. options = _.extend( this._getItemViewOptions( model ), {
  313. model : model
  314. }),
  315. view = new ViewClass( options );
  316. this._setUpItemViewListeners( view );
  317. return view;
  318. },
  319. /** Get the bbone view class based on the model */
  320. _getItemViewClass : function( model ){
  321. // override this
  322. return this.viewClass;
  323. },
  324. /** Get the options passed to the new view based on the model */
  325. _getItemViewOptions : function( model ){
  326. // override this
  327. return {
  328. //logger : this.logger,
  329. fxSpeed : this.fxSpeed,
  330. expanded : false,
  331. selectable : this.selecting,
  332. selected : _.contains( this.selected, model.id ),
  333. draggable : this.dragItems
  334. };
  335. },
  336. /** Set up listeners for new models */
  337. _setUpItemViewListeners : function( view ){
  338. var panel = this;
  339. // send all events to the panel, re-namspaceing them with the view prefix
  340. view.on( 'all', function(){
  341. var args = Array.prototype.slice.call( arguments, 0 );
  342. args[0] = 'view:' + args[0];
  343. panel.trigger.apply( panel, args );
  344. });
  345. // drag multiple - hijack ev.setData to add all selected datasets
  346. view.on( 'draggable:dragstart', function( ev, v ){
  347. //TODO: set multiple drag data here
  348. var json = {},
  349. selected = this.getSelectedModels();
  350. if( selected.length ){
  351. json = selected.toJSON();
  352. } else {
  353. json = [ v.model.toJSON() ];
  354. }
  355. ev.dataTransfer.setData( 'text', JSON.stringify( json ) );
  356. }, this );
  357. // debugging
  358. //if( this.logger ){
  359. // view.on( 'all', function( event ){
  360. // this.log( this + '(view)', arguments );
  361. // }, this );
  362. //}
  363. return panel;
  364. },
  365. /** Attach views in this.views to the model based on $whereTo */
  366. _attachItems : function( $whereTo ){
  367. //ASSUMES: $list has been emptied
  368. this.$list( $whereTo ).append( this.views.map( function( view ){
  369. return view.$el;
  370. }));
  371. return this;
  372. },
  373. /** render the empty/none-found message */
  374. _renderEmptyMessage : function( $whereTo ){
  375. this.debug( '_renderEmptyMessage', $whereTo, this.searchFor );
  376. var text = this.searchFor? this.noneFoundMsg : this.emptyMsg;
  377. return this.$emptyMessage( $whereTo ).text( text );
  378. },
  379. /** collapse all item views */
  380. expandAll : function(){
  381. _.each( this.views, function( view ){
  382. view.expand();
  383. });
  384. },
  385. /** collapse all item views */
  386. collapseAll : function(){
  387. _.each( this.views, function( view ){
  388. view.collapse();
  389. });
  390. },
  391. // ------------------------------------------------------------------------ collection/views syncing
  392. /** Add a view (if the model should be viewable) to the panel */
  393. addItemView : function( model, collection, options ){
  394. this.log( this + '.addItemView:', model );
  395. var panel = this;
  396. if( !panel._filterItem( model ) ){ return undefined; }
  397. var view = panel._createItemView( model );
  398. // hide the empty message if only view
  399. $( view ).queue( 'fx', [
  400. //TODO:? could poss. pubsub this
  401. function( next ){ panel.$emptyMessage().fadeOut( panel.fxSpeed, next ); },
  402. function( next ){
  403. panel._attachView( view );
  404. next();
  405. }
  406. ]);
  407. return view;
  408. },
  409. /** internal fn to add view (to both panel.views and panel.$list) */
  410. _attachView : function( view ){
  411. var panel = this;
  412. // override to control where the view is added, how/whether it's rendered
  413. panel.views.push( view );
  414. panel.$list().append( view.render( 0 ).$el.hide() );
  415. panel.trigger( 'view:attached', view );
  416. view.$el.slideDown( panel.fxSpeed, function(){
  417. panel.trigger( 'view:attached:rendered' );
  418. });
  419. },
  420. /** Remove a view from the panel (if found) */
  421. removeItemView : function( model, collection, options ){
  422. this.log( this + '.removeItemView:', model );
  423. var panel = this,
  424. view = panel.viewFromModel( model );
  425. if( !view ){ return undefined; }
  426. panel.views = _.without( panel.views, view );
  427. panel.trigger( 'view:removed', view );
  428. // potentially show the empty message if no views left
  429. // use anonymous queue here - since remove can happen multiple times
  430. $({}).queue( 'fx', [
  431. function( next ){ view.$el.fadeOut( panel.fxSpeed, next ); },
  432. function( next ){
  433. view.remove();
  434. panel.trigger( 'view:removed:rendered' );
  435. if( !panel.views.length ){
  436. panel._renderEmptyMessage().fadeIn( panel.fxSpeed, next );
  437. } else {
  438. next();
  439. }
  440. }
  441. ]);
  442. return view;
  443. },
  444. /** get views based on model.id */
  445. viewFromModelId : function( id ){
  446. for( var i=0; i<this.views.length; i++ ){
  447. if( this.views[i].model.id === id ){
  448. return this.views[i];
  449. }
  450. }
  451. return undefined;
  452. },
  453. /** get views based on model */
  454. viewFromModel : function( model ){
  455. if( !model ){ return undefined; }
  456. return this.viewFromModelId( model.id );
  457. },
  458. /** get views based on model properties */
  459. viewsWhereModel : function( properties ){
  460. return this.views.filter( function( view ){
  461. //return view.model.matches( properties );
  462. //TODO: replace with _.matches (underscore 1.6.0)
  463. var json = view.model.toJSON();
  464. //console.debug( '\t', json, properties );
  465. for( var key in properties ){
  466. if( properties.hasOwnProperty( key ) ){
  467. //console.debug( '\t\t', json[ key ], view.model.properties[ key ] );
  468. if( json[ key ] !== view.model.get( key ) ){
  469. return false;
  470. }
  471. }
  472. }
  473. return true;
  474. });
  475. },
  476. /** A range of views between (and including) viewA and viewB */
  477. viewRange : function( viewA, viewB ){
  478. if( viewA === viewB ){ return ( viewA )?( [ viewA ] ):( [] ); }
  479. var indexA = this.views.indexOf( viewA ),
  480. indexB = this.views.indexOf( viewB );
  481. // handle not found
  482. if( indexA === -1 || indexB === -1 ){
  483. if( indexA === indexB ){ return []; }
  484. return ( indexA === -1 )?( [ viewB ] ):( [ viewA ] );
  485. }
  486. // reverse if indeces are
  487. //note: end inclusive
  488. return ( indexA < indexB )?
  489. this.views.slice( indexA, indexB + 1 ) :
  490. this.views.slice( indexB, indexA + 1 );
  491. },
  492. // ------------------------------------------------------------------------ searching
  493. /** render a search input for filtering datasets shown
  494. * (see SearchableMixin in base-mvc for implementation of the actual searching)
  495. * return will start the search
  496. * esc will clear the search
  497. * clicking the clear button will clear the search
  498. * uses searchInput in ui.js
  499. */
  500. _renderSearch : function( $where ){
  501. $where.find( '.controls .search-input' ).searchInput({
  502. placeholder : this.searchPlaceholder,
  503. initialVal : this.searchFor,
  504. onfirstsearch : _.bind( this._firstSearch, this ),
  505. onsearch : _.bind( this.searchItems, this ),
  506. onclear : _.bind( this.clearSearch, this )
  507. });
  508. return $where;
  509. },
  510. /** What to do on the first search entered */
  511. _firstSearch : function( searchFor ){
  512. // override to load model details if necc.
  513. this.log( 'onFirstSearch', searchFor );
  514. return this.searchItems( searchFor );
  515. },
  516. /** filter view list to those that contain the searchFor terms */
  517. searchItems : function( searchFor ){
  518. this.searchFor = searchFor;
  519. this.trigger( 'search:searching', searchFor, this );
  520. this.renderItems();
  521. this.$( '> .controls .search-query' ).val( searchFor );
  522. return this;
  523. },
  524. /** clear the search filters and show all views that are normally shown */
  525. clearSearch : function( searchFor ){
  526. //this.log( 'onSearchClear', this );
  527. this.searchFor = '';
  528. this.trigger( 'search:clear', this );
  529. this.renderItems();
  530. this.$( '> .controls .search-query' ).val( '' );
  531. return this;
  532. },
  533. // ------------------------------------------------------------------------ selection
  534. /** show selectors on all visible itemViews and associated controls */
  535. showSelectors : function( speed ){
  536. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  537. this.selecting = true;
  538. this.$( '.list-actions' ).slideDown( speed );
  539. _.each( this.views, function( view ){
  540. view.showSelector( speed );
  541. });
  542. //this.selected = [];
  543. //this.lastSelected = null;
  544. },
  545. /** hide selectors on all visible itemViews and associated controls */
  546. hideSelectors : function( speed ){
  547. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  548. this.selecting = false;
  549. this.$( '.list-actions' ).slideUp( speed );
  550. _.each( this.views, function( view ){
  551. view.hideSelector( speed );
  552. });
  553. this.selected = [];
  554. this.lastSelected = null;
  555. },
  556. /** show or hide selectors on all visible itemViews and associated controls */
  557. toggleSelectors : function(){
  558. if( !this.selecting ){
  559. this.showSelectors();
  560. } else {
  561. this.hideSelectors();
  562. }
  563. },
  564. /** select all visible items */
  565. selectAll : function( event ){
  566. _.each( this.views, function( view ){
  567. view.select( event );
  568. });
  569. },
  570. /** deselect all visible items */
  571. deselectAll : function( event ){
  572. this.lastSelected = null;
  573. _.each( this.views, function( view ){
  574. view.deselect( event );
  575. });
  576. },
  577. /** select a range of datasets between A and B */
  578. selectRange : function( viewA, viewB ){
  579. var range = this.viewRange( viewA, viewB );
  580. _.each( range, function( view ){
  581. view.select();
  582. });
  583. return range;
  584. },
  585. /** return an array of all currently selected itemViews */
  586. getSelectedViews : function(){
  587. return _.filter( this.views, function( v ){
  588. return v.selected;
  589. });
  590. },
  591. /** return a collection of the models of all currenly selected items */
  592. getSelectedModels : function(){
  593. return new this.collection.constructor( _.map( this.getSelectedViews(), function( view ){
  594. return view.model;
  595. }));
  596. },
  597. // ------------------------------------------------------------------------ loading indicator
  598. //TODO: questionable
  599. /** hide the $el and display a loading indicator (in the $el's parent) when loading new data */
  600. _showLoadingIndicator : function( msg, speed, callback ){
  601. this.debug( '_showLoadingIndicator', this.indicator, msg, speed, callback );
  602. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  603. if( !this.indicator ){
  604. this.indicator = new LoadingIndicator( this.$el, this.$el.parent() );
  605. this.debug( '\t created', this.indicator );
  606. }
  607. if( !this.$el.is( ':visible' ) ){
  608. this.indicator.show( 0, callback );
  609. } else {
  610. this.$el.fadeOut( speed );
  611. this.indicator.show( msg, speed, callback );
  612. }
  613. },
  614. /** hide the loading indicator */
  615. _hideLoadingIndicator : function( speed, callback ){
  616. this.debug( '_hideLoadingIndicator', this.indicator, speed, callback );
  617. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  618. if( this.indicator ){
  619. this.indicator.hide( speed, callback );
  620. }
  621. },
  622. // ------------------------------------------------------------------------ scrolling
  623. /** get the current scroll position of the panel in its parent */
  624. scrollPosition : function(){
  625. return this.$scrollContainer().scrollTop();
  626. },
  627. /** set the current scroll position of the panel in its parent */
  628. scrollTo : function( pos, speed ){
  629. speed = speed || 0;
  630. this.$scrollContainer().animate({ scrollTop: pos }, speed );
  631. return this;
  632. },
  633. /** Scrolls the panel to the top. */
  634. scrollToTop : function( speed ){
  635. return this.scrollTo( 0, speed );
  636. },
  637. /** */
  638. scrollToItem : function( view, speed ){
  639. if( !view ){ return this; }
  640. //var itemTop = view.$el.offset().top;
  641. var itemTop = view.$el.position().top;
  642. return this.scrollTo( itemTop, speed );
  643. },
  644. /** Scrolls the panel to show the content with the given id. */
  645. scrollToId : function( id, speed ){
  646. return this.scrollToItem( this.viewFromModelId( id ), speed );
  647. },
  648. // ------------------------------------------------------------------------ panel events
  649. /** event map */
  650. events : {
  651. 'click .select-all' : 'selectAll',
  652. 'click .deselect-all' : 'deselectAll'
  653. },
  654. // ------------------------------------------------------------------------ misc
  655. /** Return a string rep of the panel */
  656. toString : function(){
  657. return 'ListPanel(' + this.collection + ')';
  658. }
  659. });
  660. // ............................................................................ TEMPLATES
  661. /** underscore templates */
  662. ListPanel.prototype.templates = (function(){
  663. //TODO: move to require text! plugin
  664. var elTemplate = BASE_MVC.wrapTemplate([
  665. // temp container
  666. '<div>',
  667. '<div class="controls"></div>',
  668. '<div class="list-items"></div>',
  669. '<div class="empty-message infomessagesmall"></div>',
  670. '</div>'
  671. ]);
  672. var controlsTemplate = BASE_MVC.wrapTemplate([
  673. '<div class="controls">',
  674. '<div class="title">',
  675. '<div class="name"><%= view.title %></div>',
  676. '</div>',
  677. '<div class="subtitle"><%= view.subtitle %></div>',
  678. // buttons, controls go here
  679. '<div class="actions"></div>',
  680. // deleted msg, etc.
  681. '<div class="messages"></div>',
  682. '<div class="search">',
  683. '<div class="search-input"></div>',
  684. '</div>',
  685. // show when selectors are shown
  686. '<div class="list-actions">',
  687. '<div class="btn-group">',
  688. '<button class="select-all btn btn-default"',
  689. 'data-mode="select">', _l( 'All' ), '</button>',
  690. '<button class="deselect-all btn btn-default"',
  691. 'data-mode="select">', _l( 'None' ), '</button>',
  692. '</div>',
  693. //'<button class="list-action-popup-btn btn btn-default">',
  694. // _l( 'For all selected' ), '...',
  695. //'</button>',
  696. '</div>',
  697. '</div>'
  698. ]);
  699. return {
  700. el : elTemplate,
  701. controls : controlsTemplate
  702. };
  703. }());
  704. //=============================================================================
  705. /** View for a model that has a sub-collection (e.g. History, DatasetCollection)
  706. * Allows:
  707. * the model to be reset
  708. * auto assign panel.collection to panel.model[ panel.modelCollectionKey ]
  709. *
  710. */
  711. var ModelListPanel = ListPanel.extend({
  712. /** key of attribute in model to assign to this.collection */
  713. modelCollectionKey : 'contents',
  714. initialize : function( attributes ){
  715. ListPanel.prototype.initialize.call( this, attributes );
  716. this.selecting = ( attributes.selecting !== undefined )? attributes.selecting : false;
  717. this.setModel( this.model, attributes );
  718. },
  719. /** release/free/shutdown old models and set up panel for new models
  720. * @fires new-model with the panel as parameter
  721. */
  722. setModel : function( model, attributes ){
  723. attributes = attributes || {};
  724. this.debug( this + '.setModel:', model, attributes );
  725. this.freeModel();
  726. this.freeViews();
  727. if( model ){
  728. var oldModelId = this.model? this.model.get( 'id' ): null;
  729. // set up the new model with user, logger, storage, events
  730. this.model = model;
  731. if( this.logger ){
  732. this.model.logger = this.logger;
  733. }
  734. this._setUpModelListeners();
  735. //TODO: relation btwn model, collection becoming tangled here
  736. // free the collection, and assign the new collection to either
  737. // the model[ modelCollectionKey ], attributes.collection, or an empty vanilla collection
  738. this.collection.off();
  739. this.collection = ( this.model[ this.modelCollectionKey ] )?
  740. this.model[ this.modelCollectionKey ]:
  741. ( attributes.collection || ( new this.collectionClass([]) ) );
  742. this._setUpCollectionListeners();
  743. if( oldModelId && model.get( 'id' ) !== oldModelId ){
  744. this.trigger( 'new-model', this );
  745. }
  746. }
  747. return this;
  748. },
  749. /** free the current model and all listeners for it, free any views for the model */
  750. freeModel : function(){
  751. // stop/release the previous model, and clear cache to sub-views
  752. if( this.model ){
  753. this.stopListening( this.model );
  754. //TODO: see base-mvc
  755. //this.model.free();
  756. //this.model = null;
  757. }
  758. return this;
  759. },
  760. // ------------------------------------------------------------------------ listening
  761. /** listening for model events */
  762. _setUpModelListeners : function(){
  763. // override
  764. this.log( this + '._setUpModelListeners', this.model );
  765. // bounce model errors up to the panel
  766. this.model.on( 'error', function(){
  767. var args = Array.prototype.slice.call( arguments, 0 );
  768. //args.unshift( 'model:error' );
  769. args.unshift( 'error' );
  770. this.trigger.apply( this, args );
  771. }, this );
  772. return this;
  773. },
  774. /** Build a temp div containing the new children for the view's $el.
  775. */
  776. _renderControls : function( $newRender ){
  777. this.debug( this + '(ListPanel)._renderControls' );
  778. var json = this.model? this.model.toJSON() : {},
  779. $controls = $( this.templates.controls( json, this ) );
  780. $newRender.find( '.controls' ).replaceWith( $controls );
  781. return $controls;
  782. },
  783. // ------------------------------------------------------------------------ misc
  784. /** Return a string rep of the panel */
  785. toString : function(){
  786. return 'ModelListPanel(' + this.model + ')';
  787. }
  788. });
  789. // ............................................................................ TEMPLATES
  790. /** underscore templates */
  791. ModelListPanel.prototype.templates = (function(){
  792. //TODO: move to require text! plugin
  793. var controlsTemplate = BASE_MVC.wrapTemplate([
  794. '<div class="controls">',
  795. '<div class="title">',
  796. //TODO: this is really the only difference - consider factoring titlebar out
  797. '<div class="name"><%= model.name %></div>',
  798. '</div>',
  799. '<div class="subtitle"><%= view.subtitle %></div>',
  800. '<div class="actions"></div>',
  801. '<div class="messages"></div>',
  802. '<div class="search">',
  803. '<div class="search-input"></div>',
  804. '</div>',
  805. '<div class="list-actions">',
  806. '<div class="btn-group">',
  807. '<button class="select-all btn btn-default"',
  808. 'data-mode="select">', _l( 'All' ), '</button>',
  809. '<button class="deselect-all btn btn-default"',
  810. 'data-mode="select">', _l( 'None' ), '</button>',
  811. '</div>',
  812. //'<button class="list-action-popup-btn btn btn-default">',
  813. // _l( 'For all selected' ), '...',
  814. //'</button>',
  815. '</div>',
  816. '</div>'
  817. ]);
  818. return _.extend( _.clone( ListPanel.prototype.templates ), {
  819. controls : controlsTemplate
  820. });
  821. }());
  822. //=============================================================================
  823. return {
  824. ListPanel : ListPanel,
  825. ModelListPanel : ModelListPanel
  826. };
  827. });