mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 09:45:23 +02:00
265 lines
8.5 KiB
JavaScript
265 lines
8.5 KiB
JavaScript
/**
|
|
* @file get-gov.js includes custom code for the .gov registrar.
|
|
*
|
|
* Constants and helper functions are at the top.
|
|
* Event handlers are in the middle.
|
|
* Initialization (run-on-load) stuff goes at the bottom.
|
|
*/
|
|
|
|
|
|
var DEFAULT_ERROR = "Please check this field for errors.";
|
|
|
|
var INFORMATIVE = "info";
|
|
var WARNING = "warning";
|
|
var ERROR = "error";
|
|
var SUCCESS = "success";
|
|
|
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
|
// Helper functions.
|
|
|
|
/** Makes an element invisible. */
|
|
function makeHidden(el) {
|
|
el.style.position = "absolute";
|
|
el.style.left = "-100vw";
|
|
// The choice of `visiblity: hidden`
|
|
// over `display: none` is due to
|
|
// UX: the former will allow CSS
|
|
// transitions when the elements appear.
|
|
el.style.visibility = "hidden";
|
|
}
|
|
|
|
/** Makes visible a perviously hidden element. */
|
|
function makeVisible(el) {
|
|
el.style.position = "relative";
|
|
el.style.left = "unset";
|
|
el.style.visibility = "visible";
|
|
}
|
|
|
|
/** Creates and returns a live region element. */
|
|
function createLiveRegion(id) {
|
|
const liveRegion = document.createElement("div");
|
|
liveRegion.setAttribute("role", "region");
|
|
liveRegion.setAttribute("aria-live", "polite");
|
|
liveRegion.setAttribute("id", id + "-live-region");
|
|
liveRegion.classList.add("usa-sr-only");
|
|
document.body.appendChild(liveRegion);
|
|
return liveRegion;
|
|
}
|
|
|
|
/** Announces changes to assistive technology users. */
|
|
function announce(id, text) {
|
|
let liveRegion = document.getElementById(id + "-live-region");
|
|
if (!liveRegion) liveRegion = createLiveRegion(id);
|
|
liveRegion.innerHTML = text;
|
|
}
|
|
|
|
/**
|
|
* Slow down event handlers by limiting how frequently they fire.
|
|
*
|
|
* A wait period must occur with no activity (activity means "this
|
|
* debounce function being called") before the handler is invoked.
|
|
*
|
|
* @param {Function} handler - any JS function
|
|
* @param {number} cooldown - the wait period, in milliseconds
|
|
*/
|
|
function debounce(handler, cooldown=600) {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => handler.apply(context, args), cooldown);
|
|
}
|
|
}
|
|
|
|
/** Asyncronously fetches JSON. No error handling. */
|
|
function fetchJSON(endpoint, callback, url="/api/v1/") {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url + endpoint);
|
|
xhr.send();
|
|
xhr.onload = function() {
|
|
if (xhr.status != 200) return;
|
|
callback(JSON.parse(xhr.response));
|
|
};
|
|
// nothing, don't care
|
|
// xhr.onerror = function() { };
|
|
}
|
|
|
|
/** Modifies CSS and HTML when an input is valid/invalid. */
|
|
function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) {
|
|
if (valid) {
|
|
el.setCustomValidity("");
|
|
el.removeAttribute("aria-invalid");
|
|
el.classList.remove('usa-input--error');
|
|
} else {
|
|
el.classList.remove('usa-input--success');
|
|
el.setAttribute("aria-invalid", "true");
|
|
el.setCustomValidity(msg);
|
|
el.classList.add('usa-input--error');
|
|
}
|
|
}
|
|
|
|
/** Display (or hide) a message beneath an element. */
|
|
function inlineToast(el, id, style, msg) {
|
|
if (!el.id && !id) {
|
|
console.error("Elements must have an `id` to show an inline toast.");
|
|
return;
|
|
}
|
|
let toast = document.getElementById((el.id || id) + "--toast");
|
|
if (style) {
|
|
if (!toast) {
|
|
// create and insert the message div
|
|
toast = document.createElement("div");
|
|
const toastBody = document.createElement("div");
|
|
const p = document.createElement("p");
|
|
toast.setAttribute("id", (el.id || id) + "--toast");
|
|
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
|
|
toastBody.classList.add("usa-alert__body");
|
|
p.classList.add("usa-alert__text");
|
|
p.innerText = msg;
|
|
toastBody.appendChild(p);
|
|
toast.appendChild(toastBody);
|
|
el.parentNode.insertBefore(toast, el.nextSibling);
|
|
} else {
|
|
// update and show the existing message div
|
|
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
|
|
toast.querySelector("div p").innerText = msg;
|
|
makeVisible(toast);
|
|
}
|
|
} else {
|
|
if (toast) makeHidden(toast);
|
|
}
|
|
}
|
|
|
|
function _checkDomainAvailability(el) {
|
|
const callback = (response) => {
|
|
toggleInputValidity(el, (response && response.available), msg=response.message);
|
|
announce(el.id, response.message);
|
|
if (el.validity.valid) {
|
|
el.classList.add('usa-input--success');
|
|
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
|
|
inlineToast(el.parentElement, el.id, SUCCESS, response.message);
|
|
} else {
|
|
inlineToast(el.parentElement, el.id, ERROR, response.message);
|
|
}
|
|
}
|
|
fetchJSON(`available/${el.value}`, callback);
|
|
}
|
|
|
|
/** Call the API to see if the domain is good. */
|
|
const checkDomainAvailability = debounce(_checkDomainAvailability);
|
|
|
|
/** Hides the toast message and clears the aira live region. */
|
|
function clearDomainAvailability(el) {
|
|
el.classList.remove('usa-input--success');
|
|
announce(el.id, "");
|
|
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
|
|
inlineToast(el.parentElement, el.id);
|
|
}
|
|
|
|
/** Runs all the validators associated with this element. */
|
|
function runValidators(el) {
|
|
const attribute = el.getAttribute("validate") || "";
|
|
if (!attribute.length) return;
|
|
const validators = attribute.split(" ");
|
|
let isInvalid = false;
|
|
for (const validator of validators) {
|
|
switch (validator) {
|
|
case "domain":
|
|
checkDomainAvailability(el);
|
|
break;
|
|
}
|
|
}
|
|
toggleInputValidity(el, !isInvalid);
|
|
}
|
|
|
|
/** Clears all the validators associated with this element. */
|
|
function clearValidators(el) {
|
|
const attribute = el.getAttribute("validate") || "";
|
|
if (!attribute.length) return;
|
|
const validators = attribute.split(" ");
|
|
for (const validator of validators) {
|
|
switch (validator) {
|
|
case "domain":
|
|
clearDomainAvailability(el);
|
|
break;
|
|
}
|
|
}
|
|
toggleInputValidity(el, true);
|
|
}
|
|
|
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
|
// Event handlers.
|
|
|
|
/** On input change, handles running any associated validators. */
|
|
function handleInputValidation(e) {
|
|
clearValidators(e.target);
|
|
if (e.target.hasAttribute("auto-validate")) runValidators(e.target);
|
|
}
|
|
|
|
/** On button click, handles running any associated validators. */
|
|
function handleValidationClick(e) {
|
|
const attribute = e.target.getAttribute("validate-for") || "";
|
|
if (!attribute.length) return;
|
|
const input = document.getElementById(attribute);
|
|
runValidators(input);
|
|
}
|
|
|
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
|
// Initialization code.
|
|
|
|
/**
|
|
* An IIFE that will attach validators to inputs.
|
|
*
|
|
* It looks for elements with `validate="<type> <type>"` and adds change handlers.
|
|
*
|
|
* These handlers know about two other attributes:
|
|
* - `validate-for="<id>"` creates a button which will run the validator(s) on <id>
|
|
* - `auto-validate` will run validator(s) when the user stops typing (otherwise,
|
|
* they will only run when a user clicks the button with `validate-for`)
|
|
*/
|
|
(function validatorsInit() {
|
|
"use strict";
|
|
const needsValidation = document.querySelectorAll('[validate]');
|
|
for(const input of needsValidation) {
|
|
input.addEventListener('input', handleInputValidation);
|
|
}
|
|
const activatesValidation = document.querySelectorAll('[validate-for]');
|
|
for(const button of activatesValidation) {
|
|
button.addEventListener('click', handleValidationClick);
|
|
}
|
|
})();
|
|
|
|
|
|
/**
|
|
* An IIFE that attaches a click handler for our dynamic nameservers form
|
|
*
|
|
* Only does something on a single page, but it should be fast enough to run
|
|
* it everywhere.
|
|
*/
|
|
(function prepareForms() {
|
|
let serverForm = document.querySelectorAll(".server-form")
|
|
let container = document.querySelector("#form-container")
|
|
let addButton = document.querySelector("#add-form")
|
|
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
|
|
|
|
let formNum = serverForm.length-1
|
|
addButton.addEventListener('click', addForm)
|
|
|
|
function addForm(e){
|
|
e.preventDefault()
|
|
|
|
let newForm = serverForm[0].cloneNode(true)
|
|
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g')
|
|
let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g')
|
|
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g')
|
|
|
|
formNum++
|
|
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`)
|
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`)
|
|
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`)
|
|
container.insertBefore(newForm, addButton)
|
|
newForm.querySelector("input").value = ""
|
|
|
|
totalForms.setAttribute('value', `${formNum+1}`)
|
|
}
|
|
})();
|