summaryrefslogtreecommitdiff
path: root/javascript/videojs/src/js/spatial-navigation.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/src/js/spatial-navigation.js')
-rw-r--r--javascript/videojs/src/js/spatial-navigation.js644
1 files changed, 0 insertions, 644 deletions
diff --git a/javascript/videojs/src/js/spatial-navigation.js b/javascript/videojs/src/js/spatial-navigation.js
deleted file mode 100644
index 08ca9bc..0000000
--- a/javascript/videojs/src/js/spatial-navigation.js
+++ /dev/null
@@ -1,644 +0,0 @@
-/**
- * @file spatial-navigation.js
- */
-import EventTarget from './event-target';
-import SpatialNavKeyCodes from './utils/spatial-navigation-key-codes';
-
-/** @import Component from './component' */
-/** @import Player from './player' */
-
-// The number of seconds the `step*` functions move the timeline.
-const STEP_SECONDS = 5;
-
-/**
- * Spatial Navigation in Video.js enhances user experience and accessibility on smartTV devices,
- * enabling seamless navigation through interactive elements within the player using remote control arrow keys.
- * This functionality allows users to effortlessly navigate through focusable components.
- *
- * @extends EventTarget
- */
-class SpatialNavigation extends EventTarget {
-
- /**
- * Constructs a SpatialNavigation instance with initial settings.
- * Sets up the player instance, and prepares the spatial navigation system.
- *
- * @class
- * @param {Player} player - The Video.js player instance to which the spatial navigation is attached.
- */
- constructor(player) {
- super();
- this.player_ = player;
- this.focusableComponents = [];
- this.isListening_ = false;
- this.isPaused_ = false;
- this.onKeyDown_ = this.onKeyDown_.bind(this);
- this.lastFocusedComponent_ = null;
- }
-
- /**
- * Starts the spatial navigation by adding a keydown event listener to the video container.
- * This method ensures that the event listener is added only once.
- */
- start() {
- // If the listener is already active, exit early.
- if (this.isListening_) {
- return;
- }
-
- // Add the event listener since the listener is not yet active.
- this.player_.on('keydown', this.onKeyDown_);
- this.player_.on('modalKeydown', this.onKeyDown_);
- // Listen for source change events
- this.player_.on('loadedmetadata', () => {
- this.focus(this.updateFocusableComponents()[0]);
- });
- this.player_.on('modalclose', () => {
- this.refocusComponent();
- });
- this.player_.on('focusin', this.handlePlayerFocus_.bind(this));
- this.player_.on('focusout', this.handlePlayerBlur_.bind(this));
- this.isListening_ = true;
- if (this.player_.errorDisplay) {
- this.player_.errorDisplay.on('aftermodalfill', () => {
- this.updateFocusableComponents();
-
- if (this.focusableComponents.length) {
- // The modal has focusable components:
-
- if (this.focusableComponents.length > 1) {
- // The modal has close button + some additional buttons.
- // Focusing first additional button:
-
- this.focusableComponents[1].focus();
- } else {
- // The modal has only close button,
- // Focusing it:
-
- this.focusableComponents[0].focus();
- }
- }
- });
- }
- }
-
- /**
- * Stops the spatial navigation by removing the keydown event listener from the video container.
- * Also sets the `isListening_` flag to false.
- */
- stop() {
- this.player_.off('keydown', this.onKeyDown_);
- this.isListening_ = false;
- }
-
- /**
- * Responds to keydown events for spatial navigation and media control.
- *
- * Determines if spatial navigation or media control is active and handles key inputs accordingly.
- *
- * @param {KeyboardEvent} event - The keydown event to be handled.
- */
- onKeyDown_(event) {
- // Determine if the event is a custom modalKeydown event
- const actualEvent = event.originalEvent ? event.originalEvent : event;
-
- if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(actualEvent.key)) {
- // Handle directional navigation
- if (this.isPaused_) {
- return;
- }
- actualEvent.preventDefault();
-
- // "ArrowLeft" => "left" etc
- const direction = actualEvent.key.substring(5).toLowerCase();
-
- this.move(direction);
- } else if (SpatialNavKeyCodes.isEventKey(actualEvent, 'play') || SpatialNavKeyCodes.isEventKey(actualEvent, 'pause') ||
- SpatialNavKeyCodes.isEventKey(actualEvent, 'ff') || SpatialNavKeyCodes.isEventKey(actualEvent, 'rw')) {
- // Handle media actions
- actualEvent.preventDefault();
- const action = SpatialNavKeyCodes.getEventName(actualEvent);
-
- this.performMediaAction_(action);
- } else if (SpatialNavKeyCodes.isEventKey(actualEvent, 'Back') &&
- event.target && typeof event.target.closeable === 'function' && event.target.closeable()) {
- actualEvent.preventDefault();
- event.target.close();
- }
- }
-
- /**
- * Performs media control actions based on the given key input.
- *
- * Controls the playback and seeking functionalities of the media player.
- *
- * @param {string} key - The key representing the media action to be performed.
- * Accepted keys: 'play', 'pause', 'ff' (fast-forward), 'rw' (rewind).
- */
- performMediaAction_(key) {
- if (this.player_) {
- switch (key) {
- case 'play':
- if (this.player_.paused()) {
- this.player_.play();
- }
- break;
- case 'pause':
- if (!this.player_.paused()) {
- this.player_.pause();
- }
- break;
- case 'ff':
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
- break;
- case 'rw':
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
- break;
- default:
- break;
- }
- }
- }
-
- /**
- * Prevent liveThreshold from causing seeks to seem like they
- * are not happening from a user perspective.
- *
- * @param {number} ct
- * current time to seek to
- */
- userSeek_(ct) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- this.player_.liveTracker.nextSeekedFromUser();
- }
-
- this.player_.currentTime(ct);
- }
-
- /**
- * Pauses the spatial navigation functionality.
- * This method sets a flag that can be used to temporarily disable the navigation logic.
- */
- pause() {
- this.isPaused_ = true;
- }
-
- /**
- * Resumes the spatial navigation functionality if it has been paused.
- * This method resets the pause flag, re-enabling the navigation logic.
- */
- resume() {
- this.isPaused_ = false;
- }
-
- /**
- * Handles Player Blur.
- *
- * @param {string|Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- *
- * Calls for handling of the Player Blur if:
- * *The next focused element is not a child of current focused element &
- * The next focused element is not a child of the Player.
- * *There is no next focused element
- */
- handlePlayerBlur_(event) {
- const nextFocusedElement = event.relatedTarget;
- let isChildrenOfPlayer = null;
- const currentComponent = this.getCurrentComponent(event.target);
-
- if (nextFocusedElement) {
- isChildrenOfPlayer = Boolean(nextFocusedElement.closest('.video-js'));
-
- // If nextFocusedElement is the 'TextTrackSettings' component
- if (nextFocusedElement.classList.contains('vjs-text-track-settings') && !this.isPaused_) {
- this.searchForTrackSelect_();
- }
- }
-
- if (!(event.currentTarget.contains(event.relatedTarget)) && !isChildrenOfPlayer || !nextFocusedElement) {
- if (currentComponent && currentComponent.name() === 'CloseButton') {
- this.refocusComponent();
- } else {
- this.pause();
-
- if (currentComponent && currentComponent.el()) {
- // Store last focused component
- this.lastFocusedComponent_ = currentComponent;
- }
- }
- }
- }
-
- /**
- * Handles the Player focus event.
- *
- * Calls for handling of the Player Focus if current element is focusable.
- */
- handlePlayerFocus_() {
- if (this.getCurrentComponent() && this.getCurrentComponent().getIsFocusable()) {
- this.resume();
- }
- }
-
- /**
- * Gets a set of focusable components.
- *
- * @return {Array}
- * Returns an array of focusable components.
- */
- updateFocusableComponents() {
- const player = this.player_;
- const focusableComponents = [];
-
- /**
- * Searches for children candidates.
- *
- * Pushes Components to array of 'focusableComponents'.
- * Calls itself if there is children elements inside iterated component.
- *
- * @param {Array} componentsArray - The array of components to search for focusable children.
- */
- function searchForChildrenCandidates(componentsArray) {
- for (const i of componentsArray) {
- if (i.hasOwnProperty('el_') && i.getIsFocusable() && i.getIsAvailableToBeFocused(i.el())) {
- focusableComponents.push(i);
- }
- if (i.hasOwnProperty('children_') && i.children_.length > 0) {
- searchForChildrenCandidates(i.children_);
- }
- }
- }
-
- // Iterate inside all children components of the player.
- player.children_.forEach((value) => {
- if (value.hasOwnProperty('el_')) {
- // If component has required functions 'getIsFocusable' & 'getIsAvailableToBeFocused', is focusable & available to be focused.
- if (value.getIsFocusable && value.getIsAvailableToBeFocused && value.getIsFocusable() && value.getIsAvailableToBeFocused(value.el())) {
- focusableComponents.push(value);
- return;
- // If component has posible children components as candidates.
- } else if (value.hasOwnProperty('children_') && value.children_.length > 0) {
- searchForChildrenCandidates(value.children_);
- // If component has posible item components as candidates.
- } else if (value.hasOwnProperty('items') && value.items.length > 0) {
- searchForChildrenCandidates(value.items);
- // If there is a suitable child element within the component's DOM element.
- } else if (this.findSuitableDOMChild(value)) {
- focusableComponents.push(value);
- }
- }
-
- // TODO - Refactor the following logic after refactor of videojs-errors elements to be components is done.
- if (value.name_ === 'ErrorDisplay' && value.opened_) {
- const buttonContainer = value.el_.querySelector('.vjs-errors-ok-button-container');
-
- if (buttonContainer) {
- const modalButtons = buttonContainer.querySelectorAll('button');
-
- modalButtons.forEach((element, index) => {
- // Add elements as objects to be handled by the spatial navigation
- focusableComponents.push({
- name: () => {
- return 'ModalButton' + (index + 1);
- },
- el: () => element,
- getPositions: () => {
- const rect = element.getBoundingClientRect();
-
- // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
- const boundingClientRect = {
- x: rect.x,
- y: rect.y,
- width: rect.width,
- height: rect.height,
- top: rect.top,
- right: rect.right,
- bottom: rect.bottom,
- left: rect.left
- };
-
- // Calculating the center position
- const center = {
- x: rect.left + rect.width / 2,
- y: rect.top + rect.height / 2,
- width: 0,
- height: 0,
- top: rect.top + rect.height / 2,
- right: rect.left + rect.width / 2,
- bottom: rect.top + rect.height / 2,
- left: rect.left + rect.width / 2
- };
-
- return {
- boundingClientRect,
- center
- };
- },
- // Asume that the following are always focusable
- getIsAvailableToBeFocused: () => true,
- getIsFocusable: (el) => true,
- focus: () => element.focus()
- });
- });
- }
- }
- });
-
- this.focusableComponents = focusableComponents;
-
- return this.focusableComponents;
- }
-
- /**
- * Finds a suitable child element within the provided component's DOM element.
- *
- * @param {Object} component - The component containing the DOM element to search within.
- * @return {HTMLElement|null} Returns the suitable child element if found, or null if not found.
- */
- findSuitableDOMChild(component) {
- /**
- * Recursively searches for a suitable child node that can be focused within a given component.
- * It first checks if the provided node itself can be focused according to the component's
- * `getIsFocusable` and `getIsAvailableToBeFocused` methods. If not, it recursively searches
- * through the node's children to find a suitable child node that meets the focusability criteria.
- *
- * @param {HTMLElement} node - The DOM node to start the search from.
- * @return {HTMLElement|null} The first child node that is focusable and available to be focused,
- * or `null` if no suitable child is found.
- */
- function searchForSuitableChild(node) {
- if (component.getIsFocusable(node) && component.getIsAvailableToBeFocused(node)) {
- return node;
- }
-
- for (let i = 0; i < node.children.length; i++) {
- const child = node.children[i];
- const suitableChild = searchForSuitableChild(child);
-
- if (suitableChild) {
- return suitableChild;
- }
- }
-
- return null;
- }
-
- if (component.el()) {
- return searchForSuitableChild(component.el());
- }
- return null;
-
- }
-
- /**
- * Gets the currently focused component from the list of focusable components.
- * If a target element is provided, it uses that element to find the corresponding
- * component. If no target is provided, it defaults to using the document's currently
- * active element.
- *
- * @param {HTMLElement} [target] - The DOM element to check against the focusable components.
- * If not provided, `document.activeElement` is used.
- * @return {Component|null} - Returns the focused component if found among the focusable components,
- * otherwise returns null if no matching component is found.
- */
- getCurrentComponent(target) {
- this.updateFocusableComponents();
- // eslint-disable-next-line
- const curComp = target || document.activeElement;
- if (this.focusableComponents.length) {
- for (const i of this.focusableComponents) {
- // If component Node is equal to the current active element.
- if (i.el() === curComp) {
- return i;
- }
- }
- }
- }
-
- /**
- * Adds a component to the array of focusable components.
- *
- * @param {Component} component
- * The `Component` to be added.
- */
- add(component) {
- const focusableComponents = [...this.focusableComponents];
-
- if (component.hasOwnProperty('el_') && component.getIsFocusable() && component.getIsAvailableToBeFocused(component.el())) {
- focusableComponents.push(component);
- }
-
- this.focusableComponents = focusableComponents;
- // Trigger the notification manually
- this.trigger({type: 'focusableComponentsChanged', focusableComponents: this.focusableComponents});
- }
-
- /**
- * Removes component from the array of focusable components.
- *
- * @param {Component} component - The component to be removed from the focusable components array.
- */
- remove(component) {
- for (let i = 0; i < this.focusableComponents.length; i++) {
- if (this.focusableComponents[i].name() === component.name()) {
- this.focusableComponents.splice(i, 1);
- // Trigger the notification manually
- this.trigger({type: 'focusableComponentsChanged', focusableComponents: this.focusableComponents});
- return;
- }
- }
- }
-
- /**
- * Clears array of focusable components.
- */
- clear() {
- // Check if the array is already empty to avoid unnecessary event triggering
- if (this.focusableComponents.length > 0) {
- // Clear the array
- this.focusableComponents = [];
-
- // Trigger the notification manually
- this.trigger({type: 'focusableComponentsChanged', focusableComponents: this.focusableComponents});
- }
- }
-
- /**
- * Navigates to the next focusable component based on the specified direction.
- *
- * @param {string} direction 'up', 'down', 'left', 'right'
- */
- move(direction) {
- const currentFocusedComponent = this.getCurrentComponent();
-
- if (!currentFocusedComponent) {
- return;
- }
-
- const currentPositions = currentFocusedComponent.getPositions();
- const candidates = this.focusableComponents.filter(component =>
- component !== currentFocusedComponent &&
- this.isInDirection_(currentPositions.boundingClientRect, component.getPositions().boundingClientRect, direction));
-
- const bestCandidate = this.findBestCandidate_(currentPositions.center, candidates, direction);
-
- if (bestCandidate) {
- this.focus(bestCandidate);
- } else {
- this.trigger({type: 'endOfFocusableComponents', direction, focusedComponent: currentFocusedComponent});
- }
- }
-
- /**
- * Finds the best candidate on the current center position,
- * the list of candidates, and the specified navigation direction.
- *
- * @param {Object} currentCenter The center position of the current focused component element.
- * @param {Array} candidates An array of candidate components to receive focus.
- * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right').
- * @return {Object|null} The component that is the best candidate for receiving focus.
- */
- findBestCandidate_(currentCenter, candidates, direction) {
- let minDistance = Infinity;
- let bestCandidate = null;
-
- for (const candidate of candidates) {
- const candidateCenter = candidate.getPositions().center;
- const distance = this.calculateDistance_(currentCenter, candidateCenter, direction);
-
- if (distance < minDistance) {
- minDistance = distance;
- bestCandidate = candidate;
- }
- }
-
- return bestCandidate;
- }
-
- /**
- * Determines if a target rectangle is in the specified navigation direction
- * relative to a source rectangle.
- *
- * @param {Object} srcRect The bounding rectangle of the source element.
- * @param {Object} targetRect The bounding rectangle of the target element.
- * @param {string} direction The navigation direction ('up', 'down', 'left', 'right').
- * @return {boolean} True if the target is in the specified direction relative to the source.
- */
- isInDirection_(srcRect, targetRect, direction) {
- switch (direction) {
- case 'right':
- return targetRect.left >= srcRect.right;
- case 'left':
- return targetRect.right <= srcRect.left;
- case 'down':
- return targetRect.top >= srcRect.bottom;
- case 'up':
- return targetRect.bottom <= srcRect.top;
- default:
- return false;
- }
- }
-
- /**
- * Focus the last focused component saved before blur on player.
- */
- refocusComponent() {
- if (this.lastFocusedComponent_) {
- // If user is not active, set it to active.
- if (!this.player_.userActive()) {
- this.player_.userActive(true);
- }
-
- this.updateFocusableComponents();
-
- // Search inside array of 'focusableComponents' for a match of name of
- // the last focused component.
- for (let i = 0; i < this.focusableComponents.length; i++) {
- if (this.focusableComponents[i].name() === this.lastFocusedComponent_.name()) {
- this.focus(this.focusableComponents[i]);
- return;
- }
- }
- } else {
- this.focus(this.updateFocusableComponents()[0]);
- }
- }
-
- /**
- * Focuses on a given component.
- * If the component is available to be focused, it focuses on the component.
- * If not, it attempts to find a suitable DOM child within the component and focuses on it.
- *
- * @param {Component} component - The component to be focused.
- */
- focus(component) {
- if (typeof component !== 'object') {
- return;
- }
-
- if (component.getIsAvailableToBeFocused(component.el())) {
- component.focus();
- } else if (this.findSuitableDOMChild(component)) {
- this.findSuitableDOMChild(component).focus();
- }
- }
-
- /**
- * Calculates the distance between two points, adjusting the calculation based on
- * the specified navigation direction.
- *
- * @param {Object} center1 The center point of the first element.
- * @param {Object} center2 The center point of the second element.
- * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right').
- * @return {number} The calculated distance between the two centers.
- */
- calculateDistance_(center1, center2, direction) {
- const dx = Math.abs(center1.x - center2.x);
- const dy = Math.abs(center1.y - center2.y);
-
- let distance;
-
- switch (direction) {
- case 'right':
- case 'left':
- // Higher weight for vertical distance in horizontal navigation.
- distance = dx + (dy * 100);
- break;
- case 'up':
- // Strongly prioritize vertical proximity for UP navigation.
- // Adjust the weight to ensure that elements directly above are favored.
- distance = (dy * 2) + (dx * 0.5);
- break;
- case 'down':
- // More balanced weight for vertical and horizontal distances.
- // Adjust the weights here to find the best balance.
- distance = (dy * 5) + dx;
- break;
- default:
- distance = dx + dy;
- }
-
- return distance;
- }
-
- /**
- * This gets called by 'handlePlayerBlur_' if 'spatialNavigation' is enabled.
- * Searches for the first 'TextTrackSelect' inside of modal to focus.
- *
- * @private
- */
- searchForTrackSelect_() {
- const spatialNavigation = this;
-
- for (const component of (spatialNavigation.updateFocusableComponents())) {
- if (component.constructor.name === 'TextTrackSelect') {
- spatialNavigation.focus(component);
- break;
- }
- }
- }
-}
-
-export default SpatialNavigation;