Created
March 29, 2017 07:52
-
-
Save piecyk/355be04da39ccd1f1c22f4406506478b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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