jellyfin-web/dashboard-ui/bower_components/emby-webcomponents/focusmanager.js
2016-05-09 23:36:43 -04:00

494 lines
14 KiB
JavaScript

define([], function () {
function autoFocus(view, defaultToFirst) {
var element = view.querySelector('*[autofocus]');
if (element) {
focus(element);
return element;
} else if (defaultToFirst !== false) {
element = getFocusableElements(view)[0];
if (element) {
focus(element);
return element;
}
}
return null;
}
function focus(element) {
var tagName = element.tagName;
if (tagName == 'PAPER-INPUT' || tagName == 'EMBY-DROPDOWN-MENU') {
element = element.querySelector('input') || element;
}
try {
element.focus();
} catch (err) {
console.log('Error in focusManager.autoFocus: ' + err);
}
}
var focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A', 'PAPER-BUTTON', 'PAPER-INPUT', 'PAPER-TEXTAREA', 'PAPER-FAB', 'PAPER-CHECKBOX', 'PAPER-ICON-ITEM', 'PAPER-MENU-ITEM', 'EMBY-DROPDOWN-MENU'];
var focusableContainerTagNames = ['BODY', 'DIALOG'];
var focusableQuery = focusableTagNames.join(',') + ',.focusable';
function isFocusable(elem) {
if (focusableTagNames.indexOf(elem.tagName) != -1) {
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;
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusable(elem) {
if (elem.disabled) {
return false;
}
if (elem.getAttribute('tabindex') == "-1") {
return false;
}
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
if (elem.offsetParent === null) {
return false;
}
return true;
}
function getFocusableElements(parent) {
var elems = (parent || document).querySelectorAll(focusableQuery);
var focusableElements = [];
for (var i = 0, length = elems.length; i < length; i++) {
var elem = elems[i];
if (isCurrentlyFocusable(elem)) {
focusableElements.push(elem);
}
}
return focusableElements;
}
function isFocusContainer(elem, direction) {
if (focusableContainerTagNames.indexOf(elem.tagName) != -1) {
return true;
}
if (elem.classList.contains('focuscontainer')) {
return true;
}
if (direction < 2) {
if (elem.classList.contains('focuscontainer-x')) {
return true;
}
}
else if (direction == 3) {
if (elem.classList.contains('focuscontainer-down')) {
return true;
}
}
return false;
}
function getFocusContainer(elem, direction) {
while (!isFocusContainer(elem, direction)) {
elem = elem.parentNode;
if (!elem) {
return document.body;
}
}
return elem;
}
function getWindowData(win, documentElement) {
return {
pageYOffset: win.pageYOffset,
pageXOffset: win.pageXOffset,
clientTop: documentElement.clientTop,
clientLeft: documentElement.clientLeft
};
}
function getOffset(elem, windowData) {
var box = { top: 0, left: 0 };
// 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();
}
return {
top: box.top + windowData.pageYOffset - windowData.clientTop,
left: box.left + windowData.pageXOffset - windowData.clientLeft
};
}
function getViewportBoundingClientRect(elem, windowData) {
var offset = getOffset(elem, windowData);
var posY = offset.top - windowData.pageXOffset;
var posX = offset.left - windowData.pageYOffset;
var width = elem.offsetWidth;
var height = elem.offsetHeight;
return {
left: posX,
top: posY,
width: width,
height: height,
right: posX + width,
bottom: posY + height
};
}
function nav(activeElement, direction) {
activeElement = activeElement || document.activeElement;
if (activeElement) {
activeElement = focusableParent(activeElement);
}
var container = activeElement ? getFocusContainer(activeElement, direction) : document.body;
if (!activeElement) {
autoFocus(container, true);
return;
}
var focusableContainer = parentWithClass(activeElement, 'focusable');
var doc = activeElement.ownerDocument;
var windowData = getWindowData(doc.defaultView, doc.documentElement);
var rect = getViewportBoundingClientRect(activeElement, windowData);
var focusableElements = [];
var focusable = container.querySelectorAll(focusableQuery);
for (var i = 0, length = focusable.length; i < length; i++) {
var curr = focusable[i];
if (curr == activeElement) {
continue;
}
// Don't refocus into the same container
if (curr == focusableContainer) {
continue;
}
if (!isCurrentlyFocusable(curr)) {
continue;
}
var elementRect = getViewportBoundingClientRect(curr, windowData);
switch (direction) {
case 0:
// left
if (elementRect.left >= rect.left) {
continue;
}
if (elementRect.right == rect.right) {
continue;
}
break;
case 1:
// right
if (elementRect.right <= rect.right) {
continue;
}
if (elementRect.left == rect.left) {
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
var nearestElementFocusableParent = parentWithClass(nearestElement, 'focusable');
if (nearestElementFocusableParent && nearestElementFocusableParent != nearestElement && activeElement) {
if (parentWithClass(activeElement, 'focusable') != nearestElementFocusableParent) {
nearestElement = nearestElementFocusableParent;
}
}
focus(nearestElement);
}
}
function parentWithClass(elem, className) {
while (!elem.classList || !elem.classList.contains(className)) {
elem = elem.parentNode;
if (!elem) {
return null;
}
}
return elem;
}
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);
}
var enableDebugInfo = false;
function getNearestElements(elementInfos, options, direction) {
if (enableDebugInfo) {
removeAll();
}
// 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,
y2 = y + off.height - 1;
var intersectX = intersects(point1x, point2x, x, x2);
var intersectY = intersects(point1y, point2y, y, y2);
var midX = off.left + (off.width / 2);
var midY = off.top + (off.height / 2);
var distX;
var distY;
var distX2;
var distY2;
switch (direction) {
case 0:
// left
distX = distX2 = Math.abs(point1x - Math.min(point1x, x2));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
distY2 = Math.abs(sourceMidY - midY);
break;
case 1:
// right
distX = distX2 = Math.abs(point2x - Math.max(point2x, x));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
distY2 = Math.abs(sourceMidY - midY);
break;
case 2:
// up
distY = distY2 = Math.abs(point1y - Math.min(point1y, y2));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
distX2 = Math.abs(sourceMidX - midX);
break;
case 3:
// down
distY = distY2 = Math.abs(point2y - Math.max(point2y, y));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
distX2 = Math.abs(sourceMidX - midX);
break;
default:
break;
}
if (enableDebugInfo) {
addDebugInfo(elem, distX, distY);
}
var distT = Math.sqrt(distX * distX + distY * distY);
var distT2 = Math.sqrt(distX2 * distX2 + distY2 * distY2);
cache.push({
node: elem,
distX: distX,
distY: distY,
distT: distT,
distT2: distT2
});
}
cache.sort(sortNodesT);
//if (direction >= 2) {
// cache.sort(sortNodesX);
//} else {
// cache.sort(sortNodesY);
//}
return cache;
}
function addDebugInfo(elem, distX, distY) {
var div = elem.querySelector('focusInfo');
if (!div) {
div = document.createElement('div');
div.classList.add('focusInfo');
elem.appendChild(div);
if (getComputedStyle(elem, null).getPropertyValue('position') == 'static') {
elem.style.position = 'relative';
}
div.style.position = 'absolute';
div.style.left = '0';
div.style.top = '0';
div.style.color = 'white';
div.style.backgroundColor = 'red';
div.style.padding = '2px';
}
div.innerHTML = Math.round(distX) + ',' + Math.round(distY);
}
function removeAll() {
var elems = document.querySelectorAll('.focusInfo');
for (var i = 0, length = elems.length; i < length; i++) {
elems[i].parentNode.removeChild(elems[i]);
}
}
function sortNodesX(a, b) {
var result = a.distX - b.distX;
if (result == 0) {
return a.distT - b.distT;
}
return result;
}
function sortNodesT(a, b) {
var result = a.distT - b.distT;
if (result == 0) {
return a.distT2 - b.distT2;
}
return result;
}
function sortNodesY(a, b) {
var result = a.distY - b.distY;
if (result == 0) {
return a.distT - b.distT;
}
return result;
}
function sendText(text) {
var elem = document.activeElement;
elem.value = text;
}
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);
},
sendText: sendText,
isCurrentlyFocusable: isCurrentlyFocusable
};
});