- <%= form_for @plan, html: {method: :post, class: "roadmap-form padded bordered"}, remote: true do |f| %>
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+
\ 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'],
},