jellyfin-web/dashboard-ui/bower_components/emby-webcomponents/focusmanager.js

448 lines
12 KiB
JavaScript
Raw Normal View History

2016-07-18 11:58:17 -07:00
define(['dom'], function (dom) {
2016-10-05 21:28:10 -07:00
'use strict';
2016-01-29 19:43:11 -07:00
2016-08-11 20:23:12 -07:00
var scopes = [];
function pushScope(elem) {
scopes.push(elem);
}
function popScope(elem) {
if (scopes.length) {
scopes.length -= 1;
}
}
2016-08-04 10:15:34 -07:00
function autoFocus(view, defaultToFirst, findAutoFocusElement) {
2016-01-29 19:43:11 -07:00
2016-08-04 10:15:34 -07:00
var element;
if (findAutoFocusElement !== false) {
element = view.querySelector('*[autofocus]');
if (element) {
focus(element);
return element;
}
}
if (defaultToFirst !== false) {
2016-08-23 10:07:50 -07:00
element = getFocusableElements(view, 1)[0];
2016-01-29 19:43:11 -07:00
if (element) {
focus(element);
2016-04-11 07:05:43 -07:00
return element;
2016-01-29 19:43:11 -07:00
}
}
2016-04-11 07:05:43 -07:00
return null;
2016-01-29 19:43:11 -07:00
}
function focus(element) {
2016-03-23 19:29:49 -07:00
try {
element.focus();
} catch (err) {
console.log('Error in focusManager.autoFocus: ' + err);
}
2016-01-29 19:43:11 -07:00
}
2016-08-07 14:57:46 -07:00
var focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
2016-05-09 20:36:43 -07:00
var focusableContainerTagNames = ['BODY', 'DIALOG'];
2016-08-11 20:23:12 -07:00
var focusableQuery = focusableTagNames.map(function (t) {
2016-10-05 21:28:10 -07:00
if (t === 'INPUT') {
2016-08-11 20:23:12 -07:00
t += ':not([type="range"])';
}
return t + ':not([tabindex="-1"]):not(:disabled)';
}).join(',') + ',.focusable';
2016-01-29 19:43:11 -07:00
function isFocusable(elem) {
2016-10-05 21:28:10 -07:00
if (focusableTagNames.indexOf(elem.tagName) !== -1) {
2016-01-29 19:43:11 -07:00
return true;
}
if (elem.classList && elem.classList.contains('focusable')) {
return true;
}
return false;
}
function focusableParent(elem) {
while (!isFocusable(elem)) {
elem = elem.parentNode;
if (!elem) {
return null;
}
}
return elem;
}
2016-04-10 14:45:06 -07:00
// Determines if a focusable element can be focused at a given point in time
2016-08-11 20:23:12 -07:00
function isCurrentlyFocusableInternal(elem) {
2016-01-29 19:43:11 -07:00
2016-08-11 20:23:12 -07:00
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
if (elem.offsetParent === null) {
2016-01-29 19:43:11 -07:00
return false;
}
2016-08-11 20:23:12 -07:00
return true;
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusable(elem) {
if (elem.disabled) {
2016-01-29 19:43:11 -07:00
return false;
}
2016-10-05 21:28:10 -07:00
if (elem.getAttribute('tabindex') === "-1") {
2016-01-29 19:43:11 -07:00
return false;
}
2016-10-05 21:28:10 -07:00
if (elem.tagName === 'INPUT') {
2016-06-21 21:39:24 -07:00
var type = elem.type;
2016-10-05 21:28:10 -07:00
if (type === 'range') {
2016-06-21 21:39:24 -07:00
return false;
}
}
2016-08-11 20:23:12 -07:00
return isCurrentlyFocusableInternal(elem);
}
function getDefaultScope() {
return scopes[0] || document.body;
2016-01-29 19:43:11 -07:00
}
2016-08-23 10:07:50 -07:00
function getFocusableElements(parent, limit) {
2016-08-11 20:23:12 -07:00
var elems = (parent || getDefaultScope()).querySelectorAll(focusableQuery);
2016-01-29 19:43:11 -07:00
var focusableElements = [];
for (var i = 0, length = elems.length; i < length; i++) {
var elem = elems[i];
2016-08-11 20:23:12 -07:00
if (isCurrentlyFocusableInternal(elem)) {
2016-01-29 19:43:11 -07:00
focusableElements.push(elem);
2016-08-23 10:07:50 -07:00
if (limit && focusableElements.length >= limit) {
break;
}
2016-01-29 19:43:11 -07:00
}
}
return focusableElements;
}
function isFocusContainer(elem, direction) {
2016-10-05 21:28:10 -07:00
if (focusableContainerTagNames.indexOf(elem.tagName) !== -1) {
2016-01-29 19:43:11 -07:00
return true;
}
2016-05-09 20:36:43 -07:00
if (elem.classList.contains('focuscontainer')) {
return true;
}
2016-01-29 19:43:11 -07:00
if (direction < 2) {
if (elem.classList.contains('focuscontainer-x')) {
return true;
}
}
2016-10-05 21:28:10 -07:00
else if (direction === 3) {
2016-01-29 19:43:11 -07:00
if (elem.classList.contains('focuscontainer-down')) {
return true;
}
}
return false;
}
function getFocusContainer(elem, direction) {
while (!isFocusContainer(elem, direction)) {
elem = elem.parentNode;
if (!elem) {
2016-08-11 20:23:12 -07:00
return getDefaultScope();
2016-01-29 19:43:11 -07:00
}
}
return elem;
}
2016-08-28 11:59:14 -07:00
function getOffset(elem) {
2016-01-29 19:43:11 -07:00
2016-08-20 14:58:28 -07:00
var box;
2016-01-29 19:43:11 -07:00
// Support: BlackBerry 5, iOS 3 (original iPhone)
// If we don't have gBCR, just use 0,0 rather than error
if (elem.getBoundingClientRect) {
box = elem.getBoundingClientRect();
2016-08-20 14:58:28 -07:00
} else {
box = {
top: 0,
left: 0,
width: 0,
height: 0
};
2016-01-29 19:43:11 -07:00
}
return {
2016-08-28 11:59:14 -07:00
top: box.top,
left: box.left,
2016-08-20 14:58:28 -07:00
width: box.width,
height: box.height
2016-01-29 19:43:11 -07:00
};
}
2016-08-28 11:59:14 -07:00
function getViewportBoundingClientRect(elem) {
2016-01-29 19:43:11 -07:00
2016-08-28 11:59:14 -07:00
var offset = getOffset(elem);
2016-01-29 19:43:11 -07:00
2016-08-29 00:12:24 -07:00
offset.right = offset.left + offset.width;
offset.bottom = offset.top + offset.height;
2016-01-29 19:43:11 -07:00
2016-08-29 00:12:24 -07:00
return offset;
2016-01-29 19:43:11 -07:00
}
function nav(activeElement, direction) {
activeElement = activeElement || document.activeElement;
if (activeElement) {
activeElement = focusableParent(activeElement);
}
2016-08-11 20:23:12 -07:00
var container = activeElement ? getFocusContainer(activeElement, direction) : getDefaultScope();
2016-01-29 19:43:11 -07:00
if (!activeElement) {
2016-08-04 10:15:34 -07:00
autoFocus(container, true, false);
2016-01-29 19:43:11 -07:00
return;
}
2016-07-18 11:58:17 -07:00
var focusableContainer = dom.parentWithClass(activeElement, 'focusable');
2016-01-29 19:43:11 -07:00
2016-08-28 11:59:14 -07:00
var rect = getViewportBoundingClientRect(activeElement);
2016-01-29 19:43:11 -07:00
var focusableElements = [];
var focusable = container.querySelectorAll(focusableQuery);
for (var i = 0, length = focusable.length; i < length; i++) {
var curr = focusable[i];
2016-10-05 21:28:10 -07:00
if (curr === activeElement) {
2016-01-29 19:43:11 -07:00
continue;
}
// Don't refocus into the same container
2016-10-05 21:28:10 -07:00
if (curr === focusableContainer) {
2016-01-29 19:43:11 -07:00
continue;
}
2016-08-11 20:23:12 -07:00
//if (!isCurrentlyFocusableInternal(curr)) {
// continue;
//}
2016-01-29 19:43:11 -07:00
2016-08-28 11:59:14 -07:00
var elementRect = getViewportBoundingClientRect(curr);
2016-01-29 19:43:11 -07:00
2016-08-22 23:42:15 -07:00
// not currently visible
if (!elementRect.width && !elementRect.height) {
continue;
}
2016-01-29 19:43:11 -07:00
switch (direction) {
case 0:
// left
if (elementRect.left >= rect.left) {
continue;
}
2016-10-05 21:28:10 -07:00
if (elementRect.right === rect.right) {
2016-01-29 19:43:11 -07:00
continue;
}
break;
case 1:
// right
if (elementRect.right <= rect.right) {
continue;
}
2016-10-05 21:28:10 -07:00
if (elementRect.left === rect.left) {
2016-01-29 19:43:11 -07:00
continue;
}
break;
case 2:
// up
if (elementRect.top >= rect.top) {
continue;
}
if (elementRect.bottom >= rect.bottom) {
continue;
}
break;
case 3:
// down
if (elementRect.bottom <= rect.bottom) {
continue;
}
if (elementRect.top <= rect.top) {
continue;
}
break;
default:
break;
}
focusableElements.push({
element: curr,
clientRect: elementRect
});
}
var nearest = getNearestElements(focusableElements, rect, direction);
if (nearest.length) {
var nearestElement = nearest[0].node;
// See if there's a focusable container, and if so, send the focus command to that
2016-07-18 11:58:17 -07:00
var nearestElementFocusableParent = dom.parentWithClass(nearestElement, 'focusable');
2016-10-05 21:28:10 -07:00
if (nearestElementFocusableParent && nearestElementFocusableParent !== nearestElement && activeElement) {
if (dom.parentWithClass(activeElement, 'focusable') !== nearestElementFocusableParent) {
2016-01-29 19:43:11 -07:00
nearestElement = nearestElementFocusableParent;
}
}
focus(nearestElement);
}
}
2016-02-28 14:31:45 -07:00
function intersectsInternal(a1, a2, b1, b2) {
return (b1 >= a1 && b1 <= a2) || (b2 >= a1 && b2 <= a2);
}
function intersects(a1, a2, b1, b2) {
return intersectsInternal(a1, a2, b1, b2) || intersectsInternal(b1, b2, a1, a2);
}
2016-01-29 19:43:11 -07:00
function getNearestElements(elementInfos, options, direction) {
// Get elements and work out x/y points
var cache = [],
point1x = parseFloat(options.left) || 0,
point1y = parseFloat(options.top) || 0,
point2x = parseFloat(point1x + options.width - 1) || point1x,
point2y = parseFloat(point1y + options.height - 1) || point1y,
// Shortcuts to help with compression
min = Math.min,
max = Math.max;
var sourceMidX = options.left + (options.width / 2);
var sourceMidY = options.top + (options.height / 2);
// Loop through all elements and check their positions
for (var i = 0, length = elementInfos.length; i < length; i++) {
var elementInfo = elementInfos[i];
var elem = elementInfo.element;
var off = elementInfo.clientRect,
x = off.left,
y = off.top,
x2 = x + off.width - 1,
2016-02-28 14:31:45 -07:00
y2 = y + off.height - 1;
var intersectX = intersects(point1x, point2x, x, x2);
var intersectY = intersects(point1y, point2y, y, y2);
2016-01-29 19:43:11 -07:00
var midX = off.left + (off.width / 2);
var midY = off.top + (off.height / 2);
var distX;
var distY;
switch (direction) {
case 0:
// left
2016-05-26 12:20:24 -07:00
distX = Math.abs(point1x - Math.min(point1x, x2));
2016-01-29 19:43:11 -07:00
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 1:
// right
2016-05-26 12:20:24 -07:00
distX = Math.abs(point2x - Math.max(point2x, x));
2016-01-29 19:43:11 -07:00
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 2:
// up
2016-05-26 12:20:24 -07:00
distY = Math.abs(point1y - Math.min(point1y, y2));
2016-01-29 19:43:11 -07:00
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
case 3:
// down
2016-05-26 12:20:24 -07:00
distY = Math.abs(point2y - Math.max(point2y, y));
2016-01-29 19:43:11 -07:00
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
default:
break;
}
var distT = Math.sqrt(distX * distX + distY * distY);
cache.push({
node: elem,
distX: distX,
distY: distY,
2016-02-28 14:31:45 -07:00
distT: distT,
2016-05-25 09:10:57 -07:00
index: i
2016-01-29 19:43:11 -07:00
});
}
cache.sort(sortNodesT);
return cache;
}
function sortNodesT(a, b) {
2016-05-25 09:10:57 -07:00
2016-02-28 14:31:45 -07:00
var result = a.distT - b.distT;
2016-10-05 21:28:10 -07:00
if (result !== 0) {
2016-05-25 09:10:57 -07:00
return result;
}
2016-02-28 14:31:45 -07:00
2016-05-25 09:10:57 -07:00
result = a.index - b.index;
2016-10-05 21:28:10 -07:00
if (result !== 0) {
2016-05-25 09:10:57 -07:00
return result;
2016-02-28 14:31:45 -07:00
}
2016-05-25 09:10:57 -07:00
return 0;
2016-01-29 19:43:11 -07:00
}
2016-04-09 12:04:14 -07:00
function sendText(text) {
var elem = document.activeElement;
elem.value = text;
}
2016-01-29 19:43:11 -07:00
return {
autoFocus: autoFocus,
focus: focus,
focusableParent: focusableParent,
getFocusableElements: getFocusableElements,
moveLeft: function (sourceElement) {
nav(sourceElement, 0);
},
moveRight: function (sourceElement) {
nav(sourceElement, 1);
},
moveUp: function (sourceElement) {
nav(sourceElement, 2);
},
moveDown: function (sourceElement) {
nav(sourceElement, 3);
2016-04-09 12:04:14 -07:00
},
2016-04-10 14:45:06 -07:00
sendText: sendText,
2016-08-11 20:23:12 -07:00
isCurrentlyFocusable: isCurrentlyFocusable,
pushScope: pushScope,
popScope: popScope
2016-01-29 19:43:11 -07:00
};
});