Skip to content

Instantly share code, notes, and snippets.

@piecyk
Created March 29, 2017 07:52
Show Gist options
  • Save piecyk/355be04da39ccd1f1c22f4406506478b to your computer and use it in GitHub Desktop.
Save piecyk/355be04da39ccd1f1c22f4406506478b to your computer and use it in GitHub Desktop.
import {React} from 'lib/vendor/react';
import _ from 'lib/vendor/lodash';
import Tooltip from 'lib/ui/Tooltip/Tooltip';
import hashCode from 'lib/algorithms/hashCode';
const EmptyString = '';
const WhiteSpace = ' ';
const Dots = '...';
const cacheTextParts = {};
const cacheTruncated = {};
const cacheStyle = {};
const cacheRegExp = {};
function parseValue(computedStyle, attr) {
return parseFloat(computedStyle.getPropertyValue(attr)) || 0;
}
// box-sizing is set to border-box, need to subtract padding and border values
function getStyleValues(element) {
const s = window.getComputedStyle(element);
const padding = parseValue(s, 'padding-top') + parseValue(s, 'padding-bottom');
const border = parseValue(s, 'border-top-width') + parseValue(s, 'border-bottom-width');
const maybeLineHeight = s.getPropertyValue('line-height');
let lineHeight = 0;
if (maybeLineHeight === 'normal') {
// https://developer.mozilla.org/en-US/docs/Web/CSS/line-height, 1.2
lineHeight = 1.2 * parseValue(s, 'font-size');
} else {
lineHeight = parseFloat(maybeLineHeight);
}
return {lineHeight, padding, border};
}
function maybeCachedRegExp(maxOneWordLength) {
const reg = cacheRegExp[maxOneWordLength];
if (reg) {
return reg;
} else {
cacheRegExp[maxOneWordLength] = new RegExp(`.{1,${maxOneWordLength}}`, 'g');
return cacheRegExp[maxOneWordLength];
}
}
function splitToParts(props) {
const {text, maxOneWordLength, maxPartsLength, minSize} = props;
if (_.size(text) > minSize) {
const words = text.split(WhiteSpace).slice(0, maxPartsLength);
let ret = [];
for (let i = 0, len = words.length; i < len; i++) {
const word = words[i];
if (word.length > maxOneWordLength) {
ret = ret.concat(word.match(maybeCachedRegExp(maxOneWordLength)));
} else {
ret.push(word);
}
}
return ret;
} else {
return [];
}
}
function maybeCachedStyleValues(cacheKey, node) {
if (cacheKey) {
const style = cacheStyle[cacheKey];
if (style) {
return style;
} else {
cacheStyle[cacheKey] = getStyleValues(node);
return cacheStyle[cacheKey];
}
} else {
return getStyleValues(node);
}
}
const TruncateText = React.createClass({
shouldComponentUpdate(nextProps, nextState) {
return nextProps.text !== this.props.text ||
nextState.open !== this.state.open ||
nextState.parts.toString() !== this.state.parts.toString();
},
propTypes: {
lines: React.PropTypes.number,
minSize: React.PropTypes.number,
maxPartsLength: React.PropTypes.number,
maxOneWordLength: React.PropTypes.number,
showTooltip: React.PropTypes.bool,
alwaysShowTooltip: React.PropTypes.bool,
tooltipText: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
]),
cacheKey: React.PropTypes.string.isRequired,
text: React.PropTypes.string
},
getDefaultProps() {
return {
lines: 2,
minSize: 40, // for performance this need to be as close as possible ~~ real value
maxPartsLength: 30,
maxOneWordLength: 20,
showTooltip: true,
alwaysShowTooltip: false
};
},
getInitialState(props) {
const p = props || this.props;
if (!cacheTruncated[p.cacheKey]) {
cacheTruncated[p.cacheKey] = {};
}
if (!cacheTextParts[p.cacheKey]) {
cacheTextParts[p.cacheKey] = {};
}
const cacheHash = hashCode(p.text || '');
const isTruncated = cacheTruncated[p.cacheKey][cacheHash];
const fromCache = _.isBoolean(isTruncated);
const parts = fromCache ? cacheTextParts[p.cacheKey][cacheHash] || [] : splitToParts(p);
return {
open: false, parts, cacheHash, isTruncated, fromCache
};
},
check(cacheKey, maxHeight, delta) {
const {parts, cacheHash} = this.state;
if (parts.length > 0 && maxHeight < this.element.offsetHeight - delta) {
this.setState({isTruncated: true, parts: parts.slice(0, -1)}, () => {
this.check(cacheKey, maxHeight, delta);
});
} else if (this.state.isTruncated) {
cacheTruncated[cacheKey][cacheHash] = true;
cacheTextParts[cacheKey][cacheHash] = parts;
} else {
cacheTruncated[cacheKey][cacheHash] = false;
}
},
run(props) {
if (!this.state.fromCache && this.element) {
const {lineHeight, padding, border} = maybeCachedStyleValues(props.cacheKey, this.element);
const maxHeight = props.lines * lineHeight;
this.check(props.cacheKey, maxHeight, padding - border);
}
},
componentDidMount() {
this.run(this.props);
},
componentWillReceiveProps(nextProps) {
if (nextProps.text !== this.props.text) {
this.setState(this.getInitialState(nextProps), () => {
this.run(nextProps);
});
}
},
onRef(ref) {
this.element = ref;
},
onMouseEnter() {
if (this.props.showTooltip && this.props.alwaysShowTooltip ? true : this.state.isTruncated) {
this.setState({open: true});
}
},
onMouseLeave() {
if (this.props.showTooltip && this.props.alwaysShowTooltip ? true : this.state.isTruncated) {
this.setState({open: false});
}
},
render() {
const hasText = _.size(this.props.text) > 0;
if (hasText) {
const {parts, isTruncated} = this.state;
const text = parts.length > 0 ? parts.join(WhiteSpace) + (isTruncated ? Dots : EmptyString) : this.props.text;
return (
<div
ref={this.onRef}
className={this.props.className}
style={this.props.style}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{this.state.open &&
<Tooltip.Content open={this.state.open} element={this.element} text={this.props.tooltipText || this.props.text}/>
}
{_.isFunction(this.props.children) ? this.props.children({text}) : text}
</div>
);
} else {
return null;
}
}
});
export default TruncateText;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment