Newer
Older
dmpopidor / lib / assets / javascripts / utils / ariatiseForm.js
@briley briley on 23 May 2018 7 KB Template Versioning
/*
 This module allows you to add the necessary aria tags to your form elements to
 make them accessible and wire up client-side error handling. Usage:

 Required Fields
 ------------------------
 To make a field required just add the `aria-required="true"` attribute to the
 input element. The element's `type` attribute will be used to determine what
 validation method will be used.
   Example: `<input type="email" aria-required="true" />`

 Validation for fields that are not required
 ------------------------
 To add validation to a field that is not required, just add the
 `data-validation="<type>"` to the input element. Validation checks will only
 be run if the field has a value.
   Example: `<input type="email" data-validation="email" />`

 Validation with a custom error message
 ------------------------
 To add a custom error message to a field's validation (default messages can be found
 in the constants.js file) just add a 'data-validation-error' attribute to the field.
   Example: `<input type="checkbox" name="user_accept_terms" val="0" aria-required="true"
                    data-validation-error="You must agree to the terms and conditions." />`

 Validation for radio buttons
 ------------------------
 To add validation to a set of `radio` buttons, use the rules listed above,
 but you should only attach them to the last `<input>` in the set.
   Example: `<input type="radio" name="radioInput" val="1" />
             <input type="radio" name="radioInput" val="2" aria-required="true" />`
*/
import { Tinymce } from './tinymce';
import { isObject, isString, isBoolean } from './isType';
import getConstant from '../constants';
import * as validator from './isValidInputType';

const validatableFields = (selector) => {
  if (isString(selector)) {
    return $(selector).find('[data-validation], [aria-required="true"]');
  }
  return [];
};
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_<input>_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 blockHelp = (id, msg) => {
  if (isString(id) && isString(msg)) {
    return `<span id="${id}" class="help-block" style="display:none;">${msg}</span>`;
  }
  return '';
};
const ariaDescribedBy = (value) => {
  if (value) {
    return { 'aria-describedby': value };
  }
  return null;
};
const ariaInvalid = (value) => {
  if (isBoolean(value)) {
    return { 'aria-invalid': value };
  }
  return { 'aria-invalid': false };
};
const addAsterisk = (el) => {
  const asterisk = '<span class="red" title="This field is required.">* </span>';
  if (isObject(el)) {
    switch (getValidationTypeForElement(el)) {
      case 'checkbox': {
        el.after(asterisk);
        break;
      }
      default: {
        const label = el.closest('.form-group').find('label');
        if (isObject(label)) {
          $(label[0]).before(asterisk);
        }
        break;
      }
    }
  }
};
const validationStates = {
  hasWarning: 'has-warning',
  hasError: 'has-error',
  hasSuccess: 'has-success',
};
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 '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 getConstant('VALIDATION_MESSAGE_TEXT');
    case 'textarea':
      return getConstant('VALIDATION_MESSAGE_TEXT');
    case 'number':
      return getConstant('VALIDATION_MESSAGE_NUMBER');
    case 'email':
      return getConstant('VALIDATION_MESSAGE_EMAIL');
    case 'password':
      return getConstant('VALIDATION_MESSAGE_PASSWORD');
    case 'radio':
      return getConstant('VALIDATION_MESSAGE_RADIO');
    case 'checkbox':
      return getConstant('VALIDATION_MESSAGE_CHECKBOX');
    case 'js-combobox':
      return getConstant('VALIDATION_MESSAGE_SELECT');
    default:
      return getConstant('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();
};

export default (options) => {
  if (isObject(options) && options.selector) {
    const validatable = validatableFields(options.selector);

    // Add validation error message sections for each validatable input element
    validatable.each((i, el) => {
      const target = $(el);
      target.attr(ariaDescribedBy(`help${i}`));
      target.after(blockHelp(`help${i}`, getValidationMessage(el)));
      if (target.attr('aria-required') === 'true' && !options.excludeAsterisks) {
        addAsterisk(target);
      }
    });

    // Bind validations to the form's submit button
    $(`${options.selector}`).submit((e) => {
      let anyInvalid = false;
      let firstInvalid;
      validatable.each((i, el) => {
        const type = getValidationTypeForElement(el);
        const required = ($(el).attr('aria-required') && $(el).attr('aria-required') === 'true');

        // If the field is required OR its not empty
        if (required || $(el).val().trim() !== '') {
          if (isValid(type, getValue(type, el))) {
            valid(el);
          } else {
            anyInvalid = true;
            invalid(el);
            if (!isObject(firstInvalid)) {
              firstInvalid = el;
            }
          }
        }
      });
      if (anyInvalid) {
        e.preventDefault();
        // Set focus on the first invalid input
        if (isObject(firstInvalid)) {
          firstInvalid.focus();
        }
      }
    });
  }
};