react-highlight-words
Advanced tools
Comparing version 0.1.1 to 0.1.2
{ | ||
"name": "react-highlight-words", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "React component to highlight words within a larger body of text", | ||
@@ -5,0 +5,0 @@ "main": "dist/main.js", |
@@ -1,2 +0,2 @@ | ||
# react-highlight-words | ||
<img src="https://cloud.githubusercontent.com/assets/29597/11913937/0d2dcd78-a629-11e5-83e7-6a17b6d765a5.png" width="260" height="260"> | ||
@@ -7,2 +7,19 @@ React component to highlight words within a larger body of text. | ||
## Usage | ||
To use it, just provide it with an array of search terms and a body of text to highlight: | ||
```html | ||
<Highlighter | ||
highlightClassName='YourHighlightClass' | ||
searchWords={['and', 'or', 'the']} | ||
textToHighlight="The dog is chasing the cat. Or perhaps they're just playing?" | ||
/> | ||
``` | ||
And the `Highlighter` will mark all occurrences of search terms within the text: | ||
<img width="368" alt="screen shot 2015-12-19 at 8 23 43 am" src="https://cloud.githubusercontent.com/assets/29597/11914033/e3c319f6-a629-11e5-896d-1a5ce22c9ea2.png"> | ||
## Installation | ||
@@ -9,0 +26,0 @@ ``` |
@@ -10,4 +10,4 @@ import React, { Component } from 'react' | ||
this.state = { | ||
searchText: 'is', | ||
textToHighlight: 'This is some text.' | ||
searchText: 'and or the', | ||
textToHighlight: `When in the Course of human events it becomes necessary for one people to dissolve the political bands which have connected them with another and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.` | ||
} | ||
@@ -22,18 +22,36 @@ } | ||
<div {...props}> | ||
<div className={styles.LabelAndInputRow}> | ||
<label className={styles.Label}>Search terms</label> | ||
<input | ||
name='searchTerms' | ||
value={searchText} | ||
onChange={event => this.setState({ searchText: event.target.value })}/> | ||
<label className={styles.Label}>Text to highlight</label> | ||
<input | ||
name='textToHighlight' | ||
value={textToHighlight} | ||
onChange={event => this.setState({ textToHighlight: event.target.value })}/> | ||
</div> | ||
<h4 className={styles.Header}> | ||
Search terms | ||
</h4> | ||
<input | ||
className={styles.Input} | ||
name='searchTerms' | ||
value={searchText} | ||
onChange={event => this.setState({ searchText: event.target.value })}/> | ||
<h4 className={styles.Header}> | ||
Body of Text | ||
</h4> | ||
<textarea | ||
className={styles.Input} | ||
name='textToHighlight' | ||
value={textToHighlight} | ||
onChange={event => this.setState({ textToHighlight: event.target.value })}/> | ||
<h4 className={styles.Header}> | ||
Output | ||
</h4> | ||
<Highlighter | ||
highlightClassName={styles.Highlight} | ||
highlightStyle={{ fontWeight: 'normal' }} | ||
searchWords={searchWords} | ||
textToHighlight={textToHighlight}/> | ||
textToHighlight={textToHighlight} | ||
/> | ||
<p className={styles.Footer}> | ||
<a href='https://github.com/bvaughn/react-highlight-words/blob/master/src/Highlighter.example.js'> | ||
View the source | ||
</a> | ||
</p> | ||
</div> | ||
@@ -40,0 +58,0 @@ ) |
/* @flow */ | ||
import React, { PropTypes } from 'react' | ||
import styles from './Highlighter.css' | ||
import * as Chunks from './utils.js' | ||
Highlighter.propTypes = { | ||
highlightClassName: PropTypes.string, | ||
highlightStyle: PropTypes.object, | ||
searchWords: PropTypes.arrayOf(PropTypes.string).isRequired, | ||
@@ -17,66 +19,32 @@ textToHighlight: PropTypes.string.isRequired | ||
highlightClassName = '', | ||
highlightStyle = {}, | ||
searchWords, | ||
textToHighlight } | ||
) { | ||
const chunks = Chunks.findAll(textToHighlight, searchWords) | ||
const highlightClassNames = `${styles.Term} ${highlightClassName}` | ||
let uidCounter = 0 | ||
const content = searchWords | ||
.filter(searchWord => searchWord) // Remove empty words | ||
.reduce((substrings, searchWord) => { | ||
const searchWordLength = searchWord.length | ||
const regex = new RegExp(searchWord, 'gi') | ||
// Step backwards so that changes to the Array won't cause us to visit the same text twice. | ||
for (var i = substrings.length - 1; i >= 0; i--) { | ||
let substring = substrings[i] | ||
return ( | ||
<span> | ||
{chunks.map((chunk, index) => { | ||
const text = textToHighlight.substr(chunk.start, chunk.end - chunk.start) | ||
// Examine the current substring for any marker-matches. | ||
// If we find matches, replace the plain text string with a styled <span> wrapper. | ||
// If this element of the array already contains a styled <span> we can ignore it. | ||
// There is no benefit to highlighting text twice. | ||
// (This means that only one of a pair of overlapping words will be highlighted, but that's okay.) | ||
if (typeof substring === 'string') { | ||
let substringReplacements = [] | ||
let startIndex | ||
while ((startIndex = substring.search(regex)) >= 0) { | ||
if (startIndex > 0) { | ||
substringReplacements.push(substring.substring(0, startIndex)) | ||
} | ||
let endIndex = startIndex + searchWordLength | ||
substringReplacements.push( | ||
<span | ||
key={++uidCounter} | ||
className={highlightClassNames} | ||
> | ||
{substring.substring(startIndex, endIndex)} | ||
</span> | ||
) | ||
substring = substring.substring(endIndex) | ||
} | ||
// If we didn't find any matches then just leave the current substring as-is. | ||
if (substringReplacements.length) { | ||
// Add any remaining text | ||
if (substring.length) { | ||
substringReplacements.push(substring) | ||
} | ||
// Replace the existing string element with the new mix of plain and styled elements. | ||
substrings.splice(i, 1, ...substringReplacements) | ||
} | ||
if (chunk.highlight) { | ||
return ( | ||
<mark | ||
className={highlightClassNames} | ||
key={index} | ||
style={highlightStyle} | ||
> | ||
{text} | ||
</mark> | ||
) | ||
} else { | ||
return ( | ||
<span key={index}>{text}</span> | ||
) | ||
} | ||
} | ||
return substrings | ||
}, [textToHighlight]) | ||
return ( | ||
<span> | ||
{content} | ||
})} | ||
</span> | ||
) | ||
} |
@@ -0,64 +1,98 @@ | ||
import React from 'react' | ||
import Highlighter from './Highlighter' | ||
import { render } from './test-utils' | ||
import expect from 'expect.js' | ||
describe('Highlighter', () => { | ||
function getHighlighterChildren (textToHighlight, searchWords, highlightClassName) { | ||
const instance = Highlighter({ textToHighlight, searchWords, highlightClassName }) | ||
const HIGHLIGHT_CLASS = 'customHighlightClass' | ||
const HIGHLIGHT_QUERY_SELECTOR = `.${HIGHLIGHT_CLASS}` | ||
return instance.props.children | ||
function getHighlighterChildren (textToHighlight, searchWords, highlightStyle) { | ||
const node = render( | ||
<div> | ||
<Highlighter | ||
highlightClassName={HIGHLIGHT_CLASS} | ||
highlightStyle={highlightStyle} | ||
searchWords={searchWords} | ||
textToHighlight={textToHighlight} | ||
/> | ||
</div> | ||
) | ||
return node.children[0] | ||
} | ||
it('should properly handle empty searchText', () => { | ||
expect(getHighlighterChildren('This is text', [])).to.eql(['This is text']) | ||
expect(getHighlighterChildren('This is text', [''])).to.eql(['This is text']) | ||
const emptyValues = [[], ['']] | ||
emptyValues.forEach((emptyValue) => { | ||
const node = getHighlighterChildren('This is text', emptyValue) | ||
expect(node.children.length).to.equal(1) | ||
expect(node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR).length).to.equal(0) | ||
expect(node.textContent).to.eql('This is text') | ||
}) | ||
}) | ||
it('should properly handle empty textToHighlight', () => { | ||
expect(getHighlighterChildren('', ['search']).length).to.eql([]) | ||
const node = getHighlighterChildren('', ['search']) | ||
expect(node.children.length).to.equal(0) | ||
expect(node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR).length).to.equal(0) | ||
expect(node.textContent).to.eql('') | ||
}) | ||
it('should highlight searchText words that exactly match words in textToHighlight', () => { | ||
const matches = getHighlighterChildren('This is text', ['text']) | ||
expect(matches.length).to.equal(2) | ||
expect(matches[0]).to.equal('This is ') | ||
expect(matches[1].props.children).to.equal('text') | ||
const node = getHighlighterChildren('This is text', ['text']) | ||
expect(node.children.length).to.equal(2) | ||
const matches = node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR) | ||
expect(matches.length).to.equal(1) | ||
expect(matches[0].textContent).to.eql('text') | ||
}) | ||
it('should highlight searchText words that partial-match text in textToHighlight', () => { | ||
const matches = getHighlighterChildren('This is text', ['Th']) | ||
expect(matches.length).to.equal(2) | ||
expect(matches[0].props.children).to.equal('Th') | ||
expect(matches[1]).to.equal('is is text') | ||
const node = getHighlighterChildren('This is text', ['Th']) | ||
expect(node.children.length).to.equal(2) | ||
const matches = node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR) | ||
expect(matches.length).to.equal(1) | ||
expect(matches[0].textContent).to.eql('Th') | ||
expect(node.children[0].textContent).to.equal('Th') | ||
expect(node.children[1].textContent).to.equal('is is text') | ||
}) | ||
it('should highlight multiple occurrences of a searchText word', () => { | ||
const matches = getHighlighterChildren('This is text', ['is']) | ||
expect(matches.length).to.equal(5) | ||
expect(matches[0]).to.equal('Th') | ||
expect(matches[1].props.children).to.equal('is') | ||
expect(matches[2]).to.equal(' ') | ||
expect(matches[3].props.children).to.equal('is') | ||
expect(matches[4]).to.equal(' text') | ||
const node = getHighlighterChildren('This is text', ['is']) | ||
expect(node.children.length).to.equal(5) | ||
expect(node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR).length).to.equal(2) | ||
expect(node.textContent).to.eql('This is text') | ||
expect(node.children[0].textContent).to.equal('Th') | ||
expect(node.children[1].textContent).to.equal('is') | ||
expect(node.children[2].textContent).to.equal(' ') | ||
expect(node.children[3].textContent).to.equal('is') | ||
expect(node.children[4].textContent).to.equal(' text') | ||
}) | ||
it('should highlight multiple searchText words', () => { | ||
const matches = getHighlighterChildren('This is text', ['This', 'text']) | ||
expect(matches.length).to.equal(3) | ||
expect(matches[0].props.children).to.equal('This') | ||
expect(matches[1]).to.equal(' is ') | ||
expect(matches[2].props.children).to.equal('text') | ||
const node = getHighlighterChildren('This is text', ['This', 'text']) | ||
expect(node.children.length).to.equal(3) | ||
expect(node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR).length).to.equal(2) | ||
expect(node.textContent).to.eql('This is text') | ||
expect(node.children[0].textContent).to.equal('This') | ||
expect(node.children[1].textContent).to.equal(' is ') | ||
expect(node.children[2].textContent).to.equal('text') | ||
}) | ||
it('should match terms in a case insensitive way but show their case-sensitive representation', () => { | ||
const matches = getHighlighterChildren('This is text', ['this']) | ||
expect(matches.length).to.equal(2) | ||
expect(matches[0].props.children).to.equal('This') | ||
expect(matches[1]).to.equal(' is text') | ||
const node = getHighlighterChildren('This is text', ['this']) | ||
const matches = node.querySelectorAll(HIGHLIGHT_QUERY_SELECTOR) | ||
expect(matches.length).to.equal(1) | ||
expect(matches[0].textContent).to.equal('This') | ||
}) | ||
it('should use the :highlightClassName if specified', () => { | ||
const matches = getHighlighterChildren('This is text', ['text'], 'customClass') | ||
expect(matches.length).to.equal(2) | ||
expect(matches[1].props.className).to.contain('customClass') | ||
const node = getHighlighterChildren('This is text', ['text']) | ||
expect(node.querySelector('mark').className).to.contain(HIGHLIGHT_CLASS) | ||
}) | ||
it('should use the :highlightStyle if specified', () => { | ||
const node = getHighlighterChildren('This is text', ['text'], { color: 'red' }) | ||
expect(node.querySelector('mark').style.color).to.contain('red') | ||
}) | ||
}) |
@@ -7,6 +7,21 @@ import React from 'react' | ||
return ( | ||
<div className={styles.demo}> | ||
<HighlighterExample/> | ||
<div> | ||
<div className={styles.headerRow}> | ||
<img | ||
className={styles.logo} | ||
width='482' | ||
height='278' | ||
src='https://cloud.githubusercontent.com/assets/29597/11903349/6da3d9c0-a56d-11e5-806d-1c7b96289523.png' | ||
/> | ||
</div> | ||
<div className={styles.body}> | ||
<div className={styles.card}> | ||
<HighlighterExample/> | ||
</div> | ||
</div> | ||
<div className={styles.footer}> | ||
react-highlight-words is available under the MIT license. | ||
</div> | ||
</div> | ||
) | ||
} |
@@ -9,2 +9,3 @@ /** | ||
import { render } from 'react-dom' | ||
import './index.css' | ||
@@ -11,0 +12,0 @@ render( |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1533589
27
18259
31