How does jQuery’s .text() work, internally? - javascript

I quickly tried to find the implementation in jQuery’s source, but only found this which doesn’t actually seem to define it completely.
From the jQuery Source
jQuery.fn.extend({
text: function( text ) {
if ( jQuery.isFunction(text) ) {
return this.each(function() {
return jQuery(this).text( text.call(this) );
});
}
if ( typeof text !== "object" && text !== undefined ) {
return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
}
return jQuery.getText( this );
},
Anyone know?
Clarification:
I know how to use it. I just want to know how to get the text of an element à la jQuery when jQuery isn’t available.

jQuery.fn.text can be used for 3 different purposes, as the source you pasted clearly shows. The case you're looking for, is the third one - returning the text value of an element.
jQuery uses jQuery.text() method to get the text value of an element, and jQuery.text points to Sizzle.getText()
jQuery.text = Sizzle.getText;
And here's the definition of getText function.
// Utility function for retreiving the text value of an array of DOM nodes
Sizzle.getText = function( elems ) {
var ret = "", elem;
for ( var i = 0; elems[i]; i++ ) {
elem = elems[i];
// Get the text from text nodes and CDATA nodes
if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
ret += elem.nodeValue;
// Traverse everything else, except comment nodes
} else if ( elem.nodeType !== 8 ) {
ret += Sizzle.getText( elem.childNodes );
}
}
return ret;
};
Working example: http://jsfiddle.net/cBsDN/

var text = element.innerText || element.textContent;
Example: http://jsfiddle.net/XnL7H/1/

It uses the Sizzle getText method:
// Utility function for retreiving the text value of an array of DOM nodes
Sizzle.getText = function( elems ) {
var ret = "", elem;
for ( var i = 0; elems[i]; i++ ) {
elem = elems[i];
// Get the text from text nodes and CDATA nodes
if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
ret += elem.nodeValue;
// Traverse everything else, except comment nodes
} else if ( elem.nodeType !== 8 ) {
ret += Sizzle.getText( elem.childNodes );
}
}
return ret;
};
http://sizzlejs.com/

Assuming you know how to get an element in JavaScript with out jQuery.
var el = document.getElementById("my-element");
And then you just have to use two properties that are available for each element innerText and innerHTML. So to get the text you use:
var text = el.innerText;
Or to get HTML, you do:
var html = el.innerHTML;

If you know the different of createTextNode and createElement, you can understand how to jquery.text work.
text function create a DOM text node. The browser will tread the value of node as text.

Related

Why am I getting "Uncaught TypeError: Cannot read property 'nodeName' of null" in my script?

