mirror of
https://github.com/alyssaxuu/omni.git
synced 2026-03-11 08:54:35 +00:00
476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
(global = global || self, global.focusLock = factory());
|
|
}(this, function () { 'use strict';
|
|
|
|
var toArray = function toArray(a) {
|
|
var ret = Array(a.length);
|
|
for (var i = 0; i < a.length; ++i) {
|
|
ret[i] = a[i];
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
var arrayFind = function arrayFind(array, search) {
|
|
return array.filter(function (a) {
|
|
return a === search;
|
|
})[0];
|
|
};
|
|
|
|
var asArray = function asArray(a) {
|
|
return Array.isArray(a) ? a : [a];
|
|
};
|
|
|
|
var tabSort = function tabSort(a, b) {
|
|
var tabDiff = a.tabIndex - b.tabIndex;
|
|
var indexDiff = a.index - b.index;
|
|
|
|
if (tabDiff) {
|
|
if (!a.tabIndex) return 1;
|
|
if (!b.tabIndex) return -1;
|
|
}
|
|
|
|
return tabDiff || indexDiff;
|
|
};
|
|
|
|
var orderByTabIndex = function orderByTabIndex(nodes, filterNegative, keepGuards) {
|
|
return toArray(nodes).map(function (node, index) {
|
|
return {
|
|
node: node,
|
|
index: index,
|
|
tabIndex: keepGuards && node.tabIndex === -1 ? (node.dataset || {}).focusGuard ? 0 : -1 : node.tabIndex
|
|
};
|
|
}).filter(function (data) {
|
|
return !filterNegative || data.tabIndex >= 0;
|
|
}).sort(tabSort);
|
|
};
|
|
|
|
var tabbables = ['button:enabled:not([readonly])', 'select:enabled:not([readonly])', 'textarea:enabled:not([readonly])', 'input:enabled:not([readonly])', 'a[href]', 'area[href]', 'iframe', 'object', 'embed', '[tabindex]', '[contenteditable]', '[autofocus]'];
|
|
|
|
var FOCUS_GROUP = 'data-focus-lock';
|
|
var FOCUS_DISABLED = 'data-focus-lock-disabled';
|
|
var FOCUS_ALLOW = 'data-no-focus-lock';
|
|
var FOCUS_AUTO = 'data-autofocus-inside';
|
|
|
|
var queryTabbables = tabbables.join(',');
|
|
var queryGuardTabbables = queryTabbables + ', [data-focus-guard]';
|
|
|
|
var getFocusables = function getFocusables(parents, withGuards) {
|
|
return parents.reduce(function (acc, parent) {
|
|
return acc.concat(
|
|
// add all tabbables inside
|
|
toArray(parent.querySelectorAll(withGuards ? queryGuardTabbables : queryTabbables)),
|
|
// add if node is tabble itself
|
|
parent.parentNode ? toArray(parent.parentNode.querySelectorAll(tabbables.join(','))).filter(function (node) {
|
|
return node === parent;
|
|
}) : []);
|
|
}, []);
|
|
};
|
|
|
|
var getParentAutofocusables = function getParentAutofocusables(parent) {
|
|
var parentFocus = parent.querySelectorAll('[' + FOCUS_AUTO + ']');
|
|
return toArray(parentFocus).map(function (node) {
|
|
return getFocusables([node]);
|
|
}).reduce(function (acc, nodes) {
|
|
return acc.concat(nodes);
|
|
}, []);
|
|
};
|
|
|
|
var isElementHidden = function isElementHidden(computedStyle) {
|
|
if (!computedStyle || !computedStyle.getPropertyValue) {
|
|
return false;
|
|
}
|
|
return computedStyle.getPropertyValue('display') === 'none' || computedStyle.getPropertyValue('visibility') === 'hidden';
|
|
};
|
|
|
|
var isVisible = function isVisible(node) {
|
|
return !node || node === document || !isElementHidden(window.getComputedStyle(node, null)) && isVisible(node.parentNode);
|
|
};
|
|
|
|
var notHiddenInput = function notHiddenInput(node) {
|
|
return !((node.tagName === 'INPUT' || node.tagName === 'BUTTON') && (node.type === 'hidden' || node.disabled));
|
|
};
|
|
|
|
var getParents = function getParents(node) {
|
|
var parents = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
|
|
parents.push(node);
|
|
if (node.parentNode) {
|
|
getParents(node.parentNode, parents);
|
|
}
|
|
return parents;
|
|
};
|
|
|
|
var getCommonParent = function getCommonParent(nodea, nodeb) {
|
|
var parentsA = getParents(nodea);
|
|
var parentsB = getParents(nodeb);
|
|
|
|
for (var i = 0; i < parentsA.length; i += 1) {
|
|
var currentParent = parentsA[i];
|
|
if (parentsB.indexOf(currentParent) >= 0) {
|
|
return currentParent;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
var filterFocusable = function filterFocusable(nodes) {
|
|
return toArray(nodes).filter(function (node) {
|
|
return isVisible(node);
|
|
}).filter(function (node) {
|
|
return notHiddenInput(node);
|
|
});
|
|
};
|
|
|
|
var getTabbableNodes = function getTabbableNodes(topNodes, withGuards) {
|
|
return orderByTabIndex(filterFocusable(getFocusables(topNodes, withGuards)), true, withGuards);
|
|
};
|
|
|
|
var getAllTabbableNodes = function getAllTabbableNodes(topNodes) {
|
|
return orderByTabIndex(filterFocusable(getFocusables(topNodes)), false);
|
|
};
|
|
|
|
var parentAutofocusables = function parentAutofocusables(topNode) {
|
|
return filterFocusable(getParentAutofocusables(topNode));
|
|
};
|
|
|
|
var isRadio = function isRadio(node) {
|
|
return node.tagName === 'INPUT' && node.type === 'radio';
|
|
};
|
|
|
|
var findSelectedRadio = function findSelectedRadio(node, nodes) {
|
|
return nodes.filter(isRadio).filter(function (el) {
|
|
return el.name === node.name;
|
|
}).filter(function (el) {
|
|
return el.checked;
|
|
})[0] || node;
|
|
};
|
|
|
|
var pickFirstFocus = function pickFirstFocus(nodes) {
|
|
if (nodes[0] && nodes.length > 1) {
|
|
if (isRadio(nodes[0]) && nodes[0].name) {
|
|
return findSelectedRadio(nodes[0], nodes);
|
|
}
|
|
}
|
|
return nodes[0];
|
|
};
|
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
|
|
|
var filterNested = function filterNested(nodes) {
|
|
var l = nodes.length;
|
|
for (var i = 0; i < l; i += 1) {
|
|
var _loop = function _loop(j) {
|
|
if (i !== j) {
|
|
if (nodes[i].contains(nodes[j])) {
|
|
return {
|
|
v: filterNested(nodes.filter(function (x) {
|
|
return x !== nodes[j];
|
|
}))
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
for (var j = 0; j < l; j += 1) {
|
|
var _ret = _loop(j);
|
|
|
|
if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
|
|
}
|
|
}
|
|
return nodes;
|
|
};
|
|
|
|
var getTopParent = function getTopParent(node) {
|
|
return node.parentNode ? getTopParent(node.parentNode) : node;
|
|
};
|
|
|
|
var getAllAffectedNodes = function getAllAffectedNodes(node) {
|
|
var nodes = asArray(node);
|
|
return nodes.filter(Boolean).reduce(function (acc, currentNode) {
|
|
var group = currentNode.getAttribute(FOCUS_GROUP);
|
|
acc.push.apply(acc, group ? filterNested(toArray(getTopParent(currentNode).querySelectorAll('[' + FOCUS_GROUP + '="' + group + '"]:not([' + FOCUS_DISABLED + '="disabled"])'))) : [currentNode]);
|
|
return acc;
|
|
}, []);
|
|
};
|
|
|
|
var findAutoFocused = function findAutoFocused(autoFocusables) {
|
|
return function (node) {
|
|
return !!node.autofocus || node.dataset && !!node.dataset.autofocus || autoFocusables.indexOf(node) >= 0;
|
|
};
|
|
};
|
|
|
|
var newFocus = function newFocus(innerNodes, outerNodes, activeElement, lastNode, autoFocused) {
|
|
var cnt = innerNodes.length;
|
|
var firstFocus = innerNodes[0];
|
|
var lastFocus = innerNodes[cnt - 1];
|
|
|
|
// focus is inside
|
|
if (innerNodes.indexOf(activeElement) >= 0) {
|
|
return undefined;
|
|
}
|
|
|
|
var activeIndex = outerNodes.indexOf(activeElement);
|
|
var lastIndex = outerNodes.indexOf(lastNode || activeIndex);
|
|
var lastNodeInside = innerNodes.indexOf(lastNode);
|
|
var indexDiff = activeIndex - lastIndex;
|
|
var firstNodeIndex = outerNodes.indexOf(firstFocus);
|
|
var lastNodeIndex = outerNodes.indexOf(lastFocus);
|
|
|
|
// new focus
|
|
if (activeIndex === -1 || lastNodeInside === -1) {
|
|
return innerNodes.indexOf(autoFocused.length ? pickFirstFocus(autoFocused) : pickFirstFocus(innerNodes));
|
|
}
|
|
// old focus
|
|
if (!indexDiff && lastNodeInside >= 0) {
|
|
return lastNodeInside;
|
|
}
|
|
// jump out
|
|
if (indexDiff && Math.abs(indexDiff) > 1) {
|
|
return lastNodeInside;
|
|
}
|
|
// focus above lock
|
|
if (activeIndex <= firstNodeIndex) {
|
|
return cnt - 1;
|
|
}
|
|
// focus below lock
|
|
if (activeIndex > lastNodeIndex) {
|
|
return 0;
|
|
}
|
|
// index is inside tab order, but outside Lock
|
|
if (indexDiff) {
|
|
if (Math.abs(indexDiff) > 1) {
|
|
return lastNodeInside;
|
|
}
|
|
return (cnt + lastNodeInside + indexDiff) % cnt;
|
|
}
|
|
// do nothing
|
|
return undefined;
|
|
};
|
|
|
|
var getTopCommonParent = function getTopCommonParent(baseActiveElement, leftEntry, rightEntries) {
|
|
var activeElements = asArray(baseActiveElement);
|
|
var leftEntries = asArray(leftEntry);
|
|
var activeElement = activeElements[0];
|
|
var topCommon = null;
|
|
leftEntries.filter(Boolean).forEach(function (entry) {
|
|
topCommon = getCommonParent(topCommon || entry, entry) || topCommon;
|
|
rightEntries.filter(Boolean).forEach(function (subEntry) {
|
|
var common = getCommonParent(activeElement, subEntry);
|
|
if (common) {
|
|
if (!topCommon || common.contains(topCommon)) {
|
|
topCommon = common;
|
|
} else {
|
|
topCommon = getCommonParent(common, topCommon);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
return topCommon;
|
|
};
|
|
|
|
var allParentAutofocusables = function allParentAutofocusables(entries) {
|
|
return entries.reduce(function (acc, node) {
|
|
return acc.concat(parentAutofocusables(node));
|
|
}, []);
|
|
};
|
|
|
|
var notAGuard = function notAGuard(node) {
|
|
return !(node.dataset && node.dataset.focusGuard);
|
|
};
|
|
|
|
var reorderNodes = function reorderNodes(srcNodes, dstNodes) {
|
|
return srcNodes.map(function (dnode) {
|
|
return dstNodes.find(function (_ref) {
|
|
var node = _ref.node;
|
|
return dnode === node;
|
|
});
|
|
}).filter(Boolean);
|
|
};
|
|
|
|
var getFocusMerge = function getFocusMerge(topNode, lastNode) {
|
|
var activeElement = document && document.activeElement;
|
|
var entries = getAllAffectedNodes(topNode).filter(notAGuard);
|
|
|
|
var commonParent = getTopCommonParent(activeElement || topNode, topNode, entries);
|
|
|
|
var innerElements = getTabbableNodes(entries).filter(function (_ref5) {
|
|
var node = _ref5.node;
|
|
return notAGuard(node);
|
|
});
|
|
|
|
if (!innerElements[0]) {
|
|
innerElements = getAllTabbableNodes(entries).filter(function (_ref6) {
|
|
var node = _ref6.node;
|
|
return notAGuard(node);
|
|
});
|
|
if (!innerElements[0]) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
var outerNodes = getTabbableNodes([commonParent]).map(function (_ref7) {
|
|
var node = _ref7.node;
|
|
return node;
|
|
});
|
|
var orderedInnerElements = reorderNodes(outerNodes, innerElements);
|
|
var innerNodes = orderedInnerElements.map(function (_ref8) {
|
|
var node = _ref8.node;
|
|
return node;
|
|
});
|
|
|
|
var newId = newFocus(innerNodes, outerNodes, activeElement, lastNode, innerNodes.filter(findAutoFocused(allParentAutofocusables(entries))));
|
|
|
|
if (newId === undefined) {
|
|
return newId;
|
|
}
|
|
return orderedInnerElements[newId];
|
|
};
|
|
|
|
var focusInFrame = function focusInFrame(frame) {
|
|
return frame === document.activeElement;
|
|
};
|
|
|
|
var focusInsideIframe = function focusInsideIframe(topNode) {
|
|
return !!arrayFind(toArray(topNode.querySelectorAll('iframe')), focusInFrame);
|
|
};
|
|
|
|
var focusInside = function focusInside(topNode) {
|
|
var activeElement = document && document.activeElement;
|
|
|
|
if (!activeElement || activeElement.dataset && activeElement.dataset.focusGuard) {
|
|
return false;
|
|
}
|
|
return getAllAffectedNodes(topNode).reduce(function (result, node) {
|
|
return result || node.contains(activeElement) || focusInsideIframe(node);
|
|
}, false);
|
|
};
|
|
|
|
var focusIsHidden = function focusIsHidden() {
|
|
return document && toArray(document.querySelectorAll('[' + FOCUS_ALLOW + ']')).some(function (node) {
|
|
return node.contains(document.activeElement);
|
|
});
|
|
};
|
|
|
|
var focusOn = function focusOn(target) {
|
|
target.focus();
|
|
if (target.contentWindow) {
|
|
target.contentWindow.focus();
|
|
}
|
|
};
|
|
|
|
var guardCount = 0;
|
|
var lockDisabled = false;
|
|
|
|
var setFocus = (function (topNode, lastNode) {
|
|
var focusable = getFocusMerge(topNode, lastNode);
|
|
|
|
if (lockDisabled) {
|
|
return;
|
|
}
|
|
|
|
if (focusable) {
|
|
if (guardCount > 2) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('FocusLock: focus-fighting detected. Only one focus management system could be active. ' + 'See https://github.com/theKashey/focus-lock/#focus-fighting');
|
|
lockDisabled = true;
|
|
setTimeout(function () {
|
|
lockDisabled = false;
|
|
}, 1);
|
|
return;
|
|
}
|
|
guardCount++;
|
|
focusOn(focusable.node);
|
|
guardCount--;
|
|
}
|
|
});
|
|
|
|
var lastActiveTrap = 0;
|
|
var lastActiveFocus = null;
|
|
|
|
var focusOnBody = function focusOnBody() {
|
|
return document && document.activeElement === document.body;
|
|
};
|
|
|
|
var isFreeFocus = function isFreeFocus() {
|
|
return focusOnBody() || focusIsHidden();
|
|
};
|
|
|
|
var activateTrap = function activateTrap() {
|
|
var result = false;
|
|
|
|
if (lastActiveTrap) {
|
|
var observed = lastActiveTrap;
|
|
|
|
if (!isFreeFocus()) {
|
|
if (observed && !focusInside(observed)) {
|
|
result = setFocus(observed, lastActiveFocus);
|
|
}
|
|
|
|
lastActiveFocus = document.activeElement;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
var reducePropsToState = function reducePropsToState(propsList) {
|
|
return propsList.filter(function (node) {
|
|
return node;
|
|
}).slice(-1)[0];
|
|
};
|
|
|
|
var handleStateChangeOnClient = function handleStateChangeOnClient(trap) {
|
|
lastActiveTrap = trap;
|
|
|
|
if (trap) {
|
|
activateTrap();
|
|
}
|
|
};
|
|
|
|
var instances = [];
|
|
|
|
var emitChange = function emitChange(event) {
|
|
if (handleStateChangeOnClient(reducePropsToState(instances))) {
|
|
event && event.preventDefault();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
var attachHandler = function attachHandler() {
|
|
document.addEventListener('focusin', emitChange);
|
|
};
|
|
|
|
var detachHandler = function detachHandler() {
|
|
document.removeEventListener('focusin', emitChange);
|
|
};
|
|
|
|
var focusLock = {
|
|
on: function on(domNode) {
|
|
if (instances.length === 0) {
|
|
attachHandler();
|
|
}
|
|
|
|
if (instances.indexOf(domNode) < 0) {
|
|
instances.push(domNode);
|
|
emitChange();
|
|
}
|
|
},
|
|
off: function off(domNode) {
|
|
instances = instances.filter(function (node) {
|
|
return node !== domNode;
|
|
});
|
|
emitChange();
|
|
|
|
if (instances.length === 0) {
|
|
detachHandler();
|
|
}
|
|
}
|
|
};
|
|
|
|
return focusLock;
|
|
|
|
}));
|