2020/harness/focusManager.js (253 lines of code) (raw):

/** * @license * Copyright 2018 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; (function() { var INFINITY = 100000; var CLOSE = 50; var MAX_FUDGE = INFINITY; var DIRECTION_WEIGHT = 0.5; var LEFT = new Pair(-1, 0); var UP = new Pair(0, -1); var RIGHT = new Pair(1, 0); var DOWN = new Pair(0, 1); function Pair(x, y) { this.x = x; this.y = y; this.add = function(operand) { return new Pair(this.x + operand.x, this.y + operand.y); }; this.sub = function(operand) { return new Pair(this.x - operand.x, this.y - operand.y); }; this.dot = function(operand) { return this.x * operand.x + this.y * operand.y; }; this.dotRelative = function(ref, operand) { return this.x * (operand.x - ref.x) + this.y * (operand.y - ref.y); }; this.distTo = function(operand) { return Math.sqrt(this.x * operand.x + this.y * operand.y); }; this.distTo2 = function(operand) { return this.x * operand.x + this.y * operand.y; }; this.cross = function(operand) { return this.x * operand.y - this.y * operand.x; }; } function Rect(left, top, width, height) { this.left = left; this.top = top; this.width = width; this.height = height; this.right = this.left + this.width; this.bottom = this.top + this.height; var rangeDist = function(start, end, startRef, endRef) { if (start < startRef) { if (end < startRef) { return startRef - end; } return 0; } if (start <= endRef) { return 0; } return start - endRef; }; this.valid = function() { return this.width !== 0 && this.height !== 0; }; this.inside = function(x, y) { return x >= this.left && x < this.left + this.width && y >= this.top && y < this.top + this.height; }; this.intersect = function(that) { return this.inside(that.left, that.top) || this.inside(that.right, that.top) || this.inside(that.left, that.bottom) || this.inside(that.right, that.bottom) || that.inside(this.left, this.top) || that.inside(this.right, this.top) || that.inside(this.left, this.bottom) || that.inside(this.right, this.bottom); }; this.intersectComplete = function(that) { var centerXThat = (that.left + that.right) * 0.5; var centerYThat = (that.top + that.bottom) * 0.5; var halfThatWidth = that.width * 0.5; var halfThatHeight = that.height * 0.5; var expandedRect = new Rect( this.left - halfThatWidth, this.top - halfThatHeight, this.width + that.width, this.height + that.height); return expandedRect.inside(centerXThat, centerYThat); }; this.distanceSquared = function(ref, dir) { var x, y; if (dir.x === -1) { x = Math.max((ref.left - this.right) * DIRECTION_WEIGHT, 0); y = rangeDist(this.top, this.bottom, ref.top, ref.bottom); } else if (dir.x === 1) { x = Math.max((this.left - ref.right) * DIRECTION_WEIGHT, 0); y = rangeDist(this.top, this.bottom, ref.top, ref.bottom); } else if (dir.y === -1) { x = rangeDist(this.left, this.right, ref.left, ref.right); y = Math.max((ref.top - this.bottom) * DIRECTION_WEIGHT, 0); } else { x = rangeDist(this.left, this.right, ref.left, ref.right); y = Math.max((this.top - ref.bottom) * DIRECTION_WEIGHT, 0); } return x * x + y * y; }; this.generateSideSliver = function(dir) { var left, right, top, bottom; if (dir === LEFT) { left = this.left - CLOSE; right = this.left - 1; top = (this.top + this.bottom) * 0.5; bottom = top; } else if (dir === RIGHT) { left = this.right + 1; right = this.right + CLOSE; top = (this.top + this.bottom) * 0.5; bottom = top; } else if (dir === UP) { left = (this.left + this.right) * 0.5; right = (this.left + this.right) * 0.5; top = this.top - CLOSE; bottom = this.top - 1; } else { left = (this.left + this.right) * 0.5; right = (this.left + this.right) * 0.5; top = this.bottom + 1; bottom = this.bottom + CLOSE; } return new Rect(left, top, right - left, bottom - top); }; // Generates a rectangle to check if there are any other rectangles strictly // to one side (defined by dir) of 'this'. this.generateSideRect = function(dir, fudge) { if (!fudge) { return this.generateSideSliver(dir); } var left, right, top, bottom; if (dir === LEFT) { left = -INFINITY; right = this.left - 1; top = this.top - fudge; bottom = this.bottom + fudge; } else if (dir === RIGHT) { left = this.right + 1; right = INFINITY; top = this.top - fudge; bottom = this.bottom + fudge; } else if (dir === UP) { left = this.left - fudge; right = this.right + fudge; top = -INFINITY; bottom = this.top - 1; } else { left = this.left - fudge; right = this.right + fudge; top = this.bottom + 1; bottom = INFINITY; } return new Rect(left, top, right - left, bottom - top); }; this.toSideOf = function(target, dir) { var testX = [0, this.right - this.center.x, this.left - this.center.x]; var testY = [0, this.bottom - this.center.y, this.top - this.center.y]; var testLineSegRel0 = new Pair( testX[dir.x - (dir.x != 0)], testY[dir.y - (dir.y != 0)]); var testLineSegRel1 = new Pair( testY[dir.x + (dir.x != 0)], testY[dir.y + (dir.y != 0)]); return dir.cross(testLineSegRel0) * dir.cross(testLineSegRel1) <= 0 && this.intersect(target); }; this.toString = function() { return '(' + this.left + ', ' + this.top + ', ' + this.right + ', ' + this.bottom + ')'; }; }; function createRect(element) { if (!!element.rect) { return element.rect; } var offsetLeft = element.offsetLeft; var offsetTop = element.offsetTop; var e = element.offsetParent; while (e && e !== document.body) { offsetLeft += e.offsetLeft; offsetTop += e.offsetTop; e = e.offsetParent; } element.rect = new Rect(offsetLeft, offsetTop, element.offsetWidth, element.offsetHeight); return element.rect; }; var elements = []; function FocusManager() { var pickElement_ = function(currElem, dir, fudge) { var rect = createRect(currElem); var rectSide = rect.generateSideRect(dir, fudge); var bestDistanceSquared = INFINITY * INFINITY; var bestElement = null; for (var i = 0; i < elements.length; ++i) { if (elements[i] !== currElem) { var r = createRect(elements[i]); if (r.valid() && r.intersectComplete(rectSide)) { var distanceSquared = r.distanceSquared(rect, dir); if (!bestElement || distanceSquared < bestDistanceSquared) { bestElement = elements[i]; bestDistanceSquared = distanceSquared; } } } } return bestElement; }; var pickElement = function(currElem, dir) { return pickElement_(currElem, dir) || pickElement_(currElem, dir, 2) || pickElement_(currElem, dir, MAX_FUDGE); }; var onkeydown = function(e) { if (elements.indexOf(e.target) !== -1) { var dir; var key = translateKeycode(e); switch (key) { case 'Left': dir = LEFT; break; case 'Right': dir = RIGHT; break; case 'Up': dir = UP; break; case 'Down': dir = DOWN; break; case 'Enter': navigate(e); default: return true; } var element = pickElement(e.target, dir); if (element) { element.focus(); e.stopPropagation(); e.preventDefault(); } // Animated WebP page scrolling if (document.getElementById('webp-animated-container')) { var target = document.getElementById('webp-animated-container'); var vpad = 0; var hpad = 0; switch(dir) { case LEFT: hpad = 0; vpad = parseInt(target.style.top) / 2; break; case UP: vpad = parseInt(target.style.top) / 2; break; case RIGHT: hpad += 200; vpad = parseInt(target.style.top) / 2; break; case DOWN: vpad = parseInt(target.style.top); break; default: break; } target.style.top = '0px'; target.style.left = '0px'; var rect = document.activeElement.getBoundingClientRect(); target.style.top = -(rect.top - vpad)/2 +'px'; target.style.left = -(rect.left + hpad)/2 +'px'; } } return true; }; this.add = function(element) { if (elements.indexOf(element) === -1) { elements.push(element); element.addEventListener('keydown', onkeydown); createRect(element); } }; }; window.navigate = function(e) { var activeElement = document.activeElement; if (!!activeElement.exec) { activeElement.exec(e); return; } var elementUrl = activeElement.getAttribute('data-href'); if (elementUrl != null) { location.href = elementUrl; return; } } window.addEventListener('load', function() { var focusManager = new FocusManager; var elements = document.getElementsByClassName('focusable'); elements[0].focus(); for (var i = 0; i < elements.length; ++i) focusManager.add(elements[i]); }); })();