UNPKG

react-pathfinder

Version:

A React-based Library, which can be used for developing keyboard/remote controller handling in SPA and smartTV apps

261 lines (256 loc) 7.32 kB
// @flow import React, { Component } from "react"; import _ from "lodash"; // keys const LEFT = 37; const RIGHT = 39; const UP = 38; const DOWN = 40; const ENTER = 13; const BROWSER_BACK = 8; const WEBOS_BACK = 461; const TIZEN_BACK = 10009; const WEBOS_YELLOW_KEY = 405; const TIZEN_YELLOW_KEY = 405; const WEBOS_BLUE_KEY = 406; const TIZEN_BLUE_KEY = 406; const PC_YELLOW_PSEUDO_KEY = 17; // ctrl const PC_BLUE_PSEUDO_KEY = 18; // alt const KEY_ZERO = 48; const KEY_ONE = 49; const KEY_TWO = 50; const KEY_THREE = 51; const KEY_FOUR = 52; const KEY_FIVE = 53; const KEY_SIX = 54; const KEY_SEVEN = 55; const KEY_EIGHT = 56; const KEY_NINE = 57; const NUM_KEY_ZERO = 96; const NUM_KEY_ONE = 97; const NUM_KEY_TWO = 98; const NUM_KEY_THREE = 99; const NUM_KEY_FOUR = 100; const NUM_KEY_FIVE = 101; const NUM_KEY_SIX = 102; const NUM_KEY_SEVEN = 103; const NUM_KEY_EIGHT = 104; const NUM_KEY_NINE = 105; export type Vertice = { node: HTMLElement, name: string, left?: string, up?: string, down?: string, right?: string, focusOnEnter: ?string }; type PathfinderState = { vertices: Array<Vertice>, current_vertice: ?Vertice, log: string }; export default class Pathfinder extends Component<any, PathfinderState> { remoteHandler: Function; handleEnter: Function; defineVertice: Function; setupInitial: Function; refChild: Component<any>; state: PathfinderState = { vertices: [], current_vertice: null, log: "Get started!" }; log = (data: string) => { this.setState({ log: data }); }; remoteHandler = ( e: SyntheticKeyboardEvent<HTMLButtonElement> | number ): void => { let _obj = typeof e === "object"; let code = _obj ? e.keyCode : e; this.log(code); switch (code) { case ENTER: _obj && e.preventDefault(); if (this.state) this.handleEnter(this.state.current_vertice); break; case BROWSER_BACK: case WEBOS_BACK: case TIZEN_BACK: if ( this.state.current_vertice && this.state.current_vertice.node && this.state.current_vertice.node.tagName !== "INPUT" ) { _obj && e.preventDefault(); this.handleBack(); } break; case WEBOS_YELLOW_KEY: case TIZEN_YELLOW_KEY: case PC_YELLOW_PSEUDO_KEY: _obj && e.preventDefault(); this.handleYellowKey(); break; case WEBOS_BLUE_KEY: case TIZEN_BLUE_KEY: case PC_BLUE_PSEUDO_KEY: _obj && e.preventDefault(); this.handleBlueKey(); break; case LEFT: this.handleLeftKey(); break; case RIGHT: this.handleRightKey(); break; case UP: this.handleUpKey(); break; case DOWN: this.handleDownKey(); break; default: this.handleKey(e.keyCode); } }; handleKey = (code: number): void => { if (typeof this.refChild.handleKey === "function") this.refChild.handleKey(code); }; handleLeftKey = (): void => { if (typeof this.refChild.handleLeftKey === "function") this.refChild.handleLeftKey(); else this.changeVerticeByDirection("left"); }; handleRightKey = (): void => { if (typeof this.refChild.handleRightKey === "function") this.refChild.handleRightKey(); else this.changeVerticeByDirection("right"); }; handleUpKey = (): void => { if (typeof this.refChild.handleUpKey === "function") this.refChild.handleUpKey(); else this.changeVerticeByDirection("up"); }; handleDownKey = (): void => { if (typeof this.refChild.handleDownKey === "function") this.refChild.handleDownKey(); else this.changeVerticeByDirection("down"); }; handleEnter = (vertice: ?Vertice): void => { if (!vertice) return; if (vertice.to) { this.props.history.push(vertice.to); return; } if (vertice.node && vertice.node.tagName !== "INPUT") { vertice.node.click(); } else if (vertice.focusOnEnter) { this.changeVertice(vertice.focusOnEnter); } }; handleBack = (): void => { this.props.history.goBack(); }; handleYellowKey = (): void => { if (typeof this.refChild.handleYellowKey === "function") this.refChild.handleYellowKey(); }; handleBlueKey = (): void => { if (typeof this.refChild.handleBlueKey === "function") this.refChild.handleBlueKey(); }; checkWrappedComponent = (ref: ?Component<any>): void => { if (!ref) return; if (ref.getWrappedInstance) { try { this.refChild = ref.getWrappedInstance(); } catch (e) { return console.error(e); } } else { this.refChild = ref; } }; setupInitial = (vertice: string | Vertice): void => { this.changeVertice(vertice); }; defineVertice = (vertice: Vertice): void => { let verticeIndex = _.findIndex(this.state.vertices, { name: vertice.name }); let vertices = this.state.vertices; if (verticeIndex >= 0) _.assign(vertices[verticeIndex], vertice); else { vertices.push(vertice); this.setState({ vertices: vertices }); } }; changeVertice = (new_vertice: Vertice | string): void => { if (!new_vertice) return; if (typeof new_vertice === "string") { new_vertice = _.find(this.state.vertices, { name: new_vertice }); } if (!new_vertice) return; this.setState({ current_vertice: new_vertice }); this.setupFocus(new_vertice.node); }; changeVerticeByDirection = (direction: string): void => { if (!this.state.current_vertice) return; // condition for free replacing caret into input element // focus replaces to another vertice only when caret starts or ends of input value if ( this.state.current_vertice.node.tagName === "INPUT" && ((this.state.current_vertice.node.selectionStart !== 0 && direction === "left") || (direction === "right" && this.state.current_vertice.node.selectionStart !== this.state.current_vertice.node.value.length)) ) { return; } if (this.state.current_vertice[direction]) this.changeVertice(this.state.current_vertice[direction]); else this.setupFocus(this.state.current_vertice.node); }; setupFocus = (node: HTMLElement): void => { if (node) node.focus(); }; componentDidMount() { window.addEventListener("keydown", this.remoteHandler); window.addEventListener("message", e => { this.remoteHandler(e.data); }); } componentWillUnmount() { window.removeEventListener("keydown", this.remoteHandler); window.removeEventListener("message"); } render() { return ( <div className="pathfinder" style={{ position: "relative" }}> {this.props && React.cloneElement(this.props.children, { ref: ref => this.checkWrappedComponent(ref), defineVertice: this.defineVertice, setupInitial: this.setupInitial })} {process.env.LOG && ( <div className="logger" style={{ position: "absolute", top: "0px", left: "50%", backgroundColor: "ghostwhite" }} > {this.state.log} </div> )} </div> ); } }