@@ -18,6 +18,8 @@ issueList.DropdownView = Backbone.View.extend({
1818 this . closeDropdown ( ) ;
1919 }
2020 } , this ) ) ;
21+
22+ issueList . events . on ( 'dropdown:update' , _ . bind ( this . selectDropdownOption , this ) ) ;
2123 } ,
2224 template : _ . template ( $ ( '#dropdown-tmpl' ) . html ( ) ) ,
2325 render : function ( ) {
@@ -32,21 +34,39 @@ issueList.DropdownView = Backbone.View.extend({
3234 this . $el . removeClass ( 'is-active' ) ;
3335 } ,
3436 selectDropdownOption : function ( e ) {
35- var option = $ ( e . target ) ;
36- var params = option . data ( 'params' ) ;
37- option . addClass ( 'is-active' )
38- . siblings ( ) . removeClass ( 'is-active' ) ;
37+ var option ;
38+ var params ;
3939
40- this . updateDropdownTitle ( option ) ;
41-
42- // persist value of selection to be used on subsequent page loads
43- if ( 'localStorage' in window ) {
44- window . localStorage . setItem ( "params" , params ) ;
40+ if ( typeof e === 'string' ) {
41+ // we received a dropdown:update event and want to update the dropdown, but
42+ // not broadcast any events (so we don't need "params")
43+ // $= because some of these will just be the beginning part (mentioned, creator)
44+ option = $ ( '[data-params$="' + e + '"]' ) ;
45+ this . manuallyUpdateDropdownTitle ( option , e ) ;
46+ } else if ( typeof e . type === 'string' ) {
47+ // we're dealing with a user click event.
48+ option = $ ( e . target ) ;
49+ params = option . data ( 'params' ) ;
50+
51+ // fire an event so other views can react to dropdown changes
52+ wcEvents . trigger ( 'dropdown:change' , params , { update : true } ) ;
53+ this . updateDropdownTitle ( option ) ;
54+ e . preventDefault ( ) ;
4555 }
4656
47- // fire an event so other views can react to dropdown changes
48- wcEvents . trigger ( 'dropdown:change' , params ) ;
49- e . preventDefault ( ) ;
57+ option . addClass ( 'is-active' )
58+ . siblings ( ) . removeClass ( 'is-active' ) ;
59+ } ,
60+ manuallyUpdateDropdownTitle : function ( optionElm , e ) {
61+ // make sure we're only updating the title if we're operating
62+ // on the correct model.
63+ var modelOpts = this . model . get ( 'dropdownOptions' ) ;
64+ if ( _ . find ( modelOpts , function ( opt ) {
65+ return opt . params === e ;
66+ } ) !== undefined ) {
67+ this . model . set ( 'dropdownTitle' , optionElm . text ( ) ) ;
68+ this . render ( ) ;
69+ }
5070 } ,
5171 updateDropdownTitle : function ( optionElm ) {
5272 this . model . set ( 'dropdownTitle' , optionElm . text ( ) ) ;
@@ -59,6 +79,7 @@ issueList.FilterView = Backbone.View.extend({
5979 events : {
6080 'click .js-filter-button' : 'toggleFilter'
6181 } ,
82+ _filterRegex : / ( n e w | n e e d s d i a g n o s i s | c o n t a c t r e a d y | s i t e w a i t | c l o s e d ) = 1 / ig,
6283 _isLoggedIn : $ ( 'body' ) . data ( 'username' ) ,
6384 _userName : $ ( 'body' ) . data ( 'username' ) ,
6485 initialize : function ( ) {
@@ -99,12 +120,27 @@ issueList.FilterView = Backbone.View.extend({
99120 this . dropdown . setElement ( this . $el . find ( '.js-dropdown-wrapper' ) ) . render ( ) ;
100121 return this ;
101122 } ,
123+ addFilterToModel : function ( filter ) {
124+ issueList . events . trigger ( 'filter:add-to-model' , filter ) ;
125+ } ,
102126 clearFilter : function ( ) {
103127 var btns = $ ( '[data-filter]' ) ;
104128 btns . removeClass ( 'is-active' ) ;
129+
130+ this . removeFiltersFromModel ( ) ;
131+
132+ if ( history . pushState ) {
133+ // remove filter from URL
134+ history . pushState ( { } , '' , location . search . replace ( this . _filterRegex , '' ) ) ;
135+ }
136+ } ,
137+ removeFiltersFromModel : function ( ) {
138+ // Sends a message to remove filter params from the model
139+ issueList . events . trigger ( 'filter:remove-from-model' ) ;
105140 } ,
106141 toggleFilter : function ( e ) {
107142 var btn ;
143+ var filterParam ;
108144 // Stringy e comes from triggered filter:activate event
109145 if ( typeof e === 'string' ) {
110146 btn = $ ( '[data-filter=' + e + ']' ) ;
@@ -119,9 +155,18 @@ issueList.FilterView = Backbone.View.extend({
119155 // Clear the search field
120156 issueList . events . trigger ( 'search:clear' ) ;
121157
158+ // Remove existing filters from model and URL
159+ this . removeFiltersFromModel ( ) ;
160+ if ( history . pushState ) {
161+ history . pushState ( { } , '' , location . search . replace ( this . _filterRegex , '' ) ) ;
162+ }
163+
122164 if ( btn . hasClass ( 'is-active' ) ) {
165+ filterParam = btn . data ( 'filter' ) + "=1" ;
123166 this . updateResults ( btn . data ( 'filter' ) ) ;
167+ this . addFilterToModel ( filterParam ) ;
124168 } else {
169+ this . removeFiltersFromModel ( ) ;
125170 this . updateResults ( ) ;
126171 }
127172 } ,
@@ -195,7 +240,6 @@ issueList.SortingView = Backbone.View.extend({
195240 events : { } ,
196241 initialize : function ( ) {
197242 this . paginationModel = new Backbone . Model ( {
198- // TODO(miket): persist selected page limit to survive page loads
199243 dropdownTitle : 'Show 50' ,
200244 dropdownOptions : [
201245 { title : 'Show 25' , params : 'per_page=25' } ,
@@ -259,36 +303,48 @@ issueList.IssueView = Backbone.View.extend({
259303 events : {
260304 'click .js-issue-label' : 'labelSearch' ,
261305 } ,
306+ _filterRegex : / & * ( n e w | n e e d s d i a g n o s i s | c o n t a c t r e a d y | s i t e w a i t | c l o s e d ) = 1 & * / i,
262307 _isLoggedIn : $ ( 'body' ) . data ( 'username' ) ,
263308 _loadingIndicator : $ ( '.js-loader' ) ,
264- _pageLimit : null ,
265309 initialize : function ( ) {
266310 this . issues = new issueList . IssueCollection ( ) ;
267- // check to see if we should pre-filter results
268- // otherwise load default (unfiltered "all")
269- this . loadIssues ( ) ;
270311
271312 // set up event listeners.
272313 issueList . events . on ( 'issues:update' , _ . bind ( this . updateIssues , this ) ) ;
314+ issueList . events . on ( 'filter:add-to-model' , _ . bind ( this . updateModelParams , this ) ) ;
315+ issueList . events . on ( 'filter:remove-from-model' , _ . bind ( this . removeAllFiltersFromModel , this ) ) ;
273316 issueList . events . on ( 'paginate:next' , _ . bind ( this . requestNextPage , this ) ) ;
274317 issueList . events . on ( 'paginate:previous' , _ . bind ( this . requestPreviousPage , this ) ) ;
275318 wcEvents . on ( 'dropdown:change' , _ . bind ( this . updateModelParams , this ) ) ;
319+
320+ this . loadIssues ( ) ;
276321 } ,
277322 template : _ . template ( $ ( '#issuelist-issue-tmpl' ) . html ( ) ) ,
278323 loadIssues : function ( ) {
279- // First checks URL params, e.g., /?new=1 and activates the new filter,
280- // or loads default unsorted/unfiltered issues
324+ // Attemps to load model state from URL params, if present,
325+ // otherwise grab model defaults and load issues
326+
327+ // popstate should load this method.
281328 var category ;
282- var filterRegex = / \? ( n e w | n e e d s d i a g n o s i s | c o n t a c t r e a d y | s i t e w a i t | c l o s e d ) = 1 / ;
283- if ( category = window . location . search . match ( filterRegex ) ) {
284- // If there was a match, load the relevant results and fire an event
285- // to notify the button to activate.
286- this . updateIssues ( category [ 1 ] ) ;
287- _ . delay ( function ( ) {
288- issueList . events . trigger ( 'filter:activate' , category [ 1 ] ) ;
289- } , 0 ) ;
329+ // get params excluding the leading ?
330+ var urlParams = location . search . slice ( 1 ) ;
331+
332+ if ( location . search . length !== 0 ) {
333+ // There are some params in the URL
334+ if ( category = window . location . search . match ( this . _filterRegex ) ) {
335+ // If there was a filter match, fire an event which loads results
336+ // and notifies the button to activate.
337+ this . updateModelParams ( urlParams ) ;
338+ _ . delay ( function ( ) {
339+ issueList . events . trigger ( 'filter:activate' , category [ 1 ] ) ;
340+ } , 0 ) ;
341+ } else {
342+ this . updateModelParams ( urlParams ) ;
343+ this . fetchAndRenderIssues ( ) ;
344+ }
290345 } else {
291- // Otherwise, load default issues.
346+ // There are no params in the URL, load the defaults
347+ this . updateURLParams ( ) ;
292348 this . fetchAndRenderIssues ( ) ;
293349 }
294350 } ,
@@ -318,9 +374,6 @@ issueList.IssueView = Backbone.View.extend({
318374 wcEvents . trigger ( 'flash:error' , { message : message , timeout : timeout } ) ;
319375 } ) ;
320376 } ,
321- getPageLimit : function ( ) {
322- return this . _pageLimit ;
323- } ,
324377 render : function ( issues ) {
325378 this . $el . html ( this . template ( {
326379 issues : issues . toJSON ( )
@@ -366,6 +419,16 @@ issueList.IssueView = Backbone.View.extend({
366419 issueList . events . trigger ( 'issues:update' , { query : labelFilter } ) ;
367420 e . preventDefault ( ) ;
368421 } ,
422+ removeAllFiltersFromModel : function ( ) {
423+ // We can't have more than one filter at once for the issues model,
424+ // and there's no meaningful notion of contactready=0, so remove them all.
425+ var filters = [ 'new' , 'needsdiagnosis' , 'contactready' ,
426+ 'sitewait' , 'closed' , 'needscontact' , 'q' ] ;
427+ _ . forEach ( filters , function ( filter ) {
428+ delete this . issues . params [ filter ] ;
429+ } , this ) ;
430+
431+ } ,
369432 requestNextPage : function ( ) {
370433 var nextPage ;
371434 if ( nextPage = this . issues . getNextPage ( ) ) {
@@ -387,7 +450,7 @@ issueList.IssueView = Backbone.View.extend({
387450 // note: until GitHub fixes a bug where requesting issues filtered by labels
388451 // doesn't return pagination via Link, we get those results via the Search API.
389452 var searchCategories = [ 'new' , 'contactready' , 'needsdiagnosis' , 'sitewait' ] ;
390- var params = $ . extend ( this . issues . params , this . getPageLimit ( ) ) ;
453+ var params = this . issues . params ;
391454
392455 // note: if query is the empty string, it will load all issues from the
393456 // '/api/issues' endpoint (which I think we want).
@@ -401,26 +464,85 @@ issueList.IssueView = Backbone.View.extend({
401464 } else {
402465 this . issues . setURLState ( '/api/issues' , params ) ;
403466 }
467+ this . updateURLParams ( ) ;
404468 this . fetchAndRenderIssues ( ) ;
405469 } ,
406- updateModelParams : function ( params ) {
470+ updateModelParams : function ( params , options ) {
471+ // TODO: profile how many times this gets called and optimize.
407472 // convert params string to an array,
408473 // splitting on & in case of multiple params
409- var paramsArray = params . split ( '&' ) ;
474+ // params are merged into issues model
475+ // call _.uniq() on it to ignore duplicate values
476+ var paramsArray = _ . uniq ( params . split ( '&' ) ) ;
410477
411- // paramsArray is an array of param 'key=value' string pairs,
478+ // paramsArray is an array of param 'key=value' string pairs
412479 _ . forEach ( paramsArray , _ . bind ( function ( param ) {
413480 var kvArray = param . split ( '=' ) ;
414481 var key = kvArray [ 0 ] ;
415482 var value = kvArray [ 1 ] ;
416483 this . issues . params [ key ] = value ;
484+ } , this ) ) ;
417485
418- if ( key === 'per_page' ) {
419- this . _pageLimit = value ;
420- }
486+ //broadcast to each of the dropdowns that they need to update
487+ var pageDropdown ;
488+ if ( 'per_page' in this . issues . params ) {
489+ pageDropdown = 'per_page=' + this . issues . params . per_page ;
490+ _ . delay ( function ( ) {
491+ issueList . events . trigger ( 'dropdown:update' , pageDropdown ) ;
492+ } , 0 ) ;
493+ }
494+
495+ var sortDropdown ;
496+ // all the sort options begin with sort, and end with direction.
497+ if ( 'sort' in this . issues . params ) {
498+ sortDropdown = 'sort=' + this . issues . params . sort + '&direction=' + this . issues . params . direction ;
499+ _ . delay ( function ( ) {
500+ issueList . events . trigger ( 'dropdown:update' , sortDropdown ) ;
501+ } , 0 ) ;
502+ }
503+
504+ // make sure we prevent more than one mutually-exclusive state param
505+ // in the model, because that's weird. the "last" param will win.
506+ var currentStateParamName ;
507+ var stateParamsSet = [ 'state' , 'creator' , 'mentioned' ] ;
508+ var stateParam = _ . find ( paramsArray , function ( paramString ) {
509+ return _ . find ( stateParamsSet , function ( stateParam ) {
510+ if ( paramString . indexOf ( stateParam ) === 0 ) {
511+ return currentStateParamName = stateParam ;
512+ }
513+ } ) ;
514+ } ) ;
515+
516+ // delete the non-current state params from the stateParamsSet
517+ var toDelete = _ . without ( stateParamsSet , currentStateParamName ) ;
518+ _ . forEach ( toDelete , _ . bind ( function ( param ) {
519+ delete this . issues . params [ param ] ;
421520 } , this ) ) ;
422521
423- this . fetchAndRenderIssues ( ) ;
522+ var stateDropdown ;
523+ if ( currentStateParamName in this . issues . params ) {
524+ stateDropdown = stateParam ;
525+ _ . delay ( function ( ) {
526+ issueList . events . trigger ( 'dropdown:update' , stateDropdown ) ;
527+ } , 0 ) ;
528+ }
529+
530+ this . updateURLParams ( ) ;
531+ // only re-request issues if explicitly asked to
532+ if ( options && options . update === true ) {
533+ this . fetchAndRenderIssues ( ) ;
534+ }
535+ } ,
536+ updateURLParams : function ( ) {
537+ // push params from the model back to the URL so it can be used for bookmarks,
538+ // link sharing, etc.
539+ // an optional category can be passed in to be added.
540+ // TODO: figure out next and prev buttons.
541+ var serializedParams = $ . param ( this . issues . params ) ;
542+
543+ if ( history . pushState ) {
544+ history . pushState ( { } , '' , '?' + serializedParams ) ;
545+ }
424546 }
425547} ) ;
426548
0 commit comments