js/focus-ring.js
/* https://github.com/WICG/focus-ring */
// Polyfill for :focus-ring state. Modified for this project to work with
// turbolinks
// Based on focus-ring 2.0.2
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';
/**
* Module export
*
* @param {Element} el
* @return {ClassList}
*/
var index = function (el) {
return new ClassList(el);
};
/**
* Initialize a new ClassList for the given element
*
* @param {Element} el DOM Element
*/
function ClassList(el) {
if (!el || el.nodeType !== 1) {
throw new Error('A DOM Element reference is required');
}
this.el = el;
this.classList = el.classList;
}
/**
* Check token validity
*
* @param token
* @param [method]
*/
function checkToken(token, method) {
method = method || 'a method';
if (typeof token != 'string') {
throw new TypeError(
'Failed to execute \'' + method + '\' on \'ClassList\': ' +
'the token provided (\'' + token + '\') is not a string.'
);
}
if (token === "") {
throw new SyntaxError(
'Failed to execute \'' + method + '\' on \'ClassList\': ' +
'the token provided must not be empty.'
);
}
if (/\s/.test(token)) {
throw new Error(
'Failed to execute \'' + method + '\' on \'ClassList\': ' +
'the token provided (\'' + token + '\') contains HTML space ' +
'characters, which are not valid in tokens.'
);
}
}
/**
* Return an array of the class names on the element.
*
* @return {Array}
*/
ClassList.prototype.toArray = function () {
var str = (this.el.getAttribute('class') || '').replace(/^\s+|\s+$/g, '');
var classes = str.split(/\s+/);
if ('' === classes[0]) { classes.shift(); }
return classes;
};
/**
* Add the given `token` to the class list if it's not already present.
*
* @param {String} token
*/
ClassList.prototype.add = function (token) {
var classes, index, updated;
checkToken(token, 'add');
if (this.classList) {
this.classList.add(token);
}
else {
// fallback
classes = this.toArray();
index = classes.indexOf(token);
if (index === -1) {
classes.push(token);
this.el.setAttribute('class', classes.join(' '));
}
}
return;
};
/**
* Check if the given `token` is in the class list.
*
* @param {String} token
* @return {Boolean}
*/
ClassList.prototype.contains = function (token) {
checkToken(token, 'contains');
return this.classList ?
this.classList.contains(token) :
this.toArray().indexOf(token) > -1;
};
/**
* Remove any class names that match the given `token`, when present.
*
* @param {String|RegExp} token
*/
ClassList.prototype.remove = function (token) {
var arr, classes, i, index, len;
if ('[object RegExp]' == Object.prototype.toString.call(token)) {
arr = this.toArray();
for (i = 0, len = arr.length; i < len; i++) {
if (token.test(arr[i])) {
this.remove(arr[i]);
}
}
}
else {
checkToken(token, 'remove');
if (this.classList) {
this.classList.remove(token);
}
else {
// fallback
classes = this.toArray();
index = classes.indexOf(token);
if (index > -1) {
classes.splice(index, 1);
this.el.setAttribute('class', classes.join(' '));
}
}
}
return;
};
/**
* Toggle the `token` in the class list. Optionally force state via `force`.
*
* Native `classList` is not used as some browsers that support `classList` do
* not support `force`. Avoiding `classList` altogether keeps this function
* simple.
*
* @param {String} token
* @param {Boolean} [force]
* @return {Boolean}
*/
ClassList.prototype.toggle = function (token, force) {
checkToken(token, 'toggle');
var hasToken = this.contains(token);
var method = hasToken ? (force !== true && 'remove') : (force !== false && 'add');
if (method) {
this[method](token);
}
return (typeof force == 'boolean' ? force : !hasToken);
};
/**
* https://github.com/WICG/focus-ring
*/
function init() {
var hadKeyboardEvent = false;
var elWithFocusRing;
var inputTypesWhitelist = {
'text': true,
'search': true,
'url': true,
'tel': true,
'email': true,
'password': true,
'number': true,
'date': true,
'month': true,
'week': true,
'time': true,
'datetime': true,
'datetime-local': true,
};
/**
* Computes whether the given element should automatically trigger the
* `focus-ring` class being added, i.e. whether it should always match
* `:focus-ring` when focused.
* @param {Element} el
* @return {boolean}
*/
function focusTriggersKeyboardModality(el) {
var type = el.type;
var tagName = el.tagName;
if (tagName == 'INPUT' && inputTypesWhitelist[type] && !el.readonly)
return true;
if (tagName == 'TEXTAREA' && !el.readonly)
return true;
if (el.contentEditable == 'true')
return true;
return false;
}
/**
* Add the `focus-ring` class to the given element if it was not added by
* the author.
* @param {Element} el
*/
function addFocusRingClass(el) {
if (index(el).contains('focus-ring'))
return;
index(el).add('focus-ring');
el.setAttribute('data-focus-ring-added', '');
}
/**
* Remove the `focus-ring` class from the given element if it was not
* originally added by the author.
* @param {Element} el
*/
function removeFocusRingClass(el) {
if (!el.hasAttribute('data-focus-ring-added'))
return;
index(el).remove('focus-ring');
el.removeAttribute('data-focus-ring-added');
}
/**
* On `keydown`, set `hadKeyboardEvent`, add `focus-ring` class if the
* key was Tab.
* @param {Event} e
*/
function onKeyDown(e) {
if (e.altKey || e.ctrlKey || e.metaKey)
return;
if (e.keyCode != 9)
return;
hadKeyboardEvent = true;
}
/**
* On `focus`, add the `focus-ring` class to the target if:
* - the target received focus as a result of keyboard navigation
* - the event target is an element that will likely require interaction
* via the keyboard (e.g. a text box)
* @param {Event} e
*/
function onFocus(e) {
if (e.target == document)
return;
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusRingClass(e.target);
hadKeyboardEvent = false;
}
}
/**
* On `blur`, remove the `focus-ring` class from the target.
* @param {Event} e
*/
function onBlur(e) {
if (e.target == document)
return;
removeFocusRingClass(e.target);
}
/**
* When the window regains focus, restore the focus-ring class to the element
* to which it was previously applied.
*/
function onWindowFocus() {
// When removing the activeElement from DOM it's possible IE11 is in state
// document.activeElement === null
if (!document.activeElement)
return;
if (document.activeElement == elWithFocusRing)
addFocusRingClass(elWithFocusRing);
elWithFocusRing = null;
}
/**
* When switching windows, keep track of the focused element if it has a
* focus-ring class.
*/
function onWindowBlur() {
// When removing the activeElement from DOM it's possible IE11 is in state
// document.activeElement === null
if (!document.activeElement)
return;
if (index(document.activeElement).contains('focus-ring')) {
// Keep a reference to the element to which the focus-ring class is applied
// so the focus-ring class can be restored to it if the window regains
// focus after being blurred.
elWithFocusRing = document.activeElement;
}
}
document.addEventListener('keydown', onKeyDown, true);
document.addEventListener('focus', onFocus, true);
document.addEventListener('blur', onBlur, true);
window.addEventListener('focus', onWindowFocus, true);
window.addEventListener('blur', onWindowBlur, true);
index(document.body).add('js-focus-ring');
}
/**
* Subscription when the DOM is ready
* @param {Function} callback
*/
function onDOMReady(callback) {
var loaded;
/**
* Callback wrapper for check loaded state
*/
function load() {
if (!loaded) {
loaded = true;
callback();
}
}
if (document.readyState === 'complete') {
callback();
} else {
loaded = false;
document.addEventListener('turbolinks:load', load, false);
window.addEventListener('load', load, false);
}
}
onDOMReady(init);
})));