infect-frontend
Version:
infect fronend
193 lines (169 loc) • 7.59 kB
JSX
import React from 'react';
import debug from 'debug';
import color from 'tinycolor2';
import { observer } from 'mobx-react';
import { computed, observable, action, runInAction, when } from 'mobx';
import { supportsDominantBaseline } from '../../helpers/svgPolyfill';
import getVisibilityClassModifier from '../../helpers/getVisibilityClassModifier';
const log = debug('infect:SubstanceClassComponent');
export default class SubstanceClass extends React.Component {
_isHovered = false;
_afterHovered = false;
constructor() {
super();
this._lineWeight = 1;
this._wasVisible = true;
}
componentDidMount() {
this._measureHeight();
window.addEventListener('resize', () => this._measureHeight());
}
_getPreviousTransformation() {
return `translate(${ (this._previousPosition && this._previousPosition.left) || 0 },
${ this._previousPosition && this._previousPosition.top || 0 })`;
}
get topTransformation() {
const parentCount = this.props.substanceClass.substanceClass.getParentSubstanceClasses().length;
const top = this.props.matrix.antibioticLabelRowHeight +
(this.props.matrix.maxAmountOfSubstanceClassHierarchies - parentCount - 1) *
(this.props.matrix.greatestSubstanceClassLabelHeight || 0) +
this.props.matrix.space;
return top;
}
get transformation() {
if (!this.visible) return this._getPreviousTransformation();
const top = this.topTransformation;
const left = this.props.substanceClass.xPosition.left;
this._previousPosition = { left, top };
log('Position for %o is %d/%d', this.props.substanceClass, left, top);
return `translate(${ left }, ${ top })`;
}
_setTextElement(el) {
if (!el) return;
this._textElement = el;
}
_measureHeight() {
if (!this._textElement) {
log(`SubstanceClass: Element not set, cannot get height.`);
return;
}
// TODO: Use clipping path – so we can measure text's height
const style = window.getComputedStyle(this._textElement);
let height = Math.ceil(parseFloat(style.fontSize));
// add line + space between line and text + space below text
height += this._lineWeight + this.props.matrix.space * 2;
this.props.matrix.setSubstanceClassHeight(this.props.substanceClass, height);
}
/**
* Returns the label's width, which is
* - the substanceClasse's right x - left x position when not hovered
* - the label text's full width when hovered
* See _updateHovereState
*/
get _labelWidth() {
const xPos = this.props.substanceClass.xPosition;
// If label is invisible, don't change its width (and just return the previous value)
// to not mess with our animations.
if (!xPos || isNaN(xPos.right) || isNaN(xPos.left)) return this._previousLabelWidth || 0;
const minWidth = xPos.right - xPos.left;
const width = this._afterHovered
? Math.max(minWidth, this._getApproximateTextWidth()) + 2 * this.props.matrix.space
: minWidth;
this._previousLabelWidth = width;
return width;
}
get headerLineHeight() {
return this.props.matrix.greatestSubstanceClassLabelHeight;
}
/**
* Model does not have a visible property. But: All invisible substance classes won't have a left
* position (on matrixView). Use it to determine if substanceClass is visible or not.
*/
get visible() {
const left = this.props.substanceClass.xPosition && this.props.substanceClass.xPosition.left;
return !isNaN(left) && left !== undefined;
}
get classModifier() {
// No animations before we are ready
// We must also be watching transitions: If not, we only watch visible – which stays the
// same when modifier should change from -was-hidden-is-visible to -was-visible-is-visible
// and therefore won't call an update.
this.transformation;
const modifier = getVisibilityClassModifier(this.visible, this._wasVisible);
this._wasVisible = this.visible;
return modifier;
}
_getApproximateTextWidth() {
return this._textElement.getBBox().width;
}
_updateAfterHoveredState(hovered) {
this._afterHovered = hovered;
}
/**
* When user hovers a label, we must
* - first remove the label's cropping (which is done through a textPath)
* - then measure the whole uncropped label
* - then update the background and line rects accordingly (width of the whole label)
* To do so, we first set the _isHovered state to true which displays the label, then
* with a timeout, set the _afterHovered state. When the _afterHovered state is changed,
* the label already has the full width and can therefore be measured.
*/
_updateHoveredState(hovered) {
this._isHovered = hovered;
setTimeout(() => this._updateAfterHoveredState(hovered), 0 );
}
_getFillColor() {
const scColor = this.props.substanceClass.substanceClass.color;
if (!scColor) return 'rgb(255, 255, 255)';
const tinyColor = color({ r: scColor.r, g: scColor.g, b: scColor.b });
// Don't use opacity as bg serves as an overlay when user hovers the label
tinyColor.brighten(40).desaturate(60);
return tinyColor;
}
/**
* Handler for click on a substance class: Add it to the filters
*/
_handleSubstanceClassClick(substanceClass) {
const filters = this.props.filters.getValuesForProperty('substanceClass', 'name');
const filter = filters.find((item) => item.value === this.props.substanceClass.substanceClass.name);
log('Clicked on substance class; corresponding filter is %o', filter);
if (this.props.selectedFilters.isSelected(filter)) this.props.selectedFilters.removeFilter(filter);
else this.props.selectedFilters.addFilter(filter);
}
render() {
return (
<g transform={ this.transformation } className={ 'resistanceMatrix__substanceClassLabel js-substance-class ' + this.classModifier }
onMouseEnter={ (ev) => this._updateHoveredState(true) } onMouseLeave={ (ev) => this._updateHoveredState(false) }
onClick={ (ev) => this._handleSubstanceClassClick() } >
{ /* use textPath to truncate text of substanceClass */ }
<defs>
<path id={ 'substance-class-' + this.props.substanceClass.substanceClass.id + '-path' }
d={ `M 0 0 L ${ this._labelWidth - 5 } 0` }>
</path>
</defs>
{ /* Background */}
<rect width={ this._labelWidth } height={ this.headerLineHeight || 0 } fill={ this._getFillColor() }></rect>
{ /* Line above substanceClass */ }
<rect width={ this._labelWidth } height={ this._lineWeight } fill={ this.props.substanceClass.lineColor }
className="resistanceMatrix__substanceClassLine resistanceMatrix__substanceClassLine--top"></rect>
{ /* Line left of substanceClass (head) */ }
<rect width={ this._lineWeight } height={ this.headerLineHeight || 0 } fill={ this.props.substanceClass.lineColor }
className="resistanceMatrix__substanceClassLine resistanceMatrix__substanceClassLine--left-header"></rect>
<text className="resistanceMatrix__substanceClassLabelText" dominantBaseline="hanging"
dy={ supportsDominantBaseline('-2', '0.8em') }
transform={ `translate(${ this.props.matrix.space }, ${ this.props.matrix.space})` } ref={ (el) => this._setTextElement(el) }>
{ /* Not hovered: Crop text by applying a textPath */ }
{ !this._isHovered &&
<textPath xlinkHref={ '#substance-class-' + this.props.substanceClass.substanceClass.id + '-path' }
dominantBaseline="hanging">
{ this.props.substanceClass.substanceClass.name }
</textPath>
}
{ /* Hovered: Don't crop text */ }
{ this._isHovered && this.props.substanceClass.substanceClass.name }
</text>
</g>
);
}
}