Skip to content

Instantly share code, notes, and snippets.

@maettig
Forked from 140bytes/LICENSE.txt
Created November 21, 2011 16:13
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save maettig/1383091 to your computer and use it in GitHub Desktop.
Save maettig/1383091 to your computer and use it in GitHub Desktop.
getElementsByClassName in 140byt.es

Several getElementsByClassName() prototype methods in about 140 bytes (more or less). Useful for Internet Explorer <9.0 and (few) other old web browsers that do not support HTML5. Done for 140byt.es.

All "full" versions perfectly match the W3C specification, as far as I know. They support multiple class names in every order and class names that start with or contain dashes or nonascii characters.

The short version (138 bytes) does not support searching for multiple class names and fails when the query string contains any whitespace character.

The "annotated" version is a compromise with a few restrictions (see the comment below). It supports searching for multiple class names and should work in most web browsers.

/**
* This is the long version with support for multiple classes, but it's simplified to make it as
* short as possible (137 bytes). It uses .all (should be supported by most modern web browsers for
* compatibility reasons) instead of .getElementsByTagName('*'). It does not support tab characters
* (neither in the query string nor in the class attributes) and fails if the query string starts
* with whitespace characters.
*/
function(a, b, c, d) //class names and three dummy arguments
{
c = []; //can't build a NodeList, use an Array
for (d in b = //iterate all properties in the NodeList
this. //to be used in a prototype
all //shortcut for getElementsByTagName('*')
)
(b[d].className || '' //for-in also returns "length", skip this
).match( //if the elements class attribute matches
a.replace(/(\S+) */g, //match names in query string, remove spaces
'(?=(^|.* )$1( |$))')) //all names become positive lookaheads
&& c.push(b[d]); //then append to the Array
return c //return the Array
}
// 167 bytes.
function(a,b,c,d){c=[];for(d in b=this.getElementsByTagName('*'))(b[d].className||'').match(a.replace(/\s*(\S+)\s*/g,'(?=(^|.*\\s)$1(\\s|$))'))&&c.push(b[d]);return c}
// 155 bytes. Works in IE9+ only.
function(a){return[].filter.call(this.getElementsByTagName('*'),function(b){return b.className.match(a.replace(/\s*(\S+)\s*/g,'(?=(^|.*\\s)$1(\\s|$))'))})}
// 72 bytes. Works in IE8+ only.
function(a){return this.querySelectorAll(a.replace(/\s+(?=\S)|^/g,'.'))}
function(a,b,c,d){c=[];for(d in b=this.getElementsByTagName('*'))(b[d].className||'').match('(^|\\s)'+a+'(\\s|$)')&&c.push(b[d]);return c}
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2011 Thiemo Mättig <http://maettig.com/>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
{
"name": "getElementsByClassName",
"description": "Prototype methods for Internet Explorer <9.0 and other old web browsers that do not support HTML5.",
"keywords": [
"class",
"dom",
"ie",
"prototype"
]
}
<!DOCTYPE HTML>
<div id="fragment">
<h1 class="findMe-not">Should not match</h1>
<h1 class="test findMe">Should match first</h1>
<p class="findmE">Should not match</p>
<p class="findMe" id="forInFailsInIE">Should match second</p>
<blockquote class="x-findMe">
<p class="findMeNot not">Should not match</p>
<p class="do findMe too">Should be red and third</p>
</blockquote>
</div>
<script type="text/javascript">
function highlight(node)
{
if (!node.getElementsByClassName) return;
var elements = node.getElementsByClassName('findMe');
for (var i = 0; i < elements.length; i++)
{
elements[i].style.background = 'yellow';
elements[i].firstChild.data += ' [' + (i + 1) + ']';
}
elements = node.getElementsByClassName('too findMe');
for (var i = 0; i < elements.length; i++)
{
elements[i].style.background = 'red';
}
}
var fragment = document.getElementById('fragment');
var clone = fragment.cloneNode(true);
fragment.parentNode.appendChild(clone);
highlight(fragment);
clone.constructor.prototype.getElementsByClassName = function(a, b, c, d)
{
c = [];
for (d in b = this.all)
(b[d].className || '').match(a.replace(/(\S+) */g, '(?=(^|.* )$1( |$))')) &&
c.push(b[d]);
return c
}
highlight(clone);
</script>
@maettig
Copy link
Author

maettig commented Nov 21, 2011

Oh, moving the RegExp(...) part into the loop where a is used saves 4 bytes. 2 bytes by moving the b=... into the for loop. 2 more by replacing if() with &&. 22 to go. A not so nice trick would be to split this into

function(a){return
function(a,b,c,d){c=[];for(d in b)d>=0&&RegExp(a.replace(/(\S+) */g,'(?=.*\\b$1\\b)')).test(b[d].className)&&c.push(b[d]);return c}
(a,this.getElementsByTagName('*'))}

and call the inner function 131 bytes (other 140byt.es snippets do similar by not counting essential strings or arrays) but I don't feel comfortable with this. I want a full replacement, including the possibility to search for multiple class names in any order. Looking for single class names only is possible in 137 bytes:

