@@ -119,6 +119,9 @@ function init () {
119
119
if ( document . querySelector ( '#search-filter' ) . value . trim ( ) ) {
120
120
filterProposals ( )
121
121
}
122
+
123
+ // apply selections from the current page's URI fragment
124
+ _applyFragment ( document . location . hash )
122
125
} )
123
126
124
127
req . addEventListener ( 'error' , function ( e ) {
@@ -192,7 +195,7 @@ function renderNav () {
192
195
var className = states [ state ] . className
193
196
194
197
return html ( 'li' , null , [
195
- html ( 'input' , { type : 'checkbox' , id : 'filter-by-' + className , value : className } ) ,
198
+ html ( 'input' , { type : 'checkbox' , className : 'filtered-by-status' , id : 'filter-by-' + className , value : className } ) ,
196
199
html ( 'label' , { className : className , tabindex : '0' , role : 'button' , 'for' : 'filter-by-' + className } , [
197
200
states [ state ] . name
198
201
] )
@@ -220,23 +223,18 @@ function renderNav () {
220
223
var versionRowHeader = html ( 'h5' , { id : 'version-options-label' , className : 'hidden' } , 'Language Version' )
221
224
var versionRow = html ( 'ul' , { id : 'version-options' , className : 'filter-by-status hidden' } )
222
225
223
- /** Helper to give versions like 3.0.1 an okay ID to use in a DOM element. (swift-3-0-1) */
224
- function idSafeName ( name ) {
225
- return 'swift-' + name . replace ( / \. / g, '-' )
226
- }
227
-
228
226
var versionOptions = languageVersions . map ( function ( version ) {
229
227
return html ( 'li' , null , [
230
228
html ( 'input' , {
231
229
type : 'checkbox' ,
232
- id : 'filter-by-swift-' + idSafeName ( version ) ,
230
+ id : 'filter-by-swift-' + _idSafeName ( version ) ,
233
231
className : 'filter-by-swift-version' ,
234
- value : 'swift-' + idSafeName ( version )
232
+ value : 'swift-' + _idSafeName ( version )
235
233
} ) ,
236
234
html ( 'label' , {
237
235
tabindex : '0' ,
238
236
role : 'button' ,
239
- 'for' : 'filter-by-swift-' + idSafeName ( version )
237
+ 'for' : 'filter-by-swift-' + _idSafeName ( version )
240
238
} , 'Swift ' + version )
241
239
] )
242
240
} )
@@ -594,20 +592,30 @@ function filterProposals () {
594
592
clearButton . classList . remove ( 'hidden' )
595
593
}
596
594
597
- // The search input treats words as order-independent.
598
- var matchingSets = filter . split ( / \s / )
599
- . filter ( function ( s ) { return s . length > 0 } )
600
- . map ( function ( part ) { return _searchProposals ( part ) } )
595
+ var matchingSets = [ proposals . concat ( ) ]
596
+
597
+ // Comma-separated lists of proposal IDs are treated as an "or" search.
598
+ if ( filter . match ( / ( S E - \d \d \d \d ) ( $ | ( ( , S E - \d \d \d \d ) + ) ) / i) ) {
599
+ var proposalIDs = filter . split ( ',' ) . map ( function ( id ) {
600
+ return id . toUpperCase ( )
601
+ } )
601
602
602
- if ( filter . trim ( ) . length === 0 ) {
603
- matchingSets = [ proposals . concat ( ) ]
603
+ matchingSets [ 0 ] = matchingSets [ 0 ] . filter ( function ( proposal ) {
604
+ return proposalIDs . indexOf ( proposal . id ) !== - 1
605
+ } )
606
+ } else if ( filter . trim ( ) . length !== 0 ) {
607
+ // The search input treats words as order-independent.
608
+ matchingSets = filter . split ( / \s / )
609
+ . filter ( function ( s ) { return s . length > 0 } )
610
+ . map ( function ( part ) { return _searchProposals ( part ) } )
604
611
}
605
612
606
613
var intersection = matchingSets . reduce ( function ( intersection , candidates ) {
607
614
return intersection . filter ( function ( alreadyIncluded ) { return candidates . indexOf ( alreadyIncluded ) !== - 1 } )
608
615
} , matchingSets [ 0 ] || [ ] )
609
616
610
617
_applyFilter ( intersection )
618
+ _updateURIFragment ( )
611
619
}
612
620
613
621
/**
@@ -727,6 +735,189 @@ function _applyFilter (matchingProposals) {
727
735
updateProposalsCount ( matchingProposals . length )
728
736
}
729
737
738
+ /**
739
+ * Parses a URI fragment and applies a search and filters to the page.
740
+ *
741
+ * Syntax (a query string within a fragment):
742
+ * fragment --> `#?` parameter-value-list
743
+ * parameter-value-list --> parameter-value | parameter-value-pair `&` parameter-value-list
744
+ * parameter-value-pair --> parameter `=` value
745
+ * parameter --> `proposal` | `status` | `version` | `search`
746
+ * value --> ** Any URL-encoded text. **
747
+ *
748
+ * For example:
749
+ * /#?proposal:SE-0180,SE-0123
750
+ * /#?status=rejected&version=3&search=access
751
+ *
752
+ * Four types of parameters are supported:
753
+ * - proposal: A comma-separated list of proposal IDs. Treated as an 'or' search.
754
+ * - filter: A comma-separated list of proposal statuses to apply as a filter.
755
+ * - version: A comma-separated list of Swift version numbers to apply as a filter.
756
+ * - search: Raw, URL-encoded text used to filter by individual term.
757
+ *
758
+ * @param {string } fragment - A URI fragment to use as the basis for a search.
759
+ */
760
+ function _applyFragment ( fragment ) {
761
+ if ( ! fragment || fragment . substr ( 0 , 2 ) !== '#?' ) return
762
+ fragment = fragment . substring ( 2 ) // remove the #?
763
+
764
+ // use this literal's keys as the source of truth for key-value pairs in the fragment
765
+ var actions = { proposal : [ ] , search : null , status : [ ] , version : [ ] }
766
+
767
+ // parse the fragment as a query string
768
+ Object . keys ( actions ) . forEach ( function ( action ) {
769
+ var pattern = new RegExp ( action + '=([^=]+)(&|$)' )
770
+ var values = fragment . match ( pattern )
771
+
772
+ if ( values ) {
773
+ var value = values [ 1 ] // 1st capture group from the RegExp
774
+ if ( action === 'search' ) {
775
+ value = decodeURIComponent ( value )
776
+ } else {
777
+ value = value . split ( ',' )
778
+ }
779
+
780
+ actions [ action ] = value
781
+ }
782
+ } )
783
+
784
+ // perform key-specific parsing and checks
785
+
786
+ if ( actions . proposal . length ) {
787
+ document . querySelector ( '#search-filter' ) . value = actions . proposal . join ( ',' )
788
+ } else if ( actions . search ) {
789
+ document . querySelector ( '#search-filter' ) . value = actions . search
790
+ }
791
+
792
+ if ( actions . version . length ) {
793
+ var versionSelections = actions . version . map ( function ( version ) {
794
+ return document . querySelector ( '#filter-by-swift-' + _idSafeName ( version ) )
795
+ } ) . filter ( function ( version ) {
796
+ return ! ! version
797
+ } )
798
+
799
+ versionSelections . forEach ( function ( versionSelection ) {
800
+ versionSelection . checked = true
801
+ } )
802
+
803
+ if ( versionSelections . length ) {
804
+ document . querySelector (
805
+ '#filter-by-' + states [ '.implemented' ] . className
806
+ ) . checked = true
807
+ }
808
+ }
809
+
810
+ // track this state specifically for toggling the version panel
811
+ var implementedSelected = false
812
+
813
+ // update the filter selections in the nav
814
+ if ( actions . status . length ) {
815
+ var statusSelections = actions . status . map ( function ( status ) {
816
+ var stateName = Object . keys ( states ) . filter ( function ( state ) {
817
+ return states [ state ] . className === status
818
+ } ) [ 0 ]
819
+
820
+ if ( ! stateName ) return // fragment contains a nonexistent state
821
+ state = states [ stateName ]
822
+
823
+ if ( stateName === '.implemented' ) implementedSelected = true
824
+
825
+ return document . querySelector ( '#filter-by-' + state . className )
826
+ } ) . filter ( function ( status ) {
827
+ return ! ! status
828
+ } )
829
+
830
+ statusSelections . forEach ( function ( statusSelection ) {
831
+ statusSelection . checked = true
832
+ } )
833
+ }
834
+
835
+ // the version panel needs to be activated if any are specified
836
+ if ( actions . version . length || implementedSelected ) {
837
+ ; [ '#version-options' , '#version-options-label' ] . forEach ( function ( selector ) {
838
+ document . querySelector ( '.filter-options' )
839
+ . querySelector ( selector ) . classList
840
+ . toggle ( 'hidden' )
841
+ } )
842
+ }
843
+
844
+ // specifying any filter in the fragment should activate the filters in the UI
845
+ if ( actions . version . length || actions . status . length ) {
846
+ toggleFilterPanel ( )
847
+ toggleFiltering ( )
848
+ }
849
+
850
+ filterProposals ( )
851
+ }
852
+
853
+ /**
854
+ * Writes out the current search and filter settings to document.location
855
+ * via window.replaceState.
856
+ */
857
+ function _updateURIFragment ( ) {
858
+ var actions = { proposal : [ ] , search : null , status : [ ] , version : [ ] }
859
+
860
+ var search = document . querySelector ( '#search-filter' )
861
+
862
+ if ( search . value && search . value . match ( / ( S E - \d \d \d \d ) ( $ | ( ( , S E - \d \d \d \d ) + ) ) / i) ) {
863
+ actions . proposal = search . value . toUpperCase ( ) . split ( ',' )
864
+ } else {
865
+ actions . search = search . value
866
+ }
867
+
868
+ var selectedVersions = document . querySelectorAll ( '.filter-by-swift-version:checked' )
869
+ var versions = [ ] . map . call ( selectedVersions , function ( checkbox ) {
870
+ return checkbox . value . split ( 'swift-swift-' ) [ 1 ] . split ( '-' ) . join ( '.' )
871
+ } )
872
+
873
+ actions . version = versions
874
+
875
+ var selectedStatuses = document . querySelectorAll ( '.filtered-by-status:checked' )
876
+ var statuses = [ ] . map . call ( selectedStatuses , function ( checkbox ) {
877
+ var className = checkbox . value
878
+
879
+ var correspondingStatus = Object . keys ( states ) . filter ( function ( status ) {
880
+ if ( states [ status ] . className === className ) return true
881
+ return false
882
+ } ) [ 0 ]
883
+
884
+ return states [ correspondingStatus ] . className
885
+ } )
886
+
887
+ // .implemented is redundant if any specific implementation versions are selected.
888
+ if ( actions . version . length ) {
889
+ statuses = statuses . filter ( function ( status ) {
890
+ return status !== states [ '.implemented' ] . className
891
+ } )
892
+ }
893
+
894
+ actions . status = statuses
895
+
896
+ // build the actual fragment string.
897
+ var fragments = [ ]
898
+ if ( actions . proposal . length ) fragments . push ( 'proposal=' + actions . proposal . join ( ',' ) )
899
+ if ( actions . status . length ) fragments . push ( 'status=' + actions . status . join ( ',' ) )
900
+ if ( actions . version . length ) fragments . push ( 'version=' + actions . version . join ( ',' ) )
901
+
902
+ // encoding the search lets you search for `??` and other edge cases.
903
+ if ( actions . search ) fragments . push ( 'search=' + encodeURIComponent ( actions . search ) )
904
+
905
+ if ( ! fragments . length ) {
906
+ window . history . replaceState ( null , null , './' )
907
+ return
908
+ }
909
+
910
+ var fragment = '#?' + fragments . join ( '&' )
911
+
912
+ // avoid creating new history entries each time a search or filter updates
913
+ window . history . replaceState ( null , null , fragment )
914
+ }
915
+
916
+ /** Helper to give versions like 3.0.1 an okay ID to use in a DOM element. (swift-3-0-1) */
917
+ function _idSafeName ( name ) {
918
+ return 'swift-' + name . replace ( / \. / g, '-' )
919
+ }
920
+
730
921
/**
731
922
* Changes the text after 'Filtered by: ' to reflect the current status filters.
732
923
*
0 commit comments