import React from 'react'; const TRUNCATE_CONTEXT = 6; const TRUNCATE_ELLIPSIS = '…'; /** * Returns an array with chunks that cover the whole text via {start, length} * objects. * * `('text', {start: 2, length: 1}) => [{text: 'te'}, {text: 'x', match: true}, {text: 't'}]` */ function chunkText(text, { start, length }) { if (text && !window.isNaN(start) && !window.isNaN(length)) { const chunks = []; // text chunk before match if (start > 0) { chunks.push({text: text.substr(0, start)}); } // matching chunk chunks.push({match: true, offset: start, text: text.substr(start, length)}); // text after match const remaining = start + length; if (remaining < text.length) { chunks.push({text: text.substr(remaining)}); } return chunks; } return [{ text }]; } /** * Truncates chunks with ellipsis * * First chunk is truncated from left, second chunk (match) is truncated in the * middle, last chunk is truncated at the end, e.g. * `[{text: "...cation is a "}, {text: "useful...or not"}, {text: "tool..."}]` */ function truncateChunks(chunks, text, maxLength) { if (chunks && chunks.length === 3 && maxLength && text && text.length > maxLength) { const res = chunks.map(c => Object.assign({}, c)); let needToCut = text.length - maxLength; // trucate end const end = res[2]; if (end.text.length > TRUNCATE_CONTEXT) { needToCut -= end.text.length - TRUNCATE_CONTEXT; end.text = `${end.text.substr(0, TRUNCATE_CONTEXT)}${TRUNCATE_ELLIPSIS}`; } if (needToCut) { // truncate front const start = res[0]; if (start.text.length > TRUNCATE_CONTEXT) { needToCut -= start.text.length - TRUNCATE_CONTEXT; start.text = `${TRUNCATE_ELLIPSIS}` + `${start.text.substr(start.text.length - TRUNCATE_CONTEXT)}`; } } if (needToCut) { // truncate match const middle = res[1]; if (middle.text.length > 2 * TRUNCATE_CONTEXT) { middle.text = `${middle.text.substr(0, TRUNCATE_CONTEXT)}` + `${TRUNCATE_ELLIPSIS}` + `${middle.text.substr(middle.text.length - TRUNCATE_CONTEXT)}`; } } return res; } return chunks; } /** * Renders text with highlighted search match. * * A match object is of shape `{text, label, match}`. * `match` is a text match object of shape `{start, length}` * that delimit text matches in `text`. `label` shows the origin of the text. */ export default class MatchedText extends React.PureComponent { render() { const { match, text, truncate, maxLength } = this.props; const showFullValue = !truncate || (match && (match.start + match.length) > truncate); const displayText = showFullValue ? text : text.slice(0, truncate); if (!match) { return {displayText}; } const chunks = chunkText(displayText, match); return ( {truncateChunks(chunks, displayText, maxLength).map((chunk) => { if (chunk.match) { return ( {chunk.text} ); } return chunk.text; })} ); } }