Skip to content

Commit

Permalink
Add print layout with ability to hide (#816)
Browse files Browse the repository at this point in the history
* Add print layout with ability to hide

* remove debug and fix rspec fail

* Re-render svg before print

* Revert collapse_preference and finalize
  • Loading branch information
joko3ono authored Dec 2, 2024
1 parent 9b79a16 commit 191affb
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 259 deletions.
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

0 comments on commit 191affb

Please sign in to comment.