auspice
Version:
Web app for visualizing pathogen evolution
176 lines (172 loc) • 6.91 kB
JavaScript
/* eslint-disable react/no-danger */
import React from "react";
import { connect } from "react-redux";
import queryString from "query-string";
import Mousetrap from "mousetrap";
import { NarrativeStyles, linkStyles, OpacityFade } from './styles';
import ReactPageScroller from "./ReactPageScroller";
import { changePage } from "../../actions/navigation";
import { CHANGE_URL_QUERY_BUT_NOT_REDUX_STATE, TOGGLE_NARRATIVE } from "../../actions/types";
import { narrativeNavBarHeight } from "../../util/globals";
/* regarding refs: https://reactjs.org/docs/refs-and-the-dom.html#exposing-dom-refs-to-parent-components */
const progressHeight = 25;
class Narrative extends React.Component {
constructor(props) {
super(props);
this.state = {showingEndOfNarrativePage: false};
this.exitNarrativeMode = () => {
this.props.dispatch({type: TOGGLE_NARRATIVE, display: false});
};
this.changeAppStateViaBlock = (reactPageScrollerIdx) => {
const idx = reactPageScrollerIdx-1;
if (idx === this.props.blocks.length) {
this.setState({showingEndOfNarrativePage: true});
} else {
if (this.state.showingEndOfNarrativePage) {
this.setState({showingEndOfNarrativePage: false});
}
this.props.dispatch(changePage({
// path: this.props.blocks[blockIdx].dataset, // not yet implemented properly
dontChangeDataset: true,
query: queryString.parse(this.props.blocks[idx].query),
queryToDisplay: {n: idx},
push: true
}));
}
};
this.goToNextSlide = () => {
if (this.state.showingEndOfNarrativePage) return; // no-op
this.reactPageScroller.goToPage(this.props.currentInFocusBlockIdx+1);
};
this.goToPreviousSlide = () => {
if (this.props.currentInFocusBlockIdx === 0) return; // no-op
this.reactPageScroller.goToPage(this.props.currentInFocusBlockIdx-1);
};
}
componentDidMount() {
if (window.twttr && window.twttr.ready) {
window.twttr.widgets.load();
}
/* if the query has defined a block to be shown (that's not the first)
then we must scroll to that block */
if (this.props.currentInFocusBlockIdx !== 0) {
this.reactPageScroller.goToPage(this.props.currentInFocusBlockIdx);
}
/* bind arrow keys to move around in narrative */
/* Note that "normal" page scrolling is not avaialble in narrative mode
and that scrolling the sidebar is associated with changing the narrative slide */
Mousetrap.bind(['left', 'up'], this.goToPreviousSlide);
Mousetrap.bind(['right', 'down'], this.goToNextSlide);
}
renderChevron(pointUp) {
const dims = {w: 30, h: 30};
const style = {
zIndex: 200,
position: "absolute",
cursor: "pointer",
left: `${this.props.width/2 - dims.w/2}px`
};
if (pointUp) style.top = narrativeNavBarHeight + progressHeight;
else style.bottom = 0;
const svgPathD = pointUp ?
"M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z" :
"M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z";
return (
<div id={`hand${pointUp?"Up":"Down"}`} style={style} onClick={pointUp ? this.goToPreviousSlide : this.goToNextSlide}>
<svg width={`${dims.w}px`} height={`${dims.h}px`} viewBox="0 0 448 512">
<path d={svgPathD} fill="black"/>
</svg>
</div>
);
}
renderProgress() {
return (
<div
style={{
height: `${progressHeight}px`,
width: "100%",
backgroundColor: "inherit",
boxShadow: '0px -3px 3px -3px rgba(0, 0, 0, 0.2) inset',
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
alignItems: "center"
}}
>
{this.props.blocks.map((b, i) => {
const d = (!this.state.showingEndOfNarrativePage) && this.props.currentInFocusBlockIdx === i ?
"14px" : "6px";
return (<div
key={b.__html.slice(0, 30)}
style={{width: d, height: d, background: "#74a9cf", borderRadius: "50%", cursor: "pointer"}}
onClick={() => this.reactPageScroller.goToPage(i)}
/>);
})}
</div>
);
}
renderBlocks() {
const ret = this.props.blocks.map((b, i) => (
<div
id={`NarrativeBlock_${i}`}
key={b.__html.slice(0, 50)}
style={{
padding: "10px 20px",
height: "inherit",
overflow: "hidden"
}}
dangerouslySetInnerHTML={b}
/>
));
ret.push((
<div key="EON" id="EndOfNarrative" style={{flexBasis: "50%", flexShrink: 0}}>
<h3 style={{textAlign: "center"}}>
END OF NARRATIVE
</h3>
<div style={{...linkStyles, textAlign: "center"}} onClick={() => this.reactPageScroller.goToPage(0)}>
Scroll back to the beginning
</div>
<div style={{...linkStyles, textAlign: "center", marginTop: "10px"}} onClick={this.exitNarrativeMode}>
Leave the narrative & explore the data yourself
</div>
</div>
));
return ret;
}
render() {
if (!this.props.loaded) {return null;}
return (
<NarrativeStyles
id="NarrativeContainer"
narrativeNavBarHeight={narrativeNavBarHeight}
>
{this.renderProgress()}
<OpacityFade position="top" topHeight={narrativeNavBarHeight + progressHeight}/>
<OpacityFade position="bottom"/>
{this.props.currentInFocusBlockIdx !== 0 ? this.renderChevron(true) : null}
{!this.state.showingEndOfNarrativePage ? this.renderChevron(false) : null}
<ReactPageScroller
ref={(c) => {this.reactPageScroller = c;}}
containerHeight={this.props.height-progressHeight}
pageOnChange={this.changeAppStateViaBlock}
>
{this.renderBlocks()}
</ReactPageScroller>
</NarrativeStyles>
);
}
componentWillUnmount() {
this.props.dispatch({
type: CHANGE_URL_QUERY_BUT_NOT_REDUX_STATE,
pathname: this.props.blocks[this.props.currentInFocusBlockIdx].dataset,
query: queryString.parse(this.props.blocks[this.props.currentInFocusBlockIdx].url)
});
Mousetrap.unbind(['left', 'right', 'up', 'down']);
}
}
export default Narrative;