I am using the imageUploaded.js library and I am getting an error in the chrome dev.
I have pasted a larger snippet of the code below, but the line that is highlighted in chrome for the error is:
if ( elem.nodeName === 'img' ) {
I understand basic js, but can't follow exactly what's happening in this library, so I'm not sure what this error means or how to fix it.
ImagesLoaded.prototype = new EventEmitter();
ImagesLoaded.prototype.options = {};
ImagesLoaded.prototype.getImages = function() {
this.images = [];
// filter & find items if we have an item selector
for ( var i=0, len = this.elements.length; i < len; i++ ) {
var elem = this.elements[i];
// filter siblings
if ( elem.nodeName === 'img' ) {
this.addImage( elem );
}
// find children
// no non-element nodes, #143
var nodeType = elem.nodeType;
if ( !nodeType || !( nodeType === 1 || nodeType === 9 || nodeType === 11 ) ) {
continue;
}
var childElems = elem.querySelectorAll('img');
// concat childElems to filterFound array
for ( var j=0, jLen = childElems.length; j < jLen; j++ ) {
var img = childElems[j];
this.addImage( img );
}
}
};
For some reason, the element's value is null, so it doesn't have nodeName property. Without seeing more code, or the library, it's hard to tell what's going on, but you can avoid errors like these by checking first for the value of element itself and then the property:
if ( elem && elem.nodeName === 'img' ) {
...
}
What this code does: if elem is falsy (which includes null) it will automatically discard the condition as false (that's how logical AND behaves). Otherwise, it will go ahead and check the nodeName just like your original code did.

Replace text in the middle of a TextNode with an element

I want to insert html tags within a text node with TreeWalker, but TreeWalker forces my html brackets into & lt; & gt; no matter what I've tried. Here is the code:
var text;
var tree = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
tree.currentNode.nodeValue = text;
}
Using \< or " instead of ' won't help. My workaround is to copy all of the DOM tree to a string and to replace the html body with that. It works on very simple webpages and solves my first problem, but is a bad hack and won't work on anything more than a trivial page. I was wondering if I could just work straight with the text node rather than use a workaround. Here is the code for the (currently buggy) workaround:
var text;
var newHTML = "";
var tree = document.createTreeWalker(document.body);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
if (tree.currentNode.nodeType == 3){
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
}
newHTML += text
}
document.body.innerHTML = newHTML;
Edit: I realize a better workaround would be to custom tag the text nodes ((Customtag_Start_Here) etc.), copy the whole DOM to a string, and use my customs tags to identify text nodes and modify them that way. But if I don't have to, I'd rather not.
To 'change' a text node into an element, you must replace it with an element. For example:
var text = tree.currentNode;
var el = document.createElement('foo');
el.setAttribute('bar','yes');
text.parentNode.replaceChild( el, text );
If you want to retain part of the text node, and inject an element "in the middle", you need to create another text node and insert it and the element into the tree at the appropriate places in the tree.
Edit: Here's a function that might be super useful to you. :)
Given a text node, it runs a regex on the text values. For each hit that it finds it calls a custom function that you supply. If that function returns a string, then the match is replaced. However, if that function returns an object like:
{ name:"element", attrs{onmouseover:"sendWord('foo')"}, content:"foo" }
then it will split the text node around the match and inject an element in that location. You can also return an array of strings or those objects (and can recursively use arrays, strings, or objects as the content property).
Demo: http://jsfiddle.net/DpqGH/8/
function textNodeReplace(node,regex,handler) {
var mom=node.parentNode, nxt=node.nextSibling,
doc=node.ownerDocument, hits;
if (regex.global) {
while(node && (hits=regex.exec(node.nodeValue))){
regex.lastIndex = 0;
node=handleResult( node, hits, handler.apply(this,hits) );
}
} else if (hits=regex.exec(node.nodeValue))
handleResult( node, hits, handler.apply(this,hits) );
function handleResult(node,hits,results){
var orig = node.nodeValue;
node.nodeValue = orig.slice(0,hits.index);
[].concat(create(mom,results)).forEach(function(n){
mom.insertBefore(n,nxt);
});
var rest = orig.slice(hits.index+hits[0].length);
return rest && mom.insertBefore(doc.createTextNode(rest),nxt);
}
function create(el,o){
if (o.map) return o.map(function(v){ return create(el,v) });
else if (typeof o==='object') {
var e = doc.createElementNS(o.namespaceURI || el.namespaceURI,o.name);
if (o.attrs) for (var a in o.attrs) e.setAttribute(a,o.attrs[a]);
if (o.content) [].concat(create(e,o.content)).forEach(e.appendChild,e);
return e;
} else return doc.createTextNode(o+"");
}
}
It's not quite perfectly generic, as it does not support namespaces on attributes. But hopefully it's enough to get you going. :)
You would use it like so:
findAllTextNodes(document.body).forEach(function(textNode){
replaceTextNode( textNode, /\b\w+/g, function(match){
return {
name:'element',
attrs:{onmouseover:"sendWord('"+match[0]+"')"},
content:match[0]
};
});
});
function findAllTextNodes(node){
var walker = node.ownerDocument.createTreeWalker(node,NodeFilter.SHOW_TEXT);
var textNodes = [];
while (walker.nextNode())
if (walker.currentNode.parentNode.tagName!='SCRIPT')
textNodes.push(walker.currentNode);
return textNodes;
}
or if you want something closer to your original regex:
replaceTextNode( textNode, /(^|\W)(\w+)/g, function(match){
return [
match[1], // might be an empty string
{
name:'element',
attrs:{onmouseover:"sendWord('"+match[2]+"')"},
content:match[2]
}
];
});
Function that returns the parent element of any text node including partial match of passed string:
function findElByText(text, mainNode) {
let textEl = null;
const traverseNodes = function (n) {
if (textEl) {
return;
}
for (var nodes = n.childNodes, i = nodes.length; i--;) {
if (textEl) {
break;
}
var n = nodes[i], nodeType = n.nodeType;
// Its a text node, check if it matches string
if (nodeType == 3) {
if (n.textContent.includes(text)) {
textEl = n.parentElement;
break;
}
}
else if (nodeType == 1 || nodeType == 9 || nodeType == 11) {
traverseNodes(n);
}
}
}
traverseNodes(mainNode);
return textEl;
}
Usage:
findElByText('Some string in document', document.body);

