我发现给定的问题非常有趣。这是我想出的:
- 使用一些插件(或自己编写一个),以便我们能够在元素进入视图时得到通知
- 解析该元素 text-nodes 并使用从单词本身派生的 unqiue css-class 名称将每个单词包装到 span 元素中
- 添加为这些 unqiue 类名添加 css-rules 的功能
示例:http: //jsbin.com/welcome/44285/
该代码非常 hacky,并且仅在最新的 Chrome 中测试,但它对我有用,并且肯定可以在此基础上构建。
/**
* Highlighter factory
*
* @return Object
*/
function highlighter() {
var
me = {},
cssClassNames = {},
cssClassNamesCount = 0,
lastAddedRuleIndex,
cbCount = 0,
sheet;
// add a stylesheet if none present
if (document.styleSheets.length === 0) {
sheet = document.createElement('style');
$('head').append(sheet);
}
// get reference to the last stylesheet
sheet = document.styleSheets.item(document.styleSheets.length - 1);
/**
* Returns a constant but unique css class name for the given word
*
* @param String word
* @return String
*/
function getClassNameForWord(word) {
var word = word.toLowerCase();
return cssClassNames[word] = cssClassNames[word] || 'highlight-' + (cssClassNamesCount += 1);
}
/**
* Highlights the given list of words by adding a new css rule to the list of active
* css rules
*
* @param Array words
* @param String cssText
* @return void
*/
function highlight(words, cssText) {
var
i = 0,
lim = words.length,
classNames = [];
// get the needed class names
for (; i < lim; i += 1) {
classNames.push('.' + getClassNameForWord(words[i]));
}
// remove the previous added rule
if (lastAddedRuleIndex !== undefined) {
sheet.deleteRule(lastAddedRuleIndex);
}
lastAddedRuleIndex = sheet.insertRule(classNames.join(', ') + ' { ' + cssText + ' }', sheet.cssRules.length);
}
/**
* Calls the given function for each text node under the given parent element
*
* @param DomElement parentElement
* @param Function onLoad
* @param Function cb
* @return void
*/
function forEachTextNode(parentElement, onLoad, cb) {
var i = parentElement.childNodes.length - 1, childNode;
for (; i > -1; i -= 1) {
childNode = parentElement.childNodes[i];
if (childNode.nodeType === 3) {
cbCount += 1;
setTimeout(function (node) {
return function () {
cb(node);
cbCount -= 1;
if (cbCount === 0 && typeof onLoad === 'Function') {
onLoad(me);
}
};
}(childNode), 0);
} else if (childNode.nodeType === 1) {
forEachTextNode(childNode, cb);
}
}
}
/**
* replace each text node by span elements wrapping each word
*
* @param DomElement contextNode
* @param onLoad the parent element
* @return void
*/
function add(contextNode, onLoad) {
forEachTextNode(contextNode, onLoad, function (textNode) {
var
doc = textNode.ownerDocument,
frag = doc.createDocumentFragment(),
words = textNode.nodeValue.split(/(\W)/g),
i = 0,
lim = words.length,
span;
for (; i < lim; i += 1) {
if (/^\s*$/m.test(words[i])) {
frag.appendChild(doc.createTextNode(words[i]));
} else {
span = doc.createElement('span');
span.setAttribute('class', getClassNameForWord(words[i]));
span.appendChild(doc.createTextNode(words[i]));
frag.appendChild(span);
}
}
textNode.parentNode.replaceChild(frag, textNode);
});
}
// set public api and return created object
me.highlight = highlight;
me.add = add;
return me
}
var h = highlighter();
h.highlight(['Lorem', 'magna', 'gubergren'], 'background: yellow;');
// on ready
$(function ($) {
// using the in-view plugin (see the full code in the link above) here, to only
// parse elements that are actual visible
$('#content > *').one('inview', function (evt, visible) {
if (visible) {
h.add(this);
}
});
$(window).scroll();
});