Last active
December 16, 2015 20:39
-
-
Save andyearnshaw/5494369 to your computer and use it in GitHub Desktop.
This snippet allows you to find a focusable element in a given direction from the currently active one. I wrote this because I'm currently working on an application for LG Smart TVs and, when you have only a TV remote, how else do you select focusable elements in a web page?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* focusClosestElement(): finds next closest focusable element in a given direction | |
* @param dir: 0 = left, 1 = up, 2 = right, 3 = down | |
*/ | |
function focusClosestElement(dir) { | |
var | |
// The increment for our radial distance | |
space = 25, | |
// Sometimes the topmost element may not be focusable, but traversing all the way up to | |
// <body> would be wasteful. Set this to -1 for full recursion, or any positive integer | |
// for max parents. | |
depth = 3, | |
// Current active element for starting and comparison | |
cActive = document.activeElement, | |
cRect = cActive.getBoundingClientRect(), | |
// Need to start from the furthest edge of the focused element for the direction we're travelling | |
startx = dir === 2 ? cRect.right | |
: cRect.left + (dir % 2 ? (cRect.right - cRect.left) / 2 : 0), | |
starty = dir === 3 ? cRect.bottom | |
: cRect.top + (dir % 2 ? 0 : (cRect.bottom - cRect.top) / 2), | |
// Points increment every time we step out further | |
points = 3, | |
// Maximum angle distance to search | |
slice = dir % 2 && cActive != document.body ? 180 : 45, | |
// We need to stop looping when none of the points are on screen | |
onScreen, | |
// Current distance (* space) that we're checking for elements | |
dist = 0, | |
// Prevent focusing elements whose edges on the same axis overlap with the current | |
noOverlap = dir % 2 ? ["top", "bottom"] : ["left", "right"], | |
// Starting angle, left = 180, right = 0 | |
angle = (dir * 90 + 180) % 360, | |
// Current angle | |
cAngle, | |
// Degrees to radians conversion | |
d2r = Math.PI / 180, | |
// Window dimensions so we know when we're outside the boundaries | |
wW = window.innerWidth, | |
wH = window.innerHeight; | |
// If document.body is the current element, we need to search inwards instead of outwards | |
if (cActive == document.body || cRect.right < 0 || cRect.left > wW | |
|| cRect.bottom < 0 || cRect.top > wH) { | |
cActive = document.body; | |
startx = dir % 2 ? wW / 2 : (dir === 0) * wW; | |
starty = dir % 2 ? (dir === 1) * wH : wH / 2; | |
} | |
// Focusable-from-point: gets it, focuses it, confirms it | |
function ffp(x, y) { | |
var rect, | |
d = depth, | |
el = document.elementFromPoint(x, y); | |
do { | |
rect = el && el.getBoundingClientRect(); | |
// Get out if there's nothing here | |
if (!el || el == document.body) | |
return false; | |
else if ( | |
el.tabIndex == -1 // Prevent focusing elements excluded from tabbing order | |
|| ( | |
// Ignore elements whose bounds overlap on the same axis as the current element | |
cActive !== document.body | |
&& ( | |
(dir > 1 && rect[noOverlap[0]] < cRect[noOverlap[1]]) | |
|| (dir < 2 && rect[noOverlap[1]] > cRect[noOverlap[0]]) | |
) | |
) | |
) | |
continue; | |
// Attempt to focus the element | |
else if (el !== document.body && el != cActive && (el.focus(), document.activeElement == el)) | |
return true; | |
} | |
while ((el = el.parentNode) && --d); | |
} | |
// We halve slice because we use as the maximum angle from the furthest point in each direction | |
slice /= 2; | |
var x, y, rad, el, pDist, cPoints; | |
outer: do { | |
onScreen = false; | |
// Work out the radius and then get the point on the circumference | |
rad = space * ++dist; | |
x = startx + (rad * Math.cos(angle * d2r)); | |
y = starty + (rad * Math.sin(angle * d2r)); | |
// Work out the distance between points | |
pDist = slice / (dist * points); | |
// Make sure our x/y is on screen | |
if (!(x < 0 || y < 0 || x > wW || y > wH)) { | |
onScreen = true; | |
if (ffp(x, y)) | |
break; | |
} | |
cAngle = pDist; | |
// Now check our points along the circumference | |
inner: while (cAngle <= slice) { | |
// Check the anti-clockwise point first | |
x = startx + (rad * Math.cos((angle - cAngle) * d2r)); | |
y = starty + (rad * Math.sin((angle - cAngle) * d2r)); | |
if (!(x < 0 || y < 0 || x > wW || y > wH)) { | |
onScreen = true; | |
if (ffp(x, y)) | |
break outer; | |
} | |
// Now the other side | |
x = startx + (rad * Math.cos((angle + cAngle) * d2r)); | |
y = starty + (rad * Math.sin((angle + cAngle) * d2r)); | |
if (!(x < 0 || y < 0 || x > wW || y > wH)) { | |
onScreen = true; | |
if (ffp(x, y)) | |
break outer; | |
} | |
cAngle += pDist; | |
} | |
} | |
while (onScreen); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment