Modify prototypes of every possible DOM element
Asked Answered
L

2

5

Updated title to better reflect what I'm trying to do.

In short, there are different constructors for different dom elements, and they don't seem to all share a common prototype. I'm looking for a way to add a function property to every DOM element by modifying these prototypes, but I'm not sure how to find them.

For example, I could do something like this:

function enhanceDom (tagNames, methods) {
  var i=-1, tagName;
  while (tagName=tagNames[++i]) {
    var tag=document.createElement(tagName);
    if (!(tag && tag.constructor)) continue;
    for (var methodName in methods) {
      tag.constructor.prototype[methodName]=methods[methodName];
    }
  }
}

var thingsToEnhance = ['a','abbr','acronym','address'/* on and on... */];

enhance(thingsToEnhance, {
  doStuff : function(){
    /* ... */
  },
  doOtherStuff : function(){
    /* ... */
  } 
  /* ... */
});

Of course, I'd like to do this without listing every single html element. Can anyone think of a better way?

(Original question follows)

Goal - make getElementsByClassName work on any DOM node in any browser.

It's been done before (sort of), but here's my shot at it.

The question I have is, is there a good way to make this work with dynamically created elements? It seems that HTML DOM elements don't share a common predictable prototype where getElementsByClassName could be added... Or am I missing something?

Here's what I've got so far (edit - updated per discussion).

(function(){

  var fn = 'getElementsByClassName'; 
  // var fn = 'gEBCN'; // test

  if (typeof document[fn] != 'undefined') return;

  // This is the part I want to get rid of...
  // Can I add getByClass to a single prototype
  // somewhere below Object and be done with it?

  document[fn]=getByClass;
  withDescendants(document, function (node) {
    node[fn]=getByClass;
  });

  function withDescendants (node, callback, userdata) {
    var nodes = node.getElementsByTagName('*'), i=-1;
    while (node=nodes[++i]) {
      callback(node, userdata);
    }
    return userdata;
  }

  function getByClass (className) {
    return withDescendants(this, getMatches, {
      query:new RegExp('(^|\\s+)' + className + '($|\\s+)'), 
      found:[]
    }).found;
  }

  function getMatches (node, data) {
    if (node.className && node.className.match(data.query)) {
      data.found.push(node);
    }
  }

}());

It works well on content loaded before the script loads, but new dynamically-created elements won't get a getElementsByClassName method. Any suggestions (besides setInterval, please)?

Labiate answered 6/9, 2010 at 19:15 Comment(14)
Is there some reason you've decided not to use a framework such as jQuery for this? If so, you might want to state that in your question or you'll get lots of "use jQuery!" answers.Parliamentarianism
I hope it will be obvious that I don't intend to use jQuery for this. Fingers crossed.Labiate
@Greg With no JQuery code in the posted code and the no presence of JQuery tag, i think is enough...Oxen
Perhaps you should have a look at Sizzle github.com/jeresig/sizzle/blob/master/sizzle.jsBridgers
@no: I don't understand, your code is encapsulated in a function, so I suppose you are calling it only ONCE. Obviously newly created DOM node won't have the getElementsByClassName function applied to them since they are created AFTER your code has already run. You would need to call your code again. Moreover I really don't understand why you walk through the DOM instead of simply getting all DOM elements using a getElementsByTagName('*'), this last one is even tremendously faster than walking recursively all DOM nodes like you do.Pyelitis
@Marco Demaio: it would only need to be called once if it could add 'getElementsById' to the prototypes of all dom element constructors. However, there seem to be many constructors for all the different elements, and I'm guessing they're going to vary somewhat from browser to browser. I'm trying to come up with a good way to find those prototypes in a cross-browser way and augment them. Good point about getElementsByTagName('*'), I never thought of using that... in fact I could use e.getElementsByTagName('*')||e.all and support everything back to IE4 I think :)Labiate
@no: if you plan to support anything before IE6 I can not be of any help. Supposing one day you change your mind and decide to support only browsers after IE6, another fix to your code could be using simply className attribute, this one is cross-browser. I don't understand the 4 OR you placed: node['class'] || node['className'] || ...Pyelitis
@no: on your last comment I think you meant 'getElementsByClassName' and not 'getElementById'. I think anyway there is no way, you could look how a framework like prototype.js do. It extends DOM but i think also in prototype.js you have to extend newly created DOM node or use the functions provided by the framework itself to create new DOM node that will be created in such case already extended. And using setInterval is completely INSANE!Pyelitis
@Caspar Kleijne: Sizzle looks good. Sly looks even better. I've just never had a use for queries of that complexity; I usually have enough control over the markup that I can give a relevant element an id or class. I'm playing with the idea of creating a compatibility library for certain browser features; this is kind of a test run. @Marco Demaio: I'm not sure what you mean; I never mentioned getElementById.Labiate
@Marco Demaio: also thanks for the tip on className, didn't realize it was so widely supported.Labiate
(insert obligatory "use jQuery!" answer here)Bawbee
If all you want is getting by classname and not a fancier selector library, consider re-using the getElementsByClassname function from YUI: developer.yahoo.com/yui/dom/#quickstart . It does not use the prototype and so avoids the whole issue of being available to dynamically created elements or not. It might be less sexy than using the prototype, but gets the job done :)Claire
I think my original post was unclear. Updated...Labiate
Did you take a look at what frameworks like MooTools do? MooTools adds functions to every element. mootools.net/docs/core/Element/ElementCholecystitis
F
5

