UNPKG

labo-components

Version:
251 lines (215 loc) 7.46 kB
import React from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import IDUtil from "../../../util/IDUtil"; import RegexUtil from "../../../util/RegexUtil"; import MediaEvents from "../_MediaEvents"; export default class TimedList extends React.PureComponent { constructor(props) { super(props); this.state = { items: props.items, currentId: "", }; this.userHasScrolled = false; this.scrollTimerId = null; // default scroll offset // corrects for filter results line // and keeps previous lines in view this.scrollOffset = 0; this.scrollContainer = React.createRef(); } componentDidMount() { // bind to PLAYER_POS to update the position this.props.mediaEvents.bind( MediaEvents.PLAYER_POS, this.updatePosition ); // load with last data const data = this.props.mediaEvents.getData(MediaEvents.PLAYER_POS); if (data) { this.updatePosition(data); } } componentDidUpdate() { this.scrollOffset = this.props.searchTerm && this.props.searchTerm.length > 2 ? -29 : 0; } componentWillUnmount() { // unbind to PLAYER_POS this.props.mediaEvents.unbind( MediaEvents.PLAYER_POS, this.updatePosition ); } onSeek = (pos) => { // Update player by triggering SET_PLAYER_PS event this.props.mediaEvents.trigger(MediaEvents.SET_PLAYER_POS, pos); }; // called via external ref updatePosition = (pos) => { const item = this.findClosestItem(Math.trunc(pos * 1000)); if (!item) { this.setState({ currentId: "" }); return; } // set current this.setState({ currentId: item.id }); // Get elem by position const elem = document.getElementById(item.id); // Scroll to elem if (elem && !this.userHasScrolled) { this.scrollContainer.current.scrollTop = elem.offsetTop + this.scrollOffset; } }; // makes sure the user can still scroll through the list onScrollInList = () => { this.userHasScrolled = true; clearTimeout(this.scrollTimerId); this.scrollTimerId = setTimeout(() => { this.userHasScrolled = false; }, 2400); }; // make item with given id current setCurrentId(id) { this.setState({ currentId: id }, () => { // scroll to current this.userHasScrolled = false; this.props.items.some((element, i) => { if (element.id === id) { this.onSeek(element.start / 1000); return true; } return false; }); }); } // FIXME make this one faster findClosestItem = (currentTime) => { let index = this.props.items.findIndex((a) => a.start >= currentTime); // if no item was found it's either the very last item (if player time is bigger than 0) or the first item if (index === -1 && this.props.items.length > 0) { index = currentTime > 0 ? this.props.items.length - 1 : 0; } // adjust the index to the previous item when the start time is larger than the current time if ( this.props.items[index] && this.props.items[index].start > currentTime ) { index = index <= 0 ? 0 : index - 1; } // correct for end time: if time is after end time; skip if ( this.props.items[index] && this.props.items[index].end && currentTime > this.props.items[index].end ) { return null; } return this.props.items[index]; }; filterList = (items, searchTerm) => { if (searchTerm.length <= 2) { return items; } searchTerm = searchTerm.toLowerCase(); let regex = null; try { // add "*" around the searchterm to have more/better matches regex = RegexUtil.generateRegexForSearchTerm( "*" + searchTerm + "*" ); } catch (err) { regex = null; } if (!regex) { return items; } // filter the list using the regex const filteredList = items .filter((item) => { return item.data.toLowerCase().search(regex) !== -1; }) .map((item) => { // return item with highlighted text return Object.assign({}, item, { data: item.data.replace( regex, (term) => "<span class='highLight-text'>" + term + "</span>" ), }); }); return filteredList; }; /* ---------------------------- RENDERING FUNCTIONS ---------------------------------- */ renderList = (list, currentId, setCurrentId, onScrollInList) => { const items = list.map((item) => { return ( <li key={item.id} id={item.id} className={classNames("item", { current: item.id === currentId, })} onClick={setCurrentId.bind(this, item.id)} > <span className="start-time"> {item.startLabel} {item.endLabel ? " - " + item.endLabel : null} </span> <span dangerouslySetInnerHTML={{ __html: item.data }} ></span> </li> ); }); return ( <ul className="list-container" onScroll={onScrollInList}> {items} </ul> ); }; /* ----------------- Rendering --------------------- */ render = () => { const items = this.filterList(this.props.items, this.props.searchTerm); const list = this.renderList( items, this.state.currentId, this.setCurrentId, this.onScrollInList ); const filterResults = items.length != this.props.items.length ? ( <div className="filter-results"> Showing {items.length} of {this.props.items.length} lines </div> ) : null; return ( <div className={IDUtil.cssClassName("timed-list")} ref={this.scrollContainer} > <div className="timed-list-items"> {filterResults} {list} </div> </div> ); }; } TimedList.propTypes = { items: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, start: PropTypes.number.isRequired, end: PropTypes.number, startLabel: PropTypes.string.isRequired, endLabel: PropTypes.string, data: PropTypes.string, }).isRequired ).isRequired, searchTerm: PropTypes.string.isRequired, mediaEvents: PropTypes.object, };