aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/cobrands/fixmystreet/fixmystreet.js20
-rw-r--r--web/cobrands/sass/_base.scss5
-rw-r--r--web/cobrands/sass/_multiselect.scss84
-rw-r--r--web/cobrands/sass/_report_list.scss47
-rw-r--r--web/js/jquery.multi-select.js256
-rw-r--r--web/js/map-OpenLayers.js44
6 files changed, 433 insertions, 23 deletions
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js
index 4aeb14d88..13635b9aa 100644
--- a/web/cobrands/fixmystreet/fixmystreet.js
+++ b/web/cobrands/fixmystreet/fixmystreet.js
@@ -583,6 +583,18 @@ $.extend(fixmystreet.set_up, {
$(this).closest("form").submit();
});
}
+
+ function make_multi(id) {
+ var $id = $('#' + id),
+ all = $id.data('all');
+ $id.multiSelect({
+ allText: all,
+ noneText: all,
+ positionMenuWithin: $('#side')
+ });
+ }
+ make_multi('statuses');
+ make_multi('filter_categories');
},
mobile_ui_tweaks: function() {
@@ -1215,11 +1227,19 @@ $(function() {
// location.href is something like foo.com/around?pc=abc-123,
// which we pass into fixmystreet.display.reports_list() as a fallback
// in case the list isn't already in the DOM.
+ $('#filter_categories').add('#statuses').find('option')
+ .prop('selected', function() { return this.defaultSelected; })
+ .trigger('change.multiselect');
fixmystreet.display.reports_list(location.href);
} else if ('reportId' in e.state) {
fixmystreet.display.report(e.state.reportPageUrl, e.state.reportId);
} else if ('newReportAtLonlat' in e.state) {
fixmystreet.display.begin_report(e.state.newReportAtLonlat, false);
+ } else if ('filter_change' in e.state) {
+ $('#filter_categories').val(e.state.filter_change.filter_categories);
+ $('#statuses').val(e.state.filter_change.statuses);
+ $('#filter_categories').add('#statuses')
+ .trigger('change.filters').trigger('change.multiselect');
} else if ('hashchange' in e.state) {
// This popstate was just here because the hash changed.
// (eg: mobile nav click.) We want to ignore it.
diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss
index 612d8cf55..96de4307b 100644
--- a/web/cobrands/sass/_base.scss
+++ b/web/cobrands/sass/_base.scss
@@ -289,6 +289,9 @@ select.form-control {
&[multiple] {
height: auto;
}
+ .js &[multiple] {
+ height: 2.2em;
+ }
}
.form-section-heading {
@@ -1384,6 +1387,7 @@ a:hover.rap-notes-trigger {
bottom: 0;
height: auto; // override `.mobile #map_box` height:10em
margin: 0;
+ z-index: 1; // stack above positioned elements later on the page (eg: .report-list-filters)
}
#fms_pan_zoom {
@@ -2012,3 +2016,4 @@ table.nicetable {
@import "_admin";
@import "_fixedthead";
@import "_dropzone";
+@import "_multiselect";
diff --git a/web/cobrands/sass/_multiselect.scss b/web/cobrands/sass/_multiselect.scss
new file mode 100644
index 000000000..6760b2282
--- /dev/null
+++ b/web/cobrands/sass/_multiselect.scss
@@ -0,0 +1,84 @@
+.multi-select-container {
+ position: relative;
+}
+
+.multi-select-menu {
+ position: absolute;
+ left: 0;
+ top: 0.8em;
+ z-index: 1;
+ float: left;
+ min-width: 100%;
+ background: #fff;
+ margin: 1em 0;
+ padding: 0.4em 0;
+ border: 1px solid #aaa;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ display: none;
+
+ label {
+ display: block;
+ font-size: 0.875em;
+ padding: 0.3em 1em 0.3em 30px;
+ white-space: nowrap;
+ }
+
+ input {
+ position: absolute;
+ margin-top: 0.25em;
+ margin-left: -20px;
+ }
+}
+
+.multi-select-button {
+ display: inline-block;
+ font-size: 0.875em;
+ padding: 0.2em 1.5em 0.2em 0.6em;
+ max-width: 20em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: -0.5em;
+ background-color: #fff;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ cursor: default;
+ position: relative;
+
+ &:after {
+ content: "";
+ display: block;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 0.4em 0.4em 0 0.4em;
+ border-color: #999 transparent transparent transparent;
+
+ position: absolute;
+ right: 0.5em;
+ top: 50%;
+ margin: -0.2em 0 0 0;
+ }
+}
+
+.multi-select-container--open {
+ .multi-select-menu {
+ display: block;
+ }
+
+ .multi-select-button:after {
+ border-width: 0 0.4em 0.4em 0.4em;
+ border-color: transparent transparent #999 transparent;
+ }
+}
+
+.multi-select-container--positioned {
+ .multi-select-menu {
+ box-sizing: border-box;
+
+ label {
+ white-space: normal;
+ }
+ }
+}
diff --git a/web/cobrands/sass/_report_list.scss b/web/cobrands/sass/_report_list.scss
index 8d34bfd77..54c2d4bf2 100644
--- a/web/cobrands/sass/_report_list.scss
+++ b/web/cobrands/sass/_report_list.scss
@@ -1,32 +1,51 @@
.report-list-filters {
- padding: 1em 1em 0;
- margin-bottom: 0.5em;
+ padding: 1em;
+ padding-bottom: 0.75em; // compensate for 0.25em border-top on .item-list__item
+ margin-bottom: 0;
color: #666;
font-size: 0.85em;
-
- label, select {
- display: inline-block;
- width: auto;
- }
+ line-height: 1.8em;
label {
+ // Override default label styling
font-weight: normal;
margin: 0;
+ }
- & + label {
- margin-#{$left}: 0.2em;
- }
+ & > label,
+ .multi-select-container {
+ display: inline-block;
+ vertical-align: top;
}
select {
+ display: inline-block;
+ width: auto;
color: inherit;
font-family: inherit;
font-size: 1em;
-
border: 1px solid #ced7c4;
- box-shadow: 0 1px 0 #fff;
- height: 2em;
- margin-#{$left}: 0.2em;
max-width: 13em;
+ margin: 0 0.2em;
+ }
+
+ .form-control {
+ margin-bottom: 0;
+ }
+
+ .multi-select-container {
+ margin: 0 0.2em;
+ }
+
+ .multi-select-menu {
+ z-index: 11; // Stack on top of .shadow-wrap which has {z-index: 10}
+ line-height: 1.4em;
+ }
+
+ .multi-select-button {
+ max-width: 10em;
+ padding: 0 1.5em 0 0.6em;
+ line-height: 1.8em;
+ vertical-align: top;
}
}
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();
}