diff --git a/app/views/shared/_links.html.erb b/app/views/shared/_links.html.erb
index c01fe62..b20a916 100644
--- a/app/views/shared/_links.html.erb
+++ b/app/views/shared/_links.html.erb
@@ -22,7 +22,7 @@
<%= label_tag "link_link#{i}", _('URL'), class: "control-label" %>
- <%= text_field_tag 'link_link', l['link'], class: "form-control", id: "link_link#{i}" %>
+ <%= text_field_tag 'link_link', l['link'], class: "form-control", id: "link_link#{i}", 'data-validation': 'url' %>
<%= label_tag "link_text#{i}", _('Link text'), class: "control-label" %>
diff --git a/lib/assets/javascripts/constants.js b/lib/assets/javascripts/constants.js
index de32cca..15fc67e 100644
--- a/lib/assets/javascripts/constants.js
+++ b/lib/assets/javascripts/constants.js
@@ -7,6 +7,7 @@
export const VALIDATION_MESSAGE_DEFAULT = 'Please enter a valid value.';
export const VALIDATION_MESSAGE_EMAIL = 'You must enter a valid email address.';
+export const VALIDATION_MESSAGE_URL = 'You must enter a valid URL (e.g. https://organisation.org).';
export const VALIDATION_MESSAGE_NUMBER = 'Please enter a valid number.';
export const VALIDATION_MESSAGE_PASSWORD = 'The password must be between 8 and 128 characters.';
export const VALIDATION_MESSAGE_PASSWORDS_MATCH = 'The passwords must match.';
diff --git a/lib/assets/javascripts/utils/isValidInputType.js b/lib/assets/javascripts/utils/isValidInputType.js
index d31550c..e2edc4a 100644
--- a/lib/assets/javascripts/utils/isValidInputType.js
+++ b/lib/assets/javascripts/utils/isValidInputType.js
@@ -16,6 +16,18 @@
};
/*
+ Validates whether or not the value passed matches to a valid url
+ @param value String to search for a match
+ @return true or false
+*/
+export const isValidUrl = (value) => {
+ if (isString(value)) {
+ return /https?:\/\/[-a-zA-Z0-9@:%_+.~#?&=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_+.~#?&=]*)?/.test(value);
+ }
+ return false;
+};
+
+/*
Validates whether or not the value passed is a valid number.
@param value Number to validate
*/
diff --git a/lib/assets/javascripts/utils/links.js b/lib/assets/javascripts/utils/links.js
index 41999a0..14cf6e1 100644
--- a/lib/assets/javascripts/utils/links.js
+++ b/lib/assets/javascripts/utils/links.js
@@ -1,6 +1,8 @@
import 'number-to-text/converters/en-us';
import { convertToText } from 'number-to-text/index';
-import { isFunction } from './isType';
+import { isFunction, isObject } from './isType';
+import { isValidText } from './isValidInputType';
+import { enableValidations, disableValidations } from './validation';
const getLinks = elem =>
$(elem).find('.link').map((i, el) => {
@@ -33,6 +35,34 @@
}
};
+const enableLinkValidations = (ctx) => {
+ if (isObject(ctx)) {
+ let validatable = false;
+ const fields = $(ctx).find('input');
+
+ // Determine if ANY one of the values has been populated
+ fields.each((i, el) => {
+ if (isValidText($(el).val())) {
+ validatable = true;
+ }
+ });
+
+ // If one field has been populated make both fields required otherwise remove any validations
+ if (validatable) {
+ $(ctx).find('input').attr('aria-required', 'true');
+ enableValidations(ctx);
+ } else {
+ $(ctx).find('input').removeAttr('aria-required');
+ disableValidations(ctx);
+ }
+ }
+};
+const addValidationHandlers = (ctx) => {
+ $(ctx).find('input').on('blur', () => {
+ enableLinkValidations(ctx);
+ });
+};
+
$(() => {
const regExp = /([^\d]*)(\d)+/;
const replacer = (match, p1, p2) => `${p1}${(p2 * 1) + 1}`;
@@ -59,6 +89,8 @@
const clonedLink = lastLink.clone();
changeIds(clonedLink);
clearVals(clonedLink);
+ disableValidations(clonedLink);
+ addValidationHandlers(clonedLink);
lastLink.after(clonedLink);
}
});
@@ -75,6 +107,12 @@
const max = target.closest('.links').attr('data-max-number-links');
target.text(convertToText(max).toLowerCase());
});
+
+ // Initialize validation on entries with data and add change handlers to toggle validation
+ $('.link').each((i, el) => {
+ enableLinkValidations(el);
+ addValidationHandlers(el);
+ });
});
export { eachLinks as default };
diff --git a/lib/assets/javascripts/utils/validation.js b/lib/assets/javascripts/utils/validation.js
new file mode 100644
index 0000000..c21b476
--- /dev/null
+++ b/lib/assets/javascripts/utils/validation.js
@@ -0,0 +1,217 @@
+import { Tinymce } from './tinymce';
+import { isObject, isString, isBoolean } from './isType';
+import * as constants from '../constants';
+import * as validator from './isValidInputType';
+
+const validatableFields = (ctx) => {
+ if (isObject(ctx)) {
+ return $(ctx).find('[data-validation], [aria-required="true"]');
+ }
+ return [];
+};
+const blockHelp = (id, msg) => {
+ if (isString(id) && isString(msg)) {
+ return `${msg}`;
+ }
+ return '';
+};
+const ariaInvalid = (value) => {
+ if (isBoolean(value)) {
+ return { 'aria-invalid': value };
+ }
+ return { 'aria-invalid': false };
+};
+
+const validationStates = {
+ hasWarning: 'has-warning',
+ hasError: 'has-error',
+ hasSuccess: 'has-success' };
+
+const getValidationTypeForElement = (el) => {
+ const validation = $(el).attr('data-validation');
+ // if the specified validation type is defined
+ if (validation) {
+ return validation;
+
+ // Otherwise if the element is required validate based on its type
+ } else if ($(el).attr('aria-required') === 'true') {
+ if ($(el).is('input')) {
+ return $(el).attr('type'); // available types at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form__types
+ } else if ($(el).is('select')) {
+ return 'select';
+ } else if ($(el).is('.tinymce')) {
+ return 'tinymce';
+ } else if ($(el).is('textarea')) {
+ return 'textarea';
+ }
+ }
+ return false;
+};
+
+const getValue = (type, el) => {
+ switch (type) {
+ case 'radio':
+ return $(el).closest('form').find(`input[name="${$(el).attr('name')}"]:checked`).val();
+ case 'select':
+ return $(el).find(':selected').val();
+ case 'tinymce':
+ return Tinymce.findEditorById($(el).attr('id')).getContent();
+ case 'checkbox':
+ return ($(el).is(':checked') ? 'checked' : '');
+ default:
+ return $(el).val();
+ }
+};
+
+const isValid = (type, value) => {
+ // TODO add more validation for each new type coming along by:
+ // 1. defining a function at dmproadmap.utils.validate
+ // 2. adding the case in the switch below
+
+ // See if a specific data-validation was specified
+ switch (type) {
+ case 'text':
+ return validator.isValidText(value);
+ case 'textarea':
+ return validator.isValidText(value);
+ case 'tinymce':
+ return validator.isValidText(value);
+ case 'number':
+ return validator.isValidNumber(value);
+ case 'email':
+ return validator.isValidEmail(value);
+ case 'url':
+ return validator.isValidUrl(value);
+ case 'password':
+ return validator.isValidPassword(value);
+ case 'radio':
+ return validator.isValidText(value);
+ case 'select':
+ case 'checkbox':
+ return validator.isValidText(value);
+ case 'js-combobox':
+ return validator.isValidText(value);
+ default:
+ return false;
+ }
+};
+
+const getDefaultValidationMessage = (type) => {
+ switch (type) {
+ case 'text':
+ return constants.VALIDATION_MESSAGE_TEXT;
+ case 'textarea':
+ return constants.VALIDATION_MESSAGE_TEXT;
+ case 'number':
+ return constants.VALIDATION_MESSAGE_NUMBER;
+ case 'email':
+ return constants.VALIDATION_MESSAGE_EMAIL;
+ case 'url':
+ return constants.VALIDATION_MESSAGE_URL;
+ case 'password':
+ return constants.VALIDATION_MESSAGE_PASSWORD;
+ case 'radio':
+ return constants.VALIDATION_MESSAGE_RADIO;
+ case 'checkbox':
+ return constants.VALIDATION_MESSAGE_CHECKBOX;
+ 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));
+ $(el).next().hide();
+};
+const invalid = (el) => {
+ $(el).parent().addClass(validationStates.hasError);
+ $(el).attr(ariaInvalid(true));
+ $(el).next().show();
+};
+const addValidationMessage = (el) => {
+ if (isString($(el).attr('id'))) {
+ const id = $(el).attr('id');
+ if (!isString($(el).attr('aria-describedby'))) {
+ $(el).after(blockHelp(`help-${id}`, getValidationMessage(el)));
+ $(el).attr('aria-describedby', `help-${id}`);
+ $(el).attr('data-validatable', 'true');
+ }
+ }
+};
+const removeValidationMessage = (el) => {
+ if (isString($(el).attr('id'))) {
+ $(el).parent().find('.help-block').remove();
+ $(el).removeAttr('aria-describedby');
+ $(el).removeAttr('data-validatable');
+ }
+};
+const isRequired = (ctx) => {
+ if (isObject(ctx)) {
+ return ($(ctx).attr('aria-required') && $(ctx).attr('aria-required') === 'true');
+ }
+ return false;
+};
+const checkValidations = (el) => {
+ const type = getValidationTypeForElement(el);
+ const value = getValue(type, el);
+
+ // If the field is required or it has a value (runs basic validations against the input type)
+ if (isRequired(el) || isString(value)) {
+ if (isValid(type, value)) {
+ valid(el);
+ } else {
+ invalid(el);
+ return false;
+ }
+ }
+ return true;
+};
+
+export const enableValidations = (ctx) => {
+ if (isObject(ctx)) {
+ if ($(ctx).is('input')) {
+ addValidationMessage(ctx);
+ } else {
+ validatableFields(ctx).each((i, el) => {
+ addValidationMessage(el);
+ });
+ }
+ }
+};
+
+export const disableValidations = (ctx) => {
+ if (isObject(ctx)) {
+ if ($(ctx).is('input')) {
+ removeValidationMessage(ctx);
+ } else {
+ validatableFields(ctx).each((i, el) => {
+ removeValidationMessage(el);
+ });
+ }
+ }
+};
+
+export const validate = (ctx) => {
+ let anyInvalid = false;
+ if (isObject(ctx)) {
+ if ($(ctx).is('input')) {
+ anyInvalid = !checkValidations(ctx);
+ } else {
+ validatableFields(ctx).each((i, el) => {
+ if (!checkValidations(el)) {
+ anyInvalid = true;
+ }
+ });
+ }
+ }
+ return !anyInvalid;
+};
diff --git a/lib/assets/javascripts/views/orgs/admin_edit.js b/lib/assets/javascripts/views/orgs/admin_edit.js
index b0645ae..4c6e799 100644
--- a/lib/assets/javascripts/views/orgs/admin_edit.js
+++ b/lib/assets/javascripts/views/orgs/admin_edit.js
@@ -1,29 +1,10 @@
// TODO: we need to be able to swap in the appropriate locale here
import 'number-to-text/converters/en-us';
-import { convertToText } from 'number-to-text/index';
-import ariatiseForm from '../../utils/ariatiseForm';
+import { enableValidations, validate } from '../../utils/validation';
import { Tinymce } from '../../utils/tinymce';
import { eachLinks } from '../../utils/links';
-import { MAX_NUMBER_ORG_URLS } from '../../constants';
$(() => {
- ariatiseForm({ selector: '#edit_org_details_form' });
-
- // We only allow up to 3 URLs
- const toggleAddUrlLink = () => {
- if ($('#org-link-section').find('div.org-link').length >= MAX_NUMBER_ORG_URLS) {
- $('a#add-org-link').hide();
- } else {
- $('a#add-org-link').show();
- }
- };
-
- // Remove a URL
- const removeUrl = (e) => {
- $(e.target).closest('.row').remove();
- toggleAddUrlLink();
- };
-
const toggleFeedback = () => {
if ($('#org_feedback_enabled_true').is(':checked')) {
$('#feeback-email input, #feeback-email textarea').removeAttr('disabled');
@@ -32,42 +13,25 @@
}
};
- // Add a URL
- $('a#add-org-link').click(() => {
- const link = $('#org-link-section').find('div.org-link').last();
- const clone = $(link).clone();
- clone.find('input').val('');
- $(clone).find('.remove-org-link').click((e) => {
- removeUrl(e);
- });
- link.after(clone);
- toggleAddUrlLink();
- });
-
- $('.remove-org-link').click((e) => {
- removeUrl(e);
- });
-
$('#edit_org_feedback_form input[type="radio"]').click(() => {
toggleFeedback();
});
- // Serialize URLs to JSON for form submission
- $('#edit_org_profile_form').submit(() => {
- const links = {};
- eachLinks((ctx, value) => {
- links[ctx] = value;
- }).done(() => {
- $('#org_links').val(JSON.stringify(links));
- });
- });
-
// Initialises tinymce for any target element with class tinymce_answer
Tinymce.init({ selector: '#org_feedback_email_msg' });
-
- // Convert the max number of URLs constant to text and display for user
- $('#max-nbr-urls').text(convertToText(MAX_NUMBER_ORG_URLS).toLowerCase());
-
- toggleAddUrlLink();
toggleFeedback();
+
+ enableValidations($('#edit_org_profile_form'));
+ $('#edit_org_profile_form').on('submit', (e) => {
+ if (!validate(e.target)) {
+ e.preventDefault();
+ } else {
+ const links = {};
+ eachLinks((ctx, value) => {
+ links[ctx] = value;
+ }).done(() => {
+ $('#org_links').val(JSON.stringify(links));
+ });
+ }
+ });
});