UNPKG

@zyrecx/tflix

Version:

TFlix transforms Cineby.app into a TV-friendly experience for Samsung TVs running TizenBrew with enhanced remote navigation.

204 lines (171 loc) 6.56 kB
/* Spatial Navigation Polyfill * * It follows W3C official specification * https://drafts.csswg.org/css-nav-1/ * * Copyright (c) 2018-2019 LG Electronics Inc. * https://github.com/WICG/spatial-navigation/polyfill * * Licensed under the MIT license (MIT) */ (function () { // The polyfill must not be executed, if it's already enabled via browser engine or browser extensions. if ('navigate' in window) { return; } const ARROW_KEY_CODE = {37: 'left', 38: 'up', 39: 'right', 40: 'down'}; const TAB_KEY_CODE = 9; let mapOfBoundRect = null; let startingPoint = null; // Saves spatial navigation starting point let savedSearchOrigin = {element: null, rect: null}; // Saves previous search origin let searchOriginRect = null; // Rect of current search origin // Initiate when the polyfill is loaded init(); /** * Get the normalized direction from the keydown event * @param {Event} e - keydown event * @return {string|undefined} - 'up', 'down', 'left', 'right' or undefined */ function getDirectionFromKey(e) { const key = ARROW_KEY_CODE[e.keyCode]; return key; } /** * Get the closest element in the specified direction * @param {HTMLElement} currentElement - starting element * @param {string} direction - 'up', 'down', 'left', 'right' * @return {HTMLElement|null} - the found element or null if not found */ function findNextFocusableElement(currentElement, direction) { const candidateElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'); const visibleCandidates = Array.from(candidateElements).filter(el => { const style = window.getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden' && !el.disabled; }); if (!visibleCandidates.length) return null; const currentRect = currentElement.getBoundingClientRect(); let closestElement = null; let closestDistance = Infinity; for (const candidate of visibleCandidates) { if (candidate === currentElement) continue; const candidateRect = candidate.getBoundingClientRect(); // Check if the candidate is in the right direction let inDirection = false; switch (direction) { case 'up': inDirection = candidateRect.bottom < currentRect.top; break; case 'down': inDirection = candidateRect.top > currentRect.bottom; break; case 'left': inDirection = candidateRect.right < currentRect.left; break; case 'right': inDirection = candidateRect.left > currentRect.right; break; } if (inDirection) { // Calculate distance based on the centers of the elements const dx = (candidateRect.left + candidateRect.width/2) - (currentRect.left + currentRect.width/2); const dy = (candidateRect.top + candidateRect.height/2) - (currentRect.top + currentRect.height/2); // Distance formula adjusted for direction priority let distance; switch (direction) { case 'up': case 'down': distance = Math.abs(dy) + Math.abs(dx) * 0.5; break; case 'left': case 'right': distance = Math.abs(dx) + Math.abs(dy) * 0.5; break; } if (distance < closestDistance) { closestDistance = distance; closestElement = candidate; } } } return closestElement; } /** * Handle key events for spatial navigation * @param {Event} e - keydown event */ function handleKeydown(e) { // Only handle arrow keys const direction = getDirectionFromKey(e); if (!direction) return; // Prevent default arrow key behavior e.preventDefault(); const currentElement = document.activeElement || document.body; const nextElement = findNextFocusableElement(currentElement, direction); if (nextElement) { // Remove previous focus styling const prevFocused = document.querySelector('.tflix-focused'); if (prevFocused) { prevFocused.classList.remove('tflix-focused'); } // Add focus styling to new element nextElement.classList.add('tflix-focused'); // Ensure the element is visible by scrolling if needed ensureElementIsVisible(nextElement); // Focus the element nextElement.focus(); } } /** * Ensure the element is visible in the viewport * @param {HTMLElement} element - element to make visible */ function ensureElementIsVisible(element) { const rect = element.getBoundingClientRect(); const isInViewport = ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); if (!isInViewport) { element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } } /** * Initialize the spatial navigation polyfill */ function init() { // Setup key event listeners document.addEventListener('keydown', handleKeydown); // Add a global object for spatial navigation window.__spatialNavigation__ = { keyMode: 'ARROW', findNextFocusableElement, ensureElementIsVisible }; // Add navigate function to window window.navigate = function(direction) { const currentElement = document.activeElement || document.body; const nextElement = findNextFocusableElement(currentElement, direction); if (nextElement) { // Remove previous focus styling const prevFocused = document.querySelector('.tflix-focused'); if (prevFocused) { prevFocused.classList.remove('tflix-focused'); } // Add focus styling to new element nextElement.classList.add('tflix-focused'); // Ensure the element is visible by scrolling if needed ensureElementIsVisible(nextElement); // Focus the element nextElement.focus(); return true; } return false; }; } })();