Implement highlighting. Closes #69.

This commit is contained in:
Jake Harding
2013-07-10 00:37:53 -07:00
parent 101e276ca7
commit 0e10cd1757
3 changed files with 212 additions and 0 deletions

82
src/highlight.js Normal file
View File

@@ -0,0 +1,82 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead
* Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
*/
// inspired by https://github.com/jharding/bearhug
var highlight = (function(doc) {
var defaults = {
node: null,
pattern: null,
tagName: 'strong',
className: null,
wordsOnly: false,
caseSensitive: false
};
return function hightlight(o) {
var regex;
o = utils.mixin({}, defaults, o);
if (!o.node || !o.pattern) {
throw new Error('both node and pattern must be set');
}
// support wrapping multiple patterns
o.pattern = utils.isArray(o.pattern) ? o.pattern : [o.pattern];
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
traverse(o.node, hightlightTextNode);
function hightlightTextNode(textNode) {
var match, patternNode;
if (match = regex.exec(textNode.data)) {
wrapperNode = doc.createElement(o.tagName);
o.className && (wrapperNode.className = o.className);
patternNode = textNode.splitText(match.index);
patternNode.splitText(match[0].length);
wrapperNode.appendChild(patternNode.cloneNode(true));
textNode.parentNode.replaceChild(wrapperNode, patternNode);
}
return !!match;
}
function traverse(el, hightlightTextNode) {
var childNode, TEXT_NODE_TYPE = 3;
for (var i = 0; i < el.childNodes.length; i++) {
childNode = el.childNodes[i];
if (childNode.nodeType === TEXT_NODE_TYPE) {
i += hightlightTextNode(childNode) ? 1 : 0;
}
else {
traverse(childNode, hightlightTextNode);
}
}
}
};
function getRegex(patterns, caseSensitive, wordsOnly) {
var escapedPatterns = [], regexStr;
for (var i = 0; i < patterns.length; i++) {
escapedPatterns.push(utils.escapeRegExChars(patterns[i]));
}
regexStr = wordsOnly ?
'\\b(' + escapedPatterns.join('|') + ')\\b' :
'(' + escapedPatterns.join('|') + ')';
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, 'i');
}
})(window.document);

View File

@@ -19,6 +19,7 @@ var SectionView = (function() {
// tracks the last query the section was updated for
this.query = null;
this.highlight = !!o.highlight;
this.dataset = o.dataset;
this.templates = o.templates || {};
@@ -51,6 +52,7 @@ var SectionView = (function() {
this.clear();
this.$el.append($suggestions);
this.highlight && highlight({ node: $suggestions[0], pattern: query });
// TODO: render header and footer
this.trigger('rendered');

128
test/highlight_spec.js Normal file
View File

@@ -0,0 +1,128 @@
describe('highlight', function() {
it('should throw an error if node is not set', function() {
expect(init).toThrow();
function init() { highlight({ pattern: 'abc' }); }
});
it('should throw an error if pattern is not set', function() {
expect(init).toThrow();
function init() { highlight({ node: document.createElement('div') }); }
});
it('should allow tagName to be specified', function() {
var before = 'abcde',
after = 'a<span>bcd</span>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', tagName: 'span' });
expect(testNode.innerHTML).toEqual(after);
});
it('should allow className to be specified', function() {
var before = 'abcde',
after = 'a<strong class="one two">bcd</strong>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', className: 'one two' });
expect(testNode.innerHTML).toEqual(after);
});
it('should be case insensitive by default', function() {
var before = 'ABCDE',
after = 'A<strong>BCD</strong>E',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd' });
expect(testNode.innerHTML).toEqual(after);
});
it('should support case sensitivity', function() {
var before = 'ABCDE',
after = 'ABCDE',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', caseSensitive: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support words only matching', function() {
var before = 'tone one phone',
after = 'tone <strong>one</strong> phone',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'one', wordsOnly: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support matching multiple patterns', function() {
var before = 'tone one phone',
after = '<strong>tone</strong> one <strong>phone</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['tone', 'phone'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should support regex chars in the pattern', function() {
var before = '*.js when?',
after = '<strong>*.</strong>js when<strong>?</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['*.', '?'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should work on complex html structures', function() {
var before = [
'<div>abcde',
'<span>abcde</span>',
'<div><p>abcde</p></div>',
'</div>'
].join(''),
after = [
'<div><strong>abc</strong>de',
'<span><strong>abc</strong>de</span>',
'<div><p><strong>abc</strong>de</p></div>',
'</div>'
].join(''),
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html tags and attributes', function() {
var before = '<span class="class"></span>',
after = '<span class="class"></span>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['span', 'class'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should not match across tags', function() {
var before = 'a<span>b</span>c',
after = 'a<span>b</span>c',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html comments', function() {
var before = '<!-- abc -->',
after = '<!-- abc -->',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
function buildTestNode(content) {
var node = document.createElement('div');
node.innerHTML = content;
return node;
}
});