I found this problem very interesting. Here is what I came up with:
- use some plugin (or write it yourself) so that we can be notified when an element appears in the view
- parse these elements with text nodes and wrap each word in a span using the unciue name of the css class derived from the word itself
- add the ability to add css rules for these unqiue class names
sample: http://jsbin.com/welcome/44285/
The code is very hacked and only testet in the latest Chrome, but it worked for me and, of course, you can rely on.
/** * 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(); });
Yoshi source share