Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add print layout with ability to hide #816

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"jstree": "^3.3.8",
"react": "18",
"react-app-polyfill": "^3.0.0",
"react-detect-print": "^0.1.2",
"react-dom": "18",
"tailwindcss": "^3.4.3",
"underscore": "^1.8.3"
Expand Down
9 changes: 9 additions & 0 deletions public/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
@tailwind components;
@tailwind utilities;

@page {
margin: 0;
}
@media print {
html, body {
width: 255mm;
}
}

/* Stop the body scrolling while a modal is open */
body:has(dialog[open]) {
overflow: hidden;
Expand Down
2 changes: 1 addition & 1 deletion public/css/app.min.css

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions public/js/collapse_preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ export default class CollapsePreferences {

toggleCollapse() {
let currentlyCollapsed = this.component.state.collapsed;

this.component.setState({ collapsed: !currentlyCollapsed });

let collapsePreferences = JSON.parse(localStorage.getItem('collapsePreferences')) || [];

if (currentlyCollapsed) {
localStorage.setItem('collapsePreferences', JSON.stringify(collapsePreferences.filter((name) => name !== this.component.name)));
localStorage.setItem('collapsePreferences', JSON.stringify(this.collapsePreferences.filter((name) => name !== this.component.name)));
} else {
let uniqueCollapsePreferences = [... new Set(collapsePreferences.concat([this.component.name]))];
let uniqueCollapsePreferences = [... new Set(this.collapsePreferences.concat([this.component.name]))];
localStorage.setItem('collapsePreferences', JSON.stringify(uniqueCollapsePreferences));
}
}
Expand All @@ -28,10 +25,10 @@ export default class CollapsePreferences {
}

minusIcon() {
return <i className="fa-regular fa-square-minus"></i>;
return <i className="print:!hidden fa-regular fa-square-minus"></i>;
}

plusIcon() {
return <i className="fa-regular fa-square-plus"></i>;
return <i className="print:!hidden fa-regular fa-square-plus"></i>;
}
}
178 changes: 90 additions & 88 deletions public/js/grapher.js
Original file line number Diff line number Diff line change
@@ -1,131 +1,133 @@
import _ from 'underscore';
import React, { createRef } from 'react';
import React, { createRef, useState, useEffect, useCallback } from 'react';

import './svgExporter'; // create handlers for SVG and PNG download buttons
import CollapsePreferences from './collapse_preferences';

// Each instance of Grapher is added to this object once the component has been
// mounted. This is so that grapher can be iterated over and redrawn on window
// resize event.
var Graphers = {};
import useDetectPrint from "react-detect-print";

// Grapher is a function that takes a Graph class and returns a React component.
// This React component provides HTML boilerplate to add heading, to make the
// graphs collapsible, to redraw graphs when window is resized, and SVG and PNG
// export buttons and functionality.
export default function Grapher(Graph) {
return class extends React.Component {
constructor(props) {
super(props);
this.name = Graph.name(this.props);
this.collapsePreferences = new CollapsePreferences(this);
let isCollapsed = this.collapsePreferences.preferenceStoredAsCollapsed();
this.state = { collapsed: Graph.canCollapse() && (this.props.collapsed || isCollapsed) };
this.svgContainerRef = createRef();
}
return function Component(props) {
const alwaysShowName = Graph.alwaysShowName === undefined ? false : Graph.alwaysShowName();
const printing = useDetectPrint();
const name = Graph.name(props);
const [width, setWidth] = useState(window.innerWidth);
const [collapsed, setCollapsed] = useState(false);
const svgContainerRef = createRef();
let graph = null;

graphId() {
return Graph.graphId(this.props);
}
const graphId = () => Graph.graphId(props)

render() {
// Do not render when Graph.name() is null
if (Graph.name(this.props) === null) {
return null;
} else {
var cssClasses = Graph.className() + ' grapher';
return (
<div className={cssClasses}>
{this.header()}
{this.svgContainerJSX()}
</div>
);
}
const graphLinksJSX = () => {
return (
<div className="hit-links float-right text-right text-blue-300 h-4 print:hidden">
<a href="#" className="btn-link text-sm text-seqblue hover:text-seqorange cursor-pointer export-to-svg">
<i className="fa fa-download" /> SVG
</a>
<span className="line px-1">|</span>
<a href="#" className="btn-link text-sm text-seqblue hover:text-seqorange cursor-pointer export-to-png">
<i className="fa fa-download" /> PNG
</a>
</div>
);
}

header() {
const header = () => {
if(Graph.canCollapse()) {
return <div className="grapher-header pr-px">
<h4
className="inline-block pl-px m-0 caption cursor-pointer text-sm"
onClick={() => this.collapsePreferences.toggleCollapse()}
onClick={() => collapsePreferences.toggleCollapse()}
>
{this.collapsePreferences.renderCollapseIcon()}
&nbsp;{Graph.name(this.props)}
{collapsePreferences.renderCollapseIcon()}
<span className="print:hidden">&nbsp;</span>{Graph.name(props)}
</h4>
{!this.state.collapsed && this.graphLinksJSX()}
{!collapsed && graphLinksJSX()}
</div>;
} else if (alwaysShowName) {
return <div className="grapher-histogram-header" style={{ position: 'relative' }}>
<h4 className="caption">&nbsp;{Graph.name(props)}</h4>
<div className="pull-right" style={{ position: 'absolute', top: 0, right: 0 }}>
{!collapsed && graphLinksJSX()}
</div>
</div>;
} else {
return <div className="pr-px">
{!this.state.collapsed && this.graphLinksJSX()}
{!collapsed && graphLinksJSX()}
</div>;
}
}

graphLinksJSX() {
return (
<div className="hit-links float-right text-right text-blue-300 h-4">
<a href="#" className="btn-link text-sm text-seqblue hover:text-seqorange cursor-pointer export-to-svg">
<i className="fa fa-download" /> SVG
</a>
<span className="line px-1">|</span>
<a href="#" className="btn-link text-sm text-seqblue hover:text-seqorange cursor-pointer export-to-png">
<i className="fa fa-download" /> PNG
</a>
</div>
);
}

svgContainerJSX() {
const svgContainerJSX = () => {
var cssClasses = Graph.className() + ' svg-container hidden';
if (!this.state.collapsed) cssClasses += ' !block';
if (!collapsed) cssClasses += ' !block';
return (
<div
ref={this.svgContainerRef}
id={this.graphId()}
ref={svgContainerRef}
id={graphId()}
className={cssClasses}
></div>
);
}

componentDidMount() {
Graphers[this.graphId()] = this;

// Draw visualisation for the first time. Visualisations are
// redrawn when browser window is resized.
this.draw();
}

componentDidUpdate() {
// Re-draw visualisation when the component change state.
this.draw();
}
svgContainer() {
return $(this.svgContainerRef.current);
const svgContainer = () => {
return $(svgContainerRef.current);
}

draw() {
const draw = (printing = false) => {
let graphWidth = 'auto';
if (printing) graphWidth = '900';
// Clean slate.
this.svgContainer().empty();
this.graph = null;
svgContainer().empty();
graph = null;

// Draw if uncollapsed.
if (this.state.collapsed) {
return;
}
this.graph = new Graph(this.svgContainer(), this.props);
this.svgContainer()
if (collapsed) return;

svgContainer().width(graphWidth);
graph = new Graph(svgContainer(), props);
svgContainer()
.find('svg')
.attr('data-name', Graph.dataName(this.props));
.attr('data-name', Graph.dataName(props));
}
};
}

// Redraw if window resized.
$(window).resize(
_.debounce(function () {
_.each(Graphers, (grapher) => {
grapher.draw();
});
}, 125)
);
useEffect(() => {
// Attach a debounced listener to handle window resize events
// Updates the width state with the current window width, throttled to run at most once every 125ms
const handleResize = _.debounce(() => setWidth(window.innerWidth), 125);
window.addEventListener("resize", handleResize);

const isCollapsed = collapsePreferences.preferenceStoredAsCollapsed();
setCollapsed(Graph.canCollapse() && (props.collapsed || isCollapsed))
draw();

return () => window.removeEventListener("resize", handleResize)
}, [])

useEffect(() => {
draw(printing);
}, [printing, width])

const setState = (state) => {
setCollapsed(state.collapsed)
}

const collapsePreferences = new CollapsePreferences({name: name, state: { collapsed: collapsed }, setState: setState});

if (Graph.name(props) === null) {
return(null);
} else {
const printCss = collapsed ? 'print:hidden' : '';
const cssClasses = Graph.className() + ' grapher' + printCss;
return (
<div className={cssClasses}>
{header()}
{svgContainerJSX()}
</div>
)
}
}
}
8 changes: 4 additions & 4 deletions public/js/hit.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ export default class extends Component {
}

return <div className="section-header border-b border-seqorange flex flex-col sm:flex-row sm:justify-between w-full">
<h4 className="text-sm cursor-pointer flex flex-col sm:flex-row items-start sm:items-center">
<h4 className="text-sm cursor-pointer flex flex-col sm:flex-row items-start sm:items-center" data-parent-id={`#${this.domID()}`}>
<div>
<i className="fa-regular fa-square-minus"></i>
<strong className="cursor-text ml-1">{this.props.hit.id}</strong>
<i className="fa-regular fa-square-minus print:!hidden"></i>
<strong className="cursor-text ml-1 print:ml-0"> {this.props.hit.id}</strong>
</div>
<span className="ml-1">{this.props.hit.title}</span>
</h4>
Expand Down Expand Up @@ -151,7 +151,7 @@ export default class extends Component {
});

return (
<div className="hit-links h-4">
<div className="hit-links h-4 print:hidden">
<label className="text-sm text-seqblue hover:seqorange cursor-pointer mb-0">
<input type="checkbox" id={this.domID() + '_checkbox'}
value={this.sequenceID()} onChange={function () {
Expand Down
Loading
Loading