PageRenderTime 54ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/mvc/list/list-panel.js

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