diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 20 | ||||
-rw-r--r-- | web/cobrands/sass/_base.scss | 5 | ||||
-rw-r--r-- | web/cobrands/sass/_multiselect.scss | 84 | ||||
-rw-r--r-- | web/cobrands/sass/_report_list.scss | 47 | ||||
-rw-r--r-- | web/js/jquery.multi-select.js | 256 | ||||
-rw-r--r-- | web/js/map-OpenLayers.js | 44 |
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(); } |