@@ -18,6 +18,8 @@ issueList.DropdownView = Backbone.View.extend({
18
18
this . closeDropdown ( ) ;
19
19
}
20
20
} , this ) ) ;
21
+
22
+ issueList . events . on ( 'dropdown:update' , _ . bind ( this . selectDropdownOption , this ) ) ;
21
23
} ,
22
24
template : _ . template ( $ ( '#dropdown-tmpl' ) . html ( ) ) ,
23
25
render : function ( ) {
@@ -32,21 +34,39 @@ issueList.DropdownView = Backbone.View.extend({
32
34
this . $el . removeClass ( 'is-active' ) ;
33
35
} ,
34
36
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 ;
39
39
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 ( ) ;
45
55
}
46
56
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
+ }
50
70
} ,
51
71
updateDropdownTitle : function ( optionElm ) {
52
72
this . model . set ( 'dropdownTitle' , optionElm . text ( ) ) ;
@@ -59,6 +79,7 @@ issueList.FilterView = Backbone.View.extend({
59
79
events : {
60
80
'click .js-filter-button' : 'toggleFilter'
61
81
} ,
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,
62
83
_isLoggedIn : $ ( 'body' ) . data ( 'username' ) ,
63
84
_userName : $ ( 'body' ) . data ( 'username' ) ,
64
85
initialize : function ( ) {
@@ -99,12 +120,27 @@ issueList.FilterView = Backbone.View.extend({
99
120
this . dropdown . setElement ( this . $el . find ( '.js-dropdown-wrapper' ) ) . render ( ) ;
100
121
return this ;
101
122
} ,
123
+ addFilterToModel : function ( filter ) {
124
+ issueList . events . trigger ( 'filter:add-to-model' , filter ) ;
125
+ } ,
102
126
clearFilter : function ( ) {
103
127
var btns = $ ( '[data-filter]' ) ;
104
128
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' ) ;
105
140
} ,
106
141
toggleFilter : function ( e ) {
107
142
var btn ;
143
+ var filterParam ;
108
144
// Stringy e comes from triggered filter:activate event
109
145
if ( typeof e === 'string' ) {
110
146
btn = $ ( '[data-filter=' + e + ']' ) ;
@@ -119,9 +155,18 @@ issueList.FilterView = Backbone.View.extend({
119
155
// Clear the search field
120
156
issueList . events . trigger ( 'search:clear' ) ;
121
157
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
+
122
164
if ( btn . hasClass ( 'is-active' ) ) {
165
+ filterParam = btn . data ( 'filter' ) + "=1" ;
123
166
this . updateResults ( btn . data ( 'filter' ) ) ;
167
+ this . addFilterToModel ( filterParam ) ;
124
168
} else {
169
+ this . removeFiltersFromModel ( ) ;
125
170
this . updateResults ( ) ;
126
171
}
127
172
} ,
@@ -195,7 +240,6 @@ issueList.SortingView = Backbone.View.extend({
195
240
events : { } ,
196
241
initialize : function ( ) {
197
242
this . paginationModel = new Backbone . Model ( {
198
- // TODO(miket): persist selected page limit to survive page loads
199
243
dropdownTitle : 'Show 50' ,
200
244
dropdownOptions : [
201
245
{ title : 'Show 25' , params : 'per_page=25' } ,
@@ -259,36 +303,48 @@ issueList.IssueView = Backbone.View.extend({
259
303
events : {
260
304
'click .js-issue-label' : 'labelSearch' ,
261
305
} ,
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,
262
307
_isLoggedIn : $ ( 'body' ) . data ( 'username' ) ,
263
308
_loadingIndicator : $ ( '.js-loader' ) ,
264
- _pageLimit : null ,
265
309
initialize : function ( ) {
266
310
this . issues = new issueList . IssueCollection ( ) ;
267
- // check to see if we should pre-filter results
268
- // otherwise load default (unfiltered "all")
269
- this . loadIssues ( ) ;
270
311
271
312
// set up event listeners.
272
313
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 ) ) ;
273
316
issueList . events . on ( 'paginate:next' , _ . bind ( this . requestNextPage , this ) ) ;
274
317
issueList . events . on ( 'paginate:previous' , _ . bind ( this . requestPreviousPage , this ) ) ;
275
318
wcEvents . on ( 'dropdown:change' , _ . bind ( this . updateModelParams , this ) ) ;
319
+
320
+ this . loadIssues ( ) ;
276
321
} ,
277
322
template : _ . template ( $ ( '#issuelist-issue-tmpl' ) . html ( ) ) ,
278
323
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.
281
328
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
+ }
290
345
} else {
291
- // Otherwise, load default issues.
346
+ // There are no params in the URL, load the defaults
347
+ this . updateURLParams ( ) ;
292
348
this . fetchAndRenderIssues ( ) ;
293
349
}
294
350
} ,
@@ -318,9 +374,6 @@ issueList.IssueView = Backbone.View.extend({
318
374
wcEvents . trigger ( 'flash:error' , { message : message , timeout : timeout } ) ;
319
375
} ) ;
320
376
} ,
321
- getPageLimit : function ( ) {
322
- return this . _pageLimit ;
323
- } ,
324
377
render : function ( issues ) {
325
378
this . $el . html ( this . template ( {
326
379
issues : issues . toJSON ( )
@@ -366,6 +419,16 @@ issueList.IssueView = Backbone.View.extend({
366
419
issueList . events . trigger ( 'issues:update' , { query : labelFilter } ) ;
367
420
e . preventDefault ( ) ;
368
421
} ,
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
+ } ,
369
432
requestNextPage : function ( ) {
370
433
var nextPage ;
371
434
if ( nextPage = this . issues . getNextPage ( ) ) {
@@ -387,7 +450,7 @@ issueList.IssueView = Backbone.View.extend({
387
450
// note: until GitHub fixes a bug where requesting issues filtered by labels
388
451
// doesn't return pagination via Link, we get those results via the Search API.
389
452
var searchCategories = [ 'new' , 'contactready' , 'needsdiagnosis' , 'sitewait' ] ;
390
- var params = $ . extend ( this . issues . params , this . getPageLimit ( ) ) ;
453
+ var params = this . issues . params ;
391
454
392
455
// note: if query is the empty string, it will load all issues from the
393
456
// '/api/issues' endpoint (which I think we want).
@@ -401,26 +464,85 @@ issueList.IssueView = Backbone.View.extend({
401
464
} else {
402
465
this . issues . setURLState ( '/api/issues' , params ) ;
403
466
}
467
+ this . updateURLParams ( ) ;
404
468
this . fetchAndRenderIssues ( ) ;
405
469
} ,
406
- updateModelParams : function ( params ) {
470
+ updateModelParams : function ( params , options ) {
471
+ // TODO: profile how many times this gets called and optimize.
407
472
// convert params string to an array,
408
473
// 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 ( '&' ) ) ;
410
477
411
- // paramsArray is an array of param 'key=value' string pairs,
478
+ // paramsArray is an array of param 'key=value' string pairs
412
479
_ . forEach ( paramsArray , _ . bind ( function ( param ) {
413
480
var kvArray = param . split ( '=' ) ;
414
481
var key = kvArray [ 0 ] ;
415
482
var value = kvArray [ 1 ] ;
416
483
this . issues . params [ key ] = value ;
484
+ } , this ) ) ;
417
485
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 ] ;
421
520
} , this ) ) ;
422
521
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
+ }
424
546
}
425
547
} ) ;
426
548
0 commit comments