aboutsummaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--perllib/FixMyStreet/App.pm10
-rw-r--r--perllib/FixMyStreet/App/Controller/Around.pm14
-rw-r--r--perllib/FixMyStreet/App/Controller/My.pm8
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm5
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm46
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Nearby.pm4
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Problem.pm4
-rw-r--r--perllib/FixMyStreet/Map.pm6
-rw-r--r--t/app/controller/reports.t2
-rw-r--r--templates/web/base/common_footer_tags.html1
-rw-r--r--templates/web/base/reports/_list-filters.html24
-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
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();
}