I think what you want can be achieved by prototyping the Element interface, like

Element.prototype.getElementsByClassName = function() {
    /* do some magic stuff */
};

but don't do this. It doesn't work reliably in all major browsers.

What you're doing in the example in your question is not advisable, too. You're actually extending host objects. Again, please don't do this.

You'll fall in exactly those pitfalls Prototype ran into.

I don't want to merely copy Kangax' article, so please read What’s wrong with extending the DOM.

Why do you want this in the first place? What's your goal?

Fabliau answered 6/9, 2010 at 23:10 Comment(2)
Great article. I'm aware of some of the dangers of modifying host objects. The overall goal of this experiment is to create a compatibility layer so that all scripts can be tested on a single browser and work cross-browser without modification. Not create a new library, just to extend browsers that don't have certain features so they all work the same as the browser our developers test in. It would only be used for in-house stuff (some of which has already accumulated), not as a GP library.Labiate
Element or HTMLElement is probably what I was looking for. Too bad about the cross-browser support on that one. Marking this correct.Labiate
L
0

This seems to work, but it's ugly. I wonder if it works in IE?

(function(){

  enhanceDom('a abbr acronym address applet area b base basefont bdo big blockquote body br button caption center cite code col colgroup dd del dfn dir div dl dt em fieldset font form frame frameset h1 h2 h3 h4 h5 h6 head hr html i iframe img input ins isindex kbd label legend li link map menu meta noframes noscript object ol optgroup option p param pre q s samp script select small span strike strong style sub sup table tbody td textarea tfoot th thead title tr tt u ul var'
  ,{
    getElementsByClassName : getByClass
    /* , ... */
  });

  function enhanceDom (tagNames, methods) {
    var i=-1, tagName;
    if (tagNames==''+tagNames) {
      tagNames=tagNames.split(' ');
    }
    for (var methodName in methods) {
      setIfMissing(document, methodName, methods[methodName]);
      while (tagName=tagNames[++i]) {
        var tag=document.createElement(tagName);
        if (tag || !tag.constructor) continue;
        var proto=tag.constructor.prototype;
        setIfMissing(proto, methodName, methods[methodName]);
      }
    }
  }

  function setIfMissing (obj, prop, val) {
    if (typeof obj[prop] == 'undefined') {
      obj[prop]=val;
    }
  }

  function withDescendants (node, callback, userdata) {
    var nodes=node.getElementsByTagName('*'), i=-1;
    while (node=nodes[++i]) {
      callback(node, userdata);
    }
    return userdata;
  }

  function getByClass (className) {
    return withDescendants(this, getMatches, {
      query:new RegExp('(^|\\s+)' + className + '($|\\s+)'), 
      found:[]
    }).found;
  }

  function getMatches (node, data) {
    if (node.className && node.className.match(data.query)) {
      data.found.push(node);
    }
  }

}());
Labiate answered 7/9, 2010 at 0:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.