function(a,b,c,d){c=[];for(d in b=this.getElementsByTagName('*'))d>=0&&RegExp('\\b'+a+'\\b').test(b[d].className)&&c.push(b[d]);return c}

@jed
Copy link

jed commented Nov 22, 2011

still tinkering with it myself, but i wonder if you'd get further with a recursive approach like walk.

@jed
Copy link

jed commented Nov 22, 2011

i ran out of time, but came up with something like this (doesn't work yet):

function d(a,b,c){c||(b=this,c=[]);if(b){~(b.className||"").search(a.replace(/\s+/g,"|"))||c.push(b);d(a,b.firstChild,c);d(a,b.nextSibling,c)}return c}

@peterjaric
Copy link

I think you want this.document and && instead of || after the search:

function d(a,b,c){c||(b=this.document,c=[]);if(b){~(b.className||"").search(a.replace(/\s+/g,"|"))&&c.push(b);d(a,b.firstChild,c);d(a,b.nextSibling,c)}return c}

Try d('tag') on this page, though. It currently is too tolerant.

Edit: how do I format long lines of code? I can not make it look good...
Edit 2: Thank you, someone, for fixing my formatting :)

@maettig
Copy link
Author

maettig commented Nov 22, 2011

@snowlord, I fixed your formatting. this is for use in a prototype.

@jed, it's good to have a recursive approach for comparison but I'm afraid firstChild + nextSibling is longer than getElementsByTagName. And searching for multiple classes does not work as expected. But thanks to you (good to know: search and match do not search for strings, they do an implicit new RegExp) we are down at 155 bytes.

function(a,b,c,d){c=[];for(d in b=this.getElementsByTagName('*'))d>=0&&b[d].className.match(a.replace(/(\S+) */g,'(?=.*\\b$1\\b)'))&&c.push(b[d]);return c}

What I don't understand is your ||"". Is this necessary?

@peterjaric
Copy link

@maettig, Thanks!

About ||"": I thought that was because className may be undefined?

@peterjaric
Copy link

I'm going to take the opportunity to learn something. So are we supposed to add this function to the Element prototype, like this?

Element.prototype.getE = function...

Because, if I do, I get different results:

var a = document.firstChild.nextSibling;
a.getElementsByClassName('actions').length === a.getE('actions').length

-> false. What am I doing wrong?

@maettig
Copy link
Author

maettig commented Nov 22, 2011

Because my regular expression also returns elements with class="forms-actions". Oh no. This will make everything a lot more complicated... About className being undefined, let's take a look at the spec...

@peterjaric
Copy link

Yes, I wasn't very specific when I wrote "too tolerant", I guess.

Regarding className: even if the specification says that className always must be defined, are not the older browsers you are targeting a little more likely to not follow the specification?

@maettig
Copy link
Author

maettig commented Nov 22, 2011

Yes, that's right. The problem is, it's still to big and removing this saves 6 bytes. I looked at the specs but could not find anything. I will add this to my tests suite and report later. Update: It seems className.match never fails. We don't need to surround it with (...||''). Still very few data but I see green lights in my tests suite for IE6, IE7 and IE8.

@peterjaric
Copy link

While googling around a bit, I've come across statements that it is not possible to extend DOM prototypes in IE prior to version 8. Is that true?

However, in IE8, there is a querySelectorAll method of Element, that probably can be used to shorten this code quite much. Idea: Split classNames string on space, for each entry call this.querySelectorAll('.' + className) (using the String.replace method of course) and return the intersection. (http://caniuse.com/queryselector)

@peterjaric
Copy link

I write "intersection", because according to MDN: "Returns a set of elements which have all the given class names" (emphasis mine). (https://developer.mozilla.org/en/DOM/document.getElementsByClassName)

@peterjaric
Copy link

Here's a simple prototype:

function(names) {return this.querySelectorAll(names.replace(/(^\s*|\s+)/g, "."))}

Known issue: can't handle spaces at the end.

@atk
Copy link

atk commented Nov 23, 2011

@snowlord: that's fine for IE8, but IE7 has no querySelectorAll method.

@peterjaric
Copy link

@atk: That was why I wrote "However, in IE8, there is a querySelectorAll method" :)

But if it is true that you can not change the prototype of Element in IE7 and before (read my question above) then at least we have something for IE8.

@maettig
Copy link
Author

maettig commented Nov 23, 2011

@snowlord, this is really cool. The following is a full replacement for getElementsByClassName in 72 bytes, including multiple classes in every order, classes that start with or contain dashes or nonascii characters, and proper handling of white space including tabs. To bad this is useful for IE8 only, as said.

function(a){return this.querySelectorAll(a.replace(/\s+(?=\S)|^/g,'.'))}

65 bytes if you are sure you don't use tabs in your selector or spaces at the end.

function(a){return this.querySelectorAll(a.replace(/ +|^/g,'.'))}

It's true, IE7 does not know HTMLElement (neither does IE8), but I can do document.getElementsByClassName = .... Not perfect, but still helpful.

@tkissing
Copy link

Assuming a drop-in for [].filter
function(a) { return [].filter.call(this.getElementsByTagName('*'), function(b) { return b.className.match(a.replace(/(\S+) */g, '(?=(^|.* )$1( |$))')); }); }
Compresses to 147 bytes. So close and yet so far...

@tkissing
Copy link

At least for the test.html file, the replacement string can be shortened by 2 bytes to '(?=.* |^$1( |$))'

@maettig
Copy link
Author

maettig commented Nov 24, 2011

@tkissing, wow, another new trick learned. Thank you very much. I think that's what 140byt.es is all about, learning new tricks. Unfortunately filter seems to be IE9+ only. The regular expression does not make much sense. Sorry. It fails in many cases.

@tkissing
Copy link

edit: As @maettig pointed out below this is not a valid solution. Leaving it here just for educational purposes (or your amusement if you prefer)

a.replace(/(\S+) */g, '(?=.* |^$1$| )') still seems to work, leading to 143 bytes:
function(a){return[].filter.call(this.getElementsByTagName('*'),function(b){return b.className.match(a.replace(/(\S+) */g,'(?=.* |^$1$| )'))})}

@tkissing
Copy link

Ah, bummer about the regex. I just used the test.html for the gist and it works for that (in FF at least). Have not actually looked up what the spec says.
filter missing would be OK within the 140bytes rules I think (there is after all a 140byte solutions to add Array.prototype.filter)
Fully agree on the "learning new tricks" part :)

