aboutsummaryrefslogtreecommitdiffstats
path: root/web/js
diff options
context:
space:
mode:
authorZarino Zappia <mail@zarino.co.uk>2016-09-30 16:34:00 +0100
committerMatthew Somerville <matthew-github@dracos.co.uk>2016-10-13 19:22:11 +0100
commita5ef113e2cc3105da41cf5449b505db6fa336c59 (patch)
treec60d906c0b5dd6fc974a35f8e921ae728dab080c /web/js
parent3872c39f5426165c3abfe397d15dd2a63f731e26 (diff)
Allow multiple selections in report list filter.
This lets people filter by multiple categories or states. It uses our jQuery multi-select plugin to turn the <select multiple>s into little overlay lists of checkboxes. HTML5 history is also supported.
Diffstat (limited to 'web/js')
-rw-r--r--web/js/jquery.multi-select.js256
-rw-r--r--web/js/map-OpenLayers.js44
2 files changed, 291 insertions, 9 deletions
diff --git a/web/js/jquery.multi-select.js b/web/js/jquery.multi-select.js
new file mode 100644
index 000000000..a41190e9c
--- /dev/null
+++ b/web/js/jquery.multi-select.js
@@ -0,0 +1,256 @@
+// jquery.multi-select.js
+// by mySociety
+// https://github.com/mysociety/jquery-multi-select
+
+;(function($) {
+
+ "use strict";
+
+ var pluginName = "multiSelect",
+ defaults = {
+ containerHTML: '<div class="multi-select-container">',
+ menuHTML: '<div class="multi-select-menu">',
+ buttonHTML: '<span class="multi-select-button">',
+ menuItemHTML: '<label class="multi-select-menuitem">',
+ activeClass: 'multi-select-container--open',
+ noneText: '-- Select --',
+ allText: undefined,
+ positionedMenuClass: 'multi-select-container--positioned',
+ positionMenuWithin: undefined
+ };
+
+ function Plugin(element, options) {
+ this.element = element;
+ this.$element = $(element);
+ this.settings = $.extend( {}, defaults, options );
+ this._defaults = defaults;
+ this._name = pluginName;
+ this.init();
+ }
+
+ $.extend(Plugin.prototype, {
+
+ init: function() {
+ this.checkSuitableInput();
+ this.findLabels();
+ this.constructContainer();
+ this.constructButton();
+ this.constructMenu();
+
+ this.setUpBodyClickListener();
+ this.setUpLabelsClickListener();
+
+ this.$element.hide();
+ },
+
+ checkSuitableInput: function(text) {
+ if ( this.$element.is('select[multiple]') === false ) {
+ throw new Error('$.multiSelect only works on <select multiple> elements');
+ }
+ },
+
+ findLabels: function() {
+ this.$labels = $('label[for="' + this.$element.attr('id') + '"]');
+ },
+
+ constructContainer: function() {
+ this.$container = $(this.settings.containerHTML);
+ this.$element.data('multi-select-container', this.$container);
+ this.$container.insertAfter(this.$element);
+ },
+
+ constructButton: function() {
+ var _this = this;
+ this.$button = $(this.settings.buttonHTML);
+ this.$button.attr({
+ 'role': 'button',
+ 'aria-haspopup': 'true',
+ 'tabindex': 0,
+ 'aria-label': this.$labels.eq(0).text()
+ })
+ .on('keydown.multiselect', function(e) {
+ var key = e.which;
+ var returnKey = 13;
+ var spaceKey = 32;
+ if ((key === returnKey) || (key === spaceKey)) {
+ _this.$button.click();
+ }
+ }).on('click.multiselect', function(e) {
+ _this.menuToggle();
+ });
+
+ this.$element.on('change.multiselect', function() {
+ _this.updateButtonContents();
+ });
+
+ this.$container.append(this.$button);
+
+ this.updateButtonContents();
+ },
+
+ constructMenu: function() {
+ var _this = this;
+
+ this.$menu = $(this.settings.menuHTML);
+ this.$menu.attr({
+ 'role': 'menu'
+ }).on('keyup.multiselect', function(e){
+ var key = e.which;
+ var escapeKey = 27;
+ if (key === escapeKey) {
+ _this.menuHide();
+ }
+ });
+
+ this.$menu.on('change.multiselect', function() {
+ _this.updateButtonContents();
+ });
+
+ this.$element.on('change.multiselect', function(e, internal) {
+ // Don't need to update the menu contents if this
+ // change event was fired by our tickbox handler.
+ if(internal !== true){
+ _this.updateMenuContents();
+ }
+ });
+
+ this.$container.append(this.$menu);
+
+ this.updateMenuContents();
+ },
+
+ setUpBodyClickListener: function() {
+ var _this = this;
+
+ // Hide the $menu when you click outside of it.
+ $('html').on('click.multiselect', function(){
+ _this.menuHide();
+ });
+
+ // Stop click events from inside the $button or $menu from
+ // bubbling up to the body and closing the menu!
+ this.$container.on('click.multiselect', function(e){
+ e.stopPropagation();
+ });
+ },
+
+ setUpLabelsClickListener: function() {
+ var _this = this;
+ this.$labels.on('click.multiselect', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ _this.menuToggle();
+ });
+ },
+
+ updateMenuContents: function() {
+ var _this = this;
+ this.$menu.empty();
+ this.$element.children('option').each(function(option_index, option) {
+ var $item = _this.constructMenuItem($(option), option_index);
+ _this.$menu.append($item);
+ });
+ },
+
+ constructMenuItem: function($option, option_index) {
+ var unique_id = this.$element.attr('name') + '_' + option_index;
+ var $item = $(this.settings.menuItemHTML)
+ .attr({
+ 'for': unique_id,
+ 'role': 'menuitem'
+ })
+ .text(' ' + $option.text());
+
+ var $input = $('<input>')
+ .attr({
+ 'type': 'checkbox',
+ 'id': unique_id,
+ 'value': $option.val()
+ });
+ if ( $option.is(':disabled') ) {
+ $input.attr('disabled', 'disabled');
+ }
+ if ( $option.is(':selected') ) {
+ $input.prop('checked', 'checked');
+ }
+
+ $input.on('change.multiselect', function() {
+ if ($(this).prop('checked')) {
+ $option.prop('selected', true);
+ } else {
+ $option.prop('selected', false);
+ }
+
+ // .prop() on its own doesn't generate a change event.
+ // Other plugins might want to do stuff onChange.
+ $option.trigger('change', [true]);
+ });
+
+ $item.prepend($input);
+ return $item;
+ },
+
+ updateButtonContents: function() {
+ var _this = this;
+ var options = [];
+ var selected = [];
+
+ this.$element.children('option').each(function() {
+ var text = $(this).text();
+ options.push(text);
+ if ($(this).is(':selected')) {
+ selected.push( $.trim(text) );
+ }
+ });
+
+ this.$button.empty();
+
+ if (selected.length == 0) {
+ this.$button.text( this.settings.noneText );
+ } else if ( (selected.length === options.length) && this.settings.allText) {
+ this.$button.text( this.settings.allText );
+ } else {
+ this.$button.text( selected.join(', ') );
+ }
+ },
+
+ menuShow: function() {
+ this.$container.addClass(this.settings.activeClass);
+ if (this.settings.positionMenuWithin && this.settings.positionMenuWithin instanceof $) {
+ var menuLeftEdge = this.$menu.offset().left + this.$menu.outerWidth();
+ var withinLeftEdge = this.settings.positionMenuWithin.offset().left +
+ this.settings.positionMenuWithin.outerWidth();
+
+ if( menuLeftEdge > withinLeftEdge ) {
+ this.$menu.css( 'width', (withinLeftEdge - this.$menu.offset().left) );
+ this.$container.addClass(this.settings.positionedMenuClass);
+ }
+ }
+ },
+
+ menuHide: function() {
+ this.$container.removeClass(this.settings.activeClass);
+ this.$container.removeClass(this.settings.positionedMenuClass);
+ this.$menu.css('width', 'auto');
+ },
+
+ menuToggle: function() {
+ if ( this.$container.hasClass(this.settings.activeClass) ) {
+ this.menuHide();
+ } else {
+ this.menuShow();
+ }
+ }
+
+ });
+
+ $.fn[ pluginName ] = function(options) {
+ return this.each(function() {
+ if ( !$.data(this, "plugin_" + pluginName) ) {
+ $.data(this, "plugin_" + pluginName,
+ new Plugin(this, options) );
+ }
+ });
+ };
+
+})(jQuery);
diff --git a/web/js/map-OpenLayers.js b/web/js/map-OpenLayers.js
index f6b2c879b..7d264860f 100644
--- a/web/js/map-OpenLayers.js
+++ b/web/js/map-OpenLayers.js
@@ -270,6 +270,36 @@ var fixmystreet = fixmystreet || {};
fixmystreet.markers.refresh({force: true});
}
+ function parse_query_string() {
+ var qs = {};
+ location.search.substring(1).split('&').forEach(function(i) {
+ var s = i.split('='),
+ k = s[0],
+ v = s[1] && decodeURIComponent(s[1].replace(/\+/g, ' '));
+ qs[k] = v;
+ });
+ return qs;
+ }
+
+ function replace_query_parameter(qs, id, key) {
+ var value = $('#' + id).val();
+ value ? qs[key] = value.join(',') : delete qs[key];
+ return value;
+ }
+
+ function categories_or_status_changed_history() {
+ if (!('pushState' in history)) {
+ return;
+ }
+ var qs = parse_query_string();
+ var filter_categories = replace_query_parameter(qs, 'filter_categories', 'filter_category');
+ var filter_statuses = replace_query_parameter(qs, 'statuses', 'status');
+ var new_url = location.href.replace(location.search, '?' + $.param(qs));
+ history.pushState({
+ filter_change: { 'filter_categories': filter_categories, 'statuses': filter_statuses }
+ }, null, new_url);
+ }
+
function setup_inspector_marker_drag() {
// On the 'inspect report' page the pin is draggable, so we need to
// update the easting/northing fields when it's dragged.
@@ -433,15 +463,11 @@ var fixmystreet = fixmystreet || {};
fixmystreet.select_feature.activate();
fixmystreet.map.events.register( 'zoomend', null, fixmystreet.maps.markers_resize );
- // If the category filter dropdown exists on the page set up the
- // event handlers to populate it and react to it changing
- if ($("select#filter_categories").length) {
- $("body").on("change", "#filter_categories", categories_or_status_changed);
- }
- // Do the same for the status dropdown
- if ($("select#statuses").length) {
- $("body").on("change", "#statuses", categories_or_status_changed);
- }
+ // Set up the event handlers to populate the filters and react to them changing
+ $("#filter_categories").on("change.filters", categories_or_status_changed);
+ $("#statuses").on("change.filters", categories_or_status_changed);
+ $("#filter_categories").on("change.user", categories_or_status_changed_history);
+ $("#statuses").on("change.user", categories_or_status_changed_history);
} else if (fixmystreet.page == 'new') {
drag.activate();
}