UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

367 lines (333 loc) 10.3 kB
import React from "react"; import { connect } from "react-redux"; import marked from "marked"; import dompurify from "dompurify"; import styled from 'styled-components'; import { dataFont, medGrey, materialButton } from "../../globalStyles"; import { TRIGGER_DOWNLOAD_MODAL } from "../../actions/types"; import Flex from "./flex"; import { applyFilter } from "../../actions/tree"; import { version } from "../../version"; import { publications } from "../download/downloadModal"; import { isValueValid } from "../../util/globals"; import hardCodedFooters from "./footer-descriptions"; const dot = ( <span style={{marginLeft: 10, marginRight: 10}}></span> ); const FooterStyles = styled.div` margin-left: 30px; padding-bottom: 30px; font-family: ${dataFont}; font-size: 15px; font-weight: 300; color: rgb(136, 136, 136); line-height: 1.4; h1 { font-weight: 700; font-size: 2.2em; margin: 0.2em 0; } h2 { font-weight: 600; font-size: 2em; margin: 0.2em 0; } h3 { font-weight: 500; font-size: 1.8em; margin: 0.2em 0; } h4 { font-weight: 500; font-size: 1.6em; margin: 0.1em 0; } h5 { font-weight: 500; font-size: 1.4em; margin: 0.1em 0; } h6 { font-weight: 500; font-size: 1.2em; margin: 0.1em 0; } // Style for code block pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 3px; } // Code within code block pre code { padding: 0; margin: 0; overflow: visible; font-size: 100%; line-height: inherit; word-wrap: normal; background-color: initial; border: 0; } // Inline code p code { padding: .2em .4em; margin: 0; font-size: 85%; background-color: rgba(27,31,35,.05); border-radius: 3px; } .line { margin-top: 20px; margin-bottom: 20px; border-bottom: 1px solid #CCC; } .finePrint { font-size: 14px; } .acknowledgments { margin-top: 10px; } .filterList { margin-top: 10px; line-height: 1.0; } .imageContainer { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; } img { margin-left: 30px; margin-right: 30px; margin-top: 2px; margin-bottom: 2px; } `; export const getAcknowledgments = (metadata, dispatch) => { /** * If the metadata contains a description key, then it will take precendence the hard-coded * acknowledgements. Expects the text in the description to be in Mardown format. * Jover. December 2019. */ if (metadata.description) { dompurify.addHook("afterSanitizeAttributes", (node) => { // Set external links to open in a new tab if ('href' in node && location.hostname !== node.hostname) { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noreferrer nofollow'); } // Find nodes that contain images and add imageContainer class to update styling const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; if (nodeContainsImg) { // For special case of image links, set imageContainer on outer parent if (node.localName === 'a') { node.parentNode.className += ' imageContainer'; } else { node.className += ' imageContainer'; } } }); const sanitizer = dompurify.sanitize; const sanitizerConfig = { ALLOWED_TAGS: ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img', '#text', 'code', 'pre', 'hr'], ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'alt'], KEEP_CONTENT: false, ALLOW_DATA_ATTR: false }; const rawDescription = marked(metadata.description); const cleanDescription = sanitizer(rawDescription, sanitizerConfig); return ( <div className='acknowledgments' dangerouslySetInnerHTML={{ __html: cleanDescription }}/> ); } const preambleContent = "This work is made possible by the open sharing of genetic data by research groups from all over the world. We gratefully acknowledge their contributions."; const genericPreamble = (<div>{preambleContent}</div>); if (window.location.hostname === 'nextstrain.org') { return hardCodedFooters(dispatch, genericPreamble); } return (<div>{genericPreamble}</div>); }; const dispatchFilter = (dispatch, activeFilters, key, value) => { const mode = activeFilters[key].indexOf(value) === -1 ? "add" : "remove"; dispatch(applyFilter(mode, key, [value])); }; export const displayFilterValueAsButton = (dispatch, activeFilters, filterName, itemName, display, showX) => { const active = activeFilters[filterName].indexOf(itemName) !== -1; if (active && showX) { return ( <div key={itemName} style={{display: "inline-block"}}> <div className={'boxed-item-icon'} onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}} role="button" tabIndex={0} > {'\xD7'} </div> <div className={"boxed-item active-with-icon"}> {display} </div> </div> ); } if (active) { return ( <div className={"boxed-item active-clickable"} key={itemName} onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}} role="button" tabIndex={0} > {display} </div> ); } return ( <div className={"boxed-item inactive"} key={itemName} onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}} role="button" tabIndex={0} > {display} </div> ); }; const removeFiltersButton = (dispatch, filterNames, outerClassName, label) => { return ( <div className={`${outerClassName} boxed-item active-clickable`} style={{paddingLeft: '5px', paddingRight: '5px', display: "inline-block"}} onClick={() => { filterNames.forEach((n) => dispatch(applyFilter("set", n, []))); }} > {label} </div> ); }; @connect((state) => { return { tree: state.tree, totalStateCounts: state.tree.totalStateCounts, metadata: state.metadata, colorOptions: state.metadata.colorOptions, browserDimensions: state.browserDimensions.browserDimensions, activeFilters: state.controls.filters }; }) class Footer extends React.Component { shouldComponentUpdate(nextProps) { if (this.props.tree.version !== nextProps.tree.version || this.props.browserDimensions !== nextProps.browserDimensions) { return true; } else if (Object.keys(this.props.activeFilters) !== Object.keys(nextProps.activeFilters)) { return true; } else if (Object.keys(this.props.activeFilters)) { for (const name of this.props.activeFilters) { if (this.props.activeFilters[name] !== nextProps.activeFilters[name]) { return true; } } } return false; } displayFilter(filterName) { const totalStateCount = this.props.totalStateCounts[filterName]; const filterTitle = this.props.metadata.colorings[filterName] ? this.props.metadata.colorings[filterName].title : filterName; return ( <div> {`Filter by ${filterTitle}`} {this.props.activeFilters[filterName].length ? removeFiltersButton(this.props.dispatch, [filterName], "inlineRight", `Clear ${filterName} filter`) : null} <div className='filterList'> <Flex wrap="wrap" justifyContent="flex-start" alignItems="center"> { Array.from(totalStateCount.keys()) .filter((itemName) => isValueValid(itemName)) // remove invalid values present across the tree .sort() // filters are sorted alphabetically .map((itemName) => { const display = ( <span> {`${itemName} (${totalStateCount.get(itemName)})`} </span> ); return displayFilterValueAsButton(this.props.dispatch, this.props.activeFilters, filterName, itemName, display, false); }) } </Flex> </div> </div> ); } getUpdated() { if (this.props.metadata.updated) { return (<span>Data updated {this.props.metadata.updated}</span>); } return null; } downloadDataButton() { return ( <button style={Object.assign({}, materialButton, {backgroundColor: "rgba(0,0,0,0)", color: medGrey, margin: 0, padding: 0})} onClick={() => { this.props.dispatch({ type: TRIGGER_DOWNLOAD_MODAL }); }} > <i className="fa fa-download" aria-hidden="true"/> <span style={{position: "relative"}}>{" download data"}</span> </button> ); } getCitation() { return ( <span> {"Nextstrain: "} <a href={publications.nextstrain.href} target="_blank"> {publications.nextstrain.author}, <i>{publications.nextstrain.journal}</i>{` (${publications.nextstrain.year})`} </a> </span> ); } render() { if (!this.props.metadata || !this.props.tree.nodes) return null; const width = this.props.width - 30; // need to subtract margin when calculating div width return ( <FooterStyles> <div style={{width: width}}> <div className='line'/> {getAcknowledgments(this.props.metadata, this.props.dispatch)} <div className='line'/> {Object.keys(this.props.activeFilters).map((name) => { return ( <div key={name}> {this.displayFilter(name)} <div className='line'/> </div> ); })} <Flex className='finePrint'> {this.getUpdated()} {dot} {this.downloadDataButton()} {dot} {"Auspice v" + version} </Flex> <div style={{height: "3px"}}/> <Flex className='finePrint'> {this.getCitation()} </Flex> </div> </FooterStyles> ); } } // {dot} // export default Footer;