How to check if an element matches the :empty pseudo-class in pure JS?

How can I check, in pure JavaScript (no jQuery, no libraries), if a given HTML element is empty? My definition of "empty" is the same as the CSS :empty pseudo-class. So, if a given element would match the :empty selector, then I want to know about it.
function isEmpty (el) {
if (!el.hasChildNodes()) return true;
for (var node = el.firstChild; node = node.nextSibling;) {
var type = node.nodeType;
if (type === 1 && !isEmpty(node) || // another element
type === 3 && node.nodeValue) { // text node
return false;
}
}
return true;
}
As per the CSS spec, this will return true if the given element has no non-empty child nodes. Long-awaited JSFiddle demo available.
A way to ensure spec compliance is to use .querySelectorAll or .matches.
function matches(el, selector) {
return !!~[].indexOf.call(el.ownerDocument.querySelectorAll(selector));
}
Depending on browser support needs, .matches is more direct and even works on detached nodes:
function matches(el, selector) {
return !!(el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector || el.oMatchesSelector).call(el, selector);
}
Use either of those like:
matches(el, ':empty')
As far as I know, jQuery's implementation of :empty matches the spec exactly, so it can be used as a reference. From the latest version as of this writing:
"empty": function( elem ) {
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
// not comment, processing instructions, or others
// Thanks to Diego Perini for the nodeName shortcut
// Greater than "#" means alpha characters (specifically not starting with "#" or "?")
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
if ( elem.nodeName > "#" || elem.nodeType === 3 || elem.nodeType === 4 ) {
return false;
}
}
return true;
}
I think the simplest and easiest way to do this is :
elem = document.getElemntById('id_of_the_elem');
function isEmpty(elem){
if(elem.innerHTML=='') console.log("empty tag");
else console.log('Non empty tag');
}

Regexp to wrap each word on HTML page