@maettig
Copy link
Author

maettig commented Nov 24, 2011

@tkissing, I'm sorry, but I don't think it's helpful to remove characters from a regular expression if you don't really understand what it does. What you did (amongst other side effects) is basically what I did now: I removed support for multiple classes. Now the short version searches for a single class only (spaces are forbidden in the query string). In return I removed all restrictions from the long version. Now it should perfectly match the specification.

@tkissing
Copy link

@maettig: Yeah, I was a bit too aggressive in removing characters and didn't think enough, blindly relying on the test.
I added <p class="DO findMeNot either">Should not match</p> to my local test.html as another negative test - just in case. Certainly learned something from it though: Better tests help, getting too excited doesn't :)

@peterjaric
Copy link

Running IE9 in 'Browser Mode: IE7' and 'Document Mode: IE7 standards' and doing this (adding the version in index-full.js) on this page:

document.getElementsByClassName = function(a,b,c,d){c=[];for(d in b=this.getElementsByTagName('*'))d>=0&&b[d].className.match(a.replace(/\s*(\S+)\s*/g,'(?=(^|.*\\s)$1(\\s|$))'))&&c.push(b[d]);return c}
document.getElementsByClassName('comment gist-comment').length

results in 0.

Running this in Chrome 15:

document.getElementsByClassName('comment gist-comment').length

Results in 23.

Should I have added it to something else than document? It works for single class names, though.

The mistake probably lies with me, but you have another test case, now :)

@maettig
Copy link
Author

maettig commented Nov 24, 2011

Wow, this is strange. The reason is the for-in loop I'm using. This approach fails in IE7 and IE8 for elements with an ID. Here is a simple test:

<ul>
  <li>Returns "0"
  <li id="identifier">Returns "1" in Firefox/Opera but "identifier" in IE7/IE8
</ul>
<script>
  var d, b = document.getElementsByTagName('UL')[0].getElementsByTagName('*');
  for (d in b)
    document.write('<li>"' + d + '" (' + (d >= 0 ? 'ok' : 'non-numeric') + ')');
</script>

I solved the problem by replacing my d>=0 (something like this is needed because the for-in loop also returns the "length" property) with the ||'' mentioned before. Same problem in @eliperelman's Array.filter.

@ybop
Copy link

ybop commented Nov 30, 2011

Given that the only browsers that don't natively implement getElementByClassName are old versions of IE I've used "all" instead of getElementsByTagName using only 132 bytes.

function(t,c)
{
  var e,Z=[],L=(e=t.all).length;
  while(L--)e[L].className.match(c.replace(/\s+(?=\S)|^/g,'.'))&&Z.push(e[L]);
  return Z;
}

But in the real world where speed counts I would use

function(t,c)
{
  if(t.getElementsByClassName)return t.getElementsByClassName(c);
  var e,Z=[],L=(e=t.all).length,R=new RegExp(c.replace(/\s+(?=\S)|^/g,'.'));
  while(L--)R.test(e[L].className)&&Z.push(e[L]);
  return Z;
}

...which blows out to 210 bytes but given that native implementations are typically around 500 times faster, it's well worth including the test. Also, moving the regex out of the loop improves the speed by at least 60%.

@maettig
Copy link
Author

maettig commented Dec 1, 2011

Nice, thanks. all also works in Opera. And Firefox. Really? Seems so. Now my annotated.js is 137 bytes. So, mission accomplished. An (almost) full replacement for getElementByClassName that works in IE6 and IE7 without depending on other code.

function(a,b,c,d){c=[];for(d in b=this.all)(b[d].className||'').match(a.replace(/(\S+) */g,'(?=(^|.* )$1( |$))'))&&c.push(b[d]);return c}

Please note you are using the wrong regular expression. That's the one designed for querySelectorAll. You are right about the speed but that's not what this challenge is about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment