labo-components
Version:
251 lines (215 loc) • 7.46 kB
JSX
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,
};