Is it possible to wrap each word on HTML page with span element?
I'm trying something like
/(\s*(?:<\/?\w+[^>]*>)|(\b\w+\b))/g
but results far from what I need.
Thanks in advance!
Well, I don't ask for the reason, you could do it like this:
function getChilds( nodes ) {
var len = nodes.length;
while( len-- ) {
if( nodes[len].childNodes && nodes[len].childNodes.length ) {
getChilds( nodes[len].childNodes );
}
var content = nodes[len].textContent || nodes[len].text;
if( nodes[len].nodeType === 3 ) {
var parent = nodes[len].parentNode,
newstr = content.split(/\s+/).forEach(function( word ) {
var s = document.createElement('span');
s.textContent = word + ' ';
parent.appendChild(s);
});
parent.removeChild( nodes[len] );
}
};
}
getChilds( document.body.childNodes );
Even tho I have to admit I didn't test the code yet. That was just the first thing which came to my mind. Might be buggy or screw up completely, but for that case I know the gentle and kind stackoverflow community will kick my ass and downvote like hell :-p
You're going to have to get down to the "Text" nodes to make this happen. Without making it specific to a tag, you really to to traverse every element on the page, wrap it, and re-append it.
With that said, try something like what a garble post makes use of (less making fitlers for words with 4+ characters and mixing the letters up).
To get all words between span tags from current page, you can use:
var spans = document.body.getElementsByTagName('span');
if (spans)
{
for (var i in spans)
{
if (spans[i].innerHTML && !/[^\w*]/.test(spans[i].innerHTML))
{
alert(spans[i].innerHTML);
}
}
}
else
{
alert('span tags not found');
}
You should probably start off by getting all the text nodes in the document, and working with their contents instead of on the HTML as a plain string. It really depends on the language you're working with, but you could usually use a simple XPath like //text() to do that.
In JavaScript, that would be document.evaluate('//text()', document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null), than iterating over the results and working with each text node separately.
See demo
Here's how I did it, may need some tweaking...
var wrapWords = function(el) {
var skipTags = { style: true, script: true, iframe: true, a: true },
child, tag;
for (var i = el.childNodes.length - 1; i >= 0; i--) {
child = el.childNodes[i];
if (child.nodeType == 1) {
tag = child.nodeName.toLowerCase();
if (!(tag in skipTags)) { wrapWords(child); }
} else if (child.nodeType == 3 && /\w+/.test(child.textContent)) {
var si, spanWrap;
while ((si = child.textContent.indexOf(' ')) >= 0) {
if (child != null && si == 0) {
child.splitText(1);
child = child.nextSibling;
} else if (child != null) {
child.splitText(si);
spanWrap = document.createElement("span");
spanWrap.innerHTML = child.textContent;
child.parentNode.replaceChild(spanWrap, child);
child = spanWrap.nextSibling;
}
}
if (child != null) {
spanWrap = document.createElement("span");
spanWrap.innerHTML = child.textContent;
child.parentNode.replaceChild(spanWrap, child);
}
}
}
};
wrapWords(document.body);
See demo

What's a good alternative to HTML rewriting?

