Skip to content

Instantly share code, notes, and snippets.

@andyearnshaw
Last active December 16, 2015 20:39
Show Gist options
  • Save andyearnshaw/5494369 to your computer and use it in GitHub Desktop.
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?
/**
* 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