react-read-more
Version:
React component for truncating multi-line spans and adding an ellipsis
359 lines (289 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Truncate = function (_Component) {
_inherits(Truncate, _Component);
function Truncate() {
var _ref;
_classCallCheck(this, Truncate);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var _this = _possibleConstructorReturn(this, (_ref = Truncate.__proto__ || Object.getPrototypeOf(Truncate)).call.apply(_ref, [this].concat(args)));
_this.state = {};
_this.styles = {
ellipsis: {
position: 'fixed',
visibility: 'hidden',
top: 0,
left: 0
}
};
_this.onResize = _this.onResize.bind(_this);
_this.onTruncate = _this.onTruncate.bind(_this);
_this.calcTargetWidth = _this.calcTargetWidth.bind(_this);
_this.measureWidth = _this.measureWidth.bind(_this);
_this.getLines = _this.getLines.bind(_this);
_this.renderLine = _this.renderLine.bind(_this);
return _this;
}
_createClass(Truncate, [{
key: 'componentDidMount',
value: function componentDidMount() {
// Node not needed in document tree to read its content
this.refs.text.parentNode.removeChild(this.refs.text);
// Keep node in document body to read .offsetWidth
document.body.appendChild(this.refs.ellipsis);
var canvas = document.createElement('canvas');
this.canvas = canvas.getContext('2d');
window.addEventListener('resize', this.onResize);
this.onResize();
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps) {
// Render was based on outdated refs and needs to be rerun
if (this.props.children !== prevProps.children) {
this.forceUpdate();
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
document.body.removeChild(this.refs.ellipsis);
window.removeEventListener('resize', this.onResize);
cancelAnimationFrame(this.timeout);
}
// Shim innerText to consistently break lines at <br/> but not at \n
}, {
key: 'innerText',
value: function innerText(node) {
var div = document.createElement('div');
div.innerHTML = node.innerHTML.replace(/\r\n|\r|\n/g, ' ');
var text = div.innerText;
var test = document.createElement('div');
test.innerHTML = 'foo<br/>bar';
if (test.innerText.replace(/\r\n|\r/g, '\n') !== 'foo\nbar') {
div.innerHTML = div.innerHTML.replace(/<br.*?[\/]?>/gi, '\n');
text = div.innerText;
}
return text;
}
}, {
key: 'onResize',
value: function onResize() {
this.calcTargetWidth();
}
}, {
key: 'onTruncate',
value: function onTruncate(didTruncate) {
var onTruncate = this.props.onTruncate;
if (typeof onTruncate === 'function') {
this.timeout = requestAnimationFrame(function () {
onTruncate(didTruncate);
});
}
}
}, {
key: 'calcTargetWidth',
value: function calcTargetWidth() {
var target = this.refs.target,
calcTargetWidth = this.calcTargetWidth,
canvas = this.canvas;
// Calculation is no longer relevant, since node has been removed
if (!target) {
return;
}
var targetWidth = this.props.options.fixedWidth || target.parentNode.getBoundingClientRect().width;
// Delay calculation until parent node is inserted to the document
// Mounting order in React is ChildComponent, ParentComponent
if (!targetWidth) {
return requestAnimationFrame(calcTargetWidth);
}
var style = window.getComputedStyle(target);
var font = [style['font-weight'], style['font-style'], style['font-size'], style['font-family']].join(' ');
canvas.font = font;
this.setState({
targetWidth: targetWidth
});
}
}, {
key: 'measureWidth',
value: function measureWidth(text) {
return this.canvas.measureText(text).width;
}
}, {
key: 'ellipsisWidth',
value: function ellipsisWidth(node) {
return node.offsetWidth;
}
}, {
key: 'getLines',
value: function getLines() {
var refs = this.refs,
_props = this.props,
numLines = _props.lines,
ellipsis = _props.ellipsis,
targetWidth = this.state.targetWidth,
innerText = this.innerText,
measureWidth = this.measureWidth,
onTruncate = this.onTruncate;
var lines = [];
var text = innerText(refs.text);
var textLines = text.split('\n').map(function (line) {
return line.split(' ');
});
var didTruncate = true;
var ellipsisWidth = this.ellipsisWidth(this.refs.ellipsis);
for (var line = 1; line <= numLines; line++) {
var textWords = textLines[0];
// Handle newline
if (textWords.length === 0) {
lines.push();
textLines.shift();
line--;
continue;
}
var resultLine = textWords.join(' ');
if (measureWidth(resultLine) < targetWidth) {
if (textLines.length === 1) {
// Line is end of text and fits without truncating //
didTruncate = false;
lines.push(resultLine);
break;
}
}
if (line === numLines) {
// Binary search determining the longest possible line inluding truncate string //
var textRest = textWords.join(' ');
var lower = 0;
var upper = textRest.length - 1;
while (lower <= upper) {
var middle = Math.floor((lower + upper) / 2);
var testLine = textRest.slice(0, middle + 1);
if (measureWidth(testLine) + ellipsisWidth <= targetWidth) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
resultLine = _react2.default.createElement(
'span',
null,
textRest.slice(0, lower),
ellipsis
);
} else {
// Binary search determining when the line breaks //
var _lower = 0;
var _upper = textWords.length - 1;
while (_lower <= _upper) {
var _middle = Math.floor((_lower + _upper) / 2);
var _testLine = textWords.slice(0, _middle + 1).join(' ');
if (measureWidth(_testLine) <= targetWidth) {
_lower = _middle + 1;
} else {
_upper = _middle - 1;
}
}
// The first word of this line is too long to fit it
if (_lower === 0) {
// Jump to processing of last line
line = numLines - 1;
continue;
}
resultLine = textWords.slice(0, _lower).join(' ');
textLines[0].splice(0, _lower);
}
lines.push(resultLine);
}
onTruncate(didTruncate);
return lines;
}
}, {
key: 'renderLine',
value: function renderLine(line, i, arr) {
if (i === arr.length - 1) {
return _react2.default.createElement(
'span',
{ key: i },
line
);
}
var br = _react2.default.createElement('br', { key: i + 'br' });
if (line) {
return [_react2.default.createElement(
'span',
{ key: i },
line
), br];
}
return br;
}
}, {
key: 'render',
value: function render() {
var target = this.refs.target,
_props2 = this.props,
children = _props2.children,
ellipsis = _props2.ellipsis,
lines = _props2.lines,
spanProps = _objectWithoutProperties(_props2, ['children', 'ellipsis', 'lines']),
targetWidth = this.state.targetWidth,
getLines = this.getLines,
renderLine = this.renderLine,
onTruncate = this.onTruncate;
var text = children;
if (typeof window !== 'undefined') {
if (target && targetWidth && lines > 0) {
text = getLines().map(renderLine);
} else {
onTruncate(false);
}
}
delete spanProps.onTruncate;
delete spanProps.options;
return _react2.default.createElement(
'span',
_extends({}, spanProps, { ref: 'target' }),
text,
_react2.default.createElement(
'span',
{ ref: 'text' },
children
),
_react2.default.createElement(
'span',
{ ref: 'ellipsis', style: this.styles.ellipsis },
ellipsis
)
);
}
}]);
return Truncate;
}(_react.Component);
Truncate.propTypes = {
children: _react.PropTypes.node,
ellipsis: _react.PropTypes.node,
lines: _react.PropTypes.oneOfType([_react.PropTypes.oneOf([false]), _react.PropTypes.number]),
options: _react.PropTypes.object,
onTruncate: _react.PropTypes.func
};
Truncate.defaultProps = {
children: '',
ellipsis: '…',
options: {},
lines: 1
};
exports.default = Truncate;
module.exports = exports['default'];