Skip to content

Commit a532ae5

Browse files
authored
Merge pull request #732 from krilnon/master
[Status] Add support for permalinks to searches and filters.
2 parents 3cd4e44 + 2e046e3 commit a532ae5

File tree

1 file changed

+206
-15
lines changed

1 file changed

+206
-15
lines changed

index.js

Lines changed: 206 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ function init () {
119119
if (document.querySelector('#search-filter').value.trim()) {
120120
filterProposals()
121121
}
122+
123+
// apply selections from the current page's URI fragment
124+
_applyFragment(document.location.hash)
122125
})
123126

124127
req.addEventListener('error', function (e) {
@@ -192,7 +195,7 @@ function renderNav () {
192195
var className = states[state].className
193196

194197
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 }),
196199
html('label', { className: className, tabindex: '0', role: 'button', 'for': 'filter-by-' + className }, [
197200
states[state].name
198201
])
@@ -220,23 +223,18 @@ function renderNav () {
220223
var versionRowHeader = html('h5', { id: 'version-options-label', className: 'hidden' }, 'Language Version')
221224
var versionRow = html('ul', { id: 'version-options', className: 'filter-by-status hidden' })
222225

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-
228226
var versionOptions = languageVersions.map(function (version) {
229227
return html('li', null, [
230228
html('input', {
231229
type: 'checkbox',
232-
id: 'filter-by-swift-' + idSafeName(version),
230+
id: 'filter-by-swift-' + _idSafeName(version),
233231
className: 'filter-by-swift-version',
234-
value: 'swift-' + idSafeName(version)
232+
value: 'swift-' + _idSafeName(version)
235233
}),
236234
html('label', {
237235
tabindex: '0',
238236
role: 'button',
239-
'for': 'filter-by-swift-' + idSafeName(version)
237+
'for': 'filter-by-swift-' + _idSafeName(version)
240238
}, 'Swift ' + version)
241239
])
242240
})
@@ -594,20 +592,30 @@ function filterProposals () {
594592
clearButton.classList.remove('hidden')
595593
}
596594

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(/(SE-\d\d\d\d)($|((,SE-\d\d\d\d)+))/i)) {
599+
var proposalIDs = filter.split(',').map(function (id) {
600+
return id.toUpperCase()
601+
})
601602

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) })
604611
}
605612

606613
var intersection = matchingSets.reduce(function (intersection, candidates) {
607614
return intersection.filter(function (alreadyIncluded) { return candidates.indexOf(alreadyIncluded) !== -1 })
608615
}, matchingSets[0] || [])
609616

610617
_applyFilter(intersection)
618+
_updateURIFragment()
611619
}
612620

613621
/**
@@ -727,6 +735,189 @@ function _applyFilter (matchingProposals) {
727735
updateProposalsCount(matchingProposals.length)
728736
}
729737

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(/(SE-\d\d\d\d)($|((,SE-\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+
730921
/**
731922
* Changes the text after 'Filtered by: ' to reflect the current status filters.
732923
*

0 commit comments

Comments
 (0)