diff options
author | Zarino Zappia <mail@zarino.co.uk> | 2016-09-30 16:34:00 +0100 |
---|---|---|
committer | Matthew Somerville <matthew-github@dracos.co.uk> | 2016-10-13 19:22:11 +0100 |
commit | a5ef113e2cc3105da41cf5449b505db6fa336c59 (patch) | |
tree | c60d906c0b5dd6fc974a35f8e921ae728dab080c | |
parent | 3872c39f5426165c3abfe397d15dd2a63f731e26 (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.
-rw-r--r-- | perllib/FixMyStreet/App.pm | 10 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Around.pm | 14 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/My.pm | 8 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 5 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Reports.pm | 46 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Nearby.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Problem.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/Map.pm | 6 | ||||
-rw-r--r-- | t/app/controller/reports.t | 2 | ||||
-rw-r--r-- | templates/web/base/common_footer_tags.html | 1 | ||||
-rw-r--r-- | templates/web/base/reports/_list-filters.html | 24 | ||||
-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 |
17 files changed, 505 insertions, 75 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index 40a99b6d3..ab5e62233 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -447,11 +447,13 @@ a list, with an empty list if no parameter is present. =cut sub get_param_list { - my ($c, $param) = @_; + my ($c, $param, $allow_commas) = @_; + die unless wantarray; my $value = $c->req->params->{$param}; - return @$value if ref $value; - return ($value) if defined $value; - return (); + return () unless defined $value; + my @value = ref $value ? @$value : ($value); + return map { split /,/, $_ } @value if $allow_commas; + return @value; } =head2 set_param diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm index ec84ca09a..1f45f8029 100644 --- a/perllib/FixMyStreet/App/Controller/Around.pm +++ b/perllib/FixMyStreet/App/Controller/Around.pm @@ -181,7 +181,7 @@ sub display_location : Private { my ( $on_map_all, $on_map, $nearby, $distance ) = FixMyStreet::Map::map_features( $c, latitude => $latitude, longitude => $longitude, - interval => $interval, category => $c->stash->{filter_category}, + interval => $interval, categories => $c->stash->{filter_category}, states => $c->stash->{filter_problem_states} ); # copy the found reports to the stash @@ -258,13 +258,11 @@ sub check_and_stash_category : Private { )->all; my @categories = map { $_->category } @contacts; $c->stash->{filter_categories} = \@categories; - - - my $category = $c->get_param('filter_category'); my %categories_mapped = map { $_ => 1 } @categories; - if ( defined $category && $categories_mapped{$category} ) { - $c->stash->{filter_category} = $category; - } + + my $categories = [ $c->get_param_list('filter_category', 1) ]; + my @valid_categories = grep { $_ && $categories_mapped{$_} } @$categories; + $c->stash->{filter_category} = \@valid_categories; } =head2 /ajax @@ -303,7 +301,7 @@ sub ajax : Path('/ajax') { my ( $on_map_all, $on_map_list, $nearby, $dist ) = FixMyStreet::Map::map_features($c, bbox => $bbox, interval => $interval, - category => $c->get_param('filter_category'), + categories => [ $c->get_param_list('filter_category', 1) ], states => $c->stash->{filter_problem_states} ); # create a list of all the pins diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm index b7fabcf4c..573c41446 100644 --- a/perllib/FixMyStreet/App/Controller/My.pm +++ b/perllib/FixMyStreet/App/Controller/My.pm @@ -63,10 +63,10 @@ sub get_problems : Private { state => [ keys %$states ], }; - my $category = $c->get_param('filter_category'); - if ( $category ) { - $params->{category} = $category; - $c->stash->{filter_category} = $category; + my $categories = [ $c->get_param_list('filter_category', 1) ]; + if ( @$categories ) { + $params->{category} = $categories; + $c->stash->{filter_category} = $categories; } my $rs = $c->stash->{problems_rs}->search( $params, { diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index 75f54facf..b3b5d00fd 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -1242,9 +1242,12 @@ sub redirect_to_around : Private { lat => $c->stash->{latitude}, lon => $c->stash->{longitude}, }; - foreach (qw(pc zoom status filter_category)) { + foreach (qw(pc zoom)) { $params->{$_} = $c->get_param($_); } + foreach (qw(status filter_category)) { + $params->{$_} = join(',', $c->get_param_list($_, 1)); + } # delete empty values for ( keys %$params ) { diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index 49f477fec..60a7d1726 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -132,7 +132,7 @@ sub ward : Path : Args(2) { } )->all; @categories = map { $_->category } @categories; $c->stash->{filter_categories} = \@categories; - $c->stash->{filter_category} = $c->get_param('filter_category'); + $c->stash->{filter_category} = [ $c->get_param_list('filter_category', 1) ]; my $pins = $c->stash->{pins}; @@ -464,23 +464,37 @@ sub redirect_body : Private { sub stash_report_filter_status : Private { my ( $self, $c ) = @_; - my $status = $c->get_param('status') || $c->cobrand->on_map_default_status; - if ( $status eq 'all' ) { - $c->stash->{filter_status} = 'all'; - $c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->visible_states(); - } elsif ( $status eq 'open' ) { - $c->stash->{filter_status} = 'open'; - $c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->open_states(); - } elsif ( $status eq 'closed' ) { - $c->stash->{filter_status} = 'closed'; - $c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->closed_states(); - } elsif ( $status eq 'fixed' ) { - $c->stash->{filter_status} = 'fixed'; - $c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->fixed_states(); - } else { - $c->stash->{filter_status} = $c->cobrand->on_map_default_status; + my @status = $c->get_param_list('status', 1); + @status = ($c->cobrand->on_map_default_status) unless @status; + my %status = map { $_ => 1 } @status; + + my %filter_problem_states; + my %filter_status; + + if ($status{open}) { + my $s = FixMyStreet::DB::Result::Problem->open_states(); + %filter_problem_states = (%filter_problem_states, %$s); + $filter_status{open} = 1; + } + if ($status{closed}) { + my $s = FixMyStreet::DB::Result::Problem->closed_states(); + %filter_problem_states = (%filter_problem_states, %$s); + $filter_status{closed} = 1; + } + if ($status{fixed}) { + my $s = FixMyStreet::DB::Result::Problem->fixed_states(); + %filter_problem_states = (%filter_problem_states, %$s); + $filter_status{fixed} = 1; + } + + if ($status{all}) { + my $s = FixMyStreet::DB::Result::Problem->visible_states(); + # %filter_status = (); + %filter_problem_states = %$s; } + $c->stash->{filter_problem_states} = \%filter_problem_states; + $c->stash->{filter_status} = \%filter_status; return 1; } diff --git a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm index 9db1c6525..8b8951007 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm @@ -10,7 +10,7 @@ sub to_body { } sub nearby { - my ( $rs, $c, $dist, $ids, $limit, $mid_lat, $mid_lon, $interval, $category, $states ) = @_; + my ( $rs, $c, $dist, $ids, $limit, $mid_lat, $mid_lon, $interval, $categories, $states ) = @_; unless ( $states ) { $states = FixMyStreet::DB::Result::Problem->visible_states(); @@ -24,7 +24,7 @@ sub nearby { if $interval; $params->{id} = { -not_in => $ids } if $ids; - $params->{category} = $category if $category; + $params->{category} = $categories if $categories && @$categories; $rs = $c->cobrand->problems_restriction($rs); diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm index 9ce7da1c0..723a6e7c2 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm @@ -140,7 +140,7 @@ sub _recent { # Problems around a location sub around_map { - my ( $rs, $min_lat, $max_lat, $min_lon, $max_lon, $interval, $limit, $category, $states ) = @_; + my ( $rs, $min_lat, $max_lat, $min_lon, $max_lon, $interval, $limit, $categories, $states ) = @_; my $attr = { order_by => { -desc => 'created' }, }; @@ -158,7 +158,7 @@ sub around_map { }; $q->{'current_timestamp - lastupdate'} = { '<', \"'$interval'::interval" } if $interval; - $q->{category} = $category if $category; + $q->{category} = $categories if $categories && @$categories; my @problems = mySociety::Locale::in_gb_locale { $rs->search( $q, $attr )->all }; return \@problems; diff --git a/perllib/FixMyStreet/Map.pm b/perllib/FixMyStreet/Map.pm index 5272e3932..f7caf51d7 100644 --- a/perllib/FixMyStreet/Map.pm +++ b/perllib/FixMyStreet/Map.pm @@ -92,9 +92,9 @@ sub map_features { my $around_limit = $c->cobrand->on_map_list_limit || undef; my @around_args = @p{"min_lat", "max_lat", "min_lon", "max_lon", "interval"}; - my $on_map_all = $c->cobrand->problems_on_map->around_map( @around_args, undef, $p{category}, $p{states} ); + my $on_map_all = $c->cobrand->problems_on_map->around_map( @around_args, undef, $p{categories}, $p{states} ); my $on_map_list = $around_limit - ? $c->cobrand->problems_on_map->around_map( @around_args, $around_limit, $p{category}, $p{states} ) + ? $c->cobrand->problems_on_map->around_map( @around_args, $around_limit, $p{categories}, $p{states} ) : $on_map_all; my $dist = FixMyStreet::Gaze::get_radius_containing_population( $p{latitude}, $p{longitude} ); @@ -102,7 +102,7 @@ sub map_features { my $limit = 20; my @ids = map { $_->id } @$on_map_list; my $nearby = $c->model('DB::Nearby')->nearby( - $c, $dist, \@ids, $limit, @p{"latitude", "longitude", "interval", "category", "states"} + $c, $dist, \@ids, $limit, @p{"latitude", "longitude", "interval", "categories", "states"} ); return ( $on_map_all, $on_map_list, $nearby, $dist ); diff --git a/t/app/controller/reports.t b/t/app/controller/reports.t index 8869adaa7..141269cd8 100644 --- a/t/app/controller/reports.t +++ b/t/app/controller/reports.t @@ -226,7 +226,7 @@ subtest "test greenwich all reports page" => sub { $mech->get_ok('/reports/Royal+Borough+of+Greenwich'); # There should not be deleted categories in the list my $category_select = $mech->forms()->[0]->find_input('filter_category'); - is $category_select->possible_values, 1, 'deleted categories are not shown'; + is $category_select, undef, 'deleted categories are not shown'; # Clean up after the test $deleted_contact->delete; diff --git a/templates/web/base/common_footer_tags.html b/templates/web/base/common_footer_tags.html index bed344f8b..45872895b 100644 --- a/templates/web/base/common_footer_tags.html +++ b/templates/web/base/common_footer_tags.html @@ -10,6 +10,7 @@ <script type="text/javascript" src="[% version('/js/validation_rules.js') %]"></script> <script src="[% version('/js/jquery.validate.min.js') %]" type="text/javascript" charset="utf-8"></script> <script type="text/javascript" src="[% version('/js/dropzone.min.js') %]"></script> +<script type="text/javascript" src="[% version('/js/jquery.multi-select.js') %]"></script> <script type="text/javascript" src="[% version('/js/geo.min.js') %]"></script> <script type="text/javascript" src="[% version('/cobrands/fixmystreet/fixmystreet.js') %]"></script> diff --git a/templates/web/base/reports/_list-filters.html b/templates/web/base/reports/_list-filters.html index e0452eb08..f4fbab042 100644 --- a/templates/web/base/reports/_list-filters.html +++ b/templates/web/base/reports/_list-filters.html @@ -1,21 +1,23 @@ [% select_status = BLOCK %] - <select class="form-control" name="status" id="statuses"> - <option value="all"[% ' selected' IF filter_status == 'all' %]>[% loc('all reports') %]</option> - <option value="open"[% ' selected' IF filter_status == 'open' %]>[% loc('unfixed reports') %]</option> - <option value="closed"[% ' selected' IF filter_status == 'closed' %]>[% loc('closed reports') %]</option> - <option value="fixed"[% ' selected' IF filter_status == 'fixed' %]>[% loc('fixed reports') %]</option> + <select class="form-control" name="status" id="statuses" multiple data-all="[% loc('all reports') %]"> + <option value="open"[% ' selected' IF filter_status.open %]>[% loc('unfixed reports') %]</option> + <option value="closed"[% ' selected' IF filter_status.closed %]>[% loc('closed reports') %]</option> + <option value="fixed"[% ' selected' IF filter_status.fixed %]>[% loc('fixed reports') %]</option> </select> [% END %] [% select_category = BLOCK %] - <select class="form-control" name="filter_category" id="filter_categories"> - <option value="">[% loc('Everything') %]</option> - [% FOR category IN filter_categories %] - <option value="[% category | html %]"[% ' selected' IF filter_category == category %]> - [% category | html %] + [% IF filter_categories.size %] + <select class="form-control" name="filter_category" id="filter_categories" multiple data-all="[% loc('everything') %]"> + [% FOR cat IN filter_categories %] + <option value="[% cat | html %]"[% ' selected' IF filter_category.grep(cat).size %]> + [% cat | html %] </option> [% END %] </select> + [% ELSE %] + [% loc('everything') %] + [% END %] [% END %] [% IF use_form_wrapper %] @@ -23,7 +25,7 @@ [% END %] <p class="report-list-filters"> - [% tprintf(loc('<label>Show %s</label> <label>about %s</label>', 'The first %s is a dropdown of all/fixed/etc, the second is a dropdown of categories'), select_status, select_category) %] + [% tprintf(loc('<label for="statuses">Show</label> %s <label for="filter_categories">about</label> %s', 'The first %s is a dropdown of all/fixed/etc, the second is a dropdown of categories'), select_status, select_category) %] <input type="submit" name="filter_update" value="[% loc('Go') %]"> </p> 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(); } |