diff options
author | Zarino Zappia <mail@zarino.co.uk> | 2019-09-27 17:00:51 +0100 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2019-10-03 17:31:39 +0100 |
commit | 7e16c49ef7bb2e6fc284ece582f7a08722f52d70 (patch) | |
tree | ad4dd7ca640050902cebebf12022e9e0de378f81 | |
parent | 977a546cd51fa0e9d88451dcdfe59c68c9017e8d (diff) |
Restructured admin category extra-metadata-form
* Much simplified display of metadata field options.
* Fields can now be dragged into the desired order.
* Drop-down question form disabling can now have
a different message per option.
Co-Authored-By: Matthew Somerville <matthew@mysociety.org>
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin.pm | 63 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 2 | ||||
-rw-r--r-- | t/app/controller/admin/reportextrafields.t | 122 | ||||
-rw-r--r-- | templates/web/base/admin/bodies/contact-form.html | 2 | ||||
-rw-r--r-- | templates/web/base/admin/extra-metadata-form.html | 114 | ||||
-rw-r--r-- | templates/web/base/admin/extra-metadata-item.html | 110 | ||||
-rw-r--r-- | templates/web/base/admin/extra-metadata-option.html | 29 | ||||
-rw-r--r-- | templates/web/base/common_scripts.html | 1 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/admin.js | 128 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 48 | ||||
-rw-r--r-- | web/cobrands/sass/_admin.scss | 134 | ||||
-rw-r--r-- | web/cobrands/sass/_base.scss | 3 | ||||
-rwxr-xr-x | web/vendor/html5sortable.min.js | 2 |
13 files changed, 453 insertions, 305 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index c2c4e7588..b00c34777 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -1187,37 +1187,44 @@ sub update_extra_fields : Private { my $meta = {}; $meta->{code} = $c->get_param("metadata[$i].code"); next unless $meta->{code}; + $meta->{order} = int $c->get_param("metadata[$i].order"); - $meta->{datatype} = $c->get_param("metadata[$i].datatype"); - my $required = $c->get_param("metadata[$i].required") && $c->get_param("metadata[$i].required") eq 'on'; - $meta->{required} = $required ? 'true' : 'false'; - my $notice = $c->get_param("metadata[$i].notice") && $c->get_param("metadata[$i].notice") eq 'on'; - $meta->{variable} = $notice ? 'false' : 'true'; - my $protected = $c->get_param("metadata[$i].protected") && $c->get_param("metadata[$i].protected") eq 'on'; - $meta->{protected} = $protected ? 'true' : 'false'; - my $disable_form = $c->get_param("metadata[$i].disable_form") && $c->get_param("metadata[$i].disable_form") eq 'on'; - $meta->{disable_form} = $disable_form ? 'true' : 'false'; - $meta->{description} = $c->get_param("metadata[$i].description"); - $meta->{datatype_description} = $c->get_param("metadata[$i].datatype_description"); - $meta->{automated} = $c->get_param("metadata[$i].automated") - if $c->get_param("metadata[$i].automated"); - - if ( $meta->{datatype} eq "singlevaluelist" ) { - $meta->{values} = []; - my $re = qr{^metadata\[$i\]\.values\[\d+\]\.key}; - my @vindices = grep { /$re/ } keys %{ $c->req->params }; - @vindices = sort map { /values\[(\d+)\]/ } @vindices; - foreach my $j (@vindices) { - my $name = $c->get_param("metadata[$i].values[$j].name"); - my $key = $c->get_param("metadata[$i].values[$j].key"); - my $disable = $c->get_param("metadata[$i].values[$j].disable"); - push(@{$meta->{values}}, { - name => $name, - key => $key, - $disable ? (disable => 1) : (), - }) if $name; + $meta->{protected} = $c->get_param("metadata[$i].protected") ? 'true' : 'false'; + + my $behaviour = $c->get_param("metadata[$i].behaviour") || 'question'; + if ($behaviour eq 'question') { + $meta->{required} = $c->get_param("metadata[$i].required") ? 'true' : 'false'; + $meta->{variable} = 'true'; + $meta->{description} = $c->get_param("metadata[$i].description"); + $meta->{datatype} = $c->get_param("metadata[$i].datatype"); + + if ( $meta->{datatype} eq "singlevaluelist" ) { + $meta->{values} = []; + my $re = qr{^metadata\[$i\]\.values\[\d+\]\.key}; + my @vindices = grep { /$re/ } keys %{ $c->req->params }; + @vindices = sort map { /values\[(\d+)\]/ } @vindices; + foreach my $j (@vindices) { + my $name = $c->get_param("metadata[$i].values[$j].name"); + my $key = $c->get_param("metadata[$i].values[$j].key"); + my $disable = $c->get_param("metadata[$i].values[$j].disable"); + my $disable_message = $c->get_param("metadata[$i].values[$j].disable_message"); + push(@{$meta->{values}}, { + name => $name, + key => $key, + $disable ? (disable => 1, disable_message => $disable_message) : (), + }) if $name; + } } + } elsif ($behaviour eq 'notice') { + $meta->{variable} = 'false'; + $meta->{description} = $c->get_param("metadata[$i].description"); + $meta->{disable_form} = $c->get_param("metadata[$i].disable_form") ? 'true' : 'false'; + } elsif ($behaviour eq 'hidden') { + $meta->{automated} = 'hidden_field'; + } elsif ($behaviour eq 'server') { + $meta->{automated} = 'server_set'; } + push @extra_fields, $meta; } @extra_fields = sort { $a->{order} <=> $b->{order} } @extra_fields; diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index 9b7a925b8..06974cd09 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -322,7 +322,7 @@ sub disable_form_message : Private { } elsif (($_->{variable} || '') eq 'true' && @{$_->{values} || []}) { foreach my $opt (@{$_->{values}}) { if ($opt->{disable}) { - $out{message} = $_->{datatype_description}; + $out{message} = $opt->{disable_message} || $_->{datatype_description}; $out{code} = $_->{code}; push @{$out{answers}}, $opt->{key}; } diff --git a/t/app/controller/admin/reportextrafields.t b/t/app/controller/admin/reportextrafields.t index 070e3e2fc..1714e8521 100644 --- a/t/app/controller/admin/reportextrafields.t +++ b/t/app/controller/admin/reportextrafields.t @@ -62,13 +62,12 @@ FixMyStreet::override_config { $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category); $mech->submit_form_ok( { with_fields => { - "metadata[0].order" => "1", - "metadata[0].code" => "string_test", - "metadata[0].required" => "on", - "metadata[0].notice" => "", - "metadata[0].description" => "this is a test description", - "metadata[0].datatype_description" => "hint here", - "metadata[0].datatype" => "string", + "metadata[9999].order" => "1", + "metadata[9999].code" => "string_test", + "metadata[9999].required" => 1, + "metadata[9999].behaviour" => "question", + "metadata[9999].description" => "this is a test description", + "metadata[9999].datatype" => "string", "note" => "Added extra field", }}); $mech->content_contains('Values updated'); @@ -80,26 +79,21 @@ FixMyStreet::override_config { variable => "true", protected => "false", description => "this is a test description", - datatype_description => "hint here", datatype => "string", - disable_form => "false", }; $contact->discard_changes; is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new string field was added'; - $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category); $mech->submit_form_ok( { with_fields => { - "metadata[1].order" => "2", - "metadata[1].code" => "list_test", - "metadata[1].required" => undef, - "metadata[1].notice" => "", - "metadata[1].disable_form" => "on", - "metadata[1].description" => "this field is a list", - "metadata[1].datatype_description" => "", - "metadata[1].datatype" => "list", - "metadata[1].values[0].key" => "key1", - "metadata[1].values[0].name" => "name1", + "metadata[9999].order" => "2", + "metadata[9999].code" => "list_test", + "metadata[9999].required" => undef, + "metadata[9999].behaviour" => "question", + "metadata[9999].description" => "this field is a list", + "metadata[9999].datatype" => "singlevaluelist", + "metadata[9999].values[8888].key" => "key1", + "metadata[9999].values[8888].name" => "name1", "note" => "Added extra list field", }}); $mech->content_contains('Values updated'); @@ -110,9 +104,7 @@ FixMyStreet::override_config { required => "false", variable => "true", protected => "false", - disable_form => "true", description => "this field is a list", - datatype_description => "", datatype => "singlevaluelist", values => [ { name => "name1", key => "key1" }, @@ -121,6 +113,28 @@ FixMyStreet::override_config { $contact->discard_changes; is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new list field was added'; + $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category); + $mech->submit_form_ok( { with_fields => { + "metadata[9999].order" => "3", + "metadata[9999].code" => "emergency", + "metadata[9999].behaviour" => "notice", + "metadata[9999].disable_form" => "1", + "metadata[9999].description" => "please ring", + "note" => "Added notice field", + }}); + $mech->content_contains('Values updated'); + + push @$contact_extra_fields, { + order => "3", + code => "emergency", + protected => "false", + description => "please ring", + disable_form => 'true', + variable => 'false', + }; + $contact->discard_changes; + is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new field was added'; + $contact->set_extra_fields(); $contact->update; }; @@ -142,13 +156,7 @@ FixMyStreet::override_config { is $contact->email, 'test4@example.com', 'contact updated'; is_deeply $meta_data, [ { order => 0, - datatype => 'string', - datatype_description => '', - description => '', - required => 'false', - variable => 'true', protected => 'false', - disable_form => 'false', code => 'POT', automated => 'server_set' } ], "automated fields not unset"; @@ -168,13 +176,12 @@ FixMyStreet::override_config { name => "Test extra fields", cobrand => "tester", language => undef, - "metadata[0].order" => "1", - "metadata[0].code" => "string_test", - "metadata[0].required" => "on", - "metadata[0].notice" => "", - "metadata[0].description" => "this is a test description", - "metadata[0].datatype_description" => "hint here", - "metadata[0].datatype" => "string", + "metadata[9999].order" => "1", + "metadata[9999].code" => "string_test", + "metadata[9999].required" => 1, + "metadata[9999].behaviour" => "question", + "metadata[9999].description" => "this is a test description", + "metadata[9999].datatype" => "string", }}); is $model->count, 1, 'new ReportExtraFields created'; @@ -185,9 +192,7 @@ FixMyStreet::override_config { required => "true", variable => "true", protected => "false", - disable_form => "false", description => "this is a test description", - datatype_description => "hint here", datatype => "string", }; is_deeply $object->get_extra_fields, $extra_fields, 'new string field was added'; @@ -197,15 +202,14 @@ FixMyStreet::override_config { $mech->get_ok("/admin/reportextrafields/" . $object->id); $mech->submit_form_ok( { with_fields => { "language" => "en-gb", - "metadata[1].order" => "2", - "metadata[1].code" => "list_test", - "metadata[1].required" => undef, - "metadata[1].notice" => "", - "metadata[1].description" => "this field is a list", - "metadata[1].datatype_description" => "", - "metadata[1].datatype" => "list", - "metadata[1].values[0].key" => "key1", - "metadata[1].values[0].name" => "name1", + "metadata[9999].order" => "2", + "metadata[9999].code" => "list_test", + "metadata[9999].required" => undef, + "metadata[9999].behaviour" => "question", + "metadata[9999].description" => "this field is a list", + "metadata[9999].datatype" => "singlevaluelist", + "metadata[9999].values[8888].key" => "key1", + "metadata[9999].values[8888].name" => "name1", }}); push @$extra_fields, { @@ -214,9 +218,7 @@ FixMyStreet::override_config { required => "false", variable => "true", protected => "false", - disable_form => "false", description => "this field is a list", - datatype_description => "", datatype => "singlevaluelist", values => [ { name => "name1", key => "key1" }, @@ -229,26 +231,16 @@ FixMyStreet::override_config { $mech->get_ok("/admin/reportextrafields/" . $object->id); $mech->submit_form_ok({ with_fields => { - "metadata[2].order" => "3", - "metadata[2].code" => "automated_test", - "metadata[2].required" => undef, - "metadata[2].notice" => "", - "metadata[2].description" => "", - "metadata[2].datatype_description" => "", - "metadata[2].datatype" => "string", - "metadata[2].automated" => "server_set", + "metadata[9999].order" => "3", + "metadata[9999].code" => "automated_test", + "metadata[9999].required" => undef, + "metadata[9999].behaviour" => "server", }}); push @$extra_fields, { order => "3", code => "automated_test", - required => "false", - variable => "true", protected => "false", - disable_form => "false", - description => "", - datatype_description => "", - datatype => "string", automated => "server_set", }; @@ -258,8 +250,8 @@ FixMyStreet::override_config { $mech->get_ok("/admin/reportextrafields/" . $object->id); $mech->submit_form_ok( { with_fields => { - "metadata[1].values[1].key" => "key2", - "metadata[1].values[1].name" => "name2", + "metadata[1].values[8888].key" => "key2", + "metadata[1].values[8888].name" => "name2", }}); push @{$extra_fields->[1]->{values}}, { name => "name2", key => "key2" }; @@ -333,7 +325,6 @@ subtest 'Reports are created with correct extra metadata' => sub { required => "true", variable => "true", description => "this is a test description", - datatype_description => "hint here", datatype => "string", }); $extra_fields->push_extra_fields({ @@ -342,7 +333,6 @@ subtest 'Reports are created with correct extra metadata' => sub { required => "false", variable => "true", description => "this field is a list", - datatype_description => "", datatype => "singlevaluelist", values => [ { name => "name1", key => "key1" }, diff --git a/templates/web/base/admin/bodies/contact-form.html b/templates/web/base/admin/bodies/contact-form.html index 7a58644e5..7f559274d 100644 --- a/templates/web/base/admin/bodies/contact-form.html +++ b/templates/web/base/admin/bodies/contact-form.html @@ -140,7 +140,7 @@ </dl> [% INCLUDE 'admin/extra-metadata-form.html' metas=(contact.get_metadata_for_editing OR []) %] - <p class="form-group"> + <p class="form-group" style="margin-top: 2em"> <label for="note">[% loc('Summarise your changes') %]</label> <span class="form-hint" id="note-hint">[% loc("If you’ve made changes, leave a note explaining what, for other admins to see.") %]</span> <input class="form-control" type="text" id="note" name="note" size="30" aria-describedby="note-hint" required> diff --git a/templates/web/base/admin/extra-metadata-form.html b/templates/web/base/admin/extra-metadata-form.html index b82eca966..9b8e4909f 100644 --- a/templates/web/base/admin/extra-metadata-form.html +++ b/templates/web/base/admin/extra-metadata-form.html @@ -1,107 +1,11 @@ -<ul class="js-metadata-items"> - [% FOR meta IN metas.merge([{}]) %] - <li class="js-metadata-item [% IF loop.last %]hidden-js js-metadata-item-template[% END %]" data-index="[% loop.index %]"> - <button type="button" class="btn btn--small js-metadata-item-remove hidden-nojs">[% loc('Remove field') %]</button> +<div class="extra-metadata-form js-metadata-items"> + [% FOR meta IN metas %] + [% INCLUDE 'admin/extra-metadata-item.html' i=loop.index collapsed=1 %] + [% END %] +</div> - <div class="admin-hint"><p>[% loc('The ordering of this field on the report page. Fields are shown in ascending order according to this value.') %]</p></div> - <label> - [% loc('Order') %] - <input name="metadata[[% loop.index %]].order" data-field-name="order" type=text value="[% meta.order OR 0 | html %]"> - </label> +<div class="hidden-js" id="js-template-extra-metadata-item"> + [% INCLUDE 'admin/extra-metadata-item.html' meta={} i=9999 collapsed=0 %] +</div> - <div class="admin-hint"><p>[% loc('Whether the field is displayed to the user, included as a hidden field and automatically populated, or set by the server upon Open311 submission. This field is usually set automatically.') %]</p></div> - <label> - [% loc('Automated') %] - <select name="metadata[[% loop.index %]].automated" data-field-name="automated"> - <option value="" [% meta.automated == '' ? 'selected' : '' %]></option> - <option value="server_set" [% meta.automated == 'server_set' ? 'selected' : '' %]>[% loc('Server Set') %]</option> - <option value="hidden_field" [% meta.automated == 'hidden_field' ? 'selected' : '' %]>[% loc('Hidden Field') %]</option> - </select> - </label> - - <div class="admin-hint"><p>[% loc('The code used to store this field value in the database. e.g. <code>address</code> would be available as <code>report.get_extra_field_value("address")</code> in the templates.') %]</p></div> - <label> - [% loc('Code') %] - <input name="metadata[[% loop.index %]].code" data-field-name="code" type=text value="[% meta.code | html %]"> - </label> - - <div class="admin-hint"><p>[% loc('Whether the user is required to provide a value for this field.') %]</p></div> - <label> - [% loc('Required') %] - <input name="metadata[[% loop.index %]].required" data-field-name="required" type=checkbox [% meta.required == 'true' ? 'checked' : '' %]> - </label> - - <div class="admin-hint"><p>[% loc('If ticked the user won’t see an input field, just the ‘Description’ text.') %]</p></div> - <label> - [% loc('Notice') %] - <input name="metadata[[% loop.index %]].notice" data-field-name="notice" type=checkbox [% meta.variable == 'false' ? 'checked' : '' %]> - </label> - - <div class="admin-hint"><p>[% loc('If ticked this extra data will not be edited or deleted by the Open311 population script.') %]</p></div> - <label> - [% loc('Protected') %] - <input name="metadata[[% loop.index %]].protected" data-field-name="protected" type=checkbox [% meta.protected == 'true' ? 'checked' : '' %]> - </label> - - <div class="admin-hint"><p>[% loc('If ticked the entire report form will be disabled when this category is selected.') %]</p></div> - <label> - [% loc('Disable form') %] - <input name="metadata[[% loop.index %]].disable_form" data-field-name="disable_form" type=checkbox [% meta.disable_form == 'true' ? 'checked' : '' %]> - </label> - - <div class="admin-hint"><p>[% loc('The field name as shown to the user on the report form.') %]</p></div> - <label> - [% loc('Description') %] - <input name="metadata[[% loop.index %]].description" data-field-name="description" type=text value="[% meta.description | html %]"> - </label> - - <div class="admin-hint"><p>[% loc('Can be used to display extra text to the user alongside the field. The default template does not show this (<code>meta.datatype_description</code>), you must add it in <code>category_extras_fields.html</code>') %]</p></div> - <label> - [% loc('Hint') %] - <input name="metadata[[% loop.index %]].datatype_description" data-field-name="datatype_description" type=text value="[% meta.datatype_description | html %]"> - </label> - - <div class="admin-hint"><p>[% loc('The type of input field to show to the user. <strong>Text</strong> is a simple text field, <strong>List</strong> is a drop-down selection.') %]</p></div> - <label> - [% loc('Type') %] - <select name="metadata[[% loop.index %]].datatype" data-field-name="datatype" class="js-metadata-item-type"> - <option value="string" [% meta.datatype == 'string' ? 'selected' : '' %]>[% loc('String') %]</option> - <option value="singlevaluelist" [% meta.datatype == 'singlevaluelist' ? 'selected' : '' %]>[% loc('List') %]</option> - </select> - </label> - - <div class="js-metadata-options"> - <div class="admin-hint"><p>[% loc('For each option, <strong>Key</strong> is the value which is stored in the database for that option and <strong>Name</strong> is the value displayed to the user.') %]</p></div> - [% loc('Options') %]<span class="hidden-js"> [% loc('(ignored if type is "String")') %]</span> - <ul> - [% outer_loop = loop %] - [% SET values = meta.item('values') ? meta.values : [] %] - [% FOREACH option IN values.merge([{}]) %] - [%# the .merge() call is so there's an empty one on the end %] - <li class="js-metadata-option [% IF loop.last %]hidden-js js-metadata-option-template[% END %]"> - <label> - [% loc('Key') %] - <input class="js-metadata-option-key" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].key" type="text" value="[% option.key | html %]"> - </label> - <label> - [% loc('Name') %] - <input class="js-metadata-option-name" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].name" type="text" value="[% option.name | html %]"> - </label> - <label> - [% loc('Disable form') %] - <input class="js-metadata-option-disable" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].disable" type="checkbox"[% ' checked' IF option.disable %]> - </label> - <button type="button" class="btn btn--small js-metadata-option-remove hidden-nojs">[% loc('Remove') %]</button> - </li> - [% END %] - <li class="hidden-nojs"> - <button type="button" class="btn btn--small js-metadata-option-add">[% loc('Add option') %]</button> - </li> - </ul> - </div> - </li> - [%- END %] - <li class="hidden-nojs"> - <button type="button" class="btn btn--small js-metadata-item-add">[% loc('Add field') %]</button> - </li> -</ul> +<button type="button" class="btn btn--small js-metadata-item-add hidden-nojs">[% loc('Add field') %]</button> diff --git a/templates/web/base/admin/extra-metadata-item.html b/templates/web/base/admin/extra-metadata-item.html new file mode 100644 index 000000000..66cb6f79c --- /dev/null +++ b/templates/web/base/admin/extra-metadata-item.html @@ -0,0 +1,110 @@ +[% +SET behaviour = ''; +DEFAULT behaviour = 'server' IF meta.automated == 'server_set'; +DEFAULT behaviour = 'hidden' IF meta.automated == 'hidden_field'; +DEFAULT behaviour = 'notice' IF meta.variable == 'false'; +DEFAULT behaviour = 'question'; +~%] + +<fieldset class="extra-metadata-item js-metadata-item" data-i="[% i %]"> + <legend class="visuallyhidden">Extra data field [% i %]</legend> + + <header class="extra-metadata-item__header hidden-nojs"> + <div class="js-metadata-item-header-grab extra-metadata-item__header__grab" aria-label="Drag to reorder"></div> + <button type="button" class="js-metadata-item-header-title extra-metadata-item__header__title" data-default="[% loc('New field') %]" data-toggle-visibility="#metadata-fieldset-[% i %]"> + [%~ IF meta.code ~%] + <strong>[% meta.code | html %]</strong> + / [% behaviour %] + [% IF meta.description %] / [% meta.description.substr(0, 50) | html %][% END %] + [%~ ELSE ~%] + <strong>[% loc('New field') %]</strong> + [%~ END ~%] + </button> + <button type="button" class="js-metadata-item-remove extra-metadata-item__header__remove">[% loc('Remove field') %]</button> + </header> + + <div class="extra-metadata-item__body [% IF collapsed %]hidden-js[% END %]" id="metadata-fieldset-[% i %]"> + + <div class="form-group"> + <label for="metadata-[% i %]-code">[% loc('Code') %]</label> + <span class="form-hint" id="metadata-[% i %]-code-hint">[% loc('The code used to store this field value in the database.') %]</span> + <input class="form-control" name="metadata[[% i %]].code" id="metadata-[% i %]-code" aria-describedby="metadata-[% i %]-code-hint" type=text value="[% meta.code | html %]"> + </div> + + <fieldset> + <legend>[% loc('Behaviour') %]</legend> + <div class="form-check form-check--inline"> + <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-question" value="question" data-show="#metadata-[% i %]-description-group, #metadata-[% i %]-description-label-question, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-label-notice, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'question' %]> + <label for="metadata-[% i %]-behaviour-question">[% loc('Extra question shown to user') %]</label> + </div> + <div class="form-check form-check--inline"> + <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-notice" value="notice" data-show="#metadata-[% i %]-description-group, #metadata-[% i %]-description-label-notice, #metadata-[% i %]-protected-group, #metadata-[% i %]-disabled-group" data-hide="#metadata-[% i %]-description-label-question, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group"[% ' checked' IF behaviour == 'notice' %]> + <label for="metadata-[% i %]-behaviour-notice">[% loc('Notice shown to user') %]</label> + </div> + <div class="form-check form-check--inline"> + <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-hidden" value="hidden" data-show="#metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-group, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'hidden' %]> + <label for="metadata-[% i %]-behaviour-hidden">[% loc('Hidden data in reporting form') %]</label> + </div> + <div class="form-check form-check--inline"> + <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-server" value="server" data-show="#metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-group, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'server' %]> + <label for="metadata-[% i %]-behaviour-server">[% loc('Internal data set by Open311') %]</label> + </div> + </fieldset> + + <div class="form-group js-sort-order"> + <label for="metadata-[% i %]-order">[% loc('Order') %]</label> + <input class="form-control" name="metadata[[% i %]].order" id="metadata-[% i %]-order" type=text value="[% meta.order OR 0 | html %]"> + </div> + + <div class="form-group hidden-js" id="metadata-[% i %]-description-group"> + <label for="metadata-[% i %]-description" id="metadata-[% i %]-description-label-question" class="[% 'hidden-js' UNLESS behaviour == 'question' %]">[% loc('Question text') %]</label> + <label for="metadata-[% i %]-description" id="metadata-[% i %]-description-label-notice" class="[% 'hidden-js' UNLESS behaviour == 'notice' %]">[% loc('Notice text') %]</label> + <textarea class="form-control" name="metadata[[% i %]].description" id="metadata-[% i %]-description" rows="2">[% meta.description | html %]</textarea> + </div> + + <div class="hidden-js" id="metadata-[% i %]-datatype-group"> + <div class="form-group"> + <label for="metadata-[% i %]-datatype">[% loc('Display as') %]</label> + <select class="form-control" name="metadata[[% i %]].datatype" id="metadata-[% i %]-datatype"> + <option value="string" data-hide="#metadata-[% i %]-options-group" [% 'selected' IF meta.datatype == 'string' %]>[% loc('Text field') %]</option> + <option value="singlevaluelist" data-show="#metadata-[% i %]-options-group" [% 'selected' IF meta.datatype == 'singlevaluelist' %]>[% loc('Drop-down list') %]</option> + </select> + </div> + + <div class="hidden-js" id="metadata-[% i %]-options-group"> + <p class="label">[% loc('Options') %]</p> + <div class="js-metadata-options"> + [% SET options = meta.item('values') ? meta.values : [] %] + [% FOR option IN options %] + [% INCLUDE 'admin/extra-metadata-option.html' option=option i=i j=loop.index %] + [% END %] + </div> + + <div class="hidden-js"[% IF i==9999 %] id="js-template-extra-metadata-option"[% END %]> + [% INCLUDE 'admin/extra-metadata-option.html' option={} i=i j=8888 %] + </div> + + <button type="button" class="btn btn--small js-metadata-option-add hidden-nojs">[% loc('Add option') %]</button> + </div> + </div> + + <p class="form-check hidden-js" id="metadata-[% i %]-required-group"> + <input type="checkbox" name="metadata[[% i %]].required" value="1" id="metadata-[% i %]-required" aria-describedby="metadata-[% i %]-required-hint" [% ' checked' IF meta.required == 'true' %]> + <label for="metadata-[% i %]-required">[% loc('Required') %]</label> + <span class="form-hint" id="metadata-[% i %]-required-hint">[% loc('Prevent user from submitting the form until this field is filled in.') %]</span> + </p> + + <p class="form-check hidden-js" id="metadata-[% i %]-disabled-group"> + <input type="checkbox" name="metadata[[% i %]].disable_form" value="1" id="metadata-[% i %]-disable_form" aria-describedby="metadata-[% i %]-disable_form-hint" [% ' checked' IF meta.disable_form == 'true' %]> + <label for="metadata-[% i %]-disable_form">[% loc('Disable form when this category is selected') %]</label> + <span class="form-hint" id="metadata[[% i %]]-disable_form-hint">[% loc('If ticked, the form will be disabled and this item’s notice text will be displayed.') %]</span> + </p> + + <p class="form-check hidden-js" id="metadata-[% i %]-protected-group"> + <input type="checkbox" name="metadata[[% i %]].protected" value="1" id="metadata-[% i %]-protected" aria-describedby="metadata-[% i %]-protected-hint" [% ' checked' IF meta.protected == 'true' %]> + <label for="metadata-[% i %]-protected">[% loc('Protect from Open311 changes') %]</label> + <span class="form-hint" id="metadata[[% i %]]-protected-hint">[% loc('If ticked, this extra data will not be edited or deleted by the Open311 population script.') %]</span> + </p> + + </div> +</fieldset> diff --git a/templates/web/base/admin/extra-metadata-option.html b/templates/web/base/admin/extra-metadata-option.html new file mode 100644 index 000000000..360dfb208 --- /dev/null +++ b/templates/web/base/admin/extra-metadata-option.html @@ -0,0 +1,29 @@ +<fieldset class="extra-metadata-option js-metadata-option"> + <legend class="visuallyhidden">Option [% j %]</legend> + + <div class="row"> + <div class="col"> + <label for="metadata-[% i %]-values-[% j %]-key">[% loc('Internal key') %]</label> + <input type="text" class="form-control" name="metadata[[% i %]].values[[% j %]].key" id="metadata-[% i %]-values-[% j %]-key" value="[% option.key | html %]"> + </div> + <div class="col"> + <label for="metadata-[% i %]-values-[% j %]-name">[% loc('Displayed label') %]</label> + <input type="text" class="form-control" name="metadata[[% i %]].values[[% j %]].name" id="metadata-[% i %]-values-[% j %]-name" value="[% option.name | html %]"> + </div> + </div> + <div class="row"> + <div class="col"> + <p class="form-check"> + <input type="checkbox" name="metadata[[% i %]].values[[% j %]].disable" value="1" id="metadata-[% i %]-values-[% j %]-disable" data-toggle-visibility="#js-disable-form-message-box-[% i %]-[% j %]" [% ' checked' IF option.disable %]> + <label for="metadata-[% i %]-values-[% j %]-disable">[% loc('Disable form when this option is selected') %]</label> + </p> + <p class="form-group [% 'hidden-js' IF NOT option.disable %]" id="js-disable-form-message-box-[% i %]-[% j %]"> + <label for="disabled-message-[% i %]-[% j %]">[% loc('Message to show when form is disabled (HTML permitted)') %]</label> + <textarea id="disabled-message-[% i %]-[% j %]" name="metadata[[% i %]].values[[% j %]].disable_message" class="form-control">[% option.disable_message OR meta.datatype_description | html %]</textarea> + </p> + </div> + <div class="col"> + <button type="button" class="js-metadata-option-remove">[% loc('Remove option') %]</button> + </div> + </div> +</fieldset> diff --git a/templates/web/base/common_scripts.html b/templates/web/base/common_scripts.html index fd7011763..60775fcae 100644 --- a/templates/web/base/common_scripts.html +++ b/templates/web/base/common_scripts.html @@ -68,6 +68,7 @@ END; IF admin; scripts.push( version('/cobrands/fixmystreet/admin.js'), + version('/vendor/html5sortable.min.js'), ); END; diff --git a/web/cobrands/fixmystreet/admin.js b/web/cobrands/fixmystreet/admin.js index b598f52dd..d3df27f33 100644 --- a/web/cobrands/fixmystreet/admin.js +++ b/web/cobrands/fixmystreet/admin.js @@ -115,39 +115,7 @@ $(function(){ $(".js-extra-fields-ui").removeClass("hidden-js"); }); - // If type is changed to 'singlevaluelist' show the options list - $(".js-metadata-items").on("change", ".js-metadata-item-type", function() { - var $this = $(this); - var shown = $this.val() === 'singlevaluelist'; - var $list = $this.closest(".js-metadata-item").find('.js-metadata-options'); - $list.toggle(shown); - }); - // call immediately to perform page setup - $(".js-metadata-item-type").change(); - - // Options can be removed by clicking the 'remove' button - $(".js-metadata-items").on("click", ".js-metadata-option-remove", function(e) { - e.preventDefault(); - var $this = $(this); - var $item = $this.closest(".js-metadata-item"); - $this.closest('li').remove(); - return true; - }); - - // New options can be added by clicking the appropriate button - $(".js-metadata-items").on("click", ".js-metadata-option-add", function(e) { - e.preventDefault(); - var $ul = $(this).closest("ul"); - var $template_option = $ul.find(".js-metadata-option-template"); - var $new_option = $template_option.clone(); - $new_option.removeClass("hidden-js js-metadata-option-template"); - $new_option.show(); - $new_option.insertBefore($template_option); - $new_option.find("input").first().focus(); - renumber_metadata_options($(this).closest(".js-metadata-item")); - return true; - }); - + // For "parent categories" $(".js-group-item-add").on("click", function(e) { e.preventDefault(); var $template_item = $(".js-group-item-template"); @@ -158,49 +126,67 @@ $(function(){ return true; }); - // Fields can be added/removed - $(".js-metadata-item-add").on("click", function(e) { - e.preventDefault(); - var $template_item = $(".js-metadata-items .js-metadata-item-template"); - var $new_item = $template_item.clone(); - $new_item.data('index', Math.max.apply( - null, - $(".js-metadata-item").map(function() { - return $(this).data('index'); - }).get() - ) + 1); - renumber_metadata_fields($new_item); - $new_item.removeClass("hidden-js js-metadata-item-template"); - $new_item.show(); - $new_item.insertBefore($template_item); - $new_item.find("input").first().focus(); - return true; - }); - $(".js-metadata-items").on("click", ".js-metadata-item-remove", function(e) { - e.preventDefault(); - $(this).closest(".js-metadata-item").remove(); - return true; + $('.js-metadata-item-add').on('click', function(){ + var $container = $(this).prevAll('.js-metadata-items'); + var i = $container.children().length + 1; + var html = $('#js-template-extra-metadata-item').html().replace(/9999/g, i); + $container.append(html); + fixmystreet.set_up.toggle_visibility(); + reloadSortableMetadataItems(); }); - function renumber_metadata_fields($item) { - var item_index = $item.data("index"); - $item.find("[data-field-name]").each(function(i) { - var $input = $(this); - var prefix = "metadata["+item_index+"]."; - var name = prefix + $input.data("fieldName"); - $input.attr("name", name); + $('.js-metadata-items').on('click', '.js-metadata-item-remove', function(){ + $(this).parents('.js-metadata-item').remove(); + }).on('change', '.js-metadata-item', updateMetadataItemTitle); + + sortable('.js-metadata-items', { + forcePlaceholderSize: true, + handle: '.js-metadata-item-header-grab', + placeholder: '<div class="extra-metadata-item-placeholder"></div>' + })[0].addEventListener('sortupdate', function(e) { + $(e.detail.destination.items).each(function(i){ + $(this).find('.js-sort-order input').val(i); }); + }); + $('.js-sort-order').addClass('hidden-js'); + + function reloadSortableMetadataItems(){ + sortable('.js-metadata-items', 'reload'); + $('.js-sort-order').addClass('hidden-js'); } - function renumber_metadata_options($item) { - var item_index = $item.data("index"); - $item.find(".js-metadata-option").each(function(i) { - var $li = $(this); - var prefix = "metadata["+item_index+"].values["+i+"]"; - $li.find(".js-metadata-option-key").attr("name", prefix+".key"); - $li.find(".js-metadata-option-name").attr("name", prefix+".name"); - $li.find(".js-metadata-option-disable").attr("name", prefix+".disable"); - }); + $('.js-metadata-item').each(updateMetadataItemTitle); + + function updateMetadataItemTitle(){ + var $title = $(this).find('.js-metadata-item-header-title'); + var defaultTitle = $title.attr('data-default'); + var html = '<strong>' + defaultTitle + '</strong>'; + var code = $(this).find('input[name$=".code"]').val(); + if ( code ) { + html = '<strong>' + code + '</strong>'; + var behaviour = $(this).find('input[name$=".behaviour"]:checked'); + if ( behaviour.length ) { + html += ' / ' + behaviour.val(); + } + var description = $(this).find('textarea[name$=".description"]').val(); + if ( description && (behaviour.val() == 'question' || behaviour.val() == 'notice') ) { + html += ' / ' + description.substring(0, 50); + } + } + $title.html(html); } + + $('.js-metadata-items').on('click', '.js-metadata-option-add', function(){ + var $container = $(this).prevAll('.js-metadata-options'); + var i = $(this).parents('.js-metadata-item').attr('data-i'); + var j = $container.children().length + 1; + var html = $('#js-template-extra-metadata-option').html().replace(/9999/g, i).replace(/8888/g, j); + $container.append(html); + fixmystreet.set_up.toggle_visibility(); + }); + + $('.js-metadata-items').on('click', '.js-metadata-option-remove', function(){ + $(this).parents('.js-metadata-option').remove(); + }); }); diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index c7749c729..6b88bc3d3 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -992,14 +992,48 @@ $.extend(fixmystreet.set_up, { }, toggle_visibility: function() { - $('input[type="checkbox"][data-toggle-visibility]').each(function(){ - var input = this; - var $target = $( $(this).attr('data-toggle-visibility') ); - var update = function() { - $target.toggleClass('hidden-js', ! input.checked ); + $('[data-toggle-visibility]').each(function(){ + var $target = $( $(this).attr('data-toggle-visibility') ); + if ( $(this).is(':checkbox') ){ + var input = this; + var update = function() { + $target.toggleClass('hidden-js', ! input.checked ); + }; + $(this).off('change.togglevisibility').on('change.togglevisibility', update); + update(); + } else { + $(this).off('click.togglevisibility').on('click.togglevisibility', function(){ + $target.toggleClass('hidden-js'); + }); + } + }); + + $('input[type="radio"][data-show], input[type="radio"][data-hide]').each(function(){ + var update = function(){ + if ( this.checked ) { + var $showTarget = $( $(this).attr('data-show') ); + var $hideTarget = $( $(this).attr('data-hide') ); + $showTarget.removeClass('hidden-js'); + $hideTarget.addClass('hidden-js'); + } + }; + // off/on to make sure event handler is only bound once. + $(this).off('change.togglevisibility').on('change.togglevisibility', update); + update.call(this); // pass DOM element as `this` + }); + + $('option[data-show], option[data-hide]').each(function(){ + var $select = $(this).parent(); + var update = function(){ + var $option = $(this).find('option:selected'); + var $showTarget = $( $option.attr('data-show') ); + var $hideTarget = $( $option.attr('data-hide') ); + $showTarget.removeClass('hidden-js'); + $hideTarget.addClass('hidden-js'); }; - $(input).on('change', update); - update(); + // off/on to make sure event handler is only bound once. + $select.off('change.togglevisibility').on('change.togglevisibility', update); + update.call($select[0]); // pass DOM element as `this` }); }, diff --git a/web/cobrands/sass/_admin.scss b/web/cobrands/sass/_admin.scss index d36c8ced0..3b47ea9aa 100644 --- a/web/cobrands/sass/_admin.scss +++ b/web/cobrands/sass/_admin.scss @@ -173,41 +173,125 @@ $button_bg_col: #a1a1a1; // also search bar (tables) } } -.js-metadata-items { - margin: 0; +.extra-metadata-item, +.extra-metadata-option { + border: 1px solid $table_border_color; + margin: 1em 0; + border-radius: 4px; + overflow: hidden; - li { - list-style: none; - position: relative; + // Make it look more "grabbable" if javascript available. + html.js & { + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.1); } +} - .js-metadata-item:nth-child(odd) { - background-color: #eee; - } +.extra-metadata-item-placeholder { + border: 1px solid #fff; + margin: 1em 0; +} - .js-metadata-options { - li { - list-style: none; +.extra-metadata-item__header { + @include flex-container(); + line-height: 1; + background: #f3f3f3; - label, input[type=text] { - display: inline-block; - margin: 0; - padding: 0.25em; - } + & > * { + padding: 1em; + border: none; + background: transparent; + text-align: inherit; + font-family: inherit; + font-size: 1em; + -webkit-appearance: none; + } - &:nth-child(even) { - background-color: #ddd; - } - &:nth-child(odd) { - background-color: #ccc; - } + .extra-metadata-item__header__remove { + color: #DB0030; + + &:hover, + &:focus { + background: #ffe1e1; + color: #AD0026; } } +} + +.extra-metadata-item__header__grab { + cursor: grab; - .js-metadata-item-remove { + // Overlap padding-left of the title element + margin-#{$right}: -1em; + position: relative; + z-index: 1; + width: 1em; + + &:before { + content: ""; + display: block; + width: 1em; + height: 2px; + background: #000; + box-shadow: 0 -4px 0 0 #000, 0 4px 0 0 #000; position: absolute; - top: 0.25em; - #{$right}: 0.25em; + top: 50%; + left: 1em; + margin-top: -1px; + } + + .sortable-dragging & { + cursor: grabbing; + } +} + +.extra-metadata-item__header__title { + @include flex(1 0 auto); + cursor: pointer; +} + +.extra-metadata-item__body { + padding: 0 1em 1em 1em; // 0em to compensate for first label margin-top + border-top: 1px solid $table_border_color; +} + +.extra-metadata-option { + margin: 0 0 1em 0; + padding: 0 1em; + + .row { + @include flex-container(); + margin: 0 -1em; + } + + .col { + @include box-sizing(border-box); + @include flex(1 0 auto); + padding: 0 1em; + width: 50%; + } + + .form-control { + margin: 0; + } + + button { + border: none; + background: transparent; + text-align: inherit; + font-family: inherit; + font-size: 1em; + -webkit-appearance: none; + color: #DB0030; + float: right; + padding: 0.5em; + margin-top: 1em; + border-radius: 4px; + + &:hover, + &:focus { + background: #ffe1e1; + color: #AD0026; + } } } diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss index 63ff19524..8e460f102 100644 --- a/web/cobrands/sass/_base.scss +++ b/web/cobrands/sass/_base.scss @@ -278,7 +278,8 @@ textarea { } label, -legend { +legend, +.label { display: block; margin-top: 1.25em; margin-bottom: 0.5em; diff --git a/web/vendor/html5sortable.min.js b/web/vendor/html5sortable.min.js new file mode 100755 index 000000000..3a7241283 --- /dev/null +++ b/web/vendor/html5sortable.min.js @@ -0,0 +1,2 @@ +var sortable=function(){"use strict";function c(e,t,n){if(void 0===n)return e&&e.h5s&&e.h5s.data&&e.h5s.data[t];e.h5s=e.h5s||{},e.h5s.data=e.h5s.data||{},e.h5s.data[t]=n}var d=function(e,t){if(!(e instanceof NodeList||e instanceof HTMLCollection||e instanceof Array))throw new Error("You must provide a nodeList/HTMLCollection/Array of elements to be filtered.");return"string"!=typeof t?Array.from(e):Array.from(e).filter(function(e){return 1===e.nodeType&&e.matches(t)})},u=new Map,t=function(){function e(){this._config=new Map,this._placeholder=void 0,this._data=new Map}return Object.defineProperty(e.prototype,"config",{get:function(){var n={};return this._config.forEach(function(e,t){n[t]=e}),n},set:function(e){if("object"!=typeof e)throw new Error("You must provide a valid configuration object to the config setter.");var t=Object.assign({},e);this._config=new Map(Object.entries(t))},enumerable:!0,configurable:!0}),e.prototype.setConfig=function(e,t){if(!this._config.has(e))throw new Error("Trying to set invalid configuration item: "+e);this._config.set(e,t)},e.prototype.getConfig=function(e){if(!this._config.has(e))throw new Error("Invalid configuration item requested: "+e);return this._config.get(e)},Object.defineProperty(e.prototype,"placeholder",{get:function(){return this._placeholder},set:function(e){if(!(e instanceof HTMLElement)&&null!==e)throw new Error("A placeholder must be an html element or null.");this._placeholder=e},enumerable:!0,configurable:!0}),e.prototype.setData=function(e,t){if("string"!=typeof e)throw new Error("The key must be a string.");this._data.set(e,t)},e.prototype.getData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.get(e)},e.prototype.deleteData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.delete(e)},e}(),p=function(e){if(!(e instanceof HTMLElement))throw new Error("Please provide a sortable to the store function.");return u.has(e)||u.set(e,new t),u.get(e)};function a(e,t,n){if(e instanceof Array)for(var r=0;r<e.length;++r)a(e[r],t,n);else e.addEventListener(t,n),p(e).setData("event"+t,n)}function i(e,t){if(e instanceof Array)for(var n=0;n<e.length;++n)i(e[n],t);else e.removeEventListener(t,p(e).getData("event"+t)),p(e).deleteData("event"+t)}function l(e,t,n){if(e instanceof Array)for(var r=0;r<e.length;++r)l(e[r],t,n);else e.setAttribute(t,n)}function r(e,t){if(e instanceof Array)for(var n=0;n<e.length;++n)r(e[n],t);else e.removeAttribute(t)}var m=function(e){if(!e.parentElement||0===e.getClientRects().length)throw new Error("target element must be part of the dom");var t=e.getClientRects()[0];return{left:t.left+window.pageXOffset,right:t.right+window.pageXOffset,top:t.top+window.pageYOffset,bottom:t.bottom+window.pageYOffset}},h=function(n,r){var o;return void 0===r&&(r=0),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];clearTimeout(o),o=setTimeout(function(){n.apply(void 0,e)},r)}},g=function(e,t){if(!(e instanceof HTMLElement&&(t instanceof NodeList||t instanceof HTMLCollection||t instanceof Array)))throw new Error("You must provide an element and a list of elements.");return Array.from(t).indexOf(e)},v=function(e){if(!(e instanceof HTMLElement))throw new Error("Element is not a node element.");return null!==e.parentNode},n=function(e,t,n){if(!(e instanceof HTMLElement&&e.parentElement instanceof HTMLElement))throw new Error("target and element must be a node");e.parentElement.insertBefore(t,"before"===n?e:e.nextElementSibling)},y=function(e,t){return n(e,t,"before")},E=function(e,t){return n(e,t,"after")},o=function(t,n,e){if(void 0===n&&(n=function(e,t){return e}),void 0===e&&(e=function(e){return e}),!(t instanceof HTMLElement)||!0==!t.isSortable)throw new Error("You need to provide a sortableContainer to be serialized.");if("function"!=typeof n||"function"!=typeof e)throw new Error("You need to provide a valid serializer for items and the container.");var r=c(t,"opts").items,o=d(t.children,r),i=o.map(function(e){return{parent:t,node:e,html:e.outerHTML,index:g(e,o)}});return{container:e({node:t,itemCount:i.length}),items:i.map(function(e){return n(e,t)})}},w=function(e,t,n){var r;if(void 0===n&&(n="sortable-placeholder"),!(e instanceof HTMLElement))throw new Error("You must provide a valid element as a sortable.");if(!(t instanceof HTMLElement)&&void 0!==t)throw new Error("You must provide a valid element as a placeholder or set ot to undefined.");return void 0===t&&(["UL","OL"].includes(e.tagName)?t=document.createElement("li"):["TABLE","TBODY"].includes(e.tagName)?(t=document.createElement("tr")).innerHTML='<td colspan="100"></td>':t=document.createElement("div")),"string"==typeof n&&(r=t.classList).add.apply(r,n.split(" ")),t},b=function(e){if(!(e instanceof HTMLElement))throw new Error("You must provide a valid dom element");var n=window.getComputedStyle(e);return["height","padding-top","padding-bottom"].map(function(e){var t=parseInt(n.getPropertyValue(e),10);return isNaN(t)?0:t}).reduce(function(e,t){return e+t})},s=function(e,t){if(!(e instanceof Array))throw new Error("You must provide a Array of HTMLElements to be filtered.");return"string"!=typeof t?e:e.filter(function(e){return e.querySelector(t)instanceof HTMLElement||e.shadowRoot&&e.shadowRoot.querySelector(t)instanceof HTMLElement}).map(function(e){return e.querySelector(t)||e.shadowRoot&&e.shadowRoot.querySelector(t)})},T=function(e){return e.composedPath&&e.composedPath()[0]||e.target},f=function(e,t,n){return{element:e,posX:n.pageX-t.left,posY:n.pageY-t.top}},L=function(e,t,n){if(!(e instanceof Event))throw new Error("setDragImage requires a DragEvent as the first argument.");if(!(t instanceof HTMLElement))throw new Error("setDragImage requires the dragged element as the second argument.");if(n||(n=f),e.dataTransfer&&e.dataTransfer.setDragImage){var r=n(t,m(t),e);if(!(r.element instanceof HTMLElement)||"number"!=typeof r.posX||"number"!=typeof r.posY)throw new Error("The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].");e.dataTransfer.effectAllowed="copyMove",e.dataTransfer.setData("text/plain",T(e).id),e.dataTransfer.setDragImage(r.element,r.posX,r.posY)}},C=function(e,t){if(!0===e.isSortable){var n=p(e).getConfig("acceptFrom");if(null!==n&&!1!==n&&"string"!=typeof n)throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.');if(null!==n)return!1!==n&&0<n.split(",").filter(function(e){return 0<e.length&&t.matches(e)}).length;if(e===t)return!0;if(void 0!==p(e).getConfig("connectWith")&&null!==p(e).getConfig("connectWith"))return p(e).getConfig("connectWith")===p(t).getConfig("connectWith")}return!1},M={items:null,connectWith:null,disableIEFix:null,acceptFrom:null,copy:!1,placeholder:null,placeholderClass:"sortable-placeholder",draggingClass:"sortable-dragging",hoverClass:!1,debounce:0,throttleTime:100,maxItems:0,itemSerializer:void 0,containerSerializer:void 0,customDragImage:null};var D,x,H,A,I,S,_,Y,O=function(e,t){if("string"==typeof p(e).getConfig("hoverClass")){var o=p(e).getConfig("hoverClass").split(" ");!0===t?(a(e,"mousemove",function(r,o){var i=this;if(void 0===o&&(o=250),"function"!=typeof r)throw new Error("You must provide a function as the first argument for throttle.");if("number"!=typeof o)throw new Error("You must provide a number as the second argument for throttle.");var a=null;return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=Date.now();(null===a||o<=n-a)&&(a=n,r.apply(i,e))}}(function(r){0===r.buttons&&d(e.children,p(e).getConfig("items")).forEach(function(e){var t,n;e!==r.target?(t=e.classList).remove.apply(t,o):(n=e.classList).add.apply(n,o)})},p(e).getConfig("throttleTime"))),a(e,"mouseleave",function(){d(e.children,p(e).getConfig("items")).forEach(function(e){var t;(t=e.classList).remove.apply(t,o)})})):(i(e,"mousemove"),i(e,"mouseleave"))}},P=function(e){i(e,"dragstart"),i(e,"dragend"),i(e,"dragover"),i(e,"dragenter"),i(e,"drop"),i(e,"mouseenter"),i(e,"mouseleave")},W=function(e,t){var n=e;return!0===p(t).getConfig("copy")&&(l(n=e.cloneNode(!0),"aria-copied","true"),e.parentElement.appendChild(n),n.style.display="none",n.oldDisplay=e.style.display),n},F=function(e){var t;(t=e).h5s&&delete t.h5s.data,r(e,"aria-dropeffect")},N=function(e){r(e,"aria-grabbed"),r(e,"aria-copied"),r(e,"draggable"),r(e,"role")};function j(e,t){if(t.composedPath)return t.composedPath().find(function(e){return e.isSortable});for(;!0!==e.isSortable;)e=e.parentElement;return e}function q(e,t){var n=c(e,"opts"),r=d(e.children,n.items).filter(function(e){return e.contains(t)||e.shadowRoot&&e.shadowRoot.contains(t)});return 0<r.length?r[0]:t}var z=function(e){var t=c(e,"opts"),n=d(e.children,t.items),r=s(n,t.handle);(l(e,"aria-dropeffect","move"),c(e,"_disabled","false"),l(r,"draggable","true"),!1===t.disableIEFix)&&("function"==typeof(document||window.document).createElement("span").dragDrop&&a(r,"mousedown",function(){if(-1!==n.indexOf(this))this.dragDrop();else{for(var e=this.parentElement;-1===n.indexOf(e);)e=e.parentElement;e.dragDrop()}}))},R=function(e){var t=c(e,"opts"),n=d(e.children,t.items),r=s(n,t.handle);c(e,"_disabled","false"),P(n),i(r,"mousedown"),i(e,"dragover"),i(e,"dragenter"),i(e,"drop")};function X(e,f){var i=String(f);return f=f||{},"string"==typeof e&&(e=document.querySelectorAll(e)),e instanceof HTMLElement&&(e=[e]),e=Array.prototype.slice.call(e),/serialize/.test(i)?e.map(function(e){var t=c(e,"opts");return o(e,t.itemSerializer,t.containerSerializer)}):(e.forEach(function(s){if(/enable|disable|destroy/.test(i))return X[i](s);["connectWith","disableIEFix"].forEach(function(e){f.hasOwnProperty(e)&&null!==f[e]&&console.warn('HTML5Sortable: You are using the deprecated configuration "'+e+'". This will be removed in an upcoming version, make sure to migrate to the new options when updating.')}),f=Object.assign({},M,p(s).config,f),p(s).config=f,c(s,"opts",f),s.isSortable=!0,R(s);var e,t=d(s.children,f.items);if(null!==f.placeholder&&void 0!==f.placeholder){var n=document.createElement(s.tagName);f.placeholder instanceof HTMLElement?n.appendChild(f.placeholder):n.innerHTML=f.placeholder,e=n.children[0]}p(s).placeholder=w(s,e,f.placeholderClass),c(s,"items",f.items),f.acceptFrom?c(s,"acceptFrom",f.acceptFrom):f.connectWith&&c(s,"connectWith",f.connectWith),z(s),l(t,"role","option"),l(t,"aria-grabbed","false"),O(s,!0),a(s,"dragstart",function(e){var t=T(e);if(!0!==t.isSortable&&(e.stopImmediatePropagation(),(!f.handle||t.matches(f.handle))&&"false"!==t.getAttribute("draggable"))){var n=j(t,e),r=q(n,t);S=d(n.children,f.items),A=S.indexOf(r),I=g(r,n.children),H=n,L(e,r,f.customDragImage),x=b(r),r.classList.add(f.draggingClass),l(D=W(r,n),"aria-grabbed","true"),n.dispatchEvent(new CustomEvent("sortstart",{detail:{origin:{elementIndex:I,index:A,container:H},item:D,originalTarget:t}}))}}),a(s,"dragenter",function(e){var t=T(e),n=j(t,e);n&&n!==_&&(Y=d(n.children,c(n,"items")).filter(function(e){return e!==p(s).placeholder}),n.dispatchEvent(new CustomEvent("sortenter",{detail:{origin:{elementIndex:I,index:A,container:H},destination:{container:n,itemsBeforeUpdate:Y},item:D,originalTarget:t}}))),_=n}),a(s,"dragend",function(e){if(D){D.classList.remove(f.draggingClass),l(D,"aria-grabbed","false"),"true"===D.getAttribute("aria-copied")&&"true"!==c(D,"dropped")&&D.remove(),D.style.display=D.oldDisplay,delete D.oldDisplay;var t=Array.from(u.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(v)[0];t&&t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:I,index:A,container:H},item:D}})),x=D=_=null}}),a(s,"drop",function(e){if(C(s,D.parentElement)){e.preventDefault(),e.stopPropagation(),c(D,"dropped","true");var t=Array.from(u.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(v)[0];E(t,D),t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:I,index:A,container:H},item:D}}));var n=p(s).placeholder,r=d(H.children,f.items).filter(function(e){return e!==n}),o=!0===this.isSortable?this:this.parentElement,i=d(o.children,c(o,"items")).filter(function(e){return e!==n}),a=g(D,Array.from(D.parentElement.children).filter(function(e){return e!==n})),l=g(D,i);I===a&&H===o||s.dispatchEvent(new CustomEvent("sortupdate",{detail:{origin:{elementIndex:I,index:A,container:H,itemsBeforeUpdate:S,items:r},destination:{index:l,elementIndex:a,container:o,itemsBeforeUpdate:Y,items:i},item:D}}))}});var o=h(function(t,e,n){if(D)if(f.forcePlaceholderSize&&(p(t).placeholder.style.height=x+"px"),-1<Array.from(t.children).indexOf(e)){var r=b(e),o=g(p(t).placeholder,e.parentElement.children),i=g(e,e.parentElement.children);if(x<r){var a=r-x,l=m(e).top;if(o<i&&n<l)return;if(i<o&&l+r-a<n)return}void 0===D.oldDisplay&&(D.oldDisplay=D.style.display),"none"!==D.style.display&&(D.style.display="none");var s=!1;try{s=m(e).top+e.offsetHeight/2<=n}catch(e){s=o<i}s?E(e,p(t).placeholder):y(e,p(t).placeholder),Array.from(u.values()).filter(function(e){return void 0!==e.placeholder}).forEach(function(e){e.placeholder!==p(t).placeholder&&e.placeholder.remove()})}else{var c=Array.from(u.values()).filter(function(e){return void 0!==e.placeholder}).map(function(e){return e.placeholder});-1!==c.indexOf(e)||t!==e||d(e.children,f.items).length||(c.forEach(function(e){return e.remove()}),e.appendChild(p(t).placeholder))}},f.debounce),r=function(e){var t=e.target,n=!0===t.isSortable?t:j(t,e);if(t=q(n,t),D&&C(n,D.parentElement)&&"true"!==c(n,"_disabled")){var r=c(n,"opts");parseInt(r.maxItems)&&d(n.children,c(n,"items")).length>=parseInt(r.maxItems)&&D.parentElement!==n||(e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect=!0===p(n).getConfig("copy")?"copy":"move",o(n,t,e.pageY))}};a(t.concat(s),"dragover",r),a(t.concat(s),"dragenter",r)}),e)}return X.destroy=function(e){var t,n,r,o;n=c(t=e,"opts")||{},r=d(t.children,n.items),o=s(r,n.handle),i(t,"dragover"),i(t,"dragenter"),i(t,"drop"),F(t),i(o,"mousedown"),P(r),N(r)},X.enable=function(e){z(e)},X.disable=function(e){var t,n,r,o;n=c(t=e,"opts"),r=d(t.children,n.items),o=s(r,n.handle),l(t,"aria-dropeffect","none"),c(t,"_disabled","true"),l(o,"draggable","false"),i(o,"mousedown")},X.__testing={_data:c,_removeItemEvents:P,_removeItemData:N,_removeSortableData:F},X}(); +//# sourceMappingURL=html5sortable.min.js.map |