diff --git a/app/views/layouts/_es5_scripts.html.erb b/app/views/layouts/_es5_scripts.html.erb index f473d97..e575d54 100644 --- a/app/views/layouts/_es5_scripts.html.erb +++ b/app/views/layouts/_es5_scripts.html.erb @@ -4,7 +4,6 @@ <%= javascript_include_tag 'jquery.min.js' %> <%= javascript_include_tag 'rails.js' %> <%= javascript_include_tag 'jquery-ui.min.js' %> - <%= javascript_include_tag 'jquery-accessible-autocomplete-list-aria.js' %> <%= javascript_include_tag 'placeholder.min.js' %> <%= javascript_include_tag 'jquery.tablesorter.min.js' %> <%= javascript_include_tag 'jquery.tablesorter.widgets.min.js' %> @@ -40,10 +39,7 @@ <%= javascript_include_tag 'views/orgs/shibboleth_ds.js' %> <%= javascript_include_tag 'views/plans/available_templates.js' %> <%= javascript_include_tag 'views/plans/index.js' %> - <%= javascript_include_tag 'views/plans/new.js' %> <%= javascript_include_tag 'views/registrations/sign_in_sign_up.js' %> - <%#= javascript_include_tag 'views/shared/accessible_combobox.js' %> - <%#= javascript_include_tag 'views/shared/accessible_submit_button.js' %> <%= javascript_include_tag 'views/shared/login_form.js' %> <%= javascript_include_tag 'views/shared/register_form.js' %> <%= javascript_include_tag 'views/users/notification_preferences.js' %> diff --git a/app/views/plans/_share.html.erb b/app/views/plans/_share.html.erb deleted file mode 100644 index e69de29..0000000 --- a/app/views/plans/_share.html.erb +++ /dev/null diff --git a/app/views/plans/_share_form.html.erb b/app/views/plans/_share_form.html.erb index a4abdd2..a62fb16 100644 --- a/app/views/plans/_share_form.html.erb +++ b/app/views/plans/_share_form.html.erb @@ -80,5 +80,5 @@ <%= f.label :access_level, raw("#{f.radio_button :access_level, 3, "aria-required": true} #{_('Co-owner: can edit project details, change visibility, and add collaborators')}") %> - <%= f.button(_('Submit'), class: "btn btn-default", type: "submit") %> + <%= f.button(_('Submit'), class: "btn btn-primary", type: "submit") %> <% end %> \ No newline at end of file diff --git a/app/views/plans/new.html.erb b/app/views/plans/new.html.erb index 0f80120..515cd05 100644 --- a/app/views/plans/new.html.erb +++ b/app/views/plans/new.html.erb @@ -10,89 +10,71 @@
- <%= form_for @plan, html: {method: :post, class: "roadmap-form padded bordered"}, remote: true do |f| %> -
- - -
- <%= _('What research project are you planning?') %> - -
- <%= f.text_field(:title, class: 'left-indent input-large', + <%= form_for @plan, html: {method: :post, id: 'create-plan'}, remote: true do |f| %> + +

<%= _('What research project are you planning?') %>

+
+
+ <%= f.text_field(:title, class: 'form-control', 'aria-describedby': 'project-title', 'aria-required': 'true', 'data-toggle': 'tooltip', 'data-content': _('If applying for funding, state the project title exactly as in the proposal.')) %> - <%= check_box_tag(:is_test, "1", false, class: 'left-indent') %> - <%= label_tag(:is_test, _('mock project for testing, practice, or educational purposes'), class: 'inline checkbox-label') %> -
-
+
+
+ <%= label_tag(:is_test, raw("#{check_box_tag(:is_test, "1", false)} #{_('mock project for testing, practice, or educational purposes')}")) %> +
+
- -
- <%= _('Select the primary research organisation') %> - -
- <%= render partial: "shared/accessible_combobox", - locals: {name: 'plan[org_name]', - id: 'plan_org_name', - default_selection: @default_org, - models: @orgs, - attribute: 'name', - tooltip: _('Please select a valid research organisation from the list.'), - error: _('You must select a research organisation from the list.'), - classes: 'input-large'} %> -
- -
-

- <%= _('or') %> -

- <%= check_box_tag(:plan_no_org) %> - <%= label_tag(:plan_no_org, raw(" - #{_('My research organisation is not on the list')} - #{_(' or ')} - #{_('no research organisation is associated with this plan')} - "), class: 'checkbox-label') %> -
-
- - -
- <%= _('Select the primary funding organisation') %> - -
- <%= render partial: "shared/accessible_combobox", - locals: {name: 'plan[funder_name]', - id: 'plan_funder_name', - default_selection: nil, - models: @funders, - attribute: 'name', - tooltip: _('Please select a valid funding organisation from the list.'), - error: _('You must select a funding organisation from the list.'), - classes: 'input-large'} %> -
-
-

- <%= _('or') %> -

- <%= check_box_tag(:plan_no_funder) %> - <%= label_tag(:plan_no_funder, _('No funder associated with this plan'), class: 'checkbox-label') %> -
-
- - -
-
- + +

<%= _('Select the primary research organisation') %>

+
+
+ <%= render partial: "shared/accessible_combobox", + locals: {name: 'plan[org_name]', + id: 'plan_org_name', + default_selection: @default_org, + models: @orgs, + attribute: 'name', + required: true, + error: _('You must select a research organisation from the list.'), + tooltip: _('Please select a valid research organisation from the list.')} %> +
+
+ <%= label_tag(:plan_no_org, raw("#{check_box_tag(:plan_no_org)} #{_('My research organisation is not on the list')} #{_(' or ')} #{_('no research organisation is associated with this plan')}")) %> +
-
-
- <%= f.hidden_field(:template_id) %> - <%= f.hidden_field(:visibility, value: @is_test ? 'is_test' : Rails.application.config.default_plan_visibility) %> - <%= render partial: 'shared/accessible_submit_button', - locals: {id: 'create_plan_submit', - val: 'Create Plan', - disabled_initially: true, - classes: "left-indent", - tooltip: _('You can not continue until you have filled in all of the required information.')} %> + +

<%= _('Select the primary funding organisation') %>

+
+
+ <%= render partial: "shared/accessible_combobox", + locals: {name: 'plan[funder_name]', + id: 'plan_funder_name', + default_selection: nil, + models: @funders, + attribute: 'name', + required: true, + error: _('You must select a funding organisation from the list.'), + tooltip: _('Please select a valid funding organisation from the list.')} %>
-
+
+ <%= label_tag(:plan_no_funder, raw("#{check_box_tag(:plan_no_funder)} #{_('No funder associated with this plan')}")) %> +
+
+ + +
+
+ +
+
+ +
+
+ + <%= f.hidden_field(:template_id) %> + <%= f.hidden_field(:visibility, value: @is_test ? 'is_test' : Rails.application.config.default_plan_visibility) %> + <%= f.button(_('Create plan'), class: "btn btn-primary", type: "submit") %> <% end %> diff --git a/app/views/shared/_accessible_combobox.html.erb b/app/views/shared/_accessible_combobox.html.erb index 6b32337..f2ef99f 100644 --- a/app/views/shared/_accessible_combobox.html.erb +++ b/app/views/shared/_accessible_combobox.html.erb @@ -1,10 +1,13 @@ <% if !models.nil? %> + <% required = required ||= false %> + <% classes = classes ||= '' %> + <% error = error ||= _('Please select an item from the list.') %> + <% json = {} %> <% models.map{|m| json[m[attribute]] = m.id} %> - <% err_msg = error ||= _('Please select an item from the list.') %> + + + " name="<%= name.gsub("_#{attribute}]", "_id]") %>" - value="<%= default_selection.id unless default_selection.nil? %>" /> + value="<%= default_selection.id unless default_selection.nil? %>" aria-required="<%= required %>" + data-validation="js-combobox" data-validation-error="<%= error %>" /> - - <% else %> <%= _('No items available.') %> <% end %> \ No newline at end of file diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js index 0a2fc35..9006f60 100644 --- a/lib/assets/javascripts/application.js +++ b/lib/assets/javascripts/application.js @@ -6,4 +6,5 @@ import './views/phases/edit'; import './views/plans/download'; import './views/plans/edit_details'; +import './views/plans/new'; import './views/plans/share'; diff --git a/lib/assets/javascripts/constants.js b/lib/assets/javascripts/constants.js index 1ac6267..cd8245f 100644 --- a/lib/assets/javascripts/constants.js +++ b/lib/assets/javascripts/constants.js @@ -7,6 +7,7 @@ export const VALIDATION_MESSAGE_PASSWORD = 'The password must be between 8 and 128 characters.'; export const VALIDATION_MESSAGE_PASSWORDS_MATCH = 'The passwords must match.'; export const VALIDATION_MESSAGE_RADIO = 'Please choose one of the options.'; +export const VALIDATION_MESSAGE_SELECT = 'Please select a value from the list.'; export const VALIDATION_MESSAGE_TEXT = 'This field is required.'; export const SHOW_PASSWORD_MESSAGE = 'Show password'; diff --git a/lib/assets/javascripts/spec/autoCompleteSpec.js b/lib/assets/javascripts/spec/autoCompleteSpec.js new file mode 100644 index 0000000..08965ea --- /dev/null +++ b/lib/assets/javascripts/spec/autoCompleteSpec.js @@ -0,0 +1,26 @@ +import initAutoComplete from '../utils/autoComplete'; + +describe('autoComplete test suite', () => { + beforeAll(() => fixture.setBase('javascripts/spec/fixtures')); + + beforeEach(() => { + $('body').html(fixture.load('autoComplete.html')); + initAutoComplete(); + // Override the form submission, we are just going to validate the ariatisation of the form + $('form').submit((e) => { e.preventDefault(); }); + }); + + afterEach(() => { + fixture.cleanup(); + $('body').html(''); + }); + + it('shows/hides the clear button correctly', () => { + + }); + + it('Selects the correct id based on the item selected in the combobox', () => { + + }); + +}); diff --git a/lib/assets/javascripts/spec/expandCollapseAllSpec.js b/lib/assets/javascripts/spec/expandCollapseAllSpec.js new file mode 100644 index 0000000..71c129a --- /dev/null +++ b/lib/assets/javascripts/spec/expandCollapseAllSpec.js @@ -0,0 +1,48 @@ +import expandCollapseAll from '../utils/expandCollapseAll'; + +describe('expandCollapseAll test suite', () => { + beforeAll(() => fixture.setBase('javascripts/spec/fixtures')); + + beforeEach(() => { + this.form = fixture.load('accordion.html'); + expandCollapseAll({ selector: '#accordion' }); + }); + + afterEach(() => { + fixture.cleanup(); + }); + + it('should be able to expand all sections when all are either expanded or collapsed', () => { + // Collapse all of the sections + // - click on 'collapse all' should have no effect + // - click on 'expand all' should expand all sections + $('#accordion div.panel-collapse').collapse('hide'); + expect($('.in').length === 0); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + + // Expand all of the sections + // - click on 'expand all' should have no effect + // - click on 'collapse all' should collapse all sections + $('#accordion div.panel-collapse').collapse('show'); + expect($('.in').length === 3); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + }); + + it('should be able to expand all sections when some are open and some collapsed', () => { + // Expand 2 of the 3 sections - click 'collapse all' - verify that all are collapsed + $('#collapseA, #collapseC').collapse('show'); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + + // Expand 2 of the 3 sections - click 'expand all' - verify that all are expanded + $('#collapseA, #collapseC').collapse('show'); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + }); +}); diff --git a/lib/assets/javascripts/spec/fixtures/accordion.html b/lib/assets/javascripts/spec/fixtures/accordion.html new file mode 100644 index 0000000..78cb5aa --- /dev/null +++ b/lib/assets/javascripts/spec/fixtures/accordion.html @@ -0,0 +1,60 @@ +
+ +
+ <%= _('expand all') %> + | + <%= _('collapse all') %> +
+ + + +
+
+ +
+
+
+ This is test section A. +
+
+ +
+ +
+
+
+ This is test section B. +
+
+ +
+ +
+
+
+ This is test section C. +
+
+
+
\ No newline at end of file diff --git a/lib/assets/javascripts/spec/fixtures/autoComplete.html b/lib/assets/javascripts/spec/fixtures/autoComplete.html new file mode 100644 index 0000000..7c3ce2f --- /dev/null +++ b/lib/assets/javascripts/spec/fixtures/autoComplete.html @@ -0,0 +1,35 @@ +
+ +

JQuery Accessible Auto-complete Combobox

+
+ + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/lib/assets/javascripts/utils/ariatiseForm.js b/lib/assets/javascripts/utils/ariatiseForm.js index fdd417a..2db61b3 100644 --- a/lib/assets/javascripts/utils/ariatiseForm.js +++ b/lib/assets/javascripts/utils/ariatiseForm.js @@ -103,12 +103,14 @@ return validator.isValidPassword(value); case 'radio': return validator.isValidText(value); + case 'js-combobox': + return validator.isValidText(value); default: return false; } }; -const getValidationMessage = (type) => { +const getDefaultValidationMessage = (type) => { switch (type) { case 'text': return constants.VALIDATION_MESSAGE_TEXT; @@ -120,11 +122,21 @@ return constants.VALIDATION_MESSAGE_PASSWORD; case 'radio': return constants.VALIDATION_MESSAGE_RADIO; + case 'js-combobox': + return constants.VALIDATION_MESSAGE_SELECT; default: return constants.VALIDATION_MESSAGE_DEFAULT; } }; +const getValidationMessage = (el) => { + if ($(el).attr('data-validation-error')) { + return $(el).attr('data-validation-error'); + } + // Use the default validation error message if none was specified + return getDefaultValidationMessage(getValidationTypeForElement(el)); +}; + const valid = (el) => { $(el).parent().removeClass(validationStates.hasError); $(el).attr(ariaInvalid(false)); @@ -142,9 +154,8 @@ // Add validation error message sections for each validatable input element validatable.each((i, el) => { - const type = getValidationTypeForElement(el); $(el).attr(ariaDescribedBy(`help${i}`)); - $(el).after(blockHelp(`help${i}`, getValidationMessage(type))); + $(el).after(blockHelp(`help${i}`, getValidationMessage(el))); }); // Bind validations to the form's submit button @@ -160,7 +171,7 @@ valid(el); } else { anyInvalid = true; - invalid(el, getValidationMessage(type)); + invalid(el); } } }); diff --git a/lib/assets/javascripts/utils/autoComplete.js b/lib/assets/javascripts/utils/autoComplete.js new file mode 100644 index 0000000..44b77f1 --- /dev/null +++ b/lib/assets/javascripts/utils/autoComplete.js @@ -0,0 +1,58 @@ +import debounce from '../utils/debounce'; + +/* + * Looks up the id for the text selected by the user in the jquery autocomplete combobox and + * then sets updates the hidden id field with the id value so that its available on form submit. + * The id-text mappings are stored as JSON in the corresponding hidden crosswalk field + * @param the combobox element + */ +const updateIdField = (el) => { + const crosswalk = $(`#${$(el).attr('id')}_crosswalk`); + const idField = $(el).attr('id').replace(/_name/, '_id'); + + if (crosswalk && idField) { + const json = JSON.parse(`${$(crosswalk).val().replace(/\\"/g, '"').replace(/\\'/g, '\'')}`); + const selection = json[$(el).val()]; + $(idField).val(selection === 'undefined' ? '' : selection).change(); + } +}; + +/* + * Shows/hides the combobox's clear button based on whether or not text is present + * @param the combobox id + */ +const toggleClearButton = (el) => { + const clearButton = $(el).parent().find('.combobox-clear-button'); + if ($(el).val().trim().length <= 0) { + $(clearButton).addClass('hidden'); + } else { + $(clearButton).removeClass('hidden'); + } +}; + +/* + * Wires up the jquery autocomplete combobox so that it calls the above 2 functions when the + * user changes the text values in the combobox by typing or selecting a value + */ +export default () => { + $('.js-combobox').each((idx, el) => { + const debounced = debounce((e) => { + toggleClearButton(e); + updateIdField(e); + }, 500); + + // When the value in the combobox changes update the hidden id field + $(el).on('keyup', (e) => { + debounced($(e.currentTarget)); + }); + + // Clear the text and hide the button when the user clicks the clear button + $(el).parent().find('.combobox-clear-button').on('click', () => { + $(el).val('').focus(); + debounced($(el)); + }); + + // Show/hide the clear button on page load + toggleClearButton(el); + }); +}; diff --git a/lib/assets/javascripts/views/plans/new.js b/lib/assets/javascripts/views/plans/new.js index 05ea6c6..815c832 100644 --- a/lib/assets/javascripts/views/plans/new.js +++ b/lib/assets/javascripts/views/plans/new.js @@ -1,75 +1,67 @@ -$(document).ready(function(){ - $("#available-templates").hide(); +import ariatiseForm from '../../utils/ariatiseForm'; +import initAutoComplete from '../../utils/autoComplete'; - var defaultVisibility = $("#plan_visibility").val(); +const handleCheckboxClick = (name, checked) => { + $(`#plan_${name}_name`).prop('disabled', checked); + $('#plan_template_id').val('').change(); + $('#available-templates').fadeOut(); - // retrieve the template options and toggle the submit button on page reload + if (checked) { + $(`#plan_${name}_name`).val(''); + $(`#plan_${name}_id`).val('-1').change(); + $(`#plan_${name}_name`).siblings('.combobox-clear-button').hide(); + } else { + $(`#plan_${name}_id`).val('').change(); + } +}; + +const handleComboboxChange = () => { + const validOrg = ($('#plan_org_id').val().trim().length > 0 || $('#plan_no_org').prop('checked')); + const validFunder = ($('#plan_funder_id').val().trim().length > 0 || $('#plan_no_funder').prop('checked')); + + if (!validOrg || !validFunder) { + $('#available-templates').fadeOut(); + $('#plan_template_id').val(''); + } +}; + +$().ready(() => { + initAutoComplete(); + ariatiseForm({ selector: '#create-plan' }); + + const defaultVisibility = $('#plan_visibility').val(); + + // Initialize the form handleComboboxChange(); - handleCheckboxClick("org", $("#plan_no_org").prop("checked")); - handleCheckboxClick("funder", $("#plan_no_funder").prop("checked")); - + handleCheckboxClick('org', $('#plan_no_org').prop('checked')); + handleCheckboxClick('funder', $('#plan_no_funder').prop('checked')); + // When the user checks the 'mock project' box we need to set the // visibility to 'is_test' - $("#is_test").click(function(){ - $("#plan_visibility").val(($(this)[0].checked ? 'is_test' : defaultVisibility)); + $('#is_test').click((e) => { + $('#plan_visibility').val(($(e.currentTarget)[0].checked ? 'is_test' : defaultVisibility)); }); // When the hidden org and funder id fields change toogle the submit button - $("#plan_org_id, #plan_funder_id").change(function(){ + $('#plan_org_id, #plan_funder_id').change(() => { handleComboboxChange(); }); // Make sure the checkbox is unchecked if we're entering text - $(".js-combobox").keyup(function(){ - var whichOne = $(this).prop('id').split('_')[1]; - $("#plan_no_" + whichOne).prop("checked", false); + $('.js-combobox').keyup((e) => { + const whichOne = $(e.currentTarget).prop('id').split('_')[1]; + $(`#plan_no_${whichOne}`).prop('checked', false); }); // If the user clicks the no Org/Funder checkbox disable the dropdown // and hide clear button - $("#plan_no_org, #plan_no_funder").click(function(){ - var whichOne = $(this).prop('id').split('_')[2]; - handleCheckboxClick(whichOne, this.checked); + $('#plan_no_org, #plan_no_funder').click((e) => { + const whichOne = $(e.currentTarget).prop('id').split('_')[2]; + handleCheckboxClick(whichOne, e.currentTarget.checked); }); - + // When the form receives a valid template id enable the button - $("#plan_template_id").change(function(){ - $("#create_plan_submit").attr('aria-disabled', ($(this).val().trim().length <= 0)); + $('#plan_template_id').change((e) => { + $('#create_plan_submit').attr('aria-disabled', ($(e.currentTarget).val().trim().length <= 0)); }); }); - -// Only display the submit button if the user has made each decision -// ------------------------------------------------------------- -function handleComboboxChange(){ - // If the (no_org checkbox is checked OR an org was selected) AND - // (no_funder checkbox is checked OR a funder was selected) AND - // (the template selector is not visible OR a template has been selected) - var retrieve = ($("#plan_no_org").prop("checked") || - ($("#plan_org_id").val() && $("#plan_org_id").val().trim().length > 0)) && - ($("#plan_no_funder").prop("checked") || - ($("#plan_org_id").val() && $("#plan_funder_id").val().trim().length > 0)); - - if(retrieve){ - if($("#plan_template_id").val().trim().length <= 0){ - $("form").submit(); - } - - }else{ - $("#available-templates").fadeOut(); - $("#plan_template_id").val(""); - } -} - -// Clear the combobox and disable it if the box was checked -// ------------------------------------------------------------- -function handleCheckboxClick(name, checked){ - $("#plan_" + name + "_name").prop("disabled", checked); - $("#plan_template_id").val("").change(); - $("#available-templates").fadeOut(); - - if(checked){ - $("#plan_" + name + "_name").val(""); - $("#plan_" + name + "_id").val("").change(); - $("#plan_" + name + "_name").siblings(".combobox-clear-button").hide(); - } -} diff --git a/lib/assets/package-lock.json b/lib/assets/package-lock.json index 10cb86b..d43dad4 100644 --- a/lib/assets/package-lock.json +++ b/lib/assets/package-lock.json @@ -4652,9 +4652,7 @@ "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" }, "jquery-accessible-autocomplete-list-aria": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/jquery-accessible-autocomplete-list-aria/-/jquery-accessible-autocomplete-list-aria-1.5.5.tgz", - "integrity": "sha1-2EG0wfBwSQaGcri1FsIkhUaPi4s=", + "version": "github:nico3333fr/jquery-accessible-autocomplete-list-aria#38a057140ccafa9a1c8a948b1bbf4a410c0181c6", "requires": { "jquery": "3.2.1" } diff --git a/lib/assets/webpack.config.js b/lib/assets/webpack.config.js index 6e20570..63fd3eb 100644 --- a/lib/assets/webpack.config.js +++ b/lib/assets/webpack.config.js @@ -15,7 +15,7 @@ context: __dirname, entry: { - vendor: ['jquery', 'timeago/jquery.timeago'], + vendor: ['jquery', 'timeago/jquery.timeago', 'jquery-accessible-autocomplete-list-aria/jquery-accessible-autocomplete-list-aria'], application: ['./javascripts/application.js', './stylesheets/application.scss'], },