Consider this document fragment:
<div id="test">
<h1>An article about John</h1>
<p>The frist paragraph is about John.</p>
<p>The second paragraph contains a link to John's CV.</p>
<div class="comments">
<h2>Comments to John's article</h2>
<ul>
<li>Some user asks John a question.</li>
<li>John responds.</li>
</ul>
</div>
</div>
I would like to replace every occurrence of the string "John" with the string "Peter". This could be done via HTML rewriting:
$('#test').html(function(i, v) {
return v.replace(/John/g, 'Peter');
});
Working demo: http://jsfiddle.net/v2yp5/
The above jQuery code looks simple and straight-forward, but this is deceiving because it is a lousy solution. HTML rewriting recreates all the DOM nodes inside the #test DIV. Subsequently, changes made on that DOM subtree programmatically (for instance "onevent" handlers), or by the user (entered form fields) are not preserved.
So what would be an appropriate way to perform this task?
How about a jQuery plugin version for a little code reduction?
http://jsfiddle.net/v2yp5/4/
jQuery.fn.textWalk = function( fn ) {
this.contents().each( jwalk );
function jwalk() {
var nn = this.nodeName.toLowerCase();
if( nn === '#text' ) {
fn.call( this );
} else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
$(this).contents().each( jwalk );
}
}
return this;
};
$('#test').textWalk(function() {
this.data = this.data.replace('John','Peter');
});
Or do a little duck typing, and have an option to pass a couple strings for the replace:
http://jsfiddle.net/v2yp5/5/
jQuery.fn.textWalk = function( fn, str ) {
var func = jQuery.isFunction( fn );
this.contents().each( jwalk );
function jwalk() {
var nn = this.nodeName.toLowerCase();
if( nn === '#text' ) {
if( func ) {
fn.call( this );
} else {
this.data = this.data.replace( fn, str );
}
} else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
$(this).contents().each( jwalk );
}
}
return this;
};
$('#test').textWalk(function() {
this.data = this.data.replace('John','Peter');
});
$('#test').textWalk( 'Peter', 'Bob' );
You want to loop through all child nodes and only replace the text nodes. Otherwise, you may match HTML, attributes or anything else that is serialised. When replacing text, you want to work with the text nodes only, not the entire HTML serialised.
I think you already know that though :)
Bobince has a great piece of JavaScript for doing that.
I needed to do something similar, but I needed to insert HTML markup. I started from the answer by #user113716 and made a couple modifications:
$.fn.textWalk = function (fn, str) {
var func = jQuery.isFunction(fn);
var remove = [];
this.contents().each(jwalk);
// remove the replaced elements
remove.length && $(remove).remove();
function jwalk() {
var nn = this.nodeName.toLowerCase();
if (nn === '#text') {
var newValue;
if (func) {
newValue = fn.call(this);
} else {
newValue = this.data.replace(fn, str);
}
$(this).before(newValue);
remove.push(this)
} else if (this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea') {
$(this).contents().each(jwalk);
}
}
return this;
};
There are a few implicit assumptions:
you are always inserting HTML. If not, you'd want to add a check to avoid manipulating the DOM when not necessary.
removing the original text elements isn't going to cause any side effects.
Slightly less intrusive, but not necessarily any more performant, is to select elements which you know only contain text nodes, and use .text(). In this case (not a general-purpose solution, obviously):
$('#test').find('h1, p, li').text(function(i, v) {
return v.replace(/John/g, 'Peter');
});
Demo: http://jsfiddle.net/mattball/jdc87/ (type something in the <input> before clicking the button)
This is how I would do it:
var textNodes = [], stack = [elementWhoseNodesToReplace], c;
while(c = stack.pop()) {
for(var i = 0; i < c.childNodes.length; i++) {
var n = c.childNodes[i];
if(n.nodeType === 1) {
stack.push(n);
} else if(n.nodeType === 3) {
textNodes.push(n);
}
}
}
for(var i = 0; i < textNodes.length; i++) textNodes[i].parentNode.replaceChild(document.createTextNode(textNodes[i].nodeValue.replace(/John/g, 'Peter')), textNodes[i]);
Pure JavaScript and no recursion.
You could wrap every textual instance that is variable (e.g. "John") in a span with a certain CSS class, and then do a .text('..') update on all those spans. Seems less intrusive to me, as the DOM isn't really manipulated.
<div id="test">
<h1>An article about <span class="name">John</span></h1>
<p>The frist paragraph is about <span class="name">John</span>.</p>
<p>The second paragraph contains a link to <span class="name">John</span>'s CV.</p>
<div class="comments">
<h2>Comments to <span class="name">John</span>'s article</h2>
<ul>
<li>Some user asks <span class="name">John</span> a question.</li>
<li><span class="name">John</span> responds.</li>
</ul>
</div>
</div>
$('#test .name').text(function(i, v) {
return v.replace(/John/g, 'Peter');
});
Another idea is to use jQuery Templates. It's definitely intrusive, as it has its way with the DOM and makes no apologies for it. But I see nothing wrong with that... I mean you're basically doing client-side data binding. So that's what the templates plugin is for.
This seems to work (demo):
$('#test :not(:has(*))').text(function(i, v) {
return v.replace(/John/g, 'Peter');
});
The POJS solution offered is ok, but I can't see why recursion is avoided. DOM nodes are usually not nested too deeply so it's fine I think. I also think it's much better to build a single regular expression than use a literal and build the expression on every call to replace.
// Repalce all instances of t0 in text descendents of
// root with t1
//
function replaceText(t0, t1, root) {
root = root || document;
var node, nodes = root.childNodes;
if (typeof t0 == 'string') {
t0 = new RegExp(t0, 'g');
}
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
if (node.nodeType == 1) {
arguments.callee(t0, t1, node);
} else if (node.nodeType == 3) {
node.data = node.data.replace(t0, t1);
}
}
}

Categories

Resources