123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- 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 <span>{displayText}</span>;
- }
- const chunks = chunkText(displayText, match);
- return (
- <span className="matched-text" title={text}>
- {truncateChunks(chunks, displayText, maxLength).map((chunk) => {
- if (chunk.match) {
- return (
- <span className="match" key={chunk.offset}>
- {chunk.text}
- </span>
- );
- }
- return chunk.text;
- })}
- </span>
- );
- }
- }
|