Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

RFC: Building the Perfect Tabbed Pane (an tutorial article)

2 views
Skip to first unread message

Peter Michaux

unread,
Feb 12, 2008, 10:26:07 PM2/12/08
to
I'm writing a blog article about building a decent tabbed pane widget
for a web page on the general web (anyone with a browser and internet
connection has access). I've never seen an article that deals with all
of the feature testing and sequential rendering issues and I think an
article like this is needed. If a copy of the article would be agreed
upon and beneficial to the jibbering site then I would support that.
It isn't exactly an easy task to write a widget for thousands of
possible browser versions and configurations. I can understand why
people give up when handed this task and only support recent browsers
and use sniffing. More written about building widgets in such a harsh
environment would help, I am sure.

Below I've included the four files involved in the example:

- index.html
- tabbedPane.js
- tabbedPane.css
- lib.js

The example is also temporarily available on the web

<URL: http://peter.michaux.ca/temp/tabbedPane/index.html>

The example code is not finished. Any feedback would be greatly
appreciated.

The goal of the HTML mark up was to make it as easy as possible for
the content author. I don't like how many tabbed pane widgets require
a ul element at the top and then the content author must connect the
tab with the pane using some convention for element id attributes.
That is far more difficult than it should be for the content author. I
know first hand that making it easier for them is greatly appreciated
and also easier on me.

I've feature tested almost maximally. All host objects are tested and
any language feature that isn't ancient is tested. The objective is to
have all the code run in IE4+ and NN4+ browsers without throwing any
errors: syntax or runtime.

I've tried to be relatively modern by including a css selector query
function. Surely this task could be done without such a function but
to a certain degree I am trying to simulate how developers use
libraries. When more widgets are in a page the cost of the library
size is not so great.

The events part of the library is a problem area for several reasons.
A big reason is due to my ignorance about screen readers. If a blind
visitor with a browser that has JavaScript enabled is using a screen
reader, do the tab elements need to have anchor elements so that the
screen reader reads the tabs. If anchor elements are required then I
want to use preventDefault to stop the browser from changing the URL
and even potentially reloading the page when a tab is clicked. Safari
1.9 and early v2 releases didn't support preventDefault when the
callback function is attached with addEventListener. If DOM0 handler
is used as a workaround I need to know that it will work before
manipulating the DOM into a tabbed pane. A feature test for DOM0 is
not so easy (although David Mark suggested one that works in at least
Firefox.)

There are many ways to "architect" the actual mechanics that run the
tabbed pane after it is all set up. I decided to go with a closure
system. This was just a choice. The mechanics that run this example
feel super light weight to me: just a little closure for each tab that
knows it's tab and pane and the current tab and pane. It feels like
the sports car version to me. OOP versions are good for some project
specs but feel like tanks. I may write and article that looks at
different code designs for different design requirements.

I look forward to any suggestions.

Thanks,
Peter


// index.html --------------------------------------------------

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">

<html>
<head>

<title>Page Title</title>

<script src="lib.js" type="text/javascript"></script>
<script src="tabbedPane.js" type="text/javascript"></script>

</head>
<body>

<div class="sections">

<div class="section first">
<h2>One</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation.
</p>
</div>

<div class="section">
<h2>Two</h2>
<p>
Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur.
</p>
</div>

<div class="section">
<h2>Three</h2>
<p>
Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>

</div>

</body>
</html>


// tabbedPane.js --------------------------------------------------

// Test what can be tested as soon as possible
// Check that the library loaded
if (typeof LIB != 'undefined' &&
// Test for library functions I use directly
LIB.getAnElement &&
LIB.getDocumentElement &&
LIB.isRealObjectProperty &&
LIB.isHostMethod &&
LIB.arrayFilter &&
LIB.arrayForEach &&
LIB.addListener &&
LIB.querySelector &&
LIB.hasClass &&
LIB.removeClass &&
LIB.addClass &&
// Test for host objects and methods I use directly
LIB.isRealObjectProperty(this, 'window') &&
LIB.isRealObjectProperty(this, 'document') &&
LIB.isHostMethod(this.document, 'write') &&
LIB.isHostMethod(this.document, 'createElement') &&
(function() {
var el = LIB.getAnElement();
return LIB.isHostMethod(el, 'appendChild') &&
LIB.isHostMethod(el, 'insertBefore') &&
LIB.isRealObjectProperty(el, 'firstChild') &&
LIB.isRealObjectProperty(el, 'childNodes') &&
typeof el.innerHTML == 'string';
})()) {

(function() {

var doc = this.document;

var cc = 'current'; // TODO

var enliven = function(c, t, p) {
LIB.addListener(t, 'click', function() {
// TODO prevent default and use anchor elements for tabs
// TODO what about keyboard accessibility?

// avoid potential flicker if user clicks current tab
if (t == c.t) {
return;
}
LIB.removeClass(c.t, cc);
LIB.removeClass(c.p, cc);
c.t = t;
c.p = p;
LIB.addClass(t, cc);
LIB.addClass(p, cc);
});
};

var init = function(w) {
var ts = doc.createElement('ul'),
first = true,
c,
t,
h,
t;
LIB.addClass(ts, 'tabs');
LIB.arrayForEach(
LIB.arrayFilter(
w.childNodes,
function(s) {
return LIB.hasClass(s, 'section');
}),
function(p) {
t = doc.createElement('li');
if (first) {
c = {t:t, p:p};
LIB.addClass(t, cc);
LIB.addClass(p, cc);
first = false;
}
enliven(c, t, p);
h = LIB.querySelector('h2', p)[0];
t.innerHTML = h ? h.innerHTML : 'tab';
ts.appendChild(t);
});
w.insertBefore(ts, w.firstChild);
};

// Test that a pane really is out of the page
// when it is not current and that it has some
// height when it is current. This is the critical
// test to make sure the tabbed pane will work.
var supportsDisplayCss = function() {
var middle = doc.createElement('div');

if (LIB.isRealObjectProperty(doc, 'body') &&
LIB.isHostMethod(doc.body, 'removeChild') &&
typeof middle.offsetHeight == 'number') {

var outer = doc.createElement('div'),
inner = doc.createElement('div');

LIB.addClass(outer, 'tabbedPanesEnabled');
LIB.addClass(middle, 'sections');
LIB.addClass(inner, 'section');
inner.innerHTML = '.';
middle.appendChild(inner);
outer.appendChild(middle);
doc.body.appendChild(outer);
var h = outer.offsetHeight;
LIB.addClass(inner, 'current')
var doesSupport = (h == 0 && outer.offsetHeight > 0);
doc.body.removeChild(outer);
return doesSupport;
}
return false;
};

// We don't know for sure at this point that the tabbed pane
// will work. We have to wait for window.onload to finish
// the tests. We do know we can give the pages some style to use
// during the page load because we can "get out of trouble"
// when window.onload fires. This is
// because the functions used to get out of trouble
// have been feature tested.
doc.write('<link href="tabbedPane.css"'+
' rel="stylesheet" type="text/css">');

LIB.addListener(this.window, 'load', function() {
// Cannot test that CSS support works until window.onload.
// This also checks that the stylesheet loaded
if (supportsDisplayCss()) {
LIB.arrayForEach(LIB.querySelector('.sections'), init);
LIB.addClass(LIB.getDocumentElement(),
'tabbedPanesEnabled');
}
else {
// "get out of trouble"
LIB.addClass(LIB.getDocumentElement(),
'tabbedPanesDisabled');
}
});

})();

}

// tabbedPane.css ----------------------------------------------

/* styles for use until window.onload if
the browser is "tentatively" capable */
.sections .section {
display:none;
}
.sections .first {
display:block;
}

/* if feature tests for tabbed panes fail */
.tabbedPanesDisabled .section {
display:block;
}

/* if feature tests for for tabbed panes pass */
.tabbedPanesEnabled li.current {
background:red;
}
.tabbedPanesEnabled .sections .section, .displayTestElement {
display:none;
}
.tabbedPanesEnabled .sections div.current {
display:block;
}


// lib.js ------------------------------------------------------

// test any JavaScript features
// new in NN4+, IE4+, ES3+
// or known to have a bug in some browser
// test all host objects

var LIB = {};

// Some array extras for the app developer.
// These are not used within the library
// to keep library interdependencies low.
// These extras don't use the optional
// thisObject argument of native JavaScript 1.6
// Array.prototype.filter but that can easily
// be added here at the cost of some file size.
LIB.arrayFilter = function(a, f) {
var rs = [];
for (var i=0, ilen=a.length; i<ilen; i++) {
if (f(a[i])) {
rs[rs.length] = a[i];
}
}
return rs;
};

LIB.arrayForEach = function(a, f) {
for (var i=0, ilen=a.length; i<ilen; i++) {
f(a[i]);
}
};

// ---------------------------------------------------

// TODO, feature testing and multiple handlers
LIB.addListener = function(element, eventType, callback) {
element['on'+eventType] = callback;
};

LIB.preventDefault = function(e) {
if (e.preventDefault) {
e.preventDefault();
return;
}
// can't test for returnValue directly?
if (e.cancelBubble !== undefined){
e.returnValue = false;
return;
}
};


// ---------------------------------------------------

(function() {

var isRealObjectProperty = function(o, p) {
return !!(typeof(o[p]) == 'object' && o[p]);
};
LIB.isRealObjectProperty = isRealObjectProperty;

var isHostMethod = function(o, m) {
var t = typeof(o[m]);
return !!(((t=='function' || t=='object') && o[m]) ||
t == 'unknown');
};
LIB.isHostMethod = isHostMethod;

if (!(isRealObjectProperty(this, 'document'))) {
return;
}
var doc = this.document;

if (isRealObjectProperty(doc, 'documentElement')) {
var getAnElement = function(d) {
return (d || doc).documentElement;
};
LIB.getAnElement = getAnElement;
LIB.getDocumentElement = getAnElement;
}

// Test both interfaces specified in the DOM
if (isHostMethod(doc, 'getElementsByTagName') &&
typeof getAnElement == 'function' &&
isHostMethod(getAnElement(), 'getElementsByTagName')) {

// One possible implementation for developers
// in a situation where it is not a problem that
// IE5 thinks doctype and comments are elements.
LIB.getEBTN = function(tag, root) {
root = root || doc;
var els = root.getElementsByTagName(tag);
if (tag == '*' &&
!els.length &&
isHostMethod(root, 'all')) {
els = root.all;
}
return els;
};

}

if (isHostMethod(doc, 'getElementById')) {
// One possible implementation for developers
// not troubled by the name and id attribute
// conflict in IE
LIB.getEBI = function(id, d) {
return (d || doc).getElementById(id);
};
}

if (LIB.getEBTN &&
LIB.getEBI &&
typeof getAnElement == 'function' &&
(function() {
var el = getAnElement();
return typeof el.nodeType == 'number' &&
typeof el.tagName == 'string' &&
typeof el.className == 'string' &&
typeof el.id == 'string'
})()) {

// One possible selector compiler implementation
// that can handle selectors with a tag name,
// class name and id.
//
// use memoization for efficiency
var cache = {};
var compile = function(s) {
if (cache[s]) {
return cache[s];
}

var m, // regexp matches
tn, // tagName in s
id, // id in s
cn, // className in s
f; // the function body

m = s.match(/^([^#\.]+)/);
tn = m ? m[1] : null;

m = s.match(/#([^\.]+)/);
id = m ? m[1] : null;

m = s.match(/\.([^#]+)/);
cn = m ? m[1] : null;

f = 'var i,els,el,m,ns=[];';
if (id) {
f += 'if (!d||(d.nodeType==9||(!d.nodeType&&!d.tagName))){'+
'els=((el=LIB.getEBI("'+id+'",d))?[el]:[]);' +
((!cn&&!tn)?'return els;':'') +
'}else{' +
'els=LIB.getEBTN("'+(tn||'*')+'",d);' +
'}';
}
else {
f += 'els=LIB.getEBTN("'+(tn||'*')+'",d);';
}

if (id || cn) {
f += 'i=els.length;' +
'while(i--){' +
'el=els[i];' +
'if(';
if (id) {
f += 'el.id=="'+id+'"';
}
if ((cn||tn) && id) {
f += '&&';
}
if (tn) {
f += 'el.tagName.toLowerCase()=="' + tn + '"';
}
if (cn && tn) {
f += '&&';
}
if (cn) {
f += '((m=el.className)&&' +
'(" "+m+" ").indexOf(" '+cn+' ")>-1)';
}
f += '){' +
'ns[ns.length]=el;' +
'}' +
'}';

f += 'return ns.reverse()';
}
else {
f += 'return els;';
}

// http://elfz.laacz.lv/beautify/
//console.log('function f(d) {' + f + '}');

f = new Function('d', f);
cache[s] = f;
return f;
}

LIB.querySelector = function(selector, rootEl) {
return (compile(selector))(rootEl);
};

}

})();

// ------------------------------------------

if (typeof LIB.getAnElement != 'undefined' &&
typeof LIB.getAnElement().className == 'string') {

// The RegExp support need here
// has been available since NN4 & IE4
LIB.hasClass = function(el, className) {
return (new RegExp(
'(^|\\s+)' + className + '(\\s+|$)')).test(el.className);
};

LIB.addClass = function(el, className) {
if (LIB.hasClass(el, className)) {
return;
}
el.className = el.className + ' ' + className;
};

LIB.removeClass = function(el, className) {
el.className = el.className.replace(
new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g'), ' ');
// in case of multiple adjacent with a single space
if ( LIB.hasClass(el, className) ) {
arguments.callee(el, className);
}
};

}

David Mark

unread,
Feb 13, 2008, 1:28:42 AM2/13/08
to
On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> I'm writing a blog article about building a decent tabbed pane widget
> for a web page on the general web (anyone with a browser and internet
> connection has access). I've never seen an article that deals with all
> of the feature testing and sequential rendering issues and I think an
> article like this is needed. If a copy of the article would be agreed
> upon and beneficial to the jibbering site then I would support that.
> It isn't exactly an easy task to write a widget for thousands of
> possible browser versions and configurations. I can understand why
> people give up when handed this task and only support recent browsers
> and use sniffing. More written about building widgets in such a harsh
> environment would help, I am sure.
>
> Below I've included the four files involved in the example:
>
> - index.html
> - tabbedPane.js
> - tabbedPane.css
> - lib.js
>
> The example is also temporarily available on the web
>
> <URL:http://peter.michaux.ca/temp/tabbedPane/index.html>

Did you mean to leave the UL unstyled (other than the background color
of the selected item?) It is possible to make list items look just
like the tabs in a property sheet.

>
> The example code is not finished. Any feedback would be greatly
> appreciated.
>
> The goal of the HTML mark up was to make it as easy as possible for
> the content author. I don't like how many tabbed pane widgets require
> a ul element at the top and then the content author must connect the
> tab with the pane using some convention for element id attributes.

Certainly I don't like anything that relies on an ID (or className)
convention to define element relationships.

> That is far more difficult than it should be for the content author. I
> know first hand that making it easier for them is greatly appreciated
> and also easier on me.
>
> I've feature tested almost maximally. All host objects are tested and
> any language feature that isn't ancient is tested. The objective is to
> have all the code run in IE4+ and NN4+ browsers without throwing any
> errors: syntax or runtime.
>
> I've tried to be relatively modern by including a css selector query
> function. Surely this task could be done without such a function but
> to a certain degree I am trying to simulate how developers use
> libraries. When more widgets are in a page the cost of the library
> size is not so great.
>
> The events part of the library is a problem area for several reasons.
> A big reason is due to my ignorance about screen readers. If a blind

There is no way to predict what screen readers and aural browsers will
do with dynamic content. Some newer ones try to deal with the issue
with varying results. Some aural browsers follow the aural style
rules, but screen readers typically read what they "see" on the
screen. Some applications are hard to pigeon-hold. Is Opera Voice an
aural browser? It follows some of the aural style rules, but it won't
read anything that isn't visible on the screen. It would be simple
enough for the sight-impaired to disable scripting, but of course that
breaks most of the current Web as most Web developers consider anyone
who disables scripting to be unworthy of their content.

> visitor with a browser that has JavaScript enabled is using a screen
> reader, do the tab elements need to have anchor elements so that the
> screen reader reads the tabs. If anchor elements are required then I

Regardless of the state of scripting, the tabs must be accessible by
keyboard. The text in the list items should be wrapped in anchors or
buttons. Buttons make more sense, but are impossible to style in some
browsers.

> want to use preventDefault to stop the browser from changing the URL
> and even potentially reloading the page when a tab is clicked. Safari
> 1.9 and early v2 releases didn't support preventDefault when the
> callback function is attached with addEventListener. If DOM0 handler
> is used as a workaround I need to know that it will work before
> manipulating the DOM into a tabbed pane. A feature test for DOM0 is
> not so easy (although David Mark suggested one that works in at least
> Firefox.)

It should work in all browsers that support setAttribute. As for
browsers like IE4/NN4, you can try to check for something other than
undefined in the onclick (or whatever) property of an arbitrary
element and then decide what your default assumption should be for the
rest (it seems likely that browsers with script support that predate
addEventListener/attachEvent will have the DOM0 event model.)

>
> There are many ways to "architect" the actual mechanics that run the
> tabbed pane after it is all set up. I decided to go with a closure
> system. This was just a choice. The mechanics that run this example
> feel super light weight to me: just a little closure for each tab that
> knows it's tab and pane and the current tab and pane. It feels like

That's what I did with my popup menu script.

> the sports car version to me. OOP versions are good for some project
> specs but feel like tanks. I may write and article that looks at

Exactly.

> different code designs for different design requirements.
>
> I look forward to any suggestions.
>

[snip]

I would test if it is an object and that it is truthy (to exclude
null.) You can't trust global variables, so it is best to interrogate
them as thoroughly as possible.

> LIB.getAnElement &&
> LIB.getDocumentElement &&
> LIB.isRealObjectProperty &&
> LIB.isHostMethod &&
> LIB.arrayFilter &&
> LIB.arrayForEach &&
> LIB.addListener &&
> LIB.querySelector &&
> LIB.hasClass &&
> LIB.removeClass &&
> LIB.addClass &&
> // Test for host objects and methods I use directly
> LIB.isRealObjectProperty(this, 'window') &&

I don't do this test as I never reference the window object. It is
faster to reference the global object directly.

> LIB.isRealObjectProperty(this, 'document') &&
> LIB.isHostMethod(this.document, 'write') &&
> LIB.isHostMethod(this.document, 'createElement') &&
> (function() {
> var el = LIB.getAnElement();
> return LIB.isHostMethod(el, 'appendChild') &&
> LIB.isHostMethod(el, 'insertBefore') &&
> LIB.isRealObjectProperty(el, 'firstChild') &&
> LIB.isRealObjectProperty(el, 'childNodes') &&

Careful with this one. I use isHostMethod to test it as I do for
document.images, document.forms, etc. (Safari makes some host objects
that take indeces as arguments callable.)

> typeof el.innerHTML == 'string';
> })()) {
>
> (function() {
>
> var doc = this.document;
>
> var cc = 'current'; // TODO

What is there to do here?

>
> var enliven = function(c, t, p) {
> LIB.addListener(t, 'click', function() {

I typically create "shortcuts" to API properties. It speeds things
property access and does wonders for (aggressive) minification. I
know you don't care for obfuscation, due to the possibility of
introducing bugs, but recently I have been using the YUI
"compressor" (misnomer) on a 7000+ line project that is teeming with
closures and it has never burned me. Granted, I always seek JSLint's
approval before running my build process.

> // TODO prevent default and use anchor elements for tabs
> // TODO what about keyboard accessibility?

You answered the second question.

<h2><a name="two">Two</a></h2>

Then turn it into a link when you create the widget.

As for Safari 1.x., if the dynamically created href is "#", the worst
case is that it will scroll to the top on click. It may not do
anything but tack an empty hash onto the address.

>
> // avoid potential flicker if user clicks current tab
> if (t == c.t) {
> return;
> }
> LIB.removeClass(c.t, cc);
> LIB.removeClass(c.p, cc);
> c.t = t;
> c.p = p;
> LIB.addClass(t, cc);
> LIB.addClass(p, cc);
> });
> };

I find this hard to follow. What are c, t and p? I think this is
another reason to minify aggressively (it allows you to use long,
descriptive variable names with impunity.) Of course, it won't help
with the t and p properties of c.

>
> var init = function(w) {
> var ts = doc.createElement('ul'),
> first = true,
> c,
> t,
> h,
> t;

Make sure ts is truthy (may be overly paranoid.)

> LIB.addClass(ts, 'tabs');
> LIB.arrayForEach(
> LIB.arrayFilter(
> w.childNodes,
> function(s) {
> return LIB.hasClass(s, 'section');
> }),
> function(p) {
> t = doc.createElement('li');

Okay, it appears t is for tab and p is for pane and cc is class added
to indicate that the tab is selected.

> if (first) {
> c = {t:t, p:p};
> LIB.addClass(t, cc);
> LIB.addClass(p, cc);

I assume you are adding and removing panes from the layout by adding
and removing the "current" class. I would set the display style
directly. Fiddling with a widget's accompanying style sheet should
never have the potential to break the its behavior.

> first = false;
> }
> enliven(c, t, p);
> h = LIB.querySelector('h2', p)[0];

You should generalize this to use any level headline. And why not
getEBTN for this (seems like a more direct approach.)

> t.innerHTML = h ? h.innerHTML : 'tab';

I would not use innerHTML for this as you have appendChild at your
disposal.

> ts.appendChild(t);
> });
> w.insertBefore(ts, w.firstChild);
> };
>
> // Test that a pane really is out of the page
> // when it is not current and that it has some
> // height when it is current. This is the critical
> // test to make sure the tabbed pane will work.
> var supportsDisplayCss = function() {
> var middle = doc.createElement('div');

Should use the createElement wrapper (2 lines IIRC) to allow for XHTML
support.

>
> if (LIB.isRealObjectProperty(doc, 'body') &&

This will fail in some browsers when using XHTML (see getBodyElement.)

> LIB.isHostMethod(doc.body, 'removeChild') &&
> typeof middle.offsetHeight == 'number') {
>
> var outer = doc.createElement('div'),
> inner = doc.createElement('div');
>
> LIB.addClass(outer, 'tabbedPanesEnabled');
> LIB.addClass(middle, 'sections');
> LIB.addClass(inner, 'section');
> inner.innerHTML = '.';
> middle.appendChild(inner);
> outer.appendChild(middle);
> doc.body.appendChild(outer);
> var h = outer.offsetHeight;
> LIB.addClass(inner, 'current')
> var doesSupport = (h == 0 && outer.offsetHeight > 0);

Looks good (presuming all browsers behave as expected with
offsetHeight and display:none. It seems you are checking if the style
sheet loaded, as well as if style is enabled and that user style
sheets are not interfering. Good deal.

I wrote a generalized "cssEnabled" function a while back that used
similar logic, but have never used it for anything in production. It
seemed to me at the time that I needed widgets to work even if CSS was
toggled after the page was loaded. I do like the specific test for
this widget as I have never dealt with the issue of a (lunatic) user
adding an !important display rule.

> doc.body.removeChild(outer);
> return doesSupport;
> }
> return false;
> };
>
> // We don't know for sure at this point that the tabbed pane
> // will work. We have to wait for window.onload to finish
> // the tests. We do know we can give the pages some style to use
> // during the page load because we can "get out of trouble"
> // when window.onload fires. This is
> // because the functions used to get out of trouble
> // have been feature tested.

You can skip all of this unless this test passes:

var el = getAnElement();
if (el && el.style && typeof el.style.display == 'string') {
// Add style rule(s)
// Attach load listener
}


> doc.write('<link href="tabbedPane.css"'+
> ' rel="stylesheet" type="text/css">');

IIRC, this will crash some (or all) revisions of NN4.x. It also will
not work with XHTML (I know it is a dead language on the Web, but it
is useful for Intranets.) Better to add the needed rules via DOM
manipulation. Of course, that logic must reside in a script block
outside of the head element, else you risk the dreaded "Operation
Aborted" error in IE.

>
> LIB.addListener(this.window, 'load', function() {

I think "this" is sufficient as this.window points back to the global
object.

> // Cannot test that CSS support works until window.onload.
> // This also checks that the stylesheet loaded
> if (supportsDisplayCss()) {
> LIB.arrayForEach(LIB.querySelector('.sections'), init);
> LIB.addClass(LIB.getDocumentElement(),
> 'tabbedPanesEnabled');
> }
> else {
> // "get out of trouble"
> LIB.addClass(LIB.getDocumentElement(),
> 'tabbedPanesDisabled');
> }

Interesting that you add classes to the documentElement. I haven't
had occasion to do that.

> });
>
> })();
>
> }
>
> // tabbedPane.css ----------------------------------------------
>
> /* styles for use until window.onload if
> the browser is "tentatively" capable */
> .sections .section {
> display:none;}
>
> .sections .first {
> display:block;
>
> }

As mentioned, I would add these rules directly and have one less style
sheet to download.

>
> /* if feature tests for tabbed panes fail */
> .tabbedPanesDisabled .section {
> display:block;
>
> }

Same here. Then you don't need to add a class to the documentElement.

>
> /* if feature tests for for tabbed panes pass */
> .tabbedPanesEnabled li.current {
> background:red;}
>
> .tabbedPanesEnabled .sections .section, .displayTestElement {
> display:none;}
>
> .tabbedPanesEnabled .sections div.current {
> display:block;
>
> }

As mentioned, I don't think these display rules belong in a style
sheet. They shouldn't be exposed to authors as fiddling with them
will only break the behavior of the widget.

>
> // lib.js ------------------------------------------------------
>
> // test any JavaScript features
> // new in NN4+, IE4+, ES3+
> // or known to have a bug in some browser
> // test all host objects
>
> var LIB = {};
>
> // Some array extras for the app developer.
> // These are not used within the library
> // to keep library interdependencies low.
> // These extras don't use the optional
> // thisObject argument of native JavaScript 1.6
> // Array.prototype.filter but that can easily
> // be added here at the cost of some file size.
> LIB.arrayFilter = function(a, f) {
> var rs = [];
> for (var i=0, ilen=a.length; i<ilen; i++) {
> if (f(a[i])) {

You can skip the call to f if a[i] is undefined. This speeds things
up for sparsely populated arrays.

> rs[rs.length] = a[i];
> }
> }
> return rs;
>
> };
>
> LIB.arrayForEach = function(a, f) {
> for (var i=0, ilen=a.length; i<ilen; i++) {
> f(a[i]);

Same here. I think Mozilla's documentation has the source for some
(or all) of these 1.6 array methods as implemented in their engine.
You can't use them verbatim though as they use the in operator.

> }
>
> };

I think I submitted a CWR ticket with wrappers for these that take
advantage of the native methods when available (which are obviously
faster.)

>
> // ---------------------------------------------------
>
> // TODO, feature testing and multiple handlers
> LIB.addListener = function(element, eventType, callback) {
> element['on'+eventType] = callback;
>
> };
>
> LIB.preventDefault = function(e) {
> if (e.preventDefault) {
> e.preventDefault();
> return;
> }
> // can't test for returnValue directly?

Why test it at all?

> if (e.cancelBubble !== undefined){

I would skip this inference.

> e.returnValue = false;
> return;

return false;

> }
>
> };
>
> // ---------------------------------------------------
>
> (function() {
>
> var isRealObjectProperty = function(o, p) {
> return !!(typeof(o[p]) == 'object' && o[p]);
> };
> LIB.isRealObjectProperty = isRealObjectProperty;
>
> var isHostMethod = function(o, m) {
> var t = typeof(o[m]);
> return !!(((t=='function' || t=='object') && o[m]) ||
> t == 'unknown');
> };
> LIB.isHostMethod = isHostMethod;
>
> if (!(isRealObjectProperty(this, 'document'))) {
> return;
> }
> var doc = this.document;

I would set this to null once feature testing is complete. I
reference the default document as global.document thereafter. It
isn't a big concern here as you didn't define your addListener
function inside here.

>
> if (isRealObjectProperty(doc, 'documentElement')) {

IIRC, this excludes IE4.

> var getAnElement = function(d) {
> return (d || doc).documentElement;
> };

The slightly longer version of this will support IE4 (and virtually
anything else.)

> LIB.getAnElement = getAnElement;
> LIB.getDocumentElement = getAnElement;
> }
>
> // Test both interfaces specified in the DOM
> if (isHostMethod(doc, 'getElementsByTagName') &&
> typeof getAnElement == 'function' &&

&& getAnElement

You know that the getAnElement variable exists.

> isHostMethod(getAnElement(), 'getElementsByTagName')) {
>
> // One possible implementation for developers
> // in a situation where it is not a problem that
> // IE5 thinks doctype and comments are elements.
> LIB.getEBTN = function(tag, root) {
> root = root || doc;
> var els = root.getElementsByTagName(tag);
> if (tag == '*' &&
> !els.length &&
> isHostMethod(root, 'all')) {

There is a potential problem here (and I made the same mistake several
times in my library.) The isHostMethod function should only be used
to test properties that will be called. It allows for properties of
type "unknown", which are safe to call, but unsafe to evaluate.

> els = root.all;

Here is where it would blow up if some future version of IE implements
documents as ActiveX objects (or whatever the hell it is that returns
"unknown" for typeof operations.) You can't use isRealObjectProperty
either as Safari makes document.all callable (typeof root.all ==
'function'.) I think I mentioned this in one of the tickets. We need
a way a parameter to tell isHostMethod to exclude "unknown" properties
when appropriate. This is a perfect example.

> }
> return els;
> };
>
> }
>
> if (isHostMethod(doc, 'getElementById')) {
> // One possible implementation for developers
> // not troubled by the name and id attribute
> // conflict in IE
> LIB.getEBI = function(id, d) {
> return (d || doc).getElementById(id);
> };
> }
>
> if (LIB.getEBTN &&
> LIB.getEBI &&
> typeof getAnElement == 'function' &&

getAnElement &&

You can skip the typeof operation for library functions in other
modules as well, as long as they are dependencies that are
automatically inserted by the build process.

> (function() {
> var el = getAnElement();
> return typeof el.nodeType == 'number' &&

IIRC, this excludes IE5.0 and under.

You really shouldn't reference LIB internally (other than to populate
it) for performance and style reasons.

> //http://elfz.laacz.lv/beautify/


> //console.log('function f(d) {' + f + '}');
>
> f = new Function('d', f);
> cache[s] = f;
> return f;
> }
>
> LIB.querySelector = function(selector, rootEl) {
> return (compile(selector))(rootEl);
> };

I really like the XPath-enabled version(s) better. The simple version
isn't that much more code and is considerably faster in browsers that
support document.evaluate.

>
> }
>
> })();
>
> // ------------------------------------------
>
> if (typeof LIB.getAnElement != 'undefined' &&

if (LIB.getAnElement &&

> typeof LIB.getAnElement().className == 'string') {

When you add the more robust version of getAnElement to support IE4,
it would be a good idea to test its result.

>
> // The RegExp support need here
> // has been available since NN4 & IE4
> LIB.hasClass = function(el, className) {
> return (new RegExp(
> '(^|\\s+)' + className + '(\\s+|$)')).test(el.className);
> };
>
> LIB.addClass = function(el, className) {
> if (LIB.hasClass(el, className)) {
> return;
> }
> el.className = el.className + ' ' + className;
> };
>
> LIB.removeClass = function(el, className) {
> el.className = el.className.replace(
> new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g'), ' ');
> // in case of multiple adjacent with a single space
> if ( LIB.hasClass(el, className) ) {
> arguments.callee(el, className);
> }

I'm missing something here. Can you elaborate on this?

> };
>

Despite the verbosity of my comments, most of them are quibbles. This
is certainly the right way to create a widget. Unfortunately, it
seems that most Web developers follow a far different path:

1. Download Prototype, jQuery or some other amateurish garbage, along
with assorted plug-ins
2. Write widget code that is married to the library in terms of
syntax, as well as (typically ineffectual) feature testing
3. Put a "Best viewed in three or four modern browsers" warning on
pages that use the widget
4. Download patches to the library for eternity, ignoring the fact
that the library authors will never really get it right
5. Upload widget to library-specific repository so developers with
similar delusions can break their sites as well
6. Tell anyone who complains that they aren't living in the "real
world" (which apparently doesn't have mobile devices, set top boxes or
off-brand browsers of any kind)
7. Exhort everyone else to follow their lead as their widget code is
so simple and compact (never mind that it has 150K of dependencies and
is prone to catastrophic failure)

But hey, it works for Google. Unfortunately for the rest of us, the
equation is not:

incompetence = Web success

But rather:

incompetence + lots of cash from investors = Web success

But I digress. For those who need an example of how to write widgets
for applications on the public Internet, this is a good example. With
some minor modifications, it could be a great example.

Peter Michaux

unread,
Feb 13, 2008, 3:40:45 AM2/13/08
to
David Mark wrote:
> On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > I'm writing a blog article about building a decent tabbed pane widget

[snip]

> > <URL:http://peter.michaux.ca/temp/tabbedPane/index.html>
>
> Did you mean to leave the UL unstyled (other than the background color
> of the selected item?)

For now yes. I don't want to cloud what is related to scripting and
what is only for looks.


[snip]


> Regardless of the state of scripting, the tabs must be accessible by
> keyboard. The text in the list items should be wrapped in anchors or
> buttons. Buttons make more sense, but are impossible to style in some
> browsers.

You are right. I need to make this keyboard accessible. Given how so
many websites are not navigable by keyboard, there must be a way for
people to move their cursor around with the keyboard to simulate a
mouse.

[snip]

> > // Test what can be tested as soon as possible
> > // Check that the library loaded
> > if (typeof LIB != 'undefined' &&
> > // Test for library functions I use directly
>
> I would test if it is an object and that it is truthy (to exclude
> null.) You can't trust global variables, so it is best to interrogate
> them as thoroughly as possible.

Even more specific would be to test that it is an object with

typeof LIB == 'object'

> > LIB.getAnElement &&
> > LIB.getDocumentElement &&
> > LIB.isRealObjectProperty &&
> > LIB.isHostMethod &&
> > LIB.arrayFilter &&
> > LIB.arrayForEach &&
> > LIB.addListener &&
> > LIB.querySelector &&
> > LIB.hasClass &&
> > LIB.removeClass &&
> > LIB.addClass &&
> > // Test for host objects and methods I use directly
> > LIB.isRealObjectProperty(this, 'window') &&
>
> I don't do this test as I never reference the window object. It is
> faster to reference the global object directly.

Interesting. Thanks. I will do that.


> > LIB.isRealObjectProperty(this, 'document') &&
> > LIB.isHostMethod(this.document, 'write') &&
> > LIB.isHostMethod(this.document, 'createElement') &&
> > (function() {
> > var el = LIB.getAnElement();
> > return LIB.isHostMethod(el, 'appendChild') &&
> > LIB.isHostMethod(el, 'insertBefore') &&
> > LIB.isRealObjectProperty(el, 'firstChild') &&
> > LIB.isRealObjectProperty(el, 'childNodes') &&
>
> Careful with this one. I use isHostMethod to test it as I do for
> document.images, document.forms, etc. (Safari makes some host objects
> that take indeces as arguments callable.)

Are you only referring to the tests for firstChild and childNodes?

For the sake of curiosity, why would I want to call them?


> > typeof el.innerHTML == 'string';
> > })()) {
> >
> > (function() {
> >
> > var doc = this.document;
> >
> > var cc = 'current'; // TODO
>
> What is there to do here?

Sorry. I meant to trim that TODO out. I was going to change the name
and may.

> > var enliven = function(c, t, p) {
> > LIB.addListener(t, 'click', function() {
>
> I typically create "shortcuts" to API properties. It speeds things
> property access

I tested this just a few days ago. Richard Cornford has referred to
these as something like "silly namespaces with performance penalties".
He is right, of course, but I don't think the single namespace is
hideous. For 10000 property lookups the version that had the shortcut
was about 30ms and the version with one "namespace" like above was
about 50 ms. I was convinced this is not worth stressing about. Ten
thousand is a lot!

More importantly, seeing as this is intended as an article, I don't
want to go to any extra length to complicate things. I think it will
be plenty complicated enough with concepts alone for most readers.

> and does wonders for (aggressive) minification. I
> know you don't care for obfuscation, due to the possibility of
> introducing bugs, but recently I have been using the YUI
> "compressor" (misnomer) on a 7000+ line project that is teeming with
> closures and it has never burned me. Granted, I always seek JSLint's
> approval before running my build process.
>
> > // TODO prevent default and use anchor elements for tabs
> > // TODO what about keyboard accessibility?
>
> You answered the second question.
>
> <h2><a name="two">Two</a></h2>
>
> Then turn it into a link when you create the widget.
>
> As for Safari 1.x., if the dynamically created href is "#", the worst
> case is that it will scroll to the top on click. It may not do
> anything but tack an empty hash onto the address.

I think something along these lines is the right idea.


> > // avoid potential flicker if user clicks current tab
> > if (t == c.t) {
> > return;
> > }
> > LIB.removeClass(c.t, cc);
> > LIB.removeClass(c.p, cc);
> > c.t = t;
> > c.p = p;
> > LIB.addClass(t, cc);
> > LIB.addClass(p, cc);
> > });
> > };
>
> I find this hard to follow. What are c, t and p? I think this is
> another reason to minify aggressively (it allows you to use long,
> descriptive variable names with impunity.) Of course, it won't help
> with the t and p properties of c.

I'll expand these names for the sake of the article

> > var init = function(w) {
> > var ts = doc.createElement('ul'),
> > first = true,
> > c,
> > t,
> > h,
> > t;
>
> Make sure ts is truthy (may be overly paranoid.)

If execution has reached this stage, then doc.createElement should be
tested to work. I have a feature test at the top for the existence of
document.createElement. I could add a test there that it also works
and a comment that it may be overly paranoid. I'm not being that
paranoid in other cases. I'd like the article to point out that you
can actually test things work as well as just testing they exist so
this may make a good example.


> > LIB.addClass(ts, 'tabs');
> > LIB.arrayForEach(
> > LIB.arrayFilter(
> > w.childNodes,
> > function(s) {
> > return LIB.hasClass(s, 'section');
> > }),
> > function(p) {
> > t = doc.createElement('li');
>
> Okay, it appears t is for tab and p is for pane and cc is class added
> to indicate that the tab is selected.

Yes. Sorry to make you muddle through that.


> > if (first) {
> > c = {t:t, p:p};
> > LIB.addClass(t, cc);
> > LIB.addClass(p, cc);
>
> I assume you are adding and removing panes from the layout by adding
> and removing the "current" class.

Yes.

> I would set the display style
> directly. Fiddling with a widget's accompanying style sheet should
> never have the potential to break the its behavior.

I've debated exactly this in my head. I'm not sure which is better.

The separatists argue that all styling should be in the stylesheet.
This breaks down the minute they want an animation.

I think it is fair to put a bit of css in the stylesheet that has a
comment /* for javascript */; however, some other rule could then
override it. If it is a direct manipulation of element.style.display
it wins the cascade for sure.

Thanks.


> > first = false;
> > }
> > enliven(c, t, p);
> > h = LIB.querySelector('h2', p)[0];
>
> You should generalize this to use any level headline.

I will mention in the article that this could be done many ways. I
don't want to bloat the actual mechanics of the widget with options. I
think that just one way is good enough for an article.


> And why not
> getEBTN for this (seems like a more direct approach.)

Given the implementation, there is really no effective difference. The
compiled function just calls getEBTN. I think that these querySelector
functions are the way of the future (there is a standard now and it is
in Safari 3, I believe) and provide a nice uniform API. Just one
function that does it all.

> > t.innerHTML = h ? h.innerHTML : 'tab';
>
> I would not use innerHTML for this as you have appendChild at your
> disposal.

The innerHTML may be more than one element and that would require a
loop. I could add a restriction to the design spec that the h2
contents can only be one text element.

I'm no longer afraid of innerHTML. It works far faster and more
predictably than the DOM functions across the various browsers. I have
a work project that builds a table with 250 rows, 7 cells per row and
some images in each row. The only way to get acceptable performance is
by building the table with HTML and inserting with innerHTML. Using
createElement, appendChild is *really* slow even if the final
attachment to the DOM happens in one appendChild.


> > ts.appendChild(t);
> > });
> > w.insertBefore(ts, w.firstChild);
> > };
> >
> > // Test that a pane really is out of the page
> > // when it is not current and that it has some
> > // height when it is current. This is the critical
> > // test to make sure the tabbed pane will work.
> > var supportsDisplayCss = function() {
> > var middle = doc.createElement('div');
>
> Should use the createElement wrapper (2 lines IIRC) to allow for XHTML
> support.

If I do that then I have to stop using innerHTML for consistency.

Personally I don't care one bit about XHTML. Given the XHTML
implementations and what happens with a page is malformed, it is a
total waste of time, in my opinion.


> > if (LIB.isRealObjectProperty(doc, 'body') &&
>
> This will fail in some browsers when using XHTML (see getBodyElement.)

For consistency, I considered writing

LIB.querySelector('body')

which would have used your fallback in getBodyElement.


> > LIB.isHostMethod(doc.body, 'removeChild') &&
> > typeof middle.offsetHeight == 'number') {
> >
> > var outer = doc.createElement('div'),
> > inner = doc.createElement('div');
> >
> > LIB.addClass(outer, 'tabbedPanesEnabled');
> > LIB.addClass(middle, 'sections');
> > LIB.addClass(inner, 'section');
> > inner.innerHTML = '.';
> > middle.appendChild(inner);
> > outer.appendChild(middle);
> > doc.body.appendChild(outer);
> > var h = outer.offsetHeight;
> > LIB.addClass(inner, 'current')
> > var doesSupport = (h == 0 && outer.offsetHeight > 0);
>
> Looks good (presuming all browsers behave as expected with
> offsetHeight and display:none. It seems you are checking if the style
> sheet loaded, as well as if style is enabled and that user style
> sheets are not interfering. Good deal.

Even though the test seems a bit long, I thought it was pretty
economical for all that it is testing.

I think this is the kind of test that is going to shock people. I
don't think many people actually test that the widget will work by
making a fake little one and trying it.

> I wrote a generalized "cssEnabled" function a while back that used
> similar logic, but have never used it for anything in production. It
> seemed to me at the time that I needed widgets to work even if CSS was
> toggled after the page was loaded.

If some user toggles the CSS after the page is loaded is on his own. I
will mention this however.

It is almost just as easy that some user might type this into his
browser URL bar

javascript:LIB.addClass = null;

There are certain things like this that someone really has to go out
of his way to break the page.


> I do like the specific test for
> this widget as I have never dealt with the issue of a (lunatic) user
> adding an !important display rule.
>
> > doc.body.removeChild(outer);
> > return doesSupport;
> > }
> > return false;
> > };
> >
> > // We don't know for sure at this point that the tabbed pane
> > // will work. We have to wait for window.onload to finish
> > // the tests. We do know we can give the pages some style to use
> > // during the page load because we can "get out of trouble"
> > // when window.onload fires. This is
> > // because the functions used to get out of trouble
> > // have been feature tested.
>
> You can skip all of this unless this test passes:
>
> var el = getAnElement();
> if (el && el.style && typeof el.style.display == 'string') {
> // Add style rule(s)
> // Attach load listener
> }

Skip all of what?

I did have this test at one point but since I put the styling in a CSS
sheet and only manipulated the classes, I figured I didn't need the
above test as I don't directly manipulate the el.style.display value.
I may switch back to the direct manipulation.


> > doc.write('<link href="tabbedPane.css"'+
> > ' rel="stylesheet" type="text/css">');
>
> IIRC, this will crash some (or all) revisions of NN4.x.

Crap.

It works for me in NN4.0 and NN4.5 with no problems.

This is actually a very important part of what I want to be able to do
with widgets and part of the reason for the article.


> It also will
> not work with XHTML (I know it is a dead language on the Web, but it
> is useful for Intranets.)

I'm ok with this not working with XHTML.


> Better to add the needed rules via DOM
> manipulation. Of course, that logic must reside in a script block
> outside of the head element, else you risk the dreaded "Operation
> Aborted" error in IE.

Officially, the DOM is supposed to be read only until window.onload. I
don't want to touch it in any way. I like following the rules. I'm
sure the people that started seeing that Operation Aborted error
didn't know what to do for a long time and the only reason they saw it
was because they didn't follow the rules.

The only sanctioned thing that can be done while the page is loading
is document.write to dynamically generate parts of the page.

So if XHTML is to be used then having the transitional CSS that makes
the page somewhat acceptable to the user would not be possible.


> > LIB.addListener(this.window, 'load', function() {
>
> I think "this" is sufficient as this.window points back to the global
> object.
>
> > // Cannot test that CSS support works until window.onload.
> > // This also checks that the stylesheet loaded
> > if (supportsDisplayCss()) {
> > LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > LIB.addClass(LIB.getDocumentElement(),
> > 'tabbedPanesEnabled');
> > }
> > else {
> > // "get out of trouble"
> > LIB.addClass(LIB.getDocumentElement(),
> > 'tabbedPanesDisabled');
> > }
>
> Interesting that you add classes to the documentElement. I haven't
> had occasion to do that.

I haven't used it before but it seems to work. I have added classes to
document.body to do what I'm trying to do here because I've assumed
document.body exists.

The reason I added to document.documentElement here is because if I am
"getting out of trouble", I can only use what I knew existed when the
first set of feature tests ran. That means I know the
document.documentElement exists but I do not know that document.body
exists. I am trying to build a consistent line of logic for what's
been tested and what is being used.

[snip]

[I'll slit the comments for the lib.js into another reply.]

Peter

Peter Michaux

unread,
Feb 13, 2008, 3:46:51 AM2/13/08
to
David Mark wrote:
> On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > I'm writing a blog article about building a decent tabbed pane widget

[snip]


> > // lib.js ------------------------------------------------------
> >
> > // test any JavaScript features
> > // new in NN4+, IE4+, ES3+
> > // or known to have a bug in some browser
> > // test all host objects
> >
> > var LIB = {};
> >
> > // Some array extras for the app developer.
> > // These are not used within the library
> > // to keep library interdependencies low.
> > // These extras don't use the optional
> > // thisObject argument of native JavaScript 1.6
> > // Array.prototype.filter but that can easily
> > // be added here at the cost of some file size.
> > LIB.arrayFilter = function(a, f) {
> > var rs = [];
> > for (var i=0, ilen=a.length; i<ilen; i++) {
> > if (f(a[i])) {
>
> You can skip the call to f if a[i] is undefined. This speeds things
> up for sparsely populated arrays.

I will add that check and the use of the native filter if it is
available.

[snip]

> > LIB.arrayForEach = function(a, f) {
> > for (var i=0, ilen=a.length; i<ilen; i++) {
> > f(a[i]);
>
> Same here. I think Mozilla's documentation has the source for some
> (or all) of these 1.6 array methods as implemented in their engine.
> You can't use them verbatim though as they use the in operator.

I'll look at yours more closely.

[snip]


> > // TODO, feature testing and multiple handlers
> > LIB.addListener = function(element, eventType, callback) {
> > element['on'+eventType] = callback;
> >
> > };
> >
> > LIB.preventDefault = function(e) {
> > if (e.preventDefault) {
> > e.preventDefault();
> > return;
> > }
> > // can't test for returnValue directly?
>
> Why test it at all?

This whole (two function) event section is lame. The
LIB.preventDefault section shouldn't be defined unless it can work.
This function came from another library and is really not up to snuff.
A group discussion about events sooner than later would be good. It is
not an easy problem in general and it is the most important library.

[snip]

> > // ---------------------------------------------------
> >
> > (function() {
> >
> > var isRealObjectProperty = function(o, p) {
> > return !!(typeof(o[p]) == 'object' && o[p]);
> > };
> > LIB.isRealObjectProperty = isRealObjectProperty;
> >
> > var isHostMethod = function(o, m) {
> > var t = typeof(o[m]);
> > return !!(((t=='function' || t=='object') && o[m]) ||
> > t == 'unknown');
> > };
> > LIB.isHostMethod = isHostMethod;
> >
> > if (!(isRealObjectProperty(this, 'document'))) {
> > return;
> > }
> > var doc = this.document;
>
> I would set this to null once feature testing is complete. I
> reference the default document as global.document thereafter. It
> isn't a big concern here as you didn't define your addListener
> function inside here.

Ahh, circular memory leaks? I was thinking about this recently. All
functions have "document" in their closure which references
document.body which has all the document elements as descendants. This
will include whichever element a function is observing. There is
always a very indirect circle. For some reason, do these indirect
circles not cause the IE memory leak. I think they should.


> > if (isRealObjectProperty(doc, 'documentElement')) {
>
> IIRC, this excludes IE4.

That is ok. For this article, as long as IE4/NN4 don't syntax or
runtime error that is ok. They may not even have enough CSS support to
make a tabbed pane look any good unless table layouts are used.


[snip]


> > // Test both interfaces specified in the DOM
> > if (isHostMethod(doc, 'getElementsByTagName') &&
> > typeof getAnElement == 'function' &&
>
> && getAnElement
>
> You know that the getAnElement variable exists.

getAnElement is defined in an if statement above.


> > isHostMethod(getAnElement(), 'getElementsByTagName')) {
> >
> > // One possible implementation for developers
> > // in a situation where it is not a problem that
> > // IE5 thinks doctype and comments are elements.
> > LIB.getEBTN = function(tag, root) {
> > root = root || doc;
> > var els = root.getElementsByTagName(tag);
> > if (tag == '*' &&
> > !els.length &&
> > isHostMethod(root, 'all')) {
>
> There is a potential problem here (and I made the same mistake several
> times in my library.) The isHostMethod function should only be used
> to test properties that will be called. It allows for properties of
> type "unknown", which are safe to call, but unsafe to evaluate.
>
> > els = root.all;
>
> Here is where it would blow up if some future version of IE implements
> documents as ActiveX objects (or whatever the hell it is that returns
> "unknown" for typeof operations.) You can't use isRealObjectProperty
> either as Safari makes document.all callable (typeof root.all ==
> 'function'.) I think I mentioned this in one of the tickets. We need
> a way a parameter to tell isHostMethod to exclude "unknown" properties
> when appropriate. This is a perfect example.

Crap. I cannot blame people for using the mainstream libraries. It
doesn't mean their code is good but they don't want to worry about
this stuff. It is not fun.


> > }
> > return els;
> > };
> >
> > }
> >
> > if (isHostMethod(doc, 'getElementById')) {
> > // One possible implementation for developers
> > // not troubled by the name and id attribute
> > // conflict in IE
> > LIB.getEBI = function(id, d) {
> > return (d || doc).getElementById(id);
> > };
> > }
> >
> > if (LIB.getEBTN &&
> > LIB.getEBI &&
> > typeof getAnElement == 'function' &&
>
> getAnElement &&
>
> You can skip the typeof operation for library functions in other
> modules as well, as long as they are dependencies that are
> automatically inserted by the build process.

But getAnElement may not be defined.

> > (function() {
> > var el = getAnElement();
> > return typeof el.nodeType == 'number' &&
>
> IIRC, this excludes IE5.0 and under.

It does work in my multi-IE install of IE5.01 but not in IE4.

[snip]

> I really like the XPath-enabled version(s) better. The simple version
> isn't that much more code and is considerably faster in browsers that
> support document.evaluate.

There are so many ways to implement these things. The library here
just has to be correct for the sake of article completeness. It
doesn't have to be optimal. In fact, less concepts are better for an
article.

[snip]

> > if (typeof LIB.getAnElement != 'undefined' &&
>
> if (LIB.getAnElement &&
>
> > typeof LIB.getAnElement().className == 'string') {
>
> When you add the more robust version of getAnElement to support IE4,

I'm not going to add that version of getAnElement here.

> it would be a good idea to test its result.

Why? If getAnElement is defined then it should work. Part of defining
it is testing that it will work.


> > // The RegExp support need here
> > // has been available since NN4 & IE4
> > LIB.hasClass = function(el, className) {
> > return (new RegExp(
> > '(^|\\s+)' + className + '(\\s+|$)')).test(el.className);
> > };
> >
> > LIB.addClass = function(el, className) {
> > if (LIB.hasClass(el, className)) {
> > return;
> > }
> > el.className = el.className + ' ' + className;
> > };
> >
> > LIB.removeClass = function(el, className) {
> > el.className = el.className.replace(
> > new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g'), ' ');
> > // in case of multiple adjacent with a single space
> > if ( LIB.hasClass(el, className) ) {
> > arguments.callee(el, className);
> > }
>
> I'm missing something here. Can you elaborate on this?

"foo foo".replace(/(^|\s+)foo(\s+|$)/g, ' ');

returns

"foo"

It is something to do with how regular expressions are matched
internally. The space after the first foo is associated with the first
foo. That means the second foo doesn't have either the string start or
space before it.

This is code I got from YUI.

> Despite the verbosity of my comments, most of them are quibbles. This
> is certainly the right way to create a widget. Unfortunately, it
> seems that most Web developers follow a far different path:
>
> 1. Download Prototype, jQuery or some other amateurish garbage, along
> with assorted plug-ins
> 2. Write widget code that is married to the library in terms of
> syntax, as well as (typically ineffectual) feature testing
> 3. Put a "Best viewed in three or four modern browsers" warning on
> pages that use the widget
> 4. Download patches to the library for eternity, ignoring the fact
> that the library authors will never really get it right
> 5. Upload widget to library-specific repository so developers with
> similar delusions can break their sites as well
> 6. Tell anyone who complains that they aren't living in the "real
> world" (which apparently doesn't have mobile devices, set top boxes or
> off-brand browsers of any kind)
> 7. Exhort everyone else to follow their lead as their widget code is
> so simple and compact (never mind that it has 150K of dependencies and
> is prone to catastrophic failure)
>
> But hey, it works for Google. Unfortunately for the rest of us, the
> equation is not:
>
> incompetence = Web success
>
> But rather:
>
> incompetence + lots of cash from investors = Web success

:-)


> But I digress. For those who need an example of how to write widgets
> for applications on the public Internet, this is a good example. With
> some minor modifications, it could be a great example.

Thanks for all your detailed comments. I am glad there is somewhere
like c.l.js where a response like this is possible. Part of the reason
for this article is to bottle up some of the discussion for
consumption.

Peter

David Mark

unread,
Feb 13, 2008, 6:32:04 AM2/13/08
to
On Feb 13, 3:40 am, Peter Michaux <petermich...@gmail.com> wrote:
> David Mark wrote:
> > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > I'm writing a blog article about building a decent tabbed pane widget
>
> [snip]
>
> > > <URL:http://peter.michaux.ca/temp/tabbedPane/index.html>
>
> > Did you mean to leave the UL unstyled (other than the background color
> > of the selected item?)
>
> For now yes. I don't want to cloud what is related to scripting and
> what is only for looks.
>
> [snip]
>
> > Regardless of the state of scripting, the tabs must be accessible by
> > keyboard.  The text in the list items should be wrapped in anchors or
> > buttons.  Buttons make more sense, but are impossible to style in some
> > browsers.
>
> You are right. I need to make this keyboard accessible. Given how so
> many websites are not navigable by keyboard, there must be a way for
> people to move their cursor around with the keyboard to simulate a
> mouse.

I imagine such devices/drivers exist, but certainly they should not be
necessary to navigate the Web. The best way to test keyboard
accessibility is to unplug the mouse. If your page isn't 100%
functional without it, there is a problem with the design.

>
> [snip]
>
> > > // Test what can be tested as soon as possible
> > >     // Check that the library loaded
> > > if (typeof LIB != 'undefined' &&
> > >     // Test for library functions I use directly
>
> > I would test if it is an object and that it is truthy (to exclude
> > null.)  You can't trust global variables, so it is best to interrogate
> > them as thoroughly as possible.
>
> Even more specific would be to test that it is an object with
>
> typeof LIB == 'object'

What I proposed translates to:

typeof LIB == 'object' && LIB

Remember that due to a mistake by the creators of JavaScript, typeof
null == 'object'.

Just childNodes.

>
> For the sake of curiosity, why would I want to call them?

You wouldn't. The point is that isRealObjectProperty will return
false for a callable property (typeof o[p] == 'function'.) So these
would fail in Safari:

isRealObjectProperty(document, 'images')
isRealObjectProperty(document, 'forms')
isRealObjectProperty(document, 'all')

And possibly childNodes if the pattern holds. The isHostMethod
function allows for 'object', 'function' or 'unknown' types. The
isRealObjectProperty allows only for a truthy object.

>
> > >              typeof el.innerHTML == 'string';
> > >     })()) {
>
> > >   (function() {
>
> > >     var doc = this.document;
>
> > >     var cc = 'current'; // TODO
>
> > What is there to do here?
>
> Sorry. I meant to trim that TODO out. I was going to change the name
> and may.
>
> > >     var enliven = function(c, t, p) {
> > >       LIB.addListener(t, 'click', function() {
>
> > I typically create "shortcuts" to API properties.  It speeds things
> > property access
>
> I tested this just a few days ago. Richard Cornford has referred to
> these as something like "silly namespaces with performance penalties".

These are penalties that can be easily avoided. Granted, "deep"
namespaces like YUI's are silly.

By habit, I have always tested the return value of createElement. It
may not be necessary. My original thinking was along the lines of a
browser that is out of resources and cannot create a new element.
Perhaps it would return null in that case (or perhaps it would just
blow up.)

>
> > >         LIB.addClass(ts, 'tabs');
> > >         LIB.arrayForEach(
> > >           LIB.arrayFilter(
> > >                  w.childNodes,
> > >                  function(s) {
> > >                    return LIB.hasClass(s, 'section');
> > >                  }),
> > >           function(p) {
> > >             t = doc.createElement('li');
>
> > Okay, it appears t is for tab and p is for pane and cc is class added
> > to indicate that the tab is selected.
>
> Yes. Sorry to make you muddle through that.
>
> > >             if (first) {
> > >                 c = {t:t, p:p};
> > >                 LIB.addClass(t, cc);
> > >                 LIB.addClass(p, cc);
>
> > I assume you are adding and removing panes from the layout by adding
> > and removing the "current" class.
>
> Yes.
>
> > I would set the display style
> > directly.  Fiddling with a widget's accompanying style sheet should
> > never have the potential to break the its behavior.
>
> I've debated exactly this in my head. I'm not sure which is better.
>
> The separatists argue that all styling should be in the stylesheet.

If the styles are for appearance only then yes. If they are part of
the mechanics of the widget, then definitely not. Display,
visibility, cursors, etc. fall into the latter category. You should
not be able to cause problems with the widget's behavior by
customizing its style sheet. Also, imagine if you want to create
alternate style sheets. You wouldn't want to repeat the display rules
in those.

> This breaks down the minute they want an animation.
>
> I think it is fair to put a bit of css in the stylesheet that has a
> comment /* for javascript */; however, some other rule could then
> override it. If it is a direct manipulation of element.style.display
> it wins the cascade for sure.

Exactly. A (very strange) user could override with an !important rule
in their local style sheet, but you covered that in your feature
testing.

>
> Thanks.
>
> > >                 first = false;
> > >             }
> > >             enliven(c, t, p);
> > >             h = LIB.querySelector('h2', p)[0];
>
> > You should generalize this to use any level headline.
>
> I will mention in the article that this could be done many ways. I
> don't want to bloat the actual mechanics of the widget with options. I
> think that just one way is good enough for an article.
>
> > And why not
> > getEBTN for this (seems like a more direct approach.)
>
> Given the implementation, there is really no effective difference. The
> compiled function just calls getEBTN. I think that these querySelector
> functions are the way of the future (there is a standard now and it is
> in Safari 3, I believe) and provide a nice uniform API. Just one
> function that does it all.

I've never had a need for them, but indeed there is a proposed
standard to query by CSS selectors.

>
> > >             t.innerHTML = h ? h.innerHTML : 'tab';
>
> > I would not use innerHTML for this as you have appendChild at your
> > disposal.
>
> The innerHTML may be more than one element and that would require a
> loop. I could add a restriction to the design spec that the h2
> contents can only be one text element.

What's wrong with a loop?

>
> I'm no longer afraid of innerHTML. It works far faster and more
> predictably than the DOM functions across the various browsers. I have
> a work project that builds a table with 250 rows, 7 cells per row and
> some images in each row. The only way to get acceptable performance is
> by building the table with HTML and inserting with innerHTML. Using
> createElement, appendChild is *really* slow even if the final
> attachment to the DOM happens in one appendChild.

In such an extreme case, I would agree that innerHTML is the way to
go.

>
> > >             ts.appendChild(t);
> > >           });
> > >         w.insertBefore(ts, w.firstChild);
> > >     };
>
> > >     // Test that a pane really is out of the page
> > >     // when it is not current and that it has some
> > >     // height when it is current. This is the critical
> > >     // test to make sure the tabbed pane will work.
> > >     var supportsDisplayCss = function() {
> > >       var middle = doc.createElement('div');
>
> > Should use the createElement wrapper (2 lines IIRC) to allow for XHTML
> > support.
>
> If I do that then I have to stop using innerHTML for consistency.

I don't think that's a problem for the contents of a headline.

>
> Personally I don't care one bit about XHTML. Given the XHTML
> implementations and what happens with a page is malformed, it is a
> total waste of time, in my opinion.

A good solution is to make your pages well-formed. However, it is a
waste of time on the public Internet. Another caveat is that some
browsers have buggy implementations. You saw the XHTML/HTML test page
on my builder site (I assume) and I had never had a problem with it
got very long (i.e. when every module is selected.) Recently, after
adding several new modules and their accompanying unit tests, I was
flabbergasted to see Opera 9 throw an obscure error (document ended
unexpectedly or something) on page load. Hitting refresh made it go
away. Refreshing repeatedly wouldn't bring it back, but I eventually
saw it again. The error always points to the same line in the markup,
which is certainly well-formed. That's enough to make me forget XHTML
on the public Internet. I am not going to remove support for it from
the library as it can be useful for Intranet applications (and only
adds a couple dozen lines to the 7000 or so that make up the complete
build.)

I do a lot of this sort of testing in my library. The offset position
function looks for about a dozen quirks in similar fashion. I've
never done it at the widget level, but it makes sense as user-defined
rules or a CSS file that fails to download can render them useless.

>
> > I wrote a generalized "cssEnabled" function a while back that used
> > similar logic, but have never used it for anything in production.  It
> > seemed to me at the time that I needed widgets to work even if CSS was
> > toggled after the page was loaded.
>
> If some user toggles the CSS after the page is loaded is on his own. I
> will mention this however.
>
> It is almost just as easy that some user might type this into his
> browser URL bar
>
> javascript:LIB.addClass = null;
>
> There are certain things like this that someone really has to go out
> of his way to break the page.

Yes, I don't think it makes sense to worry about either case.

>
> > I do like the specific test for
> > this widget as I have never dealt with the issue of a (lunatic) user
> > adding an !important display rule.
>
> > >         doc.body.removeChild(outer);
> > >         return doesSupport;
> > >       }
> > >       return false;
> > >     };
>
> > >     // We don't know for sure at this point that the tabbed pane
> > >     // will work. We have to wait for window.onload to finish
> > >     // the tests. We do know we can give the pages some style to use
> > >     // during the page load because we can "get out of trouble"
> > >     // when window.onload fires. This is
> > >     // because the functions used to get out of trouble
> > >     // have been feature tested.
>
> > You can skip all of this unless this test passes:
>
> > var el = getAnElement();
> > if (el && el.style && typeof el.style.display == 'string') {
> >   // Add style rule(s)
> >   // Attach load listener
> > }
>
> Skip all of what?

The whole widget. Regardless of how you change the display style,
agents that fail the above test will not reflect those changes in the
layout. AFAIK, this is a small group that includes Opera 6 and some
outdated phones (which were probably running an old version of Opera
Mini.)

>
> I did have this test at one point but since I put the styling in a CSS
> sheet and only manipulated the classes, I figured I didn't need the
> above test as I don't directly manipulate the el.style.display value.
> I may switch back to the direct manipulation.

Your test is fine. All I meant was that you could skip everything if
the initial display test failed.

>
> > >     doc.write('<link href="tabbedPane.css"'+
> > >               ' rel="stylesheet" type="text/css">');
>
> > IIRC, this will crash some (or all) revisions of NN4.x.
>
> Crap.
>
> It works for me in NN4.0 and NN4.5 with no problems.

Perhaps I am mistaken. I could have sworn there was an issue with
using document.write in the head. Personally, I would forget NN4 as
it was never a viable browser. You could say the same thing about
NN6.0 (though 6.2 is fairly easy to support.)

>
> This is actually a very important part of what I want to be able to do
> with widgets and part of the reason for the article.

You have to do it, but you don't have to use document.write.

>
> > It also will
> > not work with XHTML (I know it is a dead language on the Web, but it
> > is useful for Intranets.)
>
> I'm ok with this not working with XHTML.
>
> > Better to add the needed rules via DOM
> > manipulation.  Of course, that logic must reside in a script block
> > outside of the head element, else you risk the dreaded "Operation
> > Aborted" error in IE.
>
> Officially, the DOM is supposed to be read only until window.onload. I

According to whom? Personally, I don't touch the body element until
the document has completed parsing, but adding style elements to the
head shouldn't be an issue as long as the script is after the closing
head tag.

> don't want to touch it in any way. I like following the rules. I'm
> sure the people that started seeing that Operation Aborted error
> didn't know what to do for a long time and the only reason they saw it
> was because they didn't follow the rules.

No, it is a documented bug in IE6. I don't know if it exists in IE7
or not. I've never run into it, except when testing third-party
scripts. It is caused by attempting to manipulate elements that
haven't been fully parsed.

>
> The only sanctioned thing that can be done while the page is loading
> is document.write to dynamically generate parts of the page.

I've never heard of such a rule and certainly nobody conforms to it
(or enforces it.) What would be the point of DOMContentLoaded if you
had to wait for the load event?

>
> So if XHTML is to be used then having the transitional CSS that makes
> the page somewhat acceptable to the user would not be possible.

That is incorrect. You can certainly add style elements to the head
of an XHTML document while it is loading. I do it all the time. The
flash of fallback content is less pronounced with XHTML as there is no
incremental rendering going on during the load, but I can't stand even
the slightest unnecessary twitch.

>
> > >     LIB.addListener(this.window, 'load', function() {
>
> > I think "this" is sufficient as this.window points back to the global
> > object.
>
> > >       // Cannot test that CSS support works until window.onload.

Or at least until the document is parsed.

> > >       // This also checks that the stylesheet loaded
> > >       if (supportsDisplayCss()) {
> > >         LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > >         LIB.addClass(LIB.getDocumentElement(),
> > >                     'tabbedPanesEnabled');
> > >       }
> > >       else {
> > >         // "get out of trouble"
> > >         LIB.addClass(LIB.getDocumentElement(),
> > >                       'tabbedPanesDisabled');
> > >       }
>
> > Interesting that you add classes to the documentElement.  I haven't
> > had occasion to do that.
>
> I haven't used it before but it seems to work. I have added classes to

Certainly it should work.

> document.body to do what I'm trying to do here because I've assumed
> document.body exists.

Which is a fairly good assumption unless you use XHTML. I haven't had
occasion to add classes to the body either. Either should work, but
there are certainly other ways to go.

>
> The reason I added to document.documentElement here is because if I am
> "getting out of trouble", I can only use what I knew existed when the
> first set of feature tests ran. That means I know the
> document.documentElement exists but I do not know that document.body

Right.


David Mark

unread,
Feb 13, 2008, 7:49:22 AM2/13/08
to
On Feb 13, 3:46 am, Peter Michaux <petermich...@gmail.com> wrote:
> David Mark wrote:
> > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > I'm writing a blog article about building a decent tabbed pane widget
>
> [snip]
>
>
>
>
>
> > > // lib.js ------------------------------------------------------
>
> > > // test any JavaScript features
> > > //   new in NN4+, IE4+, ES3+
> > > //   or known to have a bug in some browser
> > > // test all host objects
>
> > > var LIB = {};
>
> > > // Some array extras for the app developer.
> > > // These are not used within the library

And certainly shouldn't be for performance reasons.

> > > // to keep library interdependencies low.

That too. I made all of the array stuff optional in my builder.

> > > // These extras don't use the optional
> > > // thisObject argument of native JavaScript 1.6
> > > // Array.prototype.filter but that can easily
> > > // be added here at the cost of some file size.

I think I covered that in the versions I posted to CWR. If not, see
the versions on my site.

Clearly. I assumed they were just placeholders.

> LIB.preventDefault section shouldn't be defined unless it can work.

There's really no telling. This is what I use:

cancelDefault = function(e) {
if (e.preventDefault) { e.preventDefault(); }
if (global.event) { global.event.returnValue = false; }
return false;
};

Regardless of the event model used, a listener can return
cancelDefault(e) and it will work (except click/dblclick in Safari 1.x
as I do use addEventListener when available.)

> This function came from another library and is really not up to snuff.
> A group discussion about events sooner than later would be good. It is
> not an easy problem in general and it is the most important library.

Agreed.

>
> [snip]
>
>
>
>
>
> > > // ---------------------------------------------------
>
> > > (function() {
>
> > >   var isRealObjectProperty = function(o, p) {
> > >     return !!(typeof(o[p]) == 'object' && o[p]);
> > >   };
> > >   LIB.isRealObjectProperty = isRealObjectProperty;
>
> > >   var isHostMethod = function(o, m) {
> > >     var t = typeof(o[m]);
> > >     return !!(((t=='function' || t=='object') && o[m]) ||
> > >               t == 'unknown');
> > >   };
> > >   LIB.isHostMethod = isHostMethod;
>
> > >   if (!(isRealObjectProperty(this, 'document'))) {
> > >     return;
> > >   }
> > >   var doc = this.document;
>
> > I would set this to null once feature testing is complete.  I
> > reference the default document as global.document thereafter.  It
> > isn't a big concern here as you didn't define your addListener
> > function inside here.
>
> Ahh, circular memory leaks? I was thinking about this recently. All
> functions have "document" in their closure which references
> document.body which has all the document elements as descendants. This

The issue I was referring to was related to using DOM0 or attachEvent
on the document. I don't think the descendants enter into it, but as
body is a property of document, attaching a listener to it would
create this circular reference:

body => body.onclick => document => body

As my whole library resides in a single closure, I needed to set doc
to null after feature testing to avoid this issue.

> will include whichever element a function is observing. There is
> always a very indirect circle. For some reason, do these indirect
> circles not cause the IE memory leak. I think they should.

I don't think so. Just the document and the body as it is referenced
by a property of the document. Then again, it is IE, so who knows for
sure?

>
> > >   if (isRealObjectProperty(doc, 'documentElement')) {
>
> > IIRC, this excludes IE4.
>
> That is ok. For this article, as long as IE4/NN4 don't syntax or
> runtime error that is ok. They may not even have enough CSS support to
> make a tabbed pane look any good unless table layouts are used.

Perhaps not.

>
> [snip]
>
> > >   // Test both interfaces specified in the DOM
> > >   if (isHostMethod(doc, 'getElementsByTagName') &&
> > >       typeof getAnElement == 'function' &&
>
> > && getAnElement
>
> > You know that the getAnElement variable exists.
>
> getAnElement is defined in an if statement above.

Whether it is defined or not is irrelevant. The issue is whether the
variable exists. It exists as it was created as soon as the
containing function was entered and can therefore be tested by boolean
type conversion. This is why I don't like declaring variables inside
blocks (it is misleading.)

The mainstream libraries offer zero protection from such issues.
Regardless, it is a simple matter of adding an additional parameter to
isHostMethod to exclude "unknown" types. This parameter should be set
if the property in question will be evaluated rather than called (as
in this case.) Document it as such and you are done.

>
>
>
>
>
> > >       }
> > >       return els;
> > >     };
>
> > >   }
>
> > >   if (isHostMethod(doc, 'getElementById')) {
> > >     // One possible implementation for developers
> > >     // not troubled by the name and id attribute
> > >     // conflict in IE
> > >     LIB.getEBI = function(id, d) {
> > >       return (d || doc).getElementById(id);
> > >     };
> > >   }
>
> > >   if (LIB.getEBTN &&
> > >       LIB.getEBI &&
> > >       typeof getAnElement == 'function' &&
>
> > getAnElement &&
>
> > You can skip the typeof operation for library functions in other
> > modules as well, as long as they are dependencies that are
> > automatically inserted by the build process.
>
> But getAnElement may not be defined.

But it is certainly declared at this point, which is all that matters.

>
> > >       (function() {
> > >         var el = getAnElement();
> > >         return typeof el.nodeType == 'number' &&
>
> > IIRC, this excludes IE5.0 and under.
>
> It does work in my multi-IE install of IE5.01 but not in IE4.

I could have sworn that IE5.0 did not support nodeType. Either I am
mistaken of the multi-IE install is faulty. I would check
quirksmode.org, but they have inexplicably removed most of the IE5
data at this point. I don't have a stand-alone IE5 install to test,
but I am pretty sure my multi-IE install is sound. I'll check it when
I get a chance.

>
> [snip]
>
> > I really like the XPath-enabled version(s) better.  The simple version
> > isn't that much more code and is considerably faster in browsers that
> > support document.evaluate.
>
> There are so many ways to implement these things. The library here
> just has to be correct for the sake of article completeness. It
> doesn't have to be optimal. In fact, less concepts are better for an
> article.

Agreed.

>
> [snip]
>
> > > if (typeof LIB.getAnElement != 'undefined' &&
>
> > if (LIB.getAnElement &&
>
> > >     typeof LIB.getAnElement().className == 'string') {
>
> > When you add the more robust version of getAnElement to support IE4,
>
> I'm not going to add that version of getAnElement here.
>
> > it would be a good idea to test its result.
>
> Why? If getAnElement is defined then it should work. Part of defining
> it is testing that it will work.

In some cases it is not possible to know for sure. But in this case,
you are correct as all it does is return the first element it finds.
We know the document that contains the script has at least one
element. If you pass a different document to it, that would be
another story.

>
>
>
>
>
> > >   // The RegExp support need here
> > >   // has been available since NN4 & IE4
> > >   LIB.hasClass = function(el, className) {
> > >     return (new RegExp(
> > >       '(^|\\s+)' + className + '(\\s+|$)')).test(el.className);
> > >   };
>
> > >   LIB.addClass = function(el, className) {
> > >     if (LIB.hasClass(el, className)) {
> > >       return;
> > >     }
> > >     el.className = el.className + ' ' + className;
> > >   };
>
> > >   LIB.removeClass = function(el, className) {
> > >     el.className = el.className.replace(
> > >       new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g'), ' ');
> > >     // in case of multiple adjacent with a single space
> > >     if ( LIB.hasClass(el, className) ) {
> > >       arguments.callee(el, className);
> > >     }
>
> > I'm missing something here.  Can you elaborate on this?
>
> "foo foo".replace(/(^|\s+)foo(\s+|$)/g, ' ');
>
> returns
>
> "foo"

I would consider that a valid result for data that has clearly been
corrupted by another process. I don't think I would fix it here and
shield the developer from a mistake that happened elsewhere.

>
> It is something to do with how regular expressions are matched
> internally. The space after the first foo is associated with the first
> foo. That means the second foo doesn't have either the string start or
> space before it.

Right.

>
> This is code I got from YUI.

YUI is a poor source of code. I took a little time and read through
some of it recently. To be fair, I don't know what version it was as
I just stumbled across the archive. Regardless, it was God-awful.
IIRC, the event handling portion was full of browser sniffing (and
that is the part touted as its best bit.) Certainly it is much better
code than that found in Prototype or jQuery, but that isn't saying
much. When I get a chance, I will check out the latest version and if
it hasn't improved (or if God forbid I was actually looking at the
latest version), I will give it the jQuery treatment (dissection and
condemnation.)

[snip]

>
> Thanks for all your detailed comments. I am glad there is somewhere
> like c.l.js where a response like this is possible. Part of the reason
> for this article is to bottle up some of the discussion for
> consumption.
>

Yes, the Web would be better place if more developers realized that
blogs make terrible discussion forums (particularly for technical
topics.) It seems obvious considering they don't even support
threads. I've seen several JS-related blogs that don't support
posting code, cropping it at the first instance of ">".

@scriptkiddie: Your code didn't post.
@somebodyelse: Thanks!
@blogmaven: You are wrong.

[Comments closed]

How did those things ever supplant Usenet?

Peter Michaux

unread,
Feb 13, 2008, 1:46:44 PM2/13/08
to
On Feb 13, 3:32 am, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 3:40 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > David Mark wrote:
> > > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:

[snip]

> > If execution has reached this stage, then doc.createElement should be
> > tested to work. I have a feature test at the top for the existence of
> > document.createElement. I could add a test there that it also works
> > and a comment that it may be overly paranoid. I'm not being that
> > paranoid in other cases. I'd like the article to point out that you
> > can actually test things work as well as just testing they exist so
> > this may make a good example.
>
> By habit, I have always tested the return value of createElement. It
> may not be necessary. My original thinking was along the lines of a
> browser that is out of resources and cannot create a new element.
> Perhaps it would return null in that case (or perhaps it would just
> blow up.)

That is overly paranoid! ;-)

I suppose the test could go with the cssDisplaySupport, because if the
browser has decided this late in the game that it isn't going to make
any more elements then that time for the check is before any elements
for the tabbed pane are created.

There are quite a few kinds of widgets that, when passed the point of
no return, require successful createElement calls to be successful.
There is almost no point in testing the calls worked at that late
stage in the game. I suppose an attempt to back out of the widget to
the static version could be made. An error message saying "you
computer has very little memory...upgrade." might help.

[snip]

> > > Better to add the needed rules via DOM
> > > manipulation. Of course, that logic must reside in a script block
> > > outside of the head element, else you risk the dreaded "Operation
> > > Aborted" error in IE.
>
> > Officially, the DOM is supposed to be read only until window.onload. I
>
> According to whom?

The only reference I can find right now is

<URL: http://www.devguru.com/technologies/xmldom/quickref/document_readyState.html>

I don't see anything on the Microsoft site about the DOM being read-
only in this state. I may just be wrong about this.


> Personally, I don't touch the body element until
> the document has completed parsing, but adding style elements to the
> head shouldn't be an issue as long as the script is after the closing
> head tag.
>
> > don't want to touch it in any way. I like following the rules. I'm
> > sure the people that started seeing that Operation Aborted error
> > didn't know what to do for a long time and the only reason they saw it
> > was because they didn't follow the rules.
>
> No, it is a documented bug in IE6. I don't know if it exists in IE7
> or not.

I believe IE7 does have it and that I've seen it. That was quite a
while ago so my memory may be wrong.


> I've never run into it, except when testing third-party
> scripts. It is caused by attempting to manipulate elements that
> haven't been fully parsed.
>
>
>
> > The only sanctioned thing that can be done while the page is loading
> > is document.write to dynamically generate parts of the page.
>
> I've never heard of such a rule and certainly nobody conforms to it
> (or enforces it.) What would be the point of DOMContentLoaded if you
> had to wait for the load event?

I always figured that DOMContentLoaded was a proprietary extension
that was early permission to the developer that even though the
standard says to wait, it is actually ok to start manipulating now. It
looks like I was wrong about the read-only part, however.

Peter

Peter Michaux

unread,
Feb 13, 2008, 1:47:36 PM2/13/08
to
On Feb 13, 4:49 am, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 3:46 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > David Mark wrote:
> > > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > > I'm writing a blog article about building a decent tabbed pane widget

[snip]

> > > > isHostMethod(getAnElement(), 'getElementsByTagName')) {


>
> > > > // One possible implementation for developers
> > > > // in a situation where it is not a problem that
> > > > // IE5 thinks doctype and comments are elements.
> > > > LIB.getEBTN = function(tag, root) {
> > > > root = root || doc;
> > > > var els = root.getElementsByTagName(tag);
> > > > if (tag == '*' &&
> > > > !els.length &&
> > > > isHostMethod(root, 'all')) {
>
> > > There is a potential problem here (and I made the same mistake several
> > > times in my library.) The isHostMethod function should only be used
> > > to test properties that will be called. It allows for properties of
> > > type "unknown", which are safe to call, but unsafe to evaluate.

This seems like a very ad hoc set of rules...driven by a very ad hoc
browser.


> > > > els = root.all;
>
> > > Here is where it would blow up if some future version of IE implements
> > > documents as ActiveX objects (or whatever the hell it is that returns
> > > "unknown" for typeof operations.) You can't use isRealObjectProperty
> > > either as Safari makes document.all callable (typeof root.all ==
> > > 'function'.) I think I mentioned this in one of the tickets. We need
> > > a way a parameter to tell isHostMethod to exclude "unknown" properties
> > > when appropriate. This is a perfect example.

[snip]

I think it would be more appropriate and a more future-proof API to
make a new test. The rules for the test may diverge and having the
user specifying the parameters of such a test is dangerous. The user
shouldn't have to know the mechanics of the test.

var isHostObject = function(o, m) {


var t = typeof(o[m]);

return !!((t=='function' || t=='object') && o[m]);
};
LIB.isHostObject = isHostObject;

[snip]

> > > > (function() {
> > > > var el = getAnElement();
> > > > return typeof el.nodeType == 'number' &&
>
> > > IIRC, this excludes IE5.0 and under.
>
> > It does work in my multi-IE install of IE5.01 but not in IE4.
>
> I could have sworn that IE5.0 did not support nodeType. Either I am
> mistaken of the multi-IE install is faulty. I would check
> quirksmode.org, but they have inexplicably removed most of the IE5
> data at this point. I don't have a stand-alone IE5 install to test,
> but I am pretty sure my multi-IE install is sound. I'll check it when
> I get a chance.

What did you find?


[snip]

> How did [blogs] ever supplant Usenet?

They are shinier.

Peter

Peter Michaux

unread,
Feb 13, 2008, 4:55:34 PM2/13/08
to
On Feb 12, 10:28 pm, David Mark <dmark.cins...@gmail.com> wrote:

> var el = getAnElement();
> if (el && el.style && typeof el.style.display == 'string') {
> // Add style rule(s)
> // Attach load listener

Can you think of any reason to do some indirect test that style object
exists?

isRealObjectProperty(el, 'style');

-----

I'm making a slew of changes to the code now (not the event stuff yet)
and will post a new example.

Peter

dhtml

unread,
Feb 13, 2008, 7:00:24 PM2/13/08
to
On Feb 13, 12:40 am, Peter Michaux <petermich...@gmail.com> wrote:
> David Mark wrote:
> > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > I'm writing a blog article about building a decent tabbed pane widget
>
> [snip]
[snip]

>
> If I do that then I have to stop using innerHTML for consistency.
>
> Personally I don't care one bit about XHTML. Given the XHTML
> implementations and what happens with a page is malformed, it is a
> total waste of time, in my opinion.
>
> > > if (LIB.isRealObjectProperty(doc, 'body') &&
>
> > This will fail in some browsers when using XHTML (see getBodyElement.)
>

Only old versions of FF, I think.


> For consistency, I considered writing
>
> LIB.querySelector('body')

Why not just document.body?


>
> which would have used your fallback in getBodyElement.
>
> > > LIB.isHostMethod(doc.body, 'removeChild') &&
> > > typeof middle.offsetHeight == 'number') {
>
> > > var outer = doc.createElement('div'),
> > > inner = doc.createElement('div');
>
> > > LIB.addClass(outer, 'tabbedPanesEnabled');
> > > LIB.addClass(middle, 'sections');
> > > LIB.addClass(inner, 'section');
> > > inner.innerHTML = '.';
> > > middle.appendChild(inner);
> > > outer.appendChild(middle);
> > > doc.body.appendChild(outer);

I think this creates what MS call a cross-page leak.


> > Looks good (presuming all browsers behave as expected with
> > offsetHeight and display:none. It seems you are checking if the style
> > sheet loaded, as well as if style is enabled and that user style
> > sheets are not interfering. Good deal.
>

The only issues with display: none is with Safari 2. Safari 2 I won't
send form elements with display: none, and I think it sets the value
to "". And there's that issue with getComputedStyle with display:
none John brought up.


>
> > > LIB.addListener(this.window, 'load', function() {
>
> > I think "this" is sufficient as this.window points back to the global
> > object.
>
> > > // Cannot test that CSS support works until window.onload.
> > > // This also checks that the stylesheet loaded
> > > if (supportsDisplayCss()) {
> > > LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > > LIB.addClass(LIB.getDocumentElement(),
> > > 'tabbedPanesEnabled');
> > > }
> > > else {
> > > // "get out of trouble"
> > > LIB.addClass(LIB.getDocumentElement(),
> > > 'tabbedPanesDisabled');
> > > }
>

Why wait for onload?


> > Interesting that you add classes to the documentElement. I haven't
> > had occasion to do that.
>


>


> The reason I added to document.documentElement here is because if I am
> "getting out of trouble", I can only use what I knew existed when the
> first set of feature tests ran. That means I know the
> document.documentElement exists but I do not know that document.body
> exists. I am trying to build a consistent line of logic for what's
> been tested and what is being used.
>

It works, but it's not valid to have class on HTML.


> [snip]
>
> [I'll slit the comments for the lib.js into another reply.]
>

I would design the code with separated modules.

'LIB' does not describe anything. It's like a kitchen sink.

As you build more widgets, they'll also want to reuse stuff in 'LIB'.
If anyone wants to use a LIB widget, they need LIB. It is my opinion
that the core library should be minimalistic.

Garrett

> Peter

David Mark

unread,
Feb 13, 2008, 7:29:17 PM2/13/08
to
On Feb 13, 1:47 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 4:49 am, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 13, 3:46 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > David Mark wrote:
> > > > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > > > I'm writing a blog article about building a decent tabbed pane widget
>
> [snip]
>
>
>
>
>
> > > > >       isHostMethod(getAnElement(), 'getElementsByTagName')) {
>
> > > > >     // One possible implementation for developers
> > > > >     // in a situation where it is not a problem that
> > > > >     // IE5 thinks doctype and comments are elements.
> > > > >     LIB.getEBTN = function(tag, root) {
> > > > >       root = root || doc;
> > > > >       var els = root.getElementsByTagName(tag);
> > > > >       if (tag == '*' &&
> > > > >           !els.length &&
> > > > >           isHostMethod(root, 'all')) {
>
> > > > There is a potential problem here (and I made the same mistake several
> > > > times in my library.)  The isHostMethod function should only be used
> > > > to test properties that will be called.  It allows for properties of
> > > > type "unknown", which are safe to call, but unsafe to evaluate.
>
> This seems like a very ad hoc set of rules...driven by a very ad hoc
> browser.

Indeed.

>
> > > > >         els = root.all;

It is worth noting that unlike el.href for a news links or
el.getAttribute for an XML element or el.offsetParent for an element
that is not in a document or various methods of window.external, this
one will likely never throw an error on [[Get]]. The reason I bought
it up is that it just doesn't make logical sense use a test that
allows "unknown" types and then turn around and evaluate the property
(something that is known to blow up in IE.)

>
> > > > Here is where it would blow up if some future version of IE implements
> > > > documents as ActiveX objects (or whatever the hell it is that returns
> > > > "unknown" for typeof operations.)  You can't use isRealObjectProperty
> > > > either as Safari makes document.all callable (typeof root.all ==
> > > > 'function'.)  I think I mentioned this in one of the tickets.  We need
> > > > a way a parameter to tell isHostMethod to exclude "unknown" properties
> > > > when appropriate.  This is a perfect example.
>
> [snip]
>
> I think it would be more appropriate and a more future-proof API to
> make a new test. The rules for the test may diverge and having the
> user specifying the parameters of such a test is dangerous. The user
> shouldn't have to know the mechanics of the test.
>
> var isHostObject = function(o, m) {
>   var t = typeof(o[m]);
>   return !!((t=='function' || t=='object') && o[m]);};
>
> LIB.isHostObject = isHostObject;

Works for me. Of course, once the library gets extensive enough, most
authors won't need to call these low-level feature test functions. I
think I am at that point with mine (at least for most applications.)

>
> [snip]
>
> > > > >       (function() {
> > > > >         var el = getAnElement();
> > > > >         return typeof el.nodeType == 'number' &&
>
> > > > IIRC, this excludes IE5.0 and under.
>
> > > It does work in my multi-IE install of IE5.01 but not in IE4.
>
> > I could have sworn that IE5.0 did not support nodeType.  Either I am
> > mistaken of the multi-IE install is faulty.  I would check
> > quirksmode.org, but they have inexplicably removed most of the IE5
> > data at this point.  I don't have a stand-alone IE5 install to test,
> > but I am pretty sure my multi-IE install is sound.  I'll check it when
> > I get a chance.
>
> What did you find?

I forgot to look. I will try to remember next time I am near the
multi-IE box.

>
> [snip]
>
> > How did [blogs] ever supplant Usenet?
>
> They are shinier.
>

With all of the Web-based front-ends to Usenet, that shouldn't be the
case. Of course, Google Groups is one of the most popular and it is
perhaps the worst Web-based discussion forum ever designed. When I
read posts using it, I never see the last word of each sentence
(unless I reduce text size to the point where I can't read anything.)
I once ran into one of their "usability experts" in one of the Google
development forums and he said that he hadn't heard too many
complaints. (!) That was followed by a deluge of "you've been using
it a long time, you must like it" responses from fanboys. Of course,
most of my posts at the time were via NNTP.

Oh well, last I heard, blog-based discussions were on the way out. I
have no idea what is on the way in though.

David Mark

unread,
Feb 13, 2008, 7:31:32 PM2/13/08
to
On Feb 13, 4:55 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 12, 10:28 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > var el = getAnElement();
> > if (el && el.style && typeof el.style.display == 'string') {
> >   // Add style rule(s)
> >   // Attach load listener
>
> Can you think of any reason to do some indirect test that style object
> exists?
>
> isRealObjectProperty(el, 'style');

This is as it should be. Testing a property of an element by type
conversion is too dangerous. If href can blow up, anything can.

David Mark

unread,
Feb 13, 2008, 7:46:34 PM2/13/08
to
On Feb 13, 7:00 pm, dhtml <dhtmlkitc...@gmail.com> wrote:
> On Feb 13, 12:40 am, Peter Michaux <petermich...@gmail.com> wrote:> David Mark wrote:
> > > On Feb 12, 10:26 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > > I'm writing a blog article about building a decent tabbed pane widget
>
> > [snip]
>
> [snip]
>
>
>
> > If I do that then I have to stop using innerHTML for consistency.
>
> > Personally I don't care one bit about XHTML. Given the XHTML
> > implementations and what happens with a page is malformed, it is a
> > total waste of time, in my opinion.
>
> > > >       if (LIB.isRealObjectProperty(doc, 'body') &&
>
> > > This will fail in some browsers when using XHTML (see getBodyElement.)
>
> Only old versions of FF, I think.

IIRC, Safari 3 for Windows doesn't have it either.

>
> > For consistency, I considered writing
>
> > LIB.querySelector('body')
>
> Why not just document.body?

If not using XHTML, then that will do.

>
>
>
>
>
>
>
> > which would have used your fallback in getBodyElement.
>
> > > >           LIB.isHostMethod(doc.body, 'removeChild') &&
> > > >           typeof middle.offsetHeight == 'number') {
>
> > > >         var outer = doc.createElement('div'),
> > > >             inner = doc.createElement('div');
>
> > > >         LIB.addClass(outer, 'tabbedPanesEnabled');
> > > >         LIB.addClass(middle, 'sections');
> > > >         LIB.addClass(inner, 'section');
> > > >         inner.innerHTML = '.';
> > > >         middle.appendChild(inner);
> > > >         outer.appendChild(middle);
> > > >         doc.body.appendChild(outer);
>
> I think this creates what MS call a cross-page leak.

So did I at one time, but it was pointed out to me that there example
of that in MSDN creates an element with an intrinsic event handler.

>
> > > Looks good (presuming all browsers behave as expected with
> > > offsetHeight and display:none.  It seems you are checking if the style
> > > sheet loaded, as well as if style is enabled and that user style
> > > sheets are not interfering.  Good deal.
>
> The only issues with display: none is with Safari 2. Safari 2 I won't
> send form elements with display: none, and I think it sets the value
> to "".  And there's that issue with getComputedStyle with display:
> none John brought up.

John who? Hopefully everybody knows that you can't reliably query
style rules of elements that aren't part of the layout. It makes no
logical sense to try. It has been brought up recently that some
libraries provide a useless workaround for this, but what a waste of
time and code.

>
>
>
>
>
>
>
> > > >     LIB.addListener(this.window, 'load', function() {
>
> > > I think "this" is sufficient as this.window points back to the global
> > > object.
>
> > > >       // Cannot test that CSS support works until window.onload.
> > > >       // This also checks that the stylesheet loaded
> > > >       if (supportsDisplayCss()) {
> > > >         LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > > >         LIB.addClass(LIB.getDocumentElement(),
> > > >                     'tabbedPanesEnabled');
> > > >       }
> > > >       else {
> > > >         // "get out of trouble"
> > > >         LIB.addClass(LIB.getDocumentElement(),
> > > >                       'tabbedPanesDisabled');
> > > >       }
>
> Why wait for onload?

Because DOM ready logic is beyond the scope of this article. There is
a ticket proposing such code in CWR.

>
> > > Interesting that you add classes to the documentElement.  I haven't
> > > had occasion to do that.
>
> > The reason I added to document.documentElement here is because if I am
> > "getting out of trouble", I can only use what I knew existed when the
> > first set of feature tests ran. That means I know the
> > document.documentElement exists but I do not know that document.body
> > exists. I am trying to build a consistent line of logic for what's
> > been tested and what is being used.
>
> It works, but it's not valid to have class on HTML.

That I didn't know. Good thing I never tried it. Not valid markup?

>
> > [snip]
>
> > [I'll slit the comments for the lib.js into another reply.]
>
> I would design the code with separated modules.
>
> 'LIB' does not describe anything. It's like a kitchen sink.

I use API. What do you propose?

>
> As you build more widgets, they'll also want to reuse stuff in 'LIB'.
> If anyone wants to use a LIB widget, they need LIB. It is my opinion

I don't follow exactly.

> that the core library should be minimalistic.

If it is modular and has a build process, it can be as small or large
as the author requires. Mine ranges from 1K to well over 100K,
depending on the modules selected.

Peter Michaux

unread,
Feb 13, 2008, 8:08:02 PM2/13/08
to
On Feb 13, 4:46 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 7:00 pm, dhtml <dhtmlkitc...@gmail.com> wrote:

> > Why not just document.body?
>
> If not using XHTML, then that will do.

This article is about building widgets for the general web. XHTML is
not something to be used on the general web. I will switch to just
document.body.


> > > > > LIB.addClass(middle, 'sections');
> > > > > LIB.addClass(inner, 'section');
> > > > > inner.innerHTML = '.';
> > > > > middle.appendChild(inner);
> > > > > outer.appendChild(middle);
> > > > > doc.body.appendChild(outer);
>
> > I think this creates what MS call a cross-page leak.
>
> So did I at one time, but it was pointed out to me that there example
> of that in MSDN creates an element with an intrinsic event handler.

What is a "cross-page leak"?

> > > > > LIB.addListener(this.window, 'load', function() {
>
> > > > I think "this" is sufficient as this.window points back to the global
> > > > object.
>
> > > > > // Cannot test that CSS support works until window.onload.
> > > > > // This also checks that the stylesheet loaded
> > > > > if (supportsDisplayCss()) {
> > > > > LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > > > > LIB.addClass(LIB.getDocumentElement(),
> > > > > 'tabbedPanesEnabled');
> > > > > }
> > > > > else {
> > > > > // "get out of trouble"
> > > > > LIB.addClass(LIB.getDocumentElement(),
> > > > > 'tabbedPanesDisabled');
> > > > > }
>
> > Why wait for onload?
>
> Because DOM ready logic is beyond the scope of this article. There is
> a ticket proposing such code in CWR.

This article could leave the door open by having a wrapper for DOM
ready. I am very skeptical there is a solution that works in all
browsers and that some browsers will still need to use window.onload.


> > > > Interesting that you add classes to the documentElement. I haven't
> > > > had occasion to do that.
>
> > > The reason I added to document.documentElement here is because if I am
> > > "getting out of trouble", I can only use what I knew existed when the
> > > first set of feature tests ran. That means I know the
> > > document.documentElement exists but I do not know that document.body
> > > exists. I am trying to build a consistent line of logic for what's
> > > been tested and what is being used.
>
> > It works, but it's not valid to have class on HTML.


The HTML 4.01 spec says no

<URL: http://www.w3.org/TR/REC-html40/struct/global.html#h-7.3>

The DOM 2 spec says yes

http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/html.html#ID-58190037

So since this className is being added using the DOM it should be ok
to do so. It doesn't necessarily make it a good idea, however.


> > > [snip]
>
> > > [I'll slit the comments for the lib.js into another reply.]
>
> > I would design the code with separated modules.
>
> > 'LIB' does not describe anything. It's like a kitchen sink.
>
> I use API. What do you propose?

How about...

ca.michaux.peter.javascript.example.library.dom.classAttribute.addClass()

:D


> > As you build more widgets, they'll also want to reuse stuff in 'LIB'.
> > If anyone wants to use a LIB widget, they need LIB. It is my opinion
>
> I don't follow exactly.
>
> > that the core library should be minimalistic.

The library for the example only has the code necessary to make the
example run.


> If it is modular and has a build process, it can be as small or large
> as the author requires. Mine ranges from 1K to well over 100K,
> depending on the modules selected.

This article is not about the library's modularization. I don't even
believe in this anymore. One namespace is plenty for all of a library.
The library is just for illustrative purposes. I will type that in
bold all-caps in the article so people don't focus on that. (Just
imagine that all of the emacs lisp code is in the single global
namespace.)

Peter

David Mark

unread,
Feb 13, 2008, 8:11:54 PM2/13/08
to
On Feb 13, 1:47 pm, Peter Michaux <petermich...@gmail.com> wrote:

[snip]

>
> What did you find?
>

My multi IE setup has 5.01, which has nodeType, nodeName, childNodes,
etc. It appears to be a valid test environment as it does not have
Arrary.prototype.splice, push, etc.

I don't know what property I was thinking of and since PPK has purged
the IE5 data from quirksmode.org, I can't find out without using the
Internet wayback machine (I'm not that curious at the moment.) I am
sure there was some common node-related property that was missing in
IE5, but added in IE5.5, but can't remember which. As I don't design
anything for a specific subset of browsers, I don't have to clutter my
head with such minutiae.

Peter Michaux

unread,
Feb 13, 2008, 8:17:01 PM2/13/08
to

I suppose this should be the following so the name of the test
function is appropriate.

isHostObject(el, 'style')

Perhaps some browser will make el.style callable or some other weird
contortion.

Even though I just wrote something to the contrary, shouldn't it be
the following for consistency?

isHostObject(doc, 'body')

rather than just doc.body.

Peter

Peter Michaux

unread,
Feb 13, 2008, 8:21:10 PM2/13/08
to
On Feb 13, 4:29 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 1:47 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > On Feb 13, 4:49 am, David Mark <dmark.cins...@gmail.com> wrote:

[snip]

> > > How did [blogs] ever supplant Usenet?
>
> > They are shinier.
>
> With all of the Web-based front-ends to Usenet, that shouldn't be the
> case. Of course, Google Groups is one of the most popular and it is
> perhaps the worst Web-based discussion forum ever designed.

I use, like and don't ever have problems with the Google Groups
interface.


> Oh well, last I heard, blog-based discussions were on the way out. I
> have no idea what is on the way in though.

Micro blogging.

http://twitter.com/

Why anyone would need to know what I'm doing every moment and why I
would feel the need to broadcast that information is beyond me.

Peter

David Mark

unread,
Feb 13, 2008, 8:29:21 PM2/13/08
to
On Feb 13, 8:08 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 4:46 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 13, 7:00 pm, dhtml <dhtmlkitc...@gmail.com> wrote:
> > > Why not just document.body?
>
> > If not using XHTML, then that will do.
>
> This article is about building widgets for the general web. XHTML is
> not something to be used on the general web. I will switch to just
> document.body.

For safety, make sure you test it with isRealObjectProperty. Somebody
could drop the lib and example into an XHTML page.

>
> > > > > >         LIB.addClass(middle, 'sections');
> > > > > >         LIB.addClass(inner, 'section');
> > > > > >         inner.innerHTML = '.';
> > > > > >         middle.appendChild(inner);
> > > > > >         outer.appendChild(middle);
> > > > > >         doc.body.appendChild(outer);
>
> > > I think this creates what MS call a cross-page leak.
>
> > So did I at one time, but it was pointed out to me that there example
> > of that in MSDN creates an element with an intrinsic event handler.
>
> What is a "cross-page leak"?

http://msdn2.microsoft.com/en-us/library/bb250448.aspx

>
>
>
>
>
> > > > > >     LIB.addListener(this.window, 'load', function() {
>
> > > > > I think "this" is sufficient as this.window points back to the global
> > > > > object.
>
> > > > > >       // Cannot test that CSS support works until window.onload.
> > > > > >       // This also checks that the stylesheet loaded
> > > > > >       if (supportsDisplayCss()) {
> > > > > >         LIB.arrayForEach(LIB.querySelector('.sections'), init);
> > > > > >         LIB.addClass(LIB.getDocumentElement(),
> > > > > >                     'tabbedPanesEnabled');
> > > > > >       }
> > > > > >       else {
> > > > > >         // "get out of trouble"
> > > > > >         LIB.addClass(LIB.getDocumentElement(),
> > > > > >                       'tabbedPanesDisabled');
> > > > > >       }
>
> > > Why wait for onload?
>
> > Because DOM ready logic is beyond the scope of this article.  There is
> > a ticket proposing such code in CWR.
>
> This article could leave the door open by having a wrapper for DOM
> ready. I am very skeptical there is a solution that works in all
> browsers and that some browsers will still need to use window.onload.

My solution does indeed use window.onload as a last resort. It
listens for that event in all browsers, but bails out immediately if
the DOM ready code has been run by the DOMContentLoaded listener or a
direct call from a script at the bottom of the page. It works
flawlessly in every browser I have tested.

>
> > > > > Interesting that you add classes to the documentElement.  I haven't
> > > > > had occasion to do that.
>
> > > > The reason I added to document.documentElement here is because if I am
> > > > "getting out of trouble", I can only use what I knew existed when the
> > > > first set of feature tests ran. That means I know the
> > > > document.documentElement exists but I do not know that document.body
> > > > exists. I am trying to build a consistent line of logic for what's
> > > > been tested and what is being used.
>
> > > It works, but it's not valid to have class on HTML.
>
> The HTML 4.01 spec says no

Back then the HTML element wasn't part of the layout.

That makes sense.

>
> http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/html.html#ID-...


>
> So since this className is being added using the DOM it should be ok
> to do so. It doesn't necessarily make it a good idea, however.

It's not something I have ever found a need for. Same for the body.

>
> > > > [snip]
>
> > > > [I'll slit the comments for the lib.js into another reply.]
>
> > > I would design the code with separated modules.
>
> > > 'LIB' does not describe anything. It's like a kitchen sink.
>
> > I use API.  What do you propose?
>
> How about...
>
> ca.michaux.peter.javascript.example.library.dom.classAttribute.addClass()

How very YUI-esque. I have a theory that Yahoo does that to make
their project seem bigger than it is. Granted the code is bloated,
but its functionality is hardly in need of such structure.

>
> :D
>
> > > As you build more widgets, they'll also want to reuse stuff in 'LIB'.
> > > If anyone wants to use a LIB widget, they need LIB. It is my opinion
>
> > I don't follow exactly.
>
> > > that the core library should be minimalistic.
>
> The library for the example only has the code necessary to make the
> example run.
>
> > If it is modular and has a build process, it can be as small or large
> > as the author requires.  Mine ranges from 1K to well over 100K,
> > depending on the modules selected.
>
> This article is not about the library's modularization. I don't even
> believe in this anymore. One namespace is plenty for all of a library.

Don't believe in what? And yes, a single flat namespace is plenty.
Its only purpose is to keep the global namespace clean.

Peter Michaux

unread,
Feb 13, 2008, 8:46:38 PM2/13/08
to
On Feb 13, 5:29 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 8:08 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > On Feb 13, 4:46 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > On Feb 13, 7:00 pm, dhtml <dhtmlkitc...@gmail.com> wrote:
> > > > Why not just document.body?
>
> > > If not using XHTML, then that will do.
>
> > This article is about building widgets for the general web. XHTML is
> > not something to be used on the general web. I will switch to just
> > document.body.
>
> For safety, make sure you test it with isRealObjectProperty. Somebody
> could drop the lib and example into an XHTML page.
>
>
>
> > > > > > > LIB.addClass(middle, 'sections');
> > > > > > > LIB.addClass(inner, 'section');
> > > > > > > inner.innerHTML = '.';
> > > > > > > middle.appendChild(inner);
> > > > > > > outer.appendChild(middle);
> > > > > > > doc.body.appendChild(outer);
>
> > > > I think this creates what MS call a cross-page leak.
>
> > > So did I at one time, but it was pointed out to me that there example
> > > of that in MSDN creates an element with an intrinsic event handler.
>
> > What is a "cross-page leak"?
>
> http://msdn2.microsoft.com/en-us/library/bb250448.aspx

Reading that description it doesn't say anything about JavaScript. It
does talk a lot about "scope". It seems to support what Garrett wrote.

It is not a good idea to do their solution of adding elements only to
elements already in the DOM. It is much better for rendering if the
subtree is created and then added to the DOM in one final appendChild.

> > > > I would design the code with separated modules.
>
> > > > 'LIB' does not describe anything. It's like a kitchen sink.
>
> > > I use API. What do you propose?
>
> > How about...
>
> > ca.michaux.peter.javascript.example.library.dom.classAttribute.addClass()
>
> How very YUI-esque.

It is really much more Java-esque.

> I have a theory that Yahoo does that to make
> their project seem bigger than it is.

I don't think there is any evidence for that.


> > This article is not about the library's modularization. I don't even
> > believe in this anymore. One namespace is plenty for all of a library.
>
> Don't believe in what? And yes, a single flat namespace is plenty.
> Its only purpose is to keep the global namespace clean.

I don't believe in having multiple namespaces within a library. There
is no need for LIB.Dom.addClass and LIB.Event.addListener. If I'm
going to end up with a namespace collision because I want two
LIB.addClass, then I need to rethink something. I do think the single
namespace is a great way to keep the library from colliding with the
app-specific code. This can be done with LIB_addClass also.

Peter

David Mark

unread,
Feb 13, 2008, 9:08:02 PM2/13/08
to

This is from the example in question:

var parentDiv = document.createElement("<div onClick='foo()'>");
var childDiv = document.createElement("<div onClick='foo()'>");

// This will leak a temporary object
parentDiv.appendChild(childDiv);
hostElement.appendChild(parentDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;

Below this example they say:

"The key points to understand about the leak are that DOM elements are
being created with scripts already attached."

Of course, as with virtually anything MS says (even about their own
software), it is questionable as to whether it is a fact or guesswork.

>
> It is not a good idea to do their solution of adding elements only to
> elements already in the DOM. It is much better for rendering if the
> subtree is created and then added to the DOM in one final appendChild.

Agreed.

>
> > > > > I would design the code with separated modules.
>
> > > > > 'LIB' does not describe anything. It's like a kitchen sink.
>
> > > > I use API.  What do you propose?
>
> > > How about...
>
> > > ca.michaux.peter.javascript.example.library.dom.classAttribute.addClass()
>
> > How very YUI-esque.
>
> It is really much more Java-esque.

I'm sure that is where YUI got the idea.

>
> > I have a theory that Yahoo does that to make
> > their project seem bigger than it is.
>
> I don't think there is any evidence for that.

It's just a theory. They seem to be saying that their library is so
expansive that every module needs its own space, else how would you
ever keep track of it all. Then again, maybe it was just an
unfortunate design decision.

>
> > > This article is not about the library's modularization. I don't even
> > > believe in this anymore. One namespace is plenty for all of a library.
>
> > Don't believe in what?  And yes, a single flat namespace is plenty.
> > Its only purpose is to keep the global namespace clean.
>
> I don't believe in having multiple namespaces within a library. There
> is no need for LIB.Dom.addClass and LIB.Event.addListener. If I'm
> going to end up with a namespace collision because I want two
> LIB.addClass, then I need to rethink something. I do think the single
> namespace is a great way to keep the library from colliding with the
> app-specific code. This can be done with LIB_addClass also.

Agreed, but I wouldn't want to tack hundreds of LIB_ properties onto
the global object. Speaking of globals, Opera is known to be slow in
referencing them. They have an article on their Website about that.
So in your widget code, calling API.whatever (as opposed to using a
direct reference to the whatever function) will be slower for two
reasons (at least in Opera.)

Peter Michaux

unread,
Feb 13, 2008, 11:34:32 PM2/13/08
to
Below is my current code.

I've changed the one character identifiers.

I've added isHostObject and am using it.

I haven't tackled the event part of the library but did add a
placeholder LIB.addDomReadyListener function.

I changed the "getting out of trouble" part to use the body element if
it is available (which it almost certainly will be.)

I'm not going to use the Array.prototype.filter in LIB.filter because
using the native array method means it is not possible to send a
NodeList to the LIB.filter function. The way I have implemented
LIB.filter is more like how the generic functions in ES4 will be.

I'm directly manipulating the style.display property. I think this is
just a choice and neither is "right". I will mention this in the
article.

This is strictly for HTML. (XHTML is out of consideration but a
different version for XHTML would be an interesting appendix.)

Any more suggestions or quibbles, even persistent repeats are welcome
if they seem really important.

Peter


// index.html --------------------------------------------------

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">

<html>
<head>

<title>A Tabbed Pane</title>

</head>
<body>

<div class="sections">

</div>

</body>
</html>


// tabbedPane.js --------------------------------------------------

if (typeof LIB == 'object' &&
LIB &&


// Test for library functions I use directly
LIB.getAnElement &&
LIB.getDocumentElement &&

LIB.isHostObject &&
LIB.isHostMethod &&
LIB.filter &&
LIB.forEach &&
LIB.addListener &&
LIB.addDomReadyListener &&
LIB.preventDefault &&


LIB.querySelector &&
LIB.hasClass &&
LIB.removeClass &&
LIB.addClass &&
// Test for host objects and methods I use directly

LIB.isHostObject(this, 'document') &&
LIB.isHostMethod(this.document, 'write') &&
LIB.isHostMethod(this.document, 'createTextNode') &&


LIB.isHostMethod(this.document, 'createElement') &&
(function() {
var el = LIB.getAnElement();

return LIB.isHostObject(el, 'style') &&
typeof el.style.display == 'string'


LIB.isHostMethod(el, 'appendChild') &&
LIB.isHostMethod(el, 'insertBefore') &&

LIB.isHostObject(el, 'firstChild') &&
LIB.isHostObject(el, 'childNodes') &&


typeof el.innerHTML == 'string';
})()) {

(function() {

var doc = this.document;

// configuration
var tabbedPaneCssUrl = 'tabbedPane.css',
tabbedPanesEnabledClassName = 'tabbedPanesEnabled',
tabbedPanesDisabledGroup = 'tabbedPanesDisabled',
currentClassName = 'current',
tabGroupTagName = 'ul',
tabGroupClassName = 'tabs',
tabTagName = 'li',
defaultTabText = 'tab',
paneTagName = 'div',
tabbedPaneGroupClassName = 'sections',
paneGroupTagName = 'div',
tabbedPaneClassName = 'section',
showPaneDisplay = 'block',
hidePaneDisplay = 'none';

var showPane = function(pane) {
pane.style.display = showPaneDisplay;
LIB.addClass(pane, currentClassName);
};
var hidePane = function(pane) {
pane.style.display = hidePaneDisplay;
LIB.removeClass(pane, currentClassName);
};
var makeTabCurrent = function(tab) {
LIB.addClass(tab, currentClassName);
};
var makeTabNotCurrent = function(tab) {
LIB.removeClass(tab, currentClassName);
};

var enliven = function(current, tab, pane) {
LIB.addListener(tab, 'click', function(e) {
LIB.preventDefault(e);

// avoid potential flicker if user clicks the current tab
if (tab == current.tab) {
return;
}
makeTabNotCurrent(current.tab);
hidePane(current.pane);
current.tab = tab;
current.pane = pane;
makeTabCurrent(tab);
showPane(pane);
});
};

var init = function(widgetEl) {
var tabs = doc.createElement(tabGroupTagName),
first = true,
current,
tab,
heading;
LIB.addClass(tabs, tabGroupClassName);
LIB.forEach(
LIB.filter(
widgetEl.childNodes,
function(node) {
return LIB.hasClass(node, tabbedPaneClassName);
}),
function(pane) {
tab = doc.createElement(tabTagName);
if (first) {
current = {tab:tab, pane:pane};
makeTabCurrent(tab);
showPane(pane);
first = false;
}
else {
hidePane(pane);
}
enliven(current, tab, pane);
heading = LIB.querySelector('h2', pane)[0];
tab.innerHTML = '<a href="#">' +
(heading ?
heading.innerHTML :
defaultTabText) +
'</a>';
tabs.appendChild(tab);
});
widgetEl.insertBefore(tabs, widgetEl.firstChild);
};

// Test that a pane really appears and disappears.
// This test uses a dummy tabbed pane temporarily
// inserted into the page. It is one of
// the largest granularity test possible to determine
// the tabbed pane will work.
//
// Tests that CSS is enabled, the necessary
// CSS is supported and that there are no !important rules
// that will interfere.
var supportsDisplayCss = function() {


var outer = doc.createElement('div'),

middle = doc.createElement(paneGroupTagName),
inner = doc.createElement(paneTagName);

if (LIB.isHostObject(doc, 'body') &&


LIB.isHostMethod(doc.body, 'removeChild') &&
typeof middle.offsetHeight == 'number') {

LIB.addClass(outer, tabbedPanesEnabledClassName);
LIB.addClass(middle, tabbedPaneGroupClassName);
LIB.addClass(inner, tabbedPaneClassName);


inner.innerHTML = '.';
middle.appendChild(inner);
outer.appendChild(middle);
doc.body.appendChild(outer);

showPane(inner);
var height = middle.offsetHeight;
hidePane(inner);
var doesSupport = (height > 0 && middle.offsetHeight == 0);


doc.body.removeChild(outer);
return doesSupport;
}
return false;
};

// We don't know for sure at this point that the tabbed pane

// will work. We have to wait for the DOM is ready to finish


// the tests. We do know we can give the pages some style to use
// during the page load because we can "get out of trouble"
// when window.onload fires. This is
// because the functions used to get out of trouble
// have been feature tested.

doc.write('<link href="'+tabbedPaneCssUrl+'"' +


' rel="stylesheet" type="text/css">');

LIB.addDomReadyListener(function() {


// Cannot test that CSS support works until window.onload.
// This also checks that the stylesheet loaded
if (supportsDisplayCss()) {

LIB.forEach(
LIB.querySelector('.'+tabbedPaneGroupClassName),
init);
LIB.addClass(LIB.getDocumentElement(),
tabbedPanesEnabledClassName);


}
else {
// "get out of trouble"
LIB.addClass(

isHostObject(doc, 'body') ?
doc.body :
LIB.getDocumentElement(),
tabbedPanesDisabledClassName);
}
});

})();

}


// tabbedPane.css ----------------------------------------------

/* this file will only be included if there is a chance that
tabbed panes might work */

/* styles for use until window.onload if
the browser is "tentatively" capable */
.sections .section {
display:none;
}
.sections .first {
display:block;
}

/* if feature tests for tabbed panes fail */
.tabbedPanesDisabled .section {
display:block;
}

/* if feature tests for for tabbed panes pass */
.tabbedPanesEnabled li.current {
background:red;
}


// lib.js ------------------------------------------------------

// test any JavaScript features
// new in NN4+, IE4+, ES3+
// or known to have a bug in some browser
// test all host objects

// TODO don't refer to LIB inside any function in LIB

var LIB = {};

// Some array extras for the app developer.
// These are not used within the library
// to keep library interdependencies low.
// These extras don't use the optional
// thisObject argument of native JavaScript 1.6
// Array.prototype.filter but that can easily
// be added here at the cost of some file size.

LIB.filter = function(a, f) {


var rs = [];
for (var i=0, ilen=a.length; i<ilen; i++) {

if (typeof a[i] != 'undefined' && f(a[i])) {


rs[rs.length] = a[i];
}
}
return rs;
};

LIB.forEach = function(a, f) {


for (var i=0, ilen=a.length; i<ilen; i++) {

if (typeof a[i] != 'undefined') {
f(a[i]);
}
}
};

// ---------------------------------------------------

// TODO, feature testing and multiple handlers
LIB.addListener = function(element, eventType, callback) {
element['on'+eventType] = callback;
};

LIB.addDomReadyListener = function(callback) {
LIB.addListener(window, 'load', callback);
};

LIB.preventDefault = function(e) {
if (e.preventDefault) {
e.preventDefault();
return;
}
// can't test for returnValue directly?

if (e.cancelBubble != undefined){
e.returnValue = false;
return;
}
};


// ---------------------------------------------------

(function() {

// No longer needed?
// var isRealObjectProperty = function(o, p) {
// return !!(typeof(o[p]) == 'object' && o[p]);
// };
// LIB.isRealObjectProperty = isRealObjectProperty;

var isHostMethod = function(o, m) {
var t = typeof(o[m]);
return !!(((t=='function' || t=='object') && o[m]) ||
t == 'unknown');
};
LIB.isHostMethod = isHostMethod;

var isHostObject = function(o, m) {


var t = typeof(o[m]);

return !!((t=='function' || t=='object') && o[m]);
};
LIB.isHostObject = isHostObject;

if (!(isHostObject(this, 'document'))) {


return;
}
var doc = this.document;

if (isHostObject(doc, 'documentElement')) {


var getAnElement = function(d) {
return (d || doc).documentElement;
};
LIB.getAnElement = getAnElement;
LIB.getDocumentElement = getAnElement;
}

// Test both interfaces in the DOM spec


if (isHostMethod(doc, 'getElementsByTagName') &&
typeof getAnElement == 'function' &&
isHostMethod(getAnElement(), 'getElementsByTagName')) {

// One possible implementation for developers
// in a situation where it is not a problem that
// IE5 thinks doctype and comments are elements.
LIB.getEBTN = function(tag, root) {
root = root || doc;
var els = root.getElementsByTagName(tag);
if (tag == '*' &&
!els.length &&

isHostMethod(root, 'all')) { // TODO what to do?


els = root.all;
}
return els;
};

}

if (isHostMethod(doc, 'getElementById')) {
// One possible implementation for developers
// not troubled by the name and id attribute
// conflict in IE
LIB.getEBI = function(id, d) {
return (d || doc).getElementById(id);
};
}

if (LIB.getEBTN &&
LIB.getEBI &&

getAnElement &&

}

})();

// ------------------------------------------

(function() {

if (typeof LIB.getAnElement != 'undefined' &&
typeof LIB.getAnElement().className == 'string') {

// The RegExp support need here
// has been available since NN4 & IE4

var hasClass = function(el, className) {


return (new RegExp(
'(^|\\s+)' + className + '(\\s+|$)')).test(el.className);
};

LIB.hasClass = hasClass;

LIB.addClass = function(el, className) {
if (hasClass(el, className)) {


return;
}
el.className = el.className + ' ' + className;
};

LIB.removeClass = function(el, className) {
el.className = el.className.replace(
new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g'), ' ');
// in case of multiple adjacent with a single space

if (hasClass(el, className)) {
arguments.callee(el, className);
}
};

}

})();

David Mark

unread,
Feb 14, 2008, 12:16:24 AM2/14/08
to
On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
> Below is my current code.
>
> I've changed the one character identifiers.
>
> I've added isHostObject and am using it.
>
> I haven't tackled the event part of the library but did add a
> placeholder LIB.addDomReadyListener function.
>
> I changed the "getting out of trouble" part to use the body element if
> it is available (which it almost certainly will be.)
>
> I'm not going to use the Array.prototype.filter in LIB.filter because
> using the native array method means it is not possible to send a
> NodeList to the LIB.filter function. The way I have implemented

Right. And you can't convert nodelists to arrays without iteration.
Using the slice trick blows up (at least in some browsers.)

> LIB.filter is more like how the generic functions in ES4 will be.
>
> I'm directly manipulating the style.display property. I think this is
> just a choice and neither is "right". I will mention this in the
> article.

It has the added benefit that editing the widget's style sheet changes
its appearance, but does not break its behavior.

>
> This is strictly for HTML. (XHTML is out of consideration but a
> different version for XHTML would be an interesting appendix.)

You wouldn't need a completely different version, just plug in
different versions of createElement, getBodyElement, etc. (and use DOM
manipulation to add the style rules and build the list.) That is one
reason to use getBodyElement, as opposed to referencing document.body
directly.

Use isRealObjectProperty here as document is not going to be a
function.

>     LIB.isHostMethod(this.document, 'write') &&
>     LIB.isHostMethod(this.document, 'createTextNode') &&
>     LIB.isHostMethod(this.document, 'createElement') &&
>     (function() {
>       var el = LIB.getAnElement();
>       return LIB.isHostObject(el, 'style') &&

Same here.

>              typeof el.style.display == 'string'
>              LIB.isHostMethod(el, 'appendChild') &&
>              LIB.isHostMethod(el, 'insertBefore') &&
>              LIB.isHostObject(el, 'firstChild') &&

And here.

>              LIB.isHostObject(el, 'childNodes') &&
>              typeof el.innerHTML == 'string';
>     })()) {
>
>   (function() {
>
>     var doc = this.document;
>
>     // configuration
>     var tabbedPaneCssUrl = 'tabbedPane.css',
>         tabbedPanesEnabledClassName = 'tabbedPanesEnabled',
>         tabbedPanesDisabledGroup = 'tabbedPanesDisabled',
>         currentClassName = 'current',
>         tabGroupTagName = 'ul',
>         tabGroupClassName = 'tabs',
>         tabTagName = 'li',
>         defaultTabText = 'tab',
>         paneTagName = 'div',
>         tabbedPaneGroupClassName = 'sections',
>         paneGroupTagName = 'div',
>         tabbedPaneClassName = 'section',
>         showPaneDisplay = 'block',
>         hidePaneDisplay = 'none';

This one seems unnecessary. What else could it be?

>
>     var showPane = function(pane) {
>       pane.style.display = showPaneDisplay;
>       LIB.addClass(pane, currentClassName);
>     };
>     var hidePane = function(pane) {
>       pane.style.display = hidePaneDisplay;
>       LIB.removeClass(pane, currentClassName);
>     };
>     var makeTabCurrent = function(tab) {
>       LIB.addClass(tab, currentClassName);
>     };
>     var makeTabNotCurrent = function(tab) {
>       LIB.removeClass(tab, currentClassName);
>     };

Why declare these like this? I only do this if the functions are
created in an if block.

>
>     var enliven = function(current, tab, pane) {
>       LIB.addListener(tab, 'click', function(e) {
>         LIB.preventDefault(e);

IIRC, you are using DOM0 events, so the result of this should be
returned (presuming you added the return false statement to it.)

Use isRealObjectProperty. I think the distinction is important. Each
of the three feature test functions serves a specific purpose.

You should add a print media style sheet to the markup to override
display:none on hidden panes (use !important of course.) That way if
you print it, it will show all of the content.

Okay, these are the two added to the body. The first one can be
eliminated by querying all panes and setting their inline display
styles in the "get out of trouble" code. Chances are that this code
will not run very often. The second can be changed to .sections
li.current as the list items won't exist if you bail out.

As mentioned, this should be replaced.

>
> // ---------------------------------------------------
>
> (function() {
>
> // No longer needed?

No, you don't want to test everything for object and function types.
It is extra work with no benefit and makes the intentions of the code
unclear.

> // var isRealObjectProperty = function(o, p) {
> //   return !!(typeof(o[p]) == 'object' && o[p]);
> // };
> // LIB.isRealObjectProperty = isRealObjectProperty;
>
>   var isHostMethod = function(o, m) {
>     var t = typeof(o[m]);
>     return !!(((t=='function' || t=='object') && o[m]) ||
>               t == 'unknown');
>   };
>   LIB.isHostMethod = isHostMethod;
>
>   var isHostObject = function(o, m) {
>     var t = typeof(o[m]);
>     return !!((t=='function' || t=='object') && o[m]);

You only need the truthy test for objects (they could be null.)
Functions are always truthy.

>   };
>   LIB.isHostObject = isHostObject;
>
>   if (!(isHostObject(this, 'document'))) {

Use isRealObjectProperty. If for some reason, in some bizarre host
environment, there is a global document property that is a function,
you don't want this test to pass.

>     return;
>   }
>   var doc = this.document;
>
>   if (isHostObject(doc, 'documentElement')) {
>     var getAnElement = function(d) {
>       return (d || doc).documentElement;
>     };
>     LIB.getAnElement = getAnElement;
>     LIB.getDocumentElement = getAnElement;
>   }
>
>   // Test both interfaces in the DOM spec
>   if (isHostMethod(doc, 'getElementsByTagName') &&
>       typeof getAnElement == 'function' &&
>       isHostMethod(getAnElement(), 'getElementsByTagName')) {
>
>     // One possible implementation for developers
>     // in a situation where it is not a problem that
>     // IE5 thinks doctype and comments are elements.
>     LIB.getEBTN = function(tag, root) {
>       root = root || doc;
>       var els = root.getElementsByTagName(tag);
>       if (tag == '*' &&
>           !els.length &&
>           isHostMethod(root, 'all')) { // TODO what to do?

Use isHostObject here as Safari makes document.all callable (typeof
document.all == 'function'.)

>         els = root.all;
>       }
>       return els;
>     };
>
>   }
>
>   if (isHostMethod(doc, 'getElementById')) {
>     // One possible implementation for developers
>     // not troubled by the name and id attribute
>     // conflict in IE
>     LIB.getEBI = function(id, d) {
>       return (d || doc).getElementById(id);
>     };
>   }
>
>   if (LIB.getEBTN &&
>       LIB.getEBI &&
>       getAnElement &&
>       (function() {
>         var el = getAnElement();
>         return typeof el.nodeType == 'number' &&
>                typeof el.tagName == 'string' &&
>                typeof el.className == 'string' &&
>                typeof el.id == 'string'
>        })()) {
>
>     // One possible selector compiler implementation
>     // that can handle selectors with a tag name,
>     // class name and id.
>     //
>     // use memoization for efficiency

Typo.

>       //http://elfz.laacz.lv/beautify/


>       //console.log('function f(d) {' + f + '}');

What is this about?

>
>       f = new Function('d', f);
>       cache[s] = f;
>       return f;
>     }
>
>     LIB.querySelector = function(selector, rootEl) {
>       return (compile(selector))(rootEl);
>     };
>
>   }
>
> })();
>
> // ------------------------------------------
>
> (function() {
>
>   if (typeof LIB.getAnElement != 'undefined' &&

Testing (LIB.getAnElement) is enough here.

dhtml

unread,
Feb 14, 2008, 12:42:23 AM2/14/08
to
On Feb 13, 5:08 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 4:46 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 13, 7:00 pm, dhtml <dhtmlkitc...@gmail.com> wrote:
> > > Why not just document.body?
>
> > If not using XHTML, then that will do.
>
FF 2 uses document.body for application/xhtml+xml

Not sure about opera/saf.

> This article is about building widgets for the general web. XHTML is
> not something to be used on the general web. I will switch to just
> document.body.
>

document.body works for me.

> > > > > > LIB.addClass(middle, 'sections');
> > > > > > LIB.addClass(inner, 'section');
> > > > > > inner.innerHTML = '.';
> > > > > > middle.appendChild(inner);
> > > > > > outer.appendChild(middle);
> > > > > > doc.body.appendChild(outer);
>
> > > I think this creates what MS call a cross-page leak.
>

(BTW - I think innerText/textContent, or firstChild.nodeValue is
cheaper than innerHTML.)


> > So did I at one time, but it was pointed out to me that there example
> > of that in MSDN creates an element with an intrinsic event handler.
>
> What is a "cross-page leak"?
>
>

http://msdn2.microsoft.com/en-us/library/bb250448.aspx

Nevermind, David just cleared it up with his explanation of the event
handler being added into the mix, and in fact, the MSDN article
includes that.

[snip]


>
> > I use API. What do you propose?
>
> How about...
>
> ca.michaux.peter.javascript.example.library.dom.classAttribute.addClass()
>

How about:
<your-lib-name>.dom

What I've done with my dom lib is to have it separeted into modules.
This makes unit testing easier. I have className functions, position
functions, style functions, traversal functions, (all static,
"package" functions).

(function() {
APE.mixin( APE.dom, {getOffsetCoords : getOffsetCoords,
...
});

Later on in the file, I have some "private" variables defined (for
fast reference), followed by:
function getOffsetCoords...

I can explain further and show my code: src and build.


> :D
>
> > > As you build more widgets, they'll also want to reuse stuff in 'LIB'.
> > > If anyone wants to use a LIB widget, they need LIB. It is my opinion
>
> > I don't follow exactly.
>

If you have a tooltip, or drag/drop or panel, or menus, they'll want
certain bits of reusable functions. The total sum of all of the
general purpose functions for such scripts would increase the size of
LIB and decrease it's meaning. LIB would require frequent changes.
This would make testing painful.


> > > that the core library should be minimalistic.
>
> The library for the example only has the code necessary to make the
> example run.
>

That is true.

> > If it is modular and has a build process, it can be as small or large
> > as the author requires. Mine ranges from 1K to well over 100K,
> > depending on the modules selected.
>
> This article is not about the library's modularization. I don't even
> believe in this anymore. One namespace is plenty for all of a library.
> The library is just for illustrative purposes. I will type that in
> bold all-caps in the article so people don't focus on that. (Just
> imagine that all of the emacs lisp code is in the single global
> namespace.)
>

True; Modularization is a separate topic.

Better to focus on the script, not get distracted by the architecture
of a framework or modules or build. You're right.

Garrett
> Peter

Peter Michaux

unread,
Feb 14, 2008, 12:56:47 AM2/14/08
to
On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:

[snip]

> > // tabbedPane.js --------------------------------------------------
>
> > // Test what can be tested as soon as possible
> > // Check that the library loaded
> > if (typeof LIB == 'object' &&
> > LIB &&
> > // Test for library functions I use directly
> > LIB.getAnElement &&
> > LIB.getDocumentElement &&
> > LIB.isHostObject &&
> > LIB.isHostMethod &&
> > LIB.filter &&
> > LIB.forEach &&
> > LIB.addListener &&
> > LIB.addDomReadyListener &&
> > LIB.preventDefault &&
> > LIB.querySelector &&
> > LIB.hasClass &&
> > LIB.removeClass &&
> > LIB.addClass &&
> > // Test for host objects and methods I use directly
> > LIB.isHostObject(this, 'document') &&
>
> Use isRealObjectProperty here as document is not going to be a
> function.

I didn't think that the Safari objects you mentioned would be callable
either.

If a function named "isHostObject" is part of the library then it
makes sense to me to call that for this test. It isn't so important if
isHostObject checks if it is a function or object.

If I used isRealObjectProperty then the following would pass

this.document = {};
isRealObjectProperty(this, 'document');

Does it matter that the following passes? You are screwed if the above
happens. Is it a big deal if the below happens?

this.document = function(){};
isHostObject(this, 'document');

I think it is better to have a consistent API to use if it is
possible.

I typed some more about this farther down the reply.


>
> > // configuration
> > var tabbedPaneCssUrl = 'tabbedPane.css',
> > tabbedPanesEnabledClassName = 'tabbedPanesEnabled',
> > tabbedPanesDisabledGroup = 'tabbedPanesDisabled',
> > currentClassName = 'current',
> > tabGroupTagName = 'ul',
> > tabGroupClassName = 'tabs',
> > tabTagName = 'li',
> > defaultTabText = 'tab',
> > paneTagName = 'div',
> > tabbedPaneGroupClassName = 'sections',
> > paneGroupTagName = 'div',
> > tabbedPaneClassName = 'section',
> > showPaneDisplay = 'block',
> > hidePaneDisplay = 'none';
>
> This one seems unnecessary. What else could it be?

I suppose nothing. What I was thinking was someone could use
visibility (not that it would really work) if they wanted but I didn't
extract which style property is being set.


> > var showPane = function(pane) {
> > pane.style.display = showPaneDisplay;
> > LIB.addClass(pane, currentClassName);
> > };
> > var hidePane = function(pane) {
> > pane.style.display = hidePaneDisplay;
> > LIB.removeClass(pane, currentClassName);
> > };
> > var makeTabCurrent = function(tab) {
> > LIB.addClass(tab, currentClassName);
> > };
> > var makeTabNotCurrent = function(tab) {
> > LIB.removeClass(tab, currentClassName);
> > };
>
> Why declare these like this? I only do this if the functions are
> created in an if block.

You mean do this instead...

function showPane() {}

I just don't ever do that anymore. If I cut and paste into an if block
and forget to change, even just for some reason in development, then
all is wrong. Partly I also like how this constantly reminds me of the
first-class nature of functions.

> You should add a print media style sheet to the markup to override
> display:none on hidden panes (use !important of course.) That way if
> you print it, it will show all of the content.

Good idea.


> > // tabbedPane.css ----------------------------------------------
>
> > /* this file will only be included if there is a chance that
> > tabbed panes might work */
>
> > /* styles for use until window.onload if
> > the browser is "tentatively" capable */
> > .sections .section {
> > display:none;}
>
> > .sections .first {
> > display:block;
>
> > }
>
> > /* if feature tests for tabbed panes fail */
> > .tabbedPanesDisabled .section {
> > display:block;
>
> > }
>
> > /* if feature tests for for tabbed panes pass */
> > .tabbedPanesEnabled li.current {
> > background:red;
>
> > }
>
> Okay, these are the two added to the body. The first one can be
> eliminated by querying all panes and setting their inline display
> styles in the "get out of trouble" code.

I want to leave the hooks for the designer to decide how things will
look. Only the essential display='none' should be in the JavaScript,
at a maximum.

I could query for all the class="sections" elements and add a
"disabledTabbedPane" class.


> Chances are that this code
> will not run very often. The second can be changed to .sections
> li.current as the list items won't exist if you bail out.

The .tabbedPanesEnabled will also be used for other styling tasks so
it is needed somewhere. It could be added to each tabbed pane
class="sections" element.

How is this any better than just adding the class to the body element?


> > (function() {
>
> > // No longer needed?
>
> No, you don't want to test everything for object and function types.
> It is extra work with no benefit and makes the intentions of the code
> unclear.

And here I think it makes it clearer and actually more future proof.


> > if (!(isHostObject(this, 'document'))) {
>
> Use isRealObjectProperty. If for some reason, in some bizarre host
> environment, there is a global document property that is a function,
> you don't want this test to pass.

If someone had told you to test element.childNodes to be a function,
before you had ever seen Safari, what would you have said?

I think that using a function called isHostObject is safer if
something strange is found in some browser. That way a quick fix can
be put in place by rewriting isHostObject. If isRealObjectProperty is
used sometimes for host objects then all uses of isRealObjectProperty
would need to change. It wouldn't be possible to change the definition
of isRealObjectProperty because it may be used specifically to
differentiate between *JavaScript* objects and functions as opposed to
*host* objects and functions. The isRealObjectProperty function can't
be used to check both language and host types.


> > // use memoization for efficiency
>
> Typo.

Looks weird but it is correct.


> > //http://elfz.laacz.lv/beautify/
> > //console.log('function f(d) {' + f + '}');
>
> What is this about?

It is a site that pretty prints JavaScript. It is handy when
developing a to-JavaScript compiler.


Peter

dhtml

unread,
Feb 14, 2008, 2:43:00 AM2/14/08
to
On Feb 13, 9:56 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> [snip]
>
>
> Does it matter that the following passes? You are screwed if the above
> happens. Is it a big deal if the below happens?
>
> this.document = function(){};
> isHostObject(this, 'document');
>

My intuition says the document property should be ReadOnly and
DontDelete.

And it seems to be in FF. Athough delete window.document returns true,
the property stays (I can't explain that)

Why is it wrong to assume that |document| is available?

Garrett

>
> Peter

David Mark

unread,
Feb 14, 2008, 3:01:05 AM2/14/08
to

Sure they are. Try this in (Windows) Safari:

alert(typeof document.all);
alert(typeof document.images);

>
> If a function named "isHostObject" is part of the library then it
> makes sense to me to call that for this test. It isn't so important if
> isHostObject checks if it is a function or object.

It is muddled logic to me. The only objects that could be considered
ambiguous are those which are array-like. Safari allows for either of
these:

el = document.all[0];
el = document.all(0);

Clearly document itself will never have this ambiguity and testing it
as if it would makes the code less clear (as well as adding extra
work.)

>
> If I used isRealObjectProperty then the following would pass
>
> this.document = {};
> isRealObjectProperty(this, 'document');

It would pass isHostObject as well. There's little you can do to
thwart something like that. An author who writes such code deserves
whatever they get.

>
> Does it matter that the following passes? You are screwed if the above
> happens. Is it a big deal if the below happens?
>
> this.document = function(){};
> isHostObject(this, 'document');

Same as above. I don't think such cases should be considered at all.
The main point is that each of the three functions has a clear and
defined purpose.

>
> I think it is better to have a consistent API to use if it is
> possible.

I see no inconsistency in having three distinct functions and calling
each as appropriate.

Yes. This happens to me all the time. That's why I use JSLint.

No problem.

elPane.style.display = '';

>
> I could query for all the class="sections" elements and add a
> "disabledTabbedPane" class.
>
> > Chances are that this code
> > will not run very often. The second can be changed to .sections
> > li.current as the list items won't exist if you bail out.
>
> The .tabbedPanesEnabled will also be used for other styling tasks so
> it is needed somewhere. It could be added to each tabbed pane
> class="sections" element.

Right.

>
> How is this any better than just adding the class to the body element?

Personally, I would leave the body element out of it.

>
> > > (function() {
>
> > > // No longer needed?
>
> > No, you don't want to test everything for object and function types.
> > It is extra work with no benefit and makes the intentions of the code
> > unclear.
>
> And here I think it makes it clearer and actually more future proof.
>
> > > if (!(isHostObject(this, 'document'))) {
>
> > Use isRealObjectProperty. If for some reason, in some bizarre host
> > environment, there is a global document property that is a function,
> > you don't want this test to pass.
>
> If someone had told you to test element.childNodes to be a function,
> before you had ever seen Safari, what would you have said?

Good question. I really have no idea. All I can go by is what I know
now.

>
> I think that using a function called isHostObject is safer if
> something strange is found in some browser. That way a quick fix can
> be put in place by rewriting isHostObject. If isRealObjectProperty is
> used sometimes for host objects then all uses of isRealObjectProperty
> would need to change. It wouldn't be possible to change the definition
> of isRealObjectProperty because it may be used specifically to
> differentiate between *JavaScript* objects and functions as opposed to
> *host* objects and functions. The isRealObjectProperty function can't
> be used to check both language and host types.

I see what you mean, but I don't use isRealObjectProperty for anything
but feature detection. I am trying to think of a case where I would
use it on a native object, but I can't come up with one. Lets look at
the three functions in question (versions copied from my code for
convenience):

var reFeaturedMethod = new RegExp('^function|object$', 'i');

// Test for properties of host objects that are never callable (e.g.
document nodes, elements)

var isRealObjectProperty = function(o, p) {

return !!(typeof o[p] == 'object' && o[p]);
};

API.isRealObjectProperty = isRealObjectProperty;

// Test for host object properties that are typically callable (e.g.
document.getElementById) or known to be callable in some
implementations (e.g. document.all in Safari)
// which may be of type function, object (IE and possibly others) or
unknown (IE ActiveX)

var isHostMethod = function(o, m) {

var t = typeof o[m];
return !!((reFeaturedMethod.test(t) && o[m]) || t == 'unknown');
};

API.isHostMethod = isHostMethod;

// Test for object or function types. Used when the property will be
assigned to a variable (e.g. el = document.all)
// Similar to isHostMethod, but does not allow unknown types, which
are known to throw errors when evaluated.

var isHostObjectProperty = function(o, p) {
var t = typeof o[p];
return !!(reFeaturedMethod.test(t) && o[p]);
};

API.isHostObjectProperty = isHostObjectProperty;

I just updated the comments as they were previously even more
muddled. They could be made clearer still.

I renamed the last one to be consistent with the other two. Regarding
this new addition, I went through all of my feature detection
involving document.all and changed some of them to use
isHostObjectProperty. I changed those that admit functions with code
like this:

var a = document.all;

I didn't change the detection for functions with code like this:

var el = document.all[0];

A better example of something MS could break in the future would be
document.images (or forms), but unlike document.all, I never assign
that to anything (or subject it to type conversion.)

One disclaimer to note for anything to do with unknown types is that
all workarounds are basically voodoo. Nobody but MS knows for sure
why they throw exceptions when assigned to a variable, type converted,
etc. As these are host objects, it is their right to do silly things
with them, but it would be nice if they documented their intentions.
As MS has not done this at all, we have no choice but to resort to
informed assumptions. The best we can do is to encapsulate these
tests in functions and document their intentions as clearly as
possible.

Anyway, it is my contention that the latter example above would not
break if document.all was an unknown type. The former certainly
would.

To get back to the topic at hand, I think that the
isRealObjectProperty function should actually be called
isHostObjectProperty. The isHostObject function should be called
isHostMethod. But what to call the original isHostMethod? The more I
think about it, the more I think we only need two functions. The
isHostMethod function could simply have an optional bMustBeKnownType
parameter (or something like that.) Perhaps that makes it a little
more complicated, but then none of this ever going to be simple.
These base testing functions need to be documented to death (far more
than I've done so far), but ultimately, I don't think they will be
used much outside of the API. I've based my entire framework on the
idea that calling code should test features of the API and leave host
object detection to the internal code. I have written a few example
add-ins and they use this function for convenience in their gateway
code:

var areFeatures = function() {
var i = arguments.length;
while (i--) {
if (!API[arguments[i]]) { return false; }
}
return true;
};

For example, a faux alert widget uses this test before adding its
initialization function to the document ready queue:

if (this.API && typeof this.API == 'object' && this.API.areFeatures &&
this.API.areFeatures('attachListener', 'createElement',
'setElementText'))

In its initialization, it tests API features that cannot be tested
before the DOM is ready:

if (showElement && centerElement && sizeElement && getScrollPosition
&& isHostMethod(global, 'setTimeout'))

(the API prefix is absent as shortcut references were created)

Several other tests follow to determine if various enhancements to the
basic behavior can be implemented (e.g. drag and drop, maximize,
minimize, etc.) Here is the one that decides whether to add a
minimize button:

if (getChildren && canAdjustStyle && canAdjustStyle('display'))

As you can see, there is but one call to isHostMethod (for setTimeout)
in the setup of what is a fairly involved widget. There would be none
if not for a (hopefully temporary) workaround for a Firefox issue. In
other words, its presence in this external code is an anomaly.

As an aside, I plan to make the remaining 99.9999% of the code
available for group discussion before the month is out. I believe the
script and accompanying builder are usable enough at this point to
release as a Beta.

>
> > > // use memoization for efficiency
>
> > Typo.
>
> Looks weird but it is correct.

I haven't encountered that term before.

>
> > > //http://elfz.laacz.lv/beautify/
> > > //console.log('function f(d) {' + f + '}');
>
> > What is this about?
>
> It is a site that pretty prints JavaScript. It is handy when
> developing a to-JavaScript compiler.
>

I am somewhat familiar with it, but couldn't figure out why the link
was in the example.

David Mark

unread,
Feb 14, 2008, 3:06:50 AM2/14/08
to
On Feb 14, 2:43 am, dhtml <dhtmlkitc...@gmail.com> wrote:
> On Feb 13, 9:56 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > [snip]
>
> > Does it matter that the following passes? You are screwed if the above
> > happens. Is it a big deal if the below happens?
>
> > this.document = function(){};
> > isHostObject(this, 'document');
>
> My intuition says the document property should be ReadOnly and
> DontDelete.

I would certain hope so.

>
> And it seems to be in FF. Athough delete window.document returns true,
> the property stays (I can't explain that)

You never know with host objects.

>
> Why is it wrong to assume that |document| is available?

Why assume anything? JS can be run outside of browsers and some of
the discussed code isn't browser-specific. I think in this example
the API bails out instantly if document is missing, but it doesn't
have to. For example, in a case where document is missing, the
library could still expose the array manipulation functions.

dhtml

unread,
Feb 14, 2008, 3:32:13 AM2/14/08
to
On Feb 14, 12:06 am, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 2:43 am, dhtml <dhtmlkitc...@gmail.com> wrote:
>
>
>
> > On Feb 13, 9:56 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > [snip]
>
> > > Does it matter that the following passes? You are screwed if the above
> > > happens. Is it a big deal if the below happens?
>
> > > this.document = function(){};
> > > isHostObject(this, 'document');
>
> > My intuition says the document property should be ReadOnly and
> > DontDelete.
>
> I would certain hope so.
>
>
>
> > And it seems to be in FF. Athough delete window.document returns true,
> > the property stays (I can't explain that)
>
> You never know with host objects.
>
>
>
> > Why is it wrong to assume that |document| is available?
>
> Why assume anything?

I don't have a reason to assume document might not be available for
that script. If the script is going to be run against an HTML
document, can't it just fail with an error if document is not
avaialble? What's wrong with that?


JS can be run outside of browsers and some of
> the discussed code isn't browser-specific. I think in this example
> the API bails out instantly if document is missing, but it doesn't
> have to. For example, in a case where document is missing, the
> library could still expose the array manipulation functions.

RIght, those don't need any document at all.

Where would you try to use the tab pane without a document?

David Mark

unread,
Feb 14, 2008, 3:37:18 AM2/14/08
to
On Feb 14, 3:32 am, dhtml <dhtmlkitc...@gmail.com> wrote:
> On Feb 14, 12:06 am, David Mark <dmark.cins...@gmail.com> wrote:
>
>
>
>
>
> > On Feb 14, 2:43 am, dhtml <dhtmlkitc...@gmail.com> wrote:
>
> > > On Feb 13, 9:56 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > > On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > > > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > > [snip]
>
> > > > Does it matter that the following passes? You are screwed if the above
> > > > happens. Is it a big deal if the below happens?
>
> > > > this.document = function(){};
> > > > isHostObject(this, 'document');
>
> > > My intuition says the document property should be ReadOnly and
> > > DontDelete.
>
> > I would certain hope so.
>
> > > And it seems to be in FF. Athough delete window.document returns true,
> > > the property stays (I can't explain that)
>
> > You never know with host objects.
>
> > > Why is it wrong to assume that |document| is available?
>
> > Why assume anything?
>
> I don't have a reason to assume document might not be available for
> that script. If the script is going to be run against an HTML
> document, can't it just fail with an error if document is not
> avaialble? What's wrong with that?

Why not exit gracefully instead?

>
>  JS can be run outside of browsers and some of> the discussed code isn't browser-specific.  I think in this example
> > the API bails out instantly if document is missing, but it doesn't
> > have to.  For example, in a case where document is missing, the
> > library could still expose the array manipulation functions.
>
> RIght, those don't need any document at all.
>
> Where would you try to use the tab pane without a document?

You wouldn't. The library wouldn't create methods that rely on
document and as a result the widget would not initialize.

Henry

unread,
Feb 14, 2008, 8:07:23 AM2/14/08
to
On Feb 14, 5:56 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
<snip>

> if (!(isHostObject(this, 'document'))) {
>
>> Use isRealObjectProperty. If for some reason, in some bizarre
>> host environment, there is a global document property that is
>> a function, you don't want this test to pass.
>
> If someone had told you to test element.childNodes to be a
> function, before you had ever seen Safari, what would you
> have said?
<snip>

Given that HTMLCollections and NodeLists have been of 'function' type
in all Opera versions up to about 8 (or at least well into the 7s),
Mac IE 5, IceBrowser, Konqueror and a number of scriptable embedded
browsers there would be no need to wait until Safari came along before
being convinced that such a test was a good idea. The notion that
Safari's behaviour is in some way aberrant (or unexpected) is just the
consequence of a limited exposure to web browsers.

Peter Michaux

unread,
Feb 14, 2008, 10:39:40 AM2/14/08
to
On Feb 14, 12:01 am, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 12:56 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:

[snip]


> > I think that using a function called isHostObject is safer if
> > something strange is found in some browser. That way a quick fix can
> > be put in place by rewriting isHostObject. If isRealObjectProperty is
> > used sometimes for host objects then all uses of isRealObjectProperty
> > would need to change. It wouldn't be possible to change the definition
> > of isRealObjectProperty because it may be used specifically to
> > differentiate between *JavaScript* objects and functions as opposed to
> > *host* objects and functions. The isRealObjectProperty function can't
> > be used to check both language and host types.
>
> I see what you mean, but I don't use isRealObjectProperty for anything
> but feature detection.

Ahh. Ok. Then "Host" should be in the name.

I think I'm just looking for (what I consider) semantically correct
names for these so the intention is completely clear.

> I am trying to think of a case where I would
> use it on a native object, but I can't come up with one. Lets look at
> the three functions in question (versions copied from my code for
> convenience):
>
> var reFeaturedMethod = new RegExp('^function|object$', 'i');
>
> // Test for properties of host objects that are never callable (e.g.
> document nodes, elements)
>
> var isRealObjectProperty = function(o, p) {
> return !!(typeof o[p] == 'object' && o[p]);
> };
>
> API.isRealObjectProperty = isRealObjectProperty;

Call this one "isHostObject".

I don't think there is a need for "Real" in the name. In ES4 the code
"typeof null" will return the string "null". The idea that "typeof
null" returns "object" is considered a bug.

There is no need for "Property" in the name because everything with
host objects is a property of something else.

> // Test for host object properties that are typically callable (e.g.
> document.getElementById) or known to be callable in some
> implementations (e.g. document.all in Safari)
> // which may be of type function, object (IE and possibly others) or
> unknown (IE ActiveX)
>
> var isHostMethod = function(o, m) {
> var t = typeof o[m];
> return !!((reFeaturedMethod.test(t) && o[m]) || t == 'unknown');
> };
>
> API.isHostMethod = isHostMethod;


"isHostMethod" is the right name because I'm going to call the thing
I'm testing.


> // Test for object or function types. Used when the property will be
> assigned to a variable (e.g. el = document.all)

The above comment does not seem consistent with your advice to avoid
this test for el.firstChild (and others) which may be assigned to a
variable.

> // Similar to isHostMethod, but does not allow unknown types, which
> are known to throw errors when evaluated.
>
> var isHostObjectProperty = function(o, p) {
> var t = typeof o[p];
> return !!(reFeaturedMethod.test(t) && o[p]);
> };
>
> API.isHostObjectProperty = isHostObjectProperty;

Call this one "isHostCollection". The documentation for what was
isRealObjectProperty would say "don't use this for collections, use
isHostCollection instead."


> I just updated the comments as they were previously even more
> muddled. They could be made clearer still.
>
> I renamed the last one to be consistent with the other two. Regarding
> this new addition, I went through all of my feature detection
> involving document.all and changed some of them to use
> isHostObjectProperty. I changed those that admit functions with code
> like this:
>
> var a = document.all;

But then what if "a" is later called?


> I didn't change the detection for functions with code like this:
>
> var el = document.all[0];

But the above is the still requires a [[Get]] to resolve document.all
before the [0] is dealt with.


> To get back to the topic at hand, I think that the
> isRealObjectProperty function should actually be called
> isHostObjectProperty. The isHostObject function should be called
> isHostMethod.

If I want to use document.childNodes as an "array", I don't want to
call a function "isHostMethod". That seems very counterintuitive.

[snip]

Peter

Peter Michaux

unread,
Feb 14, 2008, 11:33:28 AM2/14/08
to
On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:

[snip]

> > This is strictly for HTML. (XHTML is out of consideration but a
> > different version for XHTML would be an interesting appendix.)
>
> You wouldn't need a completely different version, just plug in
> different versions of createElement, getBodyElement, etc. (and use DOM
> manipulation to add the style rules and build the list.) That is one
> reason to use getBodyElement, as opposed to referencing document.body
> directly.

Given how impractical XHTML is in general, my knowledge of XHTML is
very limited. You mentioned there is no incremental rendering of an
XHTML document. Is the whole page just shown at window.onload? If that
is the case then the entire challenge is different. There isn't any
need for the whole tentative styling and "get out of trouble" code
that I have because the entire test can just occur at window.onload.
If my example was slightly changed to add the tenative styling using
DOM methods, then the <script src="tabbedPane.js> would have to go in
the body section and all for not because there is no incremental
rendering anyway.

[snip]

Peter

David Mark

unread,
Feb 14, 2008, 5:31:13 PM2/14/08
to

Makes sense.

>
> There is no need for "Property" in the name because everything with
> host objects is a property of something else.

The reason I added that suffix is to make it clear that the function
does not test the first parameter, but uses bracket notation to test a
named property (passed as a string.)

>
> >   // Test for host object properties that are typically callable (e.g.
> > document.getElementById) or known to be callable in some
> > implementations (e.g. document.all in Safari)
> >   // which may be of type function, object (IE and possibly others) or
> > unknown (IE ActiveX)
>
> >   var isHostMethod = function(o, m) {
> >     var t = typeof o[m];
> >     return !!((reFeaturedMethod.test(t) && o[m]) || t == 'unknown');
> >   };
>
> >   API.isHostMethod = isHostMethod;
>
> "isHostMethod" is the right name because I'm going to call the thing
> I'm testing.
>
> >   // Test for object or function types. Used when the property will be
> > assigned to a variable (e.g. el = document.all)
>
> The above comment does not seem consistent with your advice to avoid
> this test for el.firstChild (and others) which may be assigned to a
> variable.

...or type converted.

Yes, the comments could be clearer. I want to wait until we have this
finalized before rewriting the documentation again.

>
> >   // Similar to isHostMethod, but does not allow unknown types, which
> > are known to throw errors when evaluated.
>
> >   var isHostObjectProperty = function(o, p) {
> >     var t = typeof o[p];
> >     return !!(reFeaturedMethod.test(t) && o[p]);
> >   };
>
> >   API.isHostObjectProperty = isHostObjectProperty;
>
> Call this one "isHostCollection". The documentation for what was
> isRealObjectProperty would say "don't use this for collections, use
> isHostCollection instead."

That sounds good. You want to rework/rename these on your end and pos
the results? If we can agree on the results, I will take another stab
at the documentation (or at least the inline comments.)

>
> > I just updated the comments as they were previously even more
> > muddled.  They could be made clearer still.
>
> > I renamed the last one to be consistent with the other two.  Regarding
> > this new addition, I went through all of my feature detection
> > involving document.all and changed some of them to use
> > isHostObjectProperty.  I changed those that admit functions with code
> > like this:
>
> > var a = document.all;
>
> But then what if "a" is later called?

It never is (but wouldn't hurt if they were as isHostObjectProperty
checks for function types.) I don't call collections, despite the
fact that Safari (and Opera) are known to allow for that.

>
> > I didn't change the detection for functions with code like this:
>
> > var el = document.all[0];
>
> But the above is the still requires a [[Get]] to resolve document.all
> before the [0] is dealt with.

I thought about that, but I am not 100% sure that [[Get]] alone is the
culprit. Using document.images as an example, based on experience
with other IE host objects, MS could conceivably break this in the
future:

if (document.images) { ... }

But it couldn't break this (else the collection would be useless):

el = document.images[0];

I may go back and change the detection for all collections, regardless
of how they are used. The main issue I tried to address last night
was to avoid allowing unknown types for properties that would be type
converted or evaluated for assignment (two operations known to blow up
with such types.)

>
> > To get back to the topic at hand, I think that the
> > isRealObjectProperty function should actually be called
> > isHostObjectProperty.  The isHostObject function should be called
> > isHostMethod.
>
> If I want to use document.childNodes as an "array", I don't want to
> call a function "isHostMethod". That seems very counterintuitive.

As mentioned, it is hard to cover all of these scenarios with a simple
and intuitive interface. It seems we are close to agreement on what
should be done at this point. Can you consolidate the last round of
discussions into newly renamed functions? It would be good to
finalize these two (or three) functions before proceeding any further
with the library.

David Mark

unread,
Feb 14, 2008, 5:38:16 PM2/14/08
to
On Feb 14, 11:33 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 13, 9:16 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 13, 11:34 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> [snip]
>
> > > This is strictly for HTML. (XHTML is out of consideration but a
> > > different version for XHTML would be an interesting appendix.)
>
> > You wouldn't need a completely different version, just plug in
> > different versions of createElement, getBodyElement, etc. (and use DOM
> > manipulation to add the style rules and build the list.)  That is one
> > reason to use getBodyElement, as opposed to referencing document.body
> > directly.
>
> Given how impractical XHTML is in general, my knowledge of XHTML is
> very limited. You mentioned there is no incremental rendering of an
> XHTML document. Is the whole page just shown at window.onload? If
that

The whole page is shown once the document has finished parsing.

> is the case then the entire challenge is different. There isn't any
> need for the whole tentative styling and "get out of trouble" code
> that I have because the entire test can just occur at window.onload.

That is incorrect. Even when using a (real or simulated)
DOMContentLoaded listener, there can be a split-second flash of the
original content.

> If my example was slightly changed to add the tenative styling using
> DOM methods, then the <script src="tabbedPane.js> would have to go in
> the body section and all for not because there is no incremental

Yes. The code that adds the style rules to temporarily hide the
static content goes at the very top of the body element.

> rendering anyway.

Trust me, it is not all for naught. Even a split-second flash (and
subsequent twitch) is unacceptable (at least to me.)

Peter Michaux

unread,
Feb 14, 2008, 6:51:20 PM2/14/08
to
On Feb 14, 2:31 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 10:39 am, Peter Michaux <petermich...@gmail.com> wrote:
> > On Feb 14, 12:01 am, David Mark <dmark.cins...@gmail.com> wrote:

[snip]

> > > var isRealObjectProperty = function(o, p) {
> > > return !!(typeof o[p] == 'object' && o[p]);
> > > };

[snip]

> > There is no need for "Property" in the name because everything with
> > host objects is a property of something else.
>
> The reason I added that suffix is to make it clear that the function
> does not test the first parameter, but uses bracket notation to test a
> named property (passed as a string.)

That makes sense but it is an awfully long function name to type and
look at filling up the screen. I know the function takes two arguments
and don't imagine I'll forget that.

[snip]

> > > var el = document.all[0];
>
> > But the above is the still requires a [[Get]] to resolve document.all
> > before the [0] is dealt with.
>
> I thought about that, but I am not 100% sure that [[Get]] alone is the
> culprit.

[[Get]] is fictitious so it is almost certainly something else.

[snip]

> > > To get back to the topic at hand, I think that the
> > > isRealObjectProperty function should actually be called
> > > isHostObjectProperty. The isHostObject function should be called
> > > isHostMethod.
>
> > If I want to use document.childNodes as an "array", I don't want to
> > call a function "isHostMethod". That seems very counterintuitive.
>
> As mentioned, it is hard to cover all of these scenarios with a simple
> and intuitive interface.

I think the three functions are actually quite intuitive.

> It seems we are close to agreement on what
> should be done at this point. Can you consolidate the last round of
> discussions into newly renamed functions?

Right now I have the following with sketchy comments.

// Use for testing if a DOM property is present.
// Use LIB.isHostCollection if the property is
// a collection. Use LIB.isHostMethod if
// you will be calling the property.
//
// Examples:
// // "this" is global/window object
// LIB.isHostObject(this, 'document');
// // "el" is some DOM element
// LIB.isHostObject(el, 'style');
//
var isHostObject = function(o, p) {
return !!(typeof(o[p]) == 'object' && o[p]);
};
LIB.isHostObject = isHostObject;


// Use for testing if DOM collects are present
// Some browsers make collections callable and
// have a typeof 'function'.
// In Internet Explorer, document.all is callable
// like document.all(0). If this function returns
// true it does not mean you can call the property.
//
// Examples:
// // since tested document as property of "this"
// // need to refer to it as such
// var doc = this.document;
// LIB.isHostCollection(doc, 'all');
// // "el" is some DOM element
// LIB.isHostCollection(el, 'childNodes');
//
var isHostCollection = function(o, m) {
var t = typeof(o[m]);
return (!!(t == 'object' && o[m])) ||
t == 'function';
};
LIB.isHostCollection = isHostCollection;


// Use for testing a DOM property you intend on calling.
// In Internet Explorer, some ActiveX callable properties
// have a typeof "unknown". Some browsers have callable
// properties that typeof "object".
//
// Examples:
// // since tested document as property of "this"
// // need to refer to it as such
// var doc = this.document;
// LIB.isHostMethod(doc, 'createElement');
// // "el" is some DOM element
// LIB.isHostMethod(el, 'appendChild');
//


var isHostMethod = function(o, m) {

var t = typeof(o[m]);
return t == 'function' ||
(!!(t == 'object' && o[m])) ||
t == 'unknown';
};
LIB.isHostMethod = isHostMethod;


I don't use regular expressions in these tests because I think they
will be slower. I didn't test this. By not using regular expressions,
I can also control the order of the tests in what I figure is the most
likely order to be successful and short circuit.

Now I need to fire up a bunch of browsers and accumulate examples of
all these weird cases. Can insert some examples of each where you know
that the typeof returns the unexpected values or the browser blows up
if a simply type converting feature test is uses? I'll make up a table
or some presentation that will convince people these three functions
are actually necessary.

> It would be good to
> finalize these two (or three) functions before proceeding any further
> with the library.

Even though the article will probably use a custom lib to keep it
small and readable in a short time, I'd like to be able to say that
one of your automatic builds will provide the same library api.

Peter

David Mark

unread,
Feb 14, 2008, 8:03:13 PM2/14/08
to
On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 14, 2:31 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 14, 10:39 am, Peter Michaux <petermich...@gmail.com> wrote:
> > > On Feb 14, 12:01 am, David Mark <dmark.cins...@gmail.com> wrote:
>
> [snip]
>
> > > >   var isRealObjectProperty = function(o, p) {
> > > >     return !!(typeof o[p] == 'object' && o[p]);
> > > >   };
>
> [snip]
>
> > > There is no need for "Property" in the name because everything with
> > > host objects is a property of something else.
>
> > The reason I added that suffix is to make it clear that the function
> > does not test the first parameter, but uses bracket notation to test a
> > named property (passed as a string.)
>
> That makes sense but it is an awfully long function name to type and

I never consider that. If it is an issue for some, then macros should
be used.

> look at filling up the screen. I know the function takes two arguments
> and don't imagine I'll forget that.

I'm sure you won't, but to the uninitiated, it could present some
initial confusion. Either way though. I am not too concerned with
the naming convention at the moment.

>
> [snip]
>
> > > > var el = document.all[0];
>
> > > But the above is the still requires a [[Get]] to resolve document.all
> > > before the [0] is dealt with.
>
> > I thought about that, but I am not 100% sure that [[Get]] alone is the
> > culprit.
>
> [[Get]] is fictitious so it is almost certainly something else.

Fictitious? Can you elaborate on that?

>
> [snip]
>
> > > > To get back to the topic at hand, I think that the
> > > > isRealObjectProperty function should actually be called
> > > > isHostObjectProperty.  The isHostObject function should be called
> > > > isHostMethod.
>
> > > If I want to use document.childNodes as an "array", I don't want to
> > > call a function "isHostMethod". That seems very counterintuitive.
>
> > As mentioned, it is hard to cover all of these scenarios with a simple
> > and intuitive interface.
>
> I think the three functions are actually quite intuitive.

I think we are getting close to an understandable set.

>
> > It seems we are close to agreement on what
> > should be done at this point.  Can you consolidate the last round of
> > discussions into newly renamed functions?
>
> Right now I have the following with sketchy comments.
>
> // Use for testing if a DOM property is present.
> // Use LIB.isHostCollection if the property is
> // a collection. Use LIB.isHostMethod if
> // you will be calling the property.
> //
> // Examples:
> //   // "this" is global/window object
> //   LIB.isHostObject(this, 'document');
> //   // "el" is some DOM element
> //   LIB.isHostObject(el, 'style');

Sounds good.

> //
> var isHostObject = function(o, p) {
>   return !!(typeof(o[p]) == 'object' && o[p]);};
>
> LIB.isHostObject = isHostObject;
>
> // Use for testing if DOM collects are present
> // Some browsers make collections callable and
> // have a typeof 'function'.
> // In Internet Explorer, document.all is callable
> // like document.all(0). If this function returns

Perhaps it should be made clear that IE returns "object" for typeof
operations on callable host objects, but other browsers (e.g. Safari,
Opera) return "function."

> // true it does not mean you can call the property.
> //
> // Examples:
> //   // since tested document as property of "this"
> //   // need to refer to it as such
> //   var doc = this.document;

This is a good idea anyway. I don't like to assume that the
environment is a browser (i.e. treat document as an implied global.)

> //   LIB.isHostCollection(doc, 'all');
> //   // "el" is some DOM element
> //   LIB.isHostCollection(el, 'childNodes');
> //
> var isHostCollection = function(o, m) {
>   var t = typeof(o[m]);
>   return (!!(t == 'object' && o[m])) ||
>          t == 'function';};
>
> LIB.isHostCollection = isHostCollection;
>
> // Use for testing a DOM property you intend on calling.
> // In Internet Explorer, some ActiveX callable properties
> // have a typeof "unknown". Some browsers have callable
> // properties that typeof "object".
> //
> // Examples:
> //   // since tested document as property of "this"
> //   // need to refer to it as such
> //   var doc = this.document;
> //   LIB.isHostMethod(doc, 'createElement');
> //   // "el" is some DOM element
> //   LIB.isHostMethod(el, 'appendChild');

Sounds good.

> //
> var isHostMethod = function(o, m) {
>   var t = typeof(o[m]);
>   return t == 'function' ||
>          (!!(t == 'object' && o[m])) ||
>          t == 'unknown';};
>
> LIB.isHostMethod = isHostMethod;
>
> I don't use regular expressions in these tests because I think they
> will be slower. I didn't test this. By not using regular expressions,
> I can also control the order of the tests in what I figure is the most
> likely order to be successful and short circuit.

Either way. I had it in my head that some browsers would execute a
single regexp test faster than two comparisons, but I could be
mistaken.

>
> Now I need to fire up a bunch of browsers and accumulate examples of
> all these weird cases. Can insert some examples of each where you know
> that the typeof returns the unexpected values or the browser blows up
> if a simply type converting feature test is uses? I'll make up a table
> or some presentation that will convince people these three functions
> are actually necessary.

I would hope it is clear at this point that such logic is necessary.
Here are some examples that blow up IE:

1. In the getAttribute wrapper there is a test like this:

if (doc && doc.selectNodes) { return el.getAttribute(name); } // XML
document

The hasAttribute function has similar logic.

Testing my DOM import module with an XML document retrieved via Ajax
revealed that isHostMethod is required to test selectNodes, else IE
throws an exception. This is the explanation for the case made by a
notorious browser sniffer regarding a similar test for getAttribute.
The (ridiculous) conclusion drawn in that example was that IE called
getAttribute with no parameters.

2. News (NNTP) links. Type converting or assigning the href property
blows up IE7.

3. The offsetParent property blows up in similar fashion for elements
that are not part of a document. IIRC, the same goes for the filters
collection.

4. Methods of the external object are also rigged to explode on
anything but a call (e.g. addFavorite.)

It all goes to show that you can't trust host objects. It is best to
leave detection of their properties to functions that have been proven
safe (despite the lack of a definitive explanation as to why they are
safe.)

>
> > It would be good to
> > finalize these two (or three) functions before proceeding any further
> > with the library.
>
> Even though the article will probably use a custom lib to keep it
> small and readable in a short time, I'd like to be able to say that
> one of your automatic builds will provide the same library api.

Sounds like a good idea. I will update the function names before I
release the Beta. I hope to update the current (and very stale) Alpha
this weekend.

Peter Michaux

unread,
Feb 14, 2008, 8:45:16 PM2/14/08
to
On Feb 14, 5:03 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:


[snip]

> > [[Get]] is fictitious so it is almost certainly something else.
>
> Fictitious? Can you elaborate on that?

The internal properties like [[Get]] "are defined by this
specification purely for expository purposes. An implementation of
ECMAScript must behave as if it produced and operated upon internal
properties in the manner described here." ECMA-262 3rd ed, section
8.6.2.

It could be that an implementation implements something that does the
job of [[Get]] several times, different ways, for efficiency. Only one
of those implementations of [[Get]] may have a bug so a conclusion
about [[Get]] in general is not possible. It would seem to work fine
for some uses in the spec and not in others. I think this is happening
in Internet Explorer.

[snip]


> It all goes to show that you can't trust host objects. It is best to
> leave detection of their properties to functions that have been proven
> safe (despite the lack of a definitive explanation as to why they are
> safe.)

This is definitely a nitpick but they are not "proven" to work. They
just seem to work. Making this distinction is my Math degree nagging
me. I really nitpicked on the fact that these are only pragmatic tests
when I posted in December about how typeof does not have an implicit
try-catch in the spec. It is odd that "if (el['href'])" errors but
"typeof el['href']" does not. They both involve a [[Get]].

[snip]

> > Even though the article will probably use a custom lib to keep it
> > small and readable in a short time, I'd like to be able to say that
> > one of your automatic builds will provide the same library api.

Could your builder allow the user to specify the single global object
added by the library? For example, I'm using LIB.

Peter

David Mark

unread,
Feb 14, 2008, 10:18:23 PM2/14/08
to
On Feb 14, 8:45 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 14, 5:03 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> [snip]
>
> > > [[Get]] is fictitious so it is almost certainly something else.
>
> > Fictitious?  Can you elaborate on that?
>
> The internal properties like [[Get]] "are defined by this
> specification purely for expository purposes. An implementation of
> ECMAScript must behave as if it produced and operated upon internal
> properties in the manner described here." ECMA-262 3rd ed, section
> 8.6.2.
>
> It could be that an implementation implements something that does the
> job of [[Get]] several times, different ways, for efficiency. Only one
> of those implementations of [[Get]] may have a bug so a conclusion
> about [[Get]] in general is not possible. It would seem to work fine
> for some uses in the spec and not in others. I think this is happening
> in Internet Explorer.

So it would seem.

>
> [snip]
>
> > It all goes to show that you can't trust host objects.  It is best to
> > leave detection of their properties to functions that have been proven
> > safe (despite the lack of a definitive explanation as to why they are
> > safe.)
>
> This is definitely a nitpick but they are not "proven" to work. They
> just seem to work. Making this distinction is my Math degree nagging

Is the empirical data not proof enough? The cases either throw errors
or they don't. It is the reasoning behind the logic that is in
question, rather than the results. As mentioned, the testing
algorithms are based on nothing more than informed assumptions. It
seems the pattern is clear for current and past versions of IE.
Whether this pattern will hold true in IE8 is anybody's guess, but I
think there is a fair chance it will. Of course, if they add a new
"BillGates" type for ActiveX properties, we will have to update the
isHostMethod function. That is one of the reasons I have been so
adamant about isolating the testing logic.

> me. I really nitpicked on the fact that these are only pragmatic tests
> when I posted in December about how typeof does not have an implicit
> try-catch in the spec. It is odd that "if (el['href'])" errors but
> "typeof el['href']" does not. They both involve a [[Get]].

At the time I took your word for it as I hate wading through the ECMA
specs.

At this time, I think it is fair to say that [[Get]] alone is not the
cause of the exceptions. Other than MS (and perhaps not even them),
nobody can say for sure. After all of this discussion, I have to
wonder if any of the IE developers read this newsgroup. If not, I can
understand as it surely would be an ego-deflating experience for
them. If so, now would be a good time for them to chime in on this
issue.

>
> [snip]
>
> > > Even though the article will probably use a custom lib to keep it
> > > small and readable in a short time, I'd like to be able to say that
> > > one of your automatic builds will provide the same library api.
>
> Could your builder allow the user to specify the single global object
> added by the library? For example, I'm using LIB.

I was originally going to allow the client to supply the global name.
At some point I abandoned the idea. One reason is for the sake of add-
ins and a couple of extensions that use conditional compilation (which
interferes with minification.) Another is that I use a scheme that
allows me to test locally with a file that has all of the server side
code embedded. To publish updates, I simply run it through a process
that removes some JS comments and changes the extension to ASP. This
scheme does not allow for server side code to share the same line as
JS code (for obvious reasons.) Without such a scheme, it would be
hell on earth to manage the module dependencies.

I'd say that one of us would have to change the name if any sort of
compatibility between the proving ground/builder and official
repository is to be possible. At this point, I think it would be less
of a hassle to change it on your end. I also prefer API over LIB.

Peter Michaux

unread,
Feb 15, 2008, 12:33:43 AM2/15/08
to
On Feb 14, 7:18 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 8:45 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > On Feb 14, 5:03 pm, David Mark <dmark.cins...@gmail.com> wrote:
> > > On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:

> > > It all goes to show that you can't trust host objects. It is best to
> > > leave detection of their properties to functions that have been proven
> > > safe (despite the lack of a definitive explanation as to why they are
> > > safe.)
>
> > This is definitely a nitpick but they are not "proven" to work. They
> > just seem to work. Making this distinction is my Math degree nagging
>
> Is the empirical data not proof enough?

If the empirical data included every OS/Browser/Configuration
combination possible (that means testing every browser on every
computer on earth) then maybe. But only until the next computer is
built. What I'm saying is it cannot be proved. Proof is something
definitive. This solution just appears to work without any particular
support from either the ECMA spec or browser maker documentation. I
think you have done a great job discovering these tests. I also think
it is somewhat luck that paths through the feature testing mine field
are even available to find. Someone could easily create a browser that
makes any particular currently used feature test insufficient.


> The cases either throw errors
> or they don't. It is the reasoning behind the logic that is in
> question, rather than the results. As mentioned, the testing
> algorithms are based on nothing more than informed assumptions. It
> seems the pattern is clear for current and past versions of IE.
> Whether this pattern will hold true in IE8 is anybody's guess, but I
> think there is a fair chance it will. Of course, if they add a new
> "BillGates"

I hope they do that. Developers would love it!


> type for ActiveX properties, we will have to update the
> isHostMethod function. That is one of the reasons I have been so
> adamant about isolating the testing logic.

It is a good idea. Having reasonably distinct names for these
functions makes search and replace easy if one of the tests has to be
split into two different ones.


> > me. I really nitpicked on the fact that these are only pragmatic tests
> > when I posted in December about how typeof does not have an implicit
> > try-catch in the spec. It is odd that "if (el['href'])" errors but
> > "typeof el['href']" does not. They both involve a [[Get]].
>
> At the time I took your word for it as I hate wading through the ECMA
> specs.
>
> At this time, I think it is fair to say that [[Get]] alone is not the
> cause of the exceptions. Other than MS (and perhaps not even them),
> nobody can say for sure. After all of this discussion, I have to
> wonder if any of the IE developers read this newsgroup.

Not many people read this newsgroup. I'm impressed that some of the
YUI devs poke in here now and then. I think a great sign would be if
more primary devs for libraries started to participate here and ask
questions.

[snip]

> > Could your builder allow the user to specify the single global object
> > added by the library? For example, I'm using LIB.

[snip]

> I'd say that one of us would have to change the name if any sort of
> compatibility between the proving ground/builder and official
> repository is to be possible. At this point, I think it would be less
> of a hassle to change it on your end. I also prefer API over LIB.

I don't think it is a good idea for The Code Worth Recommending
Project to use a namespace. That is a political decision. I don't know
what is going to happen with that code/repository but it may stimulate
some good discussion, articles and other code projects like yours. I'm
not worried about it for now.

Peter

David Mark

unread,
Feb 15, 2008, 1:06:49 AM2/15/08
to
On Feb 15, 12:33 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 14, 7:18 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 14, 8:45 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > On Feb 14, 5:03 pm, David Mark <dmark.cins...@gmail.com> wrote:
> > > > On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > > It all goes to show that you can't trust host objects.  It is best to
> > > > leave detection of their properties to functions that have been proven
> > > > safe (despite the lack of a definitive explanation as to why they are
> > > > safe.)
>
> > > This is definitely a nitpick but they are not "proven" to work. They
> > > just seem to work. Making this distinction is my Math degree nagging
>
> > Is the empirical data not proof enough?
>
> If the empirical data included every OS/Browser/Configuration
> combination possible (that means testing every browser on every
> computer on earth) then maybe. But only until the next computer is
> built. What I'm saying is it cannot be proved. Proof is something
> definitive. This solution just appears to work without any particular
> support from either the ECMA spec or browser maker documentation. I
> think you have done a great job discovering these tests. I also think
> it is somewhat luck that paths through the feature testing mine field
> are even available to find. Someone could easily create a browser that
> makes any particular currently used feature test insufficient.

Perhaps proof was too strong a word. Regardless, the need for most of
these safeguards can be summed up in two words: Internet Explorer. I
have never heard of another browser that is prone to such inexplicable
exceptions. Granted, there is nothing in the specs that forbids it.

>
> > The cases either throw errors
> > or they don't.  It is the reasoning behind the logic that is in
> > question, rather than the results.  As mentioned, the testing
> > algorithms are based on nothing more than informed assumptions.  It
> > seems the pattern is clear for current and past versions of IE.
> > Whether this pattern will hold true in IE8 is anybody's guess, but I
> > think there is a fair chance it will.  Of course, if they add a new
> > "BillGates"
>
> I hope they do that. Developers would love it!

I wouldn't! I'd have to add another test to isHostMethod.

>
> > type for ActiveX properties, we will have to update the
> > isHostMethod function.  That is one of the reasons I have been so
> > adamant about isolating the testing logic.
>
> It is a good idea. Having reasonably distinct names for these
> functions makes search and replace easy if one of the tests has to be
> split into two different ones.
>
> > > me. I really nitpicked on the fact that these are only pragmatic tests
> > > when I posted in December about how typeof does not have an implicit
> > > try-catch in the spec. It is odd that "if (el['href'])" errors but
> > > "typeof el['href']" does not. They both involve a [[Get]].
>
> > At the time I took your word for it as I hate wading through the ECMA
> > specs.
>
> > At this time, I think it is fair to say that [[Get]] alone is not the
> > cause of the exceptions.  Other than MS (and perhaps not even them),
> > nobody can say for sure.  After all of this discussion, I have to
> > wonder if any of the IE developers read this newsgroup.
>
> Not many people read this newsgroup. I'm impressed that some of the

Define "not many." According to Google's (suspect) calculations, it
is a relatively popular group with "high activity" and hundreds of
subscribers added in the last few months. As this is a Usenet group,
the Google subscribers are clearly the tip of the iceberg. On the
other hand, the percentage of people who program JavaScript who have
even heard of this group is probably pretty low. Most seem to
congregate and swap misinformation on blog pages.

> YUI devs poke in here now and then. I think a great sign would be if
> more primary devs for libraries started to participate here and ask
> questions.

They seem to prefer the insulation of their own discussion groups and
blogs. There they can bask in praise and ignore reality. Ironically,
when they do pop in here, they dismiss any and all criticism as other-
worldly and quickly scuttle back to lolly-pop land.

[snip]

>
> I don't think it is a good idea for The Code Worth Recommending
> Project to use a namespace. That is a political decision. I don't know

The namespace should never be referenced inside the functions, but any
builder application should probably package them in a namespace.

[snip]

Peter Michaux

unread,
Feb 15, 2008, 1:38:55 AM2/15/08
to
On Feb 14, 3:51 pm, Peter Michaux <petermich...@gmail.com> wrote:

> Now I need to fire up a bunch of browsers and accumulate examples of
> all these weird cases.

Safari 3 on OS X
----------------

typeof document['all'] // 'undefined'

document['all'] // [object HTMLCollection]'


That's a really nice one!

Somewhere I wrote I was worried about exactly this in December. The
ECMAScript spec says in section 11.4.3 that a host object can return
anything for the "typeof" operator. Unfortunately, anything includes
"undefined".

This is a false negative for the isHostCollection(this.document,
'all') and that is better than a false positive. The false negative in
Safari for document.all is not a monumental loss but so much for any
amount of "proof" that the test functions work universally.

By the way, this is the very first test I tried on a few browsers and
unfortunately already there is a problem.

Peter

Peter Michaux

unread,
Feb 15, 2008, 1:58:34 AM2/15/08
to
On Feb 14, 5:03 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 14, 6:51 pm, Peter Michaux <petermich...@gmail.com> wrote:

> 2. News (NNTP) links. Type converting or assigning the href property
> blows up IE7.


Type converting to boolean and string both work for me

var el = document.getElementById('newsLink');
if (el.href) {alert('ok');}

var el = document.getElementById('newsLink');
alert(el.href);

-------------


Assigning works fine for me.

<p><a id="newsLink" href="news:uk.tech.y2k">newsgroup</a></p>

var el = document.getElementById('newsLink');
el.href = 'news:comp.lang.javascript'

When I click the link Outlook wants me to join comp.lang.javascript

This is a Win XP with a single IE7 install. No other IE's installed.

Peter

David Mark

unread,
Feb 15, 2008, 2:08:55 AM2/15/08
to
On Feb 15, 1:38 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 14, 3:51 pm, Peter Michaux <petermich...@gmail.com> wrote:
>
> > Now I need to fire up a bunch of browsers and accumulate examples of
> > all these weird cases.
>
> Safari 3 on OS X
> ----------------
>
> typeof document['all'] // 'undefined'
>
> document['all']        // [object HTMLCollection]'
>
> That's a really nice one!

Yes. Well, then Mac Safari 3 will be treated as if document.all did
not exist. Something tells me that is how the Safari developers
wanted it. It may be included chiefly to defeat object inferences
(which typically use boolean type conversion.)

>
> Somewhere I wrote I was worried about exactly this in December. The
> ECMAScript spec says in section 11.4.3 that a host object can return
> anything for the "typeof" operator. Unfortunately, anything includes
> "undefined".

True enough. But there's not much we can do about that.

>
> This is a false negative for the isHostCollection(this.document,
> 'all') and that is better than a false positive. The false negative in
> Safari for document.all is not a monumental loss but so much for any
> amount of "proof" that the test functions work universally.

I never meant to imply that they worked universally. I just meant
that the functions prevented IE's strange exceptions and allowed for
collections that implement an internal [[Call]] method. There's no
way to know if there are agents out there that return "mickeymouse" or
"fooledyou" or whatever from typeof operations. If developers of
agents want their products to pass feature tests for rich content,
they have to provide some semblance of compatibility, which will
hopefully prevent them from venturing too far off the beaten path.

>
> By the way, this is the very first test I tried on a few browsers and
> unfortunately already there is a problem.

It depends on how you look at it. As far as I am concerned, a
collection object with an undefined type is best treated as a non-
entity. I suspect you won't see similar results for document.images,
document.forms, etc. in that browser.

Peter Michaux

unread,
Feb 15, 2008, 3:32:25 AM2/15/08
to
On Feb 14, 10:06 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 15, 12:33 am, Peter Michaux <petermich...@gmail.com> wrote:

> > > The cases either throw errors
> > > or they don't. It is the reasoning behind the logic that is in
> > > question, rather than the results. As mentioned, the testing
> > > algorithms are based on nothing more than informed assumptions. It
> > > seems the pattern is clear for current and past versions of IE.
> > > Whether this pattern will hold true in IE8 is anybody's guess, but I
> > > think there is a fair chance it will. Of course, if they add a new
> > > "BillGates"
>
> > I hope they do that. Developers would love it!
>
> I wouldn't! I'd have to add another test to isHostMethod.

I meant it as a joke but now I'm taking it seriously, especially after
reviewing the spec and the odd ball examples we've examined. If they
add that we should not have to change our code because our code is too
strict. I'm going to bumble a bit in this explanation but I think it
is right...or at least not wrong.

The basic use of the current three functions are to determine if a
host object has a particular property or not. We are testing
existance. It seems that typeof has an implicit try-catch and so that
is why we are using instead of to-boolean type converting tests. Also
the type converting tests don't provide information in IE when they
error even if the type conversion test was wrapped in a real try-
catch. Using typeof may be the only option for testing.

The typeof a host object property can be *anything*. Those are the
rules. What we are doing with the three functions is too strict and
not looking to the future where more browsers may report more values
for typeof. We aren't even looking to every current browser that may
already return other typeof values.

Feature testing is a technique that has the ultimate goal that if the
code goes to a browser we have never seen, if the features are there,
the code runs.

When we are using isHostMethod we are checking if the type is
"unknown", "function", or "object" (except for null). That is too
strict. If a browser reports "BillGates" as the type then we still
know the object property exists. If it did not exist then typeof would
return "undefined". Only because of a bug in the language we have to
test if a property is null when "object" is reported.

If we are expecting a callable property and it's typeof is not
"undefined" then we should consider it to exist and be callable. This
is presumptuous but not crazy.

What we are doing with the current isHostMethod is equally
presumputous because we are say that just because the browser reports
"unknown" we assume the property is callable because we expected it to
be callable. Yes it is based on experience but the idea it is no
better than what I'm suggesting.

Because of all this, I think the following is sufficient instead of
all three

var isHostProp = function(o, p) {


var t = typeof o[p];

return t != 'undefined' &&
(t != 'object' ||
o[p]);
};

This allows for any legitimate host object that complies with the spec
to pass. It avoid the IE [[Get]] error problem. It allows the
collections "function" objects pass. It takes care of the typeof null
bug in the language.

What the three functions do is reduce the number of things that will
pass the above code based on experience of what popular hosts return
for "typeof". However by limiting based on experience, compliant
objects can fail. These are false negatives which is ok but with what
benefit?

If we are basing design on experience, are their examples where
isHostObject, isHostCollection, isHostMethod are better than
isHostProp in practice?

Peter

Peter Michaux

unread,
Feb 15, 2008, 1:29:01 PM2/15/08
to
On Feb 15, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:

> var isHostProp = function(o, p) {
> var t = typeof o[p];
> return t != 'undefined' &&
> (t != 'object' ||
> o[p]);
> };

The intention of the above is to find a replacement for a type
converting test which is the most natural test

if (doc.all) {
// use document.all
}

becomes

if (isHostProp(doc, 'all')) {
// use document.all
}

But we know that type converting tests aren't useful in a practical
sense given the behavior of IE and the chance that Microsoft may
change any of their callable host objects to be ActiveXObjects at any
time.

Both the type converting test and the isHostProp test give false
positives if document.all === true but is that a practical concern
that exists in some browser?

Suppose we use isHostCollection

if (isHostCollection(doc, 'all')) {
// use document.all
}

We still get false positives if document.all is some dummy function,
or defective object. We get less false positives but we start getting
false negatives for standard compliant objects. Are there real world
example where this price of false negatives is worth paying?

I can imagine that the following probably doesn't have any real world
problems. It would still have false negative for document.all on
Safari because it seems the WebKit devs want it that way.

var isHostProp2 = function(o, p) {
return typeof o[p] != 'undefined';
};

The following all work

isHostProp2(doc, 'all')
isHostProp2(doc, 'createElement')
isHostProp2(el, 'childNodes')


In which browsers do isHostProp or isHostProp2 or give false positives
for some property that isHostMethod/isHostObject/isHostCollection do
not? Which host object property out there is <code>null</code> in some
browser?

What I'm starting to dislike about isHostMethod, for example, is that
somehow there is the assumption that there is one way to test that any
property name is a function: check for three different typeof strings.
It would be very easy to design two new browsers that make this kind
of test fail.

-----------------------------

I'm starting to get a better big picture concept of how all this
feature testing business fits together.

General Goal: Feature testing is to avoid runtime errors. (Not
necessarily just JavaScript runtime errors but errors like rendering
in a browser that doesn't support enough CSS to make a widget "work"
correctly, for example.)

There are various kinds of inference and (almost) no matter which type
of test we use we will be at some level of inference.

1) navigator.userAgent inference (sniffing)

Since browsers can and do lie in navigator.userAgent looking at
substrings of this property is pointless. Very likely to give false
positives. This is the worst case of unrelated existence inference.

2) unrelated existence inference (sniffing)

If one object property exists, then infer that a different object
property will work. Checking for the existence of document.all or
ActiveXObject and then assuming the IE event model is available. Very
likely to give false positives.

3) related existence inference (sniffing)

Checking for the element.addEventListener and then
event.preventDefault is available. Less likely to give false positives
than unrelated existence inference but still sniffing. Gives a false
positive in Safari.

4) exemplar existence inference

Check that a property exists on an exemplary object. If it exists then
infer that it will work. This is the most common form of feature
testing. It should be sufficient. Are there examples where it is not?
Possible false positives.

5) specific existence inference

This checks for the existence of a property on a particular object but
doesn't assume another similar object will also have that property. If
the feature exists on a particular object then assume it works on that
object. For example, if we test for the existence of
document.documentElement.appendChild, we would not assume any other
DOM element has appendChild. Possible false positives. Somewhere
between existence inference and specific existence inference is why we
check both document.getElementsByTagName and
element.getElementsByTagName.

----

In addition to existence inference, you can analyze the object in a
variety of ways to insure the object satisfies your expectations about
how it should "look".

6) exemplar typeof inference

Check that a property exists and if that its typeof value is one of
the known values for a particular set of browsers then infer the
feature will work. Less likely to produce false positives than simple
existence testing. A host object can return any value when used in a
typeof operation so this can produce false negatives for ECMAScript
compliant host objects that have typeof values you don't know about.
Using typeof inference in addition to existence inference is more
conservative but perhaps without any known benefit over existence
inference alone.

7) specific typeof inference

similar

8) exemplar non-bad-value inference

Check that a host object property exists is not in a particular set of
known bad values, then infer the feature will work. For example, a
host object property is <code>null</code> it is defined and exists but
that value may be no good to you. Reduces the false positives of
existence inference alone.

9) specific non-bad-value inference

similar

----

10) exemplar object, exemplar use inference

Check that a property exists on an exemplar object and then try using
it. If it works then infer it will work again on similar objects.
Unlikely to give a false positive.

11) specific object, exemplar use inference

similar

12) specific object, specific use *testing*

Check that a property exists on a specific object, use it, and check
that it worked as expected after each use. Does not give false
positives. This is computationally expensive and likely not practical
in many situations. If the feature executes but produces a poor result
there is no guarantee you can reverse the operation. The reversal may
fail.

----------

Determining the right level of inference is not an objective decision.
File download size, computation expense and chance of false negatives
trade-off against more code that is less likely to runtime error.

Peter

Peter Michaux

unread,
Feb 15, 2008, 2:08:40 PM2/15/08
to
On Feb 15, 10:29 am, Peter Michaux <petermich...@gmail.com> wrote:

> I'm starting to get a better big picture concept of how all this
> feature testing business fits together.

There is also

Syntax Inference (sniffing)

Using syntax when it is not required where it is used, so that a
particular browser will syntax error. For example, using === where ==
is sufficient just because somewhere else in the script the browser
needs to support document.getElementById and IE4 doesn't have this
feature. This if very likely to produce false positives.

Peter

Peter Michaux

unread,
Feb 15, 2008, 3:32:15 PM2/15/08
to
On Feb 15, 11:08 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 15, 10:29 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > I'm starting to get a better big picture concept of how all this
> > feature testing business fits together.
>

There is also

Related Execution Inference (sniffing)

If the code is executing infer that a language features exist and
works. This seems to be acceptable for old language features (e.g.
String.prototype.substring which was in JavaScript 1.0, JScript 1.0
and ECMAScript v1). The tradeoff for more robust inference tests is
considered unjustified.

Unrelated Execution Inference (sniffing)

If the code is executing infer that a host features exist and works.
For example, many programmers assume that if JavaScript is executing
then the necessary CSS support is also available.


Language Version Inference (sniffing)

The script type attributes with its version number will start to play
a role in the future when ES4 is released. This happened a long time
ago with the script language attribute and JavaScript versioning. That
was before my time however.


Peter

David Mark

unread,
Feb 15, 2008, 6:33:04 PM2/15/08
to
On Feb 15, 3:32 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 14, 10:06 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 15, 12:33 am, Peter Michaux <petermich...@gmail.com> wrote:
> > > > The cases either throw errors
> > > > or they don't.  It is the reasoning behind the logic that is in
> > > > question, rather than the results.  As mentioned, the testing
> > > > algorithms are based on nothing more than informed assumptions.  It
> > > > seems the pattern is clear for current and past versions of IE.
> > > > Whether this pattern will hold true in IE8 is anybody's guess, but I
> > > > think there is a fair chance it will.  Of course, if they add a new
> > > > "BillGates"
>
> > > I hope they do that. Developers would love it!
>
> > I wouldn't!  I'd have to add another test to isHostMethod.
>
> I meant it as a joke but now I'm taking it seriously, especially after
> reviewing the spec and the odd ball examples we've examined. If they

Other than Mac Safari's seemingly intentional goof related to
document.all, what else is giving unexpected results?

> add that we should not have to change our code because our code is too
> strict. I'm going to bumble a bit in this explanation but I think it
> is right...or at least not wrong.
>
> The basic use of the current three functions are to determine if a
> host object has a particular property or not. We are testing
> existance. It seems that typeof has an implicit try-catch and so that
> is why we are using instead of to-boolean type converting tests. Also
> the type converting tests don't provide information in IE when they
> error even if the type conversion test was wrapped in a real try-
> catch. Using typeof may be the only option for testing.

I am sure that it is.

>
> The typeof a host object property can be *anything*. Those are the
> rules. What we are doing with the three functions is too strict and
> not looking to the future where more browsers may report more values
> for typeof. We aren't even looking to every current browser that may
> already return other typeof values.

If the browser developers want to (deliberately or not) exclude
certain features from detection (e.g. by returning "fooledyou" for a
typeof operation), then that is their privilege. The only case I
think needs a workaround (as provided in isHostMethod) is the
"unknown" type in IE, as affected properties throw exceptions for some
operations.

>
> Feature testing is a technique that has the ultimate goal that if the
> code goes to a browser we have never seen, if the features are there,
> the code runs.

In a perfect world, yes.

>
> When we are using isHostMethod we are checking if the type is
> "unknown", "function", or "object" (except for null). That is too
> strict. If a browser reports "BillGates" as the type then we still

I wouldn't trust any object that reports such a type. Better to treat
it as a non-entity IMO.

> know the object property exists. If it did not exist then typeof would
> return "undefined". Only because of a bug in the language we have to
> test if a property is null when "object" is reported.
>
> If we are expecting a callable property and it's typeof is not
> "undefined" then we should consider it to exist and be callable. This
> is presumptuous but not crazy.

I disagree. If, for instance, it reports "boolean" or "number", I
wouldn't expect it to be callable.

>
> What we are doing with the current isHostMethod is equally
> presumputous because we are say that just because the browser reports
> "unknown" we assume the property is callable because we expected it to

Not exactly. If it reports "unknown" and we are *expecting* a
callable property, then we can make the presumption. That's why you
should never do something like:

if (isHostMethod(el, 'href')) {
...
}

As we have seen, href properties of news links (and possibly others)
can be of type "unknown." However, it would be folly to expect them
to be callable and therefore isHostMethod is the wrong function to
test them with. I didn't bother to make a function to test non-
callable "unknown" properties as they are unusable for anything (can't
assign them, can't type convert them, etc.) See the getAttribute
wrapper, which returns the string "[unknown]" if the fetched property
is of an unknown type (only done in the IE workaround branch.)

> be callable. Yes it is based on experience but the idea it is no
> better than what I'm suggesting.
>
> Because of all this, I think the following is sufficient instead of
> all three
>
> var isHostProp = function(o, p) {
>   var t = typeof o[p];
>   return t != 'undefined' &&
>          (t != 'object' ||
>           o[p]);
>
> };

I don't like that. There is no way that I would allow my code to call
a property that returns "boolean", "number", etc. It is possible that
I might have to edit one function in the future if browser developers
introduce a "callable" or "iamafunction" type or whatever, but I am
fine with that (and don't see it happening anyway.)

>
> This allows for any legitimate host object that complies with the spec
> to pass. It avoid the IE [[Get]] error problem. It allows the
> collections "function" objects pass. It takes care of the typeof null
> bug in the language.

It is too generalized. For instance:

if (isHostProp(doc, 'images')) {
docimages = function(docNode) {
return (docNode || doc).images;
};
}

That allows for the images property to be an unknown type, which would
cause the docimages function to throw an exception every time (at
least that is the observed pattern.)

>
> What the three functions do is reduce the number of things that will
> pass the above code based on experience of what popular hosts return
> for "typeof". However by limiting based on experience, compliant
> objects can fail. These are false negatives which is ok but with what
> benefit?

But what compliant objects? What browser developer would deliberately
exclude their host objects from such commonly used feature detection
logic? Other than the "unknown" workaround, the logic in the three
functions isn't ground-breaking.

>
> If we are basing design on experience, are their examples where
> isHostObject, isHostCollection, isHostMethod are better than
> isHostProp in practice?

Similar to the above example:

if (isHostProp(this, 'document')) { ... }

This would allow a global "document" variable of any defined type to
trick the calling code into thinking it is running in a browser.

I firmly believe that each of the three functions has its place.
Using each as appropriate makes the intentions of gateway code clear.
Future maintenance may be needed for any of the three, but I wouldn't
assume that such maintenance would be avoidable if all three are
lumped together (in fact, it may require them to be broken up, which
would require rewriting all of the calling code.)

David Mark

unread,
Feb 15, 2008, 7:26:22 PM2/15/08
to
On Feb 15, 1:29 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 15, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > var isHostProp = function(o, p) {
> >   var t = typeof o[p];
> >   return t != 'undefined' &&
> >          (t != 'object' ||
> >           o[p]);
> > };
>
> The intention of the above is to find a replacement for a type
> converting test which is the most natural test
>
> if (doc.all) {
>   // use document.all
>
> }
>
> becomes
>
> if (isHostProp(doc, 'all')) {
>   // use document.all
>
> }

If you think about it, the two are basically equivalent (and too
general IMO), except for "unknown" types and Mac Safari's
implementation of document.all.

>
> But we know that type converting tests aren't useful in a practical
> sense given the behavior of IE and the chance that Microsoft may
> change any of their callable host objects to be ActiveXObjects at any
> time.

It has been my assumption that ActiveX implementations are responsible
for the "unknown" types. However, I don't believe that the callable
host object properties (or non-callable examples like href's of news
links) are ActiveX objects. It seems to me that "unknown" indicates
that they are properties of an ActiveX object (i.e. a news link
element is an ActiveX object and therefore its properties are of
unknown types.) The ActiveX object itself returns "object" for typeof
operations and does not throw an exception when assigned or type
converted (unlike its "unknown" properties.) Only MS knows for sure
if ActiveX is even involved in this mess, but it seems like a good
assumption in that "IUnknown" is the base interface for COM objects
(which were renamed "ActiveX" by the marketing department when IE3
came out, presumably because it sounded cooler.)

>
> Both the type converting test and the isHostProp test give false
> positives if document.all === true but is that a practical concern
> that exists in some browser?

Just as with the "BillGates" example, it is hard to qualify concerns
about a hypothetical situation. I think the above example is more
likely though. For instance, an off-brand browser developer might do
such a thing to defeat object inferences (which seems to be what the
Mac Safari developers did with document.all.)

Bill, if you are out there, can you chime in on this? Will MS be
honoring your departure with a namesake host object type?

>
> Suppose we use isHostCollection
>
> if (isHostCollection(doc, 'all')) {
>   // use document.all
>
> }
>
> We still get false positives if document.all is some dummy function,

There's nothing we can do about that in the detection phase. However,
the method can be tested further if needed (i.e. does it return an
object with a length property and does the first item have a tagName
property?)

> or defective object.  We get less false positives but we start getting
> false negatives for standard compliant objects. Are there real world
> example where this price of false negatives is worth paying?

I can't think of a known false negative that would be avoided by
combining the three functions. As for the hypothetical future
"mickeymouse" types, I would rather they return a negative result, at
least until their behavior could be studied in the same way the
"unknown" types were.

>
> I can imagine that the following probably doesn't have any real world
> problems. It would still have false negative for document.all on
> Safari because it seems the WebKit devs want it that way.
>
> var isHostProp2 = function(o, p) {
>   return typeof o[p] != 'undefined';
>
> };

That allows for null. Without checking for "unknown", there is no way
to know if a further type conversion test to exclude it will throw an
exception in IE.

>
> The following all work
>
> isHostProp2(doc, 'all')
> isHostProp2(doc, 'createElement')
> isHostProp2(el, 'childNodes')

It also allows for:

if (isHostProp2(el, 'offsetParent')) {
alert(el.offsetParent.tagName);
}

This example will blow up for fixed position elements in Opera
(offsetParent === null.)

This one will throw an exception for elements in IE that are not part
of a document:

if (isHostProp2(el, 'offsetParent')) {
alert(el.offsetParent);
}

Which of the three tests to use depends on how the tested property is
to be used.

>
> In which browsers do isHostProp or isHostProp2 or give false positives

I covered isHostProp2 above. The issue with isHostProp is not
necessarily false positives, but inappropriate positives (results that
are not meaningful in the context of a specific test.)

> for some property that isHostMethod/isHostObject/isHostCollection do
> not? Which host object property out there is <code>null</code> in some
> browser?

There's no telling, but I think it is a more likely scenario than IE8
returning "BillGates" for typeof document.getElementById or some
undiscovered agent returning "xyz" for the same.

>
> What I'm starting to dislike about isHostMethod, for example, is that
> somehow there is the assumption that there is one way to test that any
> property name is a function: check for three different typeof strings.

It is only used to verify properties that are expected to be methods
(e.g. gEBI.)

> It would be very easy to design two new browsers that make this kind
> of test fail.

Sure, just return "xyz" for typeof anything. But what browser
developer would do such a thing? Perhaps one that doesn't want its
host objects to be used?

>
> -----------------------------
>
> I'm starting to get a better big picture concept of how all this
> feature testing business fits together.
>
> General Goal: Feature testing is to avoid runtime errors. (Not

It is important to make a distinction between feature detection (the
purpose of the three functions at hand) and feature testing, which is
beyond their scope.

[snip]

I read through the rest of it. It is interesting and seems a useful
discussion for another article. However, for the purposes of the CWR
project (and this example widget), I think we need to settle the
feature detection layer sooner rather than later (we could debate this
subject forever.)

I really thought we were set with the slightly renamed trio as
proposed yesterday. That's what I am going with for my library (for
better or worse.)

Peter Michaux

unread,
Feb 16, 2008, 12:32:30 AM2/16/08
to
On Feb 15, 4:26 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 15, 1:29 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > On Feb 15, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:


> I can't think of a known false negative that would be avoided by
> combining the three functions. As for the hypothetical future
> "mickeymouse" types, I would rather they return a negative result, at
> least until their behavior could be studied in the same way the
> "unknown" types were.

I think that is a very strong argument for the three.

> > The following all work
>
> > isHostProp2(doc, 'all')
> > isHostProp2(doc, 'createElement')
> > isHostProp2(el, 'childNodes')
>
> It also allows for:
>
> if (isHostProp2(el, 'offsetParent')) {
> alert(el.offsetParent.tagName);
>
> }
>
> This example will blow up for fixed position elements in Opera
> (offsetParent === null.)
>
> This one will throw an exception for elements in IE that are not part
> of a document:
>
> if (isHostProp2(el, 'offsetParent')) {
> alert(el.offsetParent);
>
> }

My thinking was a little overzealous with regard to null values in
particular.


> I read through the rest of it. It is interesting and seems a useful
> discussion for another article.

It is one now :-)

<URL: http://peter.michaux.ca/article/7146>

I'd like to put a link to your code if you have one publicly
available.

> I really thought we were set with the slightly renamed trio as
> proposed yesterday. That's what I am going with for my library (for
> better or worse.)

I agree with that set. I just had some last minute second thoughts.

Peter

Peter Michaux

unread,
Feb 16, 2008, 12:32:49 AM2/16/08
to
On Feb 15, 3:33 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 15, 3:32 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > On Feb 14, 10:06 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > On Feb 15, 12:33 am, Peter Michaux <petermich...@gmail.com> wrote:
> > > > > The cases either throw errors
> > > > > or they don't. It is the reasoning behind the logic that is in
> > > > > question, rather than the results. As mentioned, the testing
> > > > > algorithms are based on nothing more than informed assumptions. It
> > > > > seems the pattern is clear for current and past versions of IE.
> > > > > Whether this pattern will hold true in IE8 is anybody's guess, but I
> > > > > think there is a fair chance it will. Of course, if they add a new
> > > > > "BillGates"
>
> > > > I hope they do that. Developers would love it!
>
> > > I wouldn't! I'd have to add another test to isHostMethod.
>
> > I meant it as a joke but now I'm taking it seriously, especially after
> > reviewing the spec and the odd ball examples we've examined. If they
>
> Other than Mac Safari's seemingly intentional goof related to
> document.all, what else is giving unexpected results?

The ActiveX objects are weird.

[snip]

> > What the three functions do is reduce the number of things that will
> > pass the above code based on experience of what popular hosts return
> > for "typeof". However by limiting based on experience, compliant
> > objects can fail. These are false negatives which is ok but with what
> > benefit?
>
> But what compliant objects?

None that I know of.


> What browser developer would deliberately
> exclude their host objects from such commonly used feature detection
> logic?

Probably none.

[snip]

> I firmly believe that each of the three functions has its place.
> Using each as appropriate makes the intentions of gateway code clear.
> Future maintenance may be needed for any of the three, but I wouldn't
> assume that such maintenance would be avoidable if all three are
> lumped together (in fact, it may require them to be broken up, which
> would require rewriting all of the calling code.)

It is true.

Peter

David Mark

unread,
Feb 16, 2008, 1:33:30 AM2/16/08
to
On Feb 16, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 15, 4:26 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 15, 1:29 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > On Feb 15, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:
> > I can't think of a known false negative that would be avoided by
> > combining the three functions.  As for the hypothetical future
> > "mickeymouse" types, I would rather they return a negative result, at
> > least until their behavior could be studied in the same way the
> > "unknown" types were.
>
> I think that is a very strong argument for the three.

I am glad you agree. I agree that none of this is guaranteed to be
100% future-proof or to support every browser ever released (a claim
that would be impossible to prove.)

>
>
>
>
>
> > > The following all work
>
> > > isHostProp2(doc, 'all')
> > > isHostProp2(doc, 'createElement')
> > > isHostProp2(el, 'childNodes')
>
> > It also allows for:
>
> > if (isHostProp2(el, 'offsetParent')) {
> >   alert(el.offsetParent.tagName);
>
> > }
>
> > This example will blow up for fixed position elements in Opera
> > (offsetParent === null.)
>
> > This one will throw an exception for elements in IE that are not part
> > of a document:
>
> > if (isHostProp2(el, 'offsetParent')) {
> >   alert(el.offsetParent);
>
> > }
>
> My thinking was a little overzealous with regard to null values in
> particular.
>
> > I read through the rest of it.  It is interesting and seems a useful
> > discussion for another article.
>
> It is one now :-)
>
> <URL:http://peter.michaux.ca/article/7146>

I'll check it out.

>
> I'd like to put a link to your code if you have one publicly
> available.

You mean "My Library?" I don't want the public in there yet. The
version that is online is a very stale Alpha. When I get a free
moment (been busy with other projects for the last few weeks), I will
update it and post a link here.

>
> > I really thought we were set with the slightly renamed trio as
> > proposed yesterday.  That's what I am going with for my library (for
> > better or worse.)
>
> I agree with that set. I just had some last minute second thoughts.
>

You brought up some valid points that needed to be addressed. The
specs vs. reality issue is a tricky one. In the end, I think that the
reality-based solution wins out (for now.)

David Mark

unread,
Feb 16, 2008, 1:46:43 AM2/16/08
to
On Feb 16, 12:32 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 15, 3:33 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
>
>
>
>
> > On Feb 15, 3:32 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > On Feb 14, 10:06 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > > On Feb 15, 12:33 am, Peter Michaux <petermich...@gmail.com> wrote:
> > > > > > The cases either throw errors
> > > > > > or they don't.  It is the reasoning behind the logic that is in
> > > > > > question, rather than the results.  As mentioned, the testing
> > > > > > algorithms are based on nothing more than informed assumptions.  It
> > > > > > seems the pattern is clear for current and past versions of IE.
> > > > > > Whether this pattern will hold true in IE8 is anybody's guess, but I
> > > > > > think there is a fair chance it will.  Of course, if they add a new
> > > > > > "BillGates"
>
> > > > > I hope they do that. Developers would love it!
>
> > > > I wouldn't!  I'd have to add another test to isHostMethod.
>
> > > I meant it as a joke but now I'm taking it seriously, especially after
> > > reviewing the spec and the odd ball examples we've examined. If they
>
> > Other than Mac Safari's seemingly intentional goof related to
> > document.all, what else is giving unexpected results?
>
> The ActiveX objects are weird.

Indeed. Thanks MS!

>
> [snip]
>
> > > What the three functions do is reduce the number of things that will
> > > pass the above code based on experience of what popular hosts return
> > > for "typeof". However by limiting based on experience, compliant
> > > objects can fail. These are false negatives which is ok but with what
> > > benefit?
>
> > But what compliant objects?
>
> None that I know of.

Hopefully none will turn up. If one does, scripts should still
degrade gracefully, as long as gateway code calls the appropriate
functions.

>
> > What browser developer would deliberately
> > exclude their host objects from such commonly used feature detection
> > logic?
>
> Probably none.

I think that if the majority of developers use testing functions like
these, it goes a long way towards forcing the browser makers to stay
on the beaten path. Unfortunately, at the present time, most
developers are still addicted to browser sniffing, which encourages
browser makers to deploy creative countermeasures.

>
> [snip]
>
> > I firmly believe that each of the three functions has its place.
> > Using each as appropriate makes the intentions of gateway code clear.
> > Future maintenance may be needed for any of the three, but I wouldn't
> > assume that such maintenance would be avoidable if all three are
> > lumped together (in fact, it may require them to be broken up, which
> > would require rewriting all of the calling code.)
>
> It is true.
>

I am glad we are in agreement. These are important issues, certainly
worthy of debate, but it was starting to seem like we would never
reach a common ground.

Peter Michaux

unread,
Feb 16, 2008, 1:57:01 PM2/16/08
to
One of the primary goals of the tabbed pane example is to allow for
the tentative styling of the widget if the widget might work. This is
because HTML does incremental rendering as the document loads and the
user should see something pleasing during that potentially long period
of time.

The basic pattern is this

---------

if (tests that can run during initial JavaScript execution pass) {

add a stylesheet that gives tentative and final stylings
in both cases where the widget is enabled or not

addDomReadyListener(function() {

if (final tests that need the full DOM pass) {
enabled the widget
}
else {
disabled the widget
}

})

}

--------

My big concern has been that if I add the tentative styling before
final testing, the final testing may never run because the DOM ready
listeners may not fire. I was confusing mixing in the Safari/anchor/
click/addEventListener/preventDefault problem into this by thinking I
would use DOM0 handlers for all kinds of events.

If the addDomReadyListener function is using either addEventListener
or attachEvent, then I can know in the first set of tests (i.e. in the
outer "if" above) that the domReadyListeners will run. There aren't
any known problems with this. Now I'm satisfied that I can at least
"get out of trouble".

Peter

Thomas 'PointedEars' Lahn

unread,
Feb 17, 2008, 9:06:56 AM2/17/08
to
Peter Michaux wrote:
> Below I've included the four files involved in the example:
> [ca. 500 lines of code]

Ahh, *there's* the K key.


F'up2 poster

PointedEars
--
Use any version of Microsoft Frontpage to create your site.
(This won't prevent people from viewing your source, but no one
will want to steal it.)
-- from <http://www.vortex-webdesign.com/help/hidesource.htm>

Peter Michaux

unread,
Feb 17, 2008, 11:08:14 AM2/17/08
to
On Feb 17, 6:06 am, Thomas 'PointedEars' Lahn <PointedE...@web.de>
wrote:

> Peter Michaux wrote:
> > Below I've included the four files involved in the example:
> > [ca. 500 lines of code]
>
> Ahh, *there's* the K key.

I'm glad you are learning the keybindings of your editor. I'm sure you
will be a much more efficient computer user now. Thanks for sharing.


> F'up2 poster

I've told you that I don't know the uncommon abbreviations you insist
on using.

Peter

Peter Michaux

unread,
Feb 17, 2008, 2:19:58 PM2/17/08
to
Below is my code for the event part of the tabbed pane example. I'm
pretty happy with this. It is much more compact than other workarounds
that solve the Safari problem. It would be easy to add a
LIB.removeListener function to the following code. I don't need that
for the tabbed pane demo.

This solves the Safari problem. I tested on S2.0 this morning and will
check 1.x versions later today.) The more I have thought about
cancelBubble the more I think using it is just a bad practice anyway
so the fact that the Safari workaround breaks if cancelBubble is used
doesn't bother me at all.

IE circular reference leaks should be handled properly. I think I have
done so. I don't know exactly why I continue a tradition of setting
some variables to null in an event module as part of the clean up. I
don't think these lines are necessary.

I don't care in which order event handlers run and don't think making
sure they run in the order they are added encourages good programming
or is worth a single character.

David, I looked at some of your event code and noticed that you don't
use DOMContentLoaded with IE. I think that if IE gets their act
together they will probably have an 'onDOMContentLoaded' event so I
have used it below.

Peter

// -----------------------------

(function() {

if (Function.prototype.apply &&
LIB.getAnElement) {

var global = this,
anElement = LIB.getAnElement(),
listeners = [];

var wrapHandler = function(element, handler, options) {
options = options || {};
var thisObj = options.thisObj || element;
return function(e) {
return handler.apply(thisObj, [e || global.event]);
};
};

var createListener = function(element, eventType,
handler, options) {
return {
element: element,
eventType: eventType,
handler: handler,
wrappedHandler: wrapHandler(element, handler, options)
};
};

// BEGIN DOM2 event model
if (LIB.isHostMethod(global, 'addEventListener') &&
LIB.isHostMethod(anElement, 'addEventListener')) {

LIB.addListener = function(element, eventType,
handler, options) {
var listener = createListener(element, eventType,
handler, options);
element.addEventListener(
listener.eventType,
listener.wrappedHandler,
false);
listeners[listeners.length] = listener;
};

// Safari preventDefault workaround
if (LIB.getDocumentElement) {
var docElement = LIB.getDocumentElement();

var prevented = false;

// Clobbers any HTML inline element but it is
// trivial to build a workaround for that.
// The reason to use the docElement is because
// it is the least likely place where clobbering
// will cause an accidental problem.
docElement.onclick =
docElement.ondblclick =
function() {
var result = !prevented;
prevented = false;
return result;
};

// clean up for memory leaks
// No known problems but just being careful
docElement = null;
}

// event must bubble up to documentElement
// so no cancelBubble. It is a bad practice
// anyway because canceling bubble foils
// unrelated delegate listeners
LIB.preventDefault = function(e) {
prevented = true;
if (LIB.isHostMethod(e, 'preventDefault')) {
e.preventDefault();
}
};

} // END DOM2 event model
// BEGIN IE event model
else if (LIB.isHostMethod(global, 'attachEvent') &&
LIB.isHostMethod(anElement, 'attachEvent')
// uncomment next line if using
// the try-catch below
//&& global.setTimeout
) {

// IE leaks memory and want to cleanup
// before when the document unloads
var unloadListeners = [];

LIB.addListener = function(element, eventType,
handler, options) {
var listener = createListener(element, 'on'+eventType,
handler, options);

if (eventType == 'unload') {
unloadListeners[unloadListeners.length] = listener;
}
else {
element.attachEvent(
listener.eventType,
listener.wrappedHandler);
listeners[listeners.length] = listener;
}
};

global.attachEvent(
'onunload',
function(e) {
e = e || window.event;
var i, listener;
for (i=unloadListeners.length; i--; ) {
listener = unloadListeners[i];
if (listener) {
// Uncomment try-catch if you do not need to support
// old browsers or some modern cell phones.
// The try-catch will ensure all handlers run.
// Also uncomment test for global.setTimeout above
//try {
listener.wrappedHandler(e);
//}
//catch (err) {
// (function(err) {
// global.setTimeout(function() {throw err}, 10);
// })(err);
//}
}
}
global.detachEvent(
'onunload',
arguments.callee);

for (i=listeners.length; i-- ; ) {
listener = listeners[i];
if (listener) {
// break circular reference to
// fix IE memory leak
listener.element.detachEvent(
listener.eventType,
listener.wrappedHandler);
// next lines are just to help
// garbage collection. No known
// problems.
listener.element =
listener.handler =
listener.wrappedHandler =
null;
listeners[i] = null;
}
}
}
);

LIB.preventDefault = function(e) {
e.returnValue = false;
};

} // END IE event model


if (LIB.addListener &&
LIB.getDocumentElement) {

var readyListeners = [];

LIB.addDomReadyListener = function(handler, options) {
var options = options || {};
readyListeners[readyListeners.length] =
wrapHandler(
options.docNode || LIB.getDocumentElement(),
handler,
options);
}

var bReady = false;
var handleDomReady = function() {
if (!bReady) {
bReady = true;
for (var i = readyListeners.length; i--; ) {
readyListeners[i]();
}
}
};
LIB.fireDomReady = handleDomReady;
LIB.addListener(global, 'DOMContentLoaded', handleDomReady);
LIB.addListener(global, 'load', handleDomReady);
}

// clean up for memory leaks
// no known problems
anElement = null;
}
})();

David Mark

unread,
Feb 17, 2008, 8:37:42 PM2/17/08
to
On Feb 17, 2:19 pm, Peter Michaux <petermich...@gmail.com> wrote:
> Below is my code for the event part of the tabbed pane example. I'm
> pretty happy with this. It is much more compact than other workarounds
> that solve the Safari problem. It would be easy to add a
> LIB.removeListener function to the following code. I don't need that
> for the tabbed pane demo.
>
> This solves the Safari problem. I tested on S2.0 this morning and will
> check 1.x versions later today.) The more I have thought about
> cancelBubble the more I think using it is just a bad practice anyway
> so the fact that the Safari workaround breaks if cancelBubble is used
> doesn't bother me at all.

I can't think of a recent project where I needed to cancel event
bubbling.

>
> IE circular reference leaks should be handled properly. I think I have
> done so. I don't know exactly why I continue a tradition of setting
> some variables to null in an event module as part of the clean up. I
> don't think these lines are necessary.

It is often necessary to prevent the circular references that cause IE
memory leaks.

>
> I don't care in which order event handlers run and don't think making

You don't need to worry about that. The standard explicitly allows
for any arbitrary firing order. Code that attempts to predict the
order is broken by design.

> sure they run in the order they are added encourages good programming
> or is worth a single character.

It would clearly be a waste of time.

>
> David, I looked at some of your event code and noticed that you don't
> use DOMContentLoaded with IE. I think that if IE gets their act
> together they will probably have an 'onDOMContentLoaded' event so I
> have used it below.

Do you mean I don't use it with attachEvent? That sounds like an
oversight. However, I am hopeful that IE8 will implement
addEventListener and not attempt to fix attachEvent. Additionally, if
they do implement addEventListener, I hope they will follow the
standards. It won't make a difference for my code (other than the
oversight you pointed out), but it could wreak havoc on existing
sites.

[snip]

> // -----------------------------
>
> (function() {
>
>   if (Function.prototype.apply &&
>       LIB.getAnElement) {
>
>       var global = this,
>           anElement = LIB.getAnElement(),
>           listeners = [];
>
>       var wrapHandler = function(element, handler, options) {
>         options = options || {};
>         var thisObj = options.thisObj || element;
>         return function(e) {
>           return handler.apply(thisObj, [e || global.event]);
>         };

Why not use call here? And do you need to "wrap" handlers for DOM2
implementations? I only do it if an optional context is specified
(i.e. "this" needs to be set to something other than the element at
hand.)

>       };
>
>       var createListener = function(element, eventType,
>                                              handler, options) {
>         return {
>           element: element,
>           eventType: eventType,
>           handler: handler,
>           wrappedHandler: wrapHandler(element, handler, options)
>         };
>       };
>
>     // BEGIN DOM2 event model
>     if (LIB.isHostMethod(global, 'addEventListener') &&
>         LIB.isHostMethod(anElement, 'addEventListener')) {

Watch out here. There is no standard for the window object and it has
been reported that some agents don't bother to implement
addEventListener for that object.

>
>       LIB.addListener = function(element, eventType,
>                                                handler, options) {
>         var listener = createListener(element, eventType,
>                                                 handler, options);
>         element.addEventListener(
>           listener.eventType,
>           listener.wrappedHandler,
>           false);
>         listeners[listeners.length] = listener;

Make sure that the listeners array does not end up sharing a closure
with this code (as it does here.) I assume that unload cleanup logic
will follow at some point, but AIUI, such code doesn't do a bit of
good for elements that have been removed from their documents (e.g. by
innerHTML replacement.) IIRC, you covered this in your FORK library.

>       };
>
>       // Safari preventDefault workaround
>       if (LIB.getDocumentElement) {
>         var docElement = LIB.getDocumentElement();
>
>         var prevented = false;
>
>         // Clobbers any HTML inline element but it is
>         // trivial to build a workaround for that.
>         // The reason to use the docElement is because
>         // it is the least likely place where clobbering
>         // will cause an accidental problem.

Makes sense.

>         docElement.onclick =
>         docElement.ondblclick =
>           function() {
>             var result = !prevented;
>             prevented = false;
>             return result;
>           };
>
>         // clean up for memory leaks
>         // No known problems but just being careful

Skipping this step would create a potential for leakage in IE. This
would only be an issue if the code that normalizes and attaches events
shares a closures with this reference. Imagine if calling code
attached a listener to the documentElement.

>         docElement = null;
>       }
>
>       // event must bubble up to documentElement
>       // so no cancelBubble. It is a bad practice
>       // anyway because canceling bubble foils
>       // unrelated delegate listeners

That's certainly one reason not to use it.

>       LIB.preventDefault = function(e) {
>         prevented = true;
>         if (LIB.isHostMethod(e, 'preventDefault')) {
>           e.preventDefault();
>         }
>       };
>
>     } // END DOM2 event model
>     // BEGIN IE event model
>     else if (LIB.isHostMethod(global, 'attachEvent') &&
>              LIB.isHostMethod(anElement, 'attachEvent')
>              // uncomment next line if using
>              // the try-catch below
>              //&& global.setTimeout
>              ) {
>
>       // IE leaks memory and want to cleanup
>       // before when the document unloads
>       var unloadListeners = [];
>
>       LIB.addListener = function(element, eventType,
>                                                handler, options) {
>         var listener = createListener(element, 'on'+eventType,
>                                                 handler, options);
>
>         if (eventType == 'unload') {

Shouldn't this deal only with unload listeners on the window object?
Are there even distinct unload events for DOM objects (e.g. body) or
are they all lumped together (as with body.onload?) If an
implementation does make a distinction, I would assume that the
window's unload listeners would be the last to fire.

>           unloadListeners[unloadListeners.length] = listener;

This is a good idea and something I need to add to mine. Otherwise,
you can't clean up unload listeners (might remove them before they
have run.) I think I just left the window's unload listeners alone in
my most recent version. I am not sure whether listeners attached to
the window object can create memory leaks in IE. (?)

>         }
>         else {
>           element.attachEvent(
>             listener.eventType,
>             listener.wrappedHandler);
>           listeners[listeners.length] = listener;
>         }
>       };
>
>       global.attachEvent(
>         'onunload',
>         function(e) {
>           e = e || window.event;
>           var i, listener;
>           for (i=unloadListeners.length; i--; ) {
>             listener = unloadListeners[i];
>             if (listener) {
>               // Uncomment try-catch if you do not need to support
>               // old browsers or some modern cell phones.
>               // The try-catch will ensure all handlers run.
>               // Also uncomment test for global.setTimeout above
>               //try {
>                 listener.wrappedHandler(e);

Shouldn't you call these?

This last line should suffice. I assume nothing else references the
"listener" objects in the array.

>             }
>           }
>         }
>       );
>
>       LIB.preventDefault = function(e) {
>         e.returnValue = false;
>       };
>
>     } // END IE event model
>
>     if (LIB.addListener &&
>         LIB.getDocumentElement) {
>
>       var readyListeners = [];
>
>       LIB.addDomReadyListener = function(handler, options) {
>         var options = options || {};
>         readyListeners[readyListeners.length] =
>           wrapHandler(
>             options.docNode || LIB.getDocumentElement(),

This looks wrong. Should be:

options.docNode || global.document

However, as with my version, you only have one array for document
ready listeners (and one ready flag), so you cannot support multiple
documents here.


>             handler,
>             options);
>       }
>
>       var bReady = false;
>       var handleDomReady = function() {
>         if (!bReady) {
>           bReady = true;
>           for (var i = readyListeners.length; i--; ) {
>             readyListeners[i]();
>           }
>         }
>       };
>       LIB.fireDomReady = handleDomReady;

I wish we could agree on the naming of the various methods (e.g.
getDocumentElement vs. getHtmlElement, fireDomReady vs. whatever I
called that, etc.)

>       LIB.addListener(global, 'DOMContentLoaded', handleDomReady);
>       LIB.addListener(global, 'load', handleDomReady);
>     }
>
>     // clean up for memory leaks
>     // no known problems

There could be a problem if an application attached an event to
whatever anElement references. Your wrapped function's scope would
include anElement. Of course, the cleanup code should clear things up
on unload, but shouldn't be counted on. To this end, I made my
"attachedListeners" array, as well as an array that stores contexts
for each listener, properties of the global API object.

That reminds me, both versions will grow infinitely long arrays if
listeners are attached and detached repeatedly. A good example of a
module that does this is drag and drop, which temporarily attaches
mouseup, mousemove and keyup listeners (for keyboard-controlled drag
operations) to the document object. Each time an element is dragged,
the "cleanup queue" gets a little bigger. Adding a splice operation
to prevent that is at the top of my list.

>     anElement = null;
>   }
>
>
>
> })();

Peter Michaux

unread,
Feb 17, 2008, 11:44:28 PM2/17/08
to
On Feb 17, 5:37 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 17, 2:19 pm, Peter Michaux <petermich...@gmail.com> wrote:

> > David, I looked at some of your event code and noticed that you don't
> > use DOMContentLoaded with IE. I think that if IE gets their act
> > together they will probably have an 'onDOMContentLoaded' event so I
> > have used it below.
>
> Do you mean I don't use it with attachEvent?

It looks like you do not. I see three in your code

docNode.addEventListener('DOMContentLoaded', documentReadyListener,
false);
global.addEventListener('load', documentReadyListener, false);
global.attachEvent('onload', documentReadyListener);


> That sounds like an
> oversight. However, I am hopeful that IE8 will implement
> addEventListener

I don't think they will. It is in their best interest to make sure
browsers are not interchangeable commodities. They have corporate
environments with intranets for IE only locked in.


> > (function() {
>
> > if (Function.prototype.apply &&
> > LIB.getAnElement) {
>
> > var global = this,
> > anElement = LIB.getAnElement(),
> > listeners = [];
>
> > var wrapHandler = function(element, handler, options) {
> > options = options || {};
> > var thisObj = options.thisObj || element;
> > return function(e) {
> > return handler.apply(thisObj, [e || global.event]);
> > };
>
> Why not use call here?

Apply is older and works so there is I figured there was no need to
raise the bar in this case.

I also usually have an options.args array the caller can supply and so
that needs to be dealt with using apply and concatenating the event
onto the front.

> And do you need to "wrap" handlers for DOM2
> implementations? I only do it if an optional context is specified
> (i.e. "this" needs to be set to something other than the element at
> hand.)

In my options object I allow for parameters to to be passed to the
handler function. So in general it needs to be wrapped and I don't
look for the situations where I don't need to wrap it.


> > };
>
> > var createListener = function(element, eventType,
> > handler, options) {
> > return {
> > element: element,
> > eventType: eventType,
> > handler: handler,
> > wrappedHandler: wrapHandler(element, handler, options)
> > };
> > };
>
> > // BEGIN DOM2 event model
> > if (LIB.isHostMethod(global, 'addEventListener') &&
> > LIB.isHostMethod(anElement, 'addEventListener')) {
>
> Watch out here. There is no standard for the window object and it has
> been reported that some agents don't bother to implement
> addEventListener for that object.

I suppose I'm willing to let those one fall into the graceful
degradation path. They aren't common.


> > LIB.addListener = function(element, eventType,
> > handler, options) {
> > var listener = createListener(element, eventType,
> > handler, options);
> > element.addEventListener(
> > listener.eventType,
> > listener.wrappedHandler,
> > false);
> > listeners[listeners.length] = listener;
>
> Make sure that the listeners array does not end up sharing a closure
> with this code (as it does here.) I assume that unload cleanup logic
> will follow at some point, but AIUI, such code doesn't do a bit of
> good for elements that have been removed from their documents (e.g. by
> innerHTML replacement.) IIRC, you covered this in your FORK library.

I don't really follow and these circular references make my head hurt.

I don't have any unload cleanup for DOM2 as the only circular memory
leak browser I know of is IE.

I could add the IE unload listener cleanup just to be cautious.

[snip]

> > // BEGIN IE event model
> > else if (LIB.isHostMethod(global, 'attachEvent') &&
> > LIB.isHostMethod(anElement, 'attachEvent')
> > // uncomment next line if using
> > // the try-catch below
> > //&& global.setTimeout
> > ) {
>
> > // IE leaks memory and want to cleanup
> > // before when the document unloads
> > var unloadListeners = [];
>
> > LIB.addListener = function(element, eventType,
> > handler, options) {
> > var listener = createListener(element, 'on'+eventType,
> > handler, options);
>
> > if (eventType == 'unload') {
>
> Shouldn't this deal only with unload listeners on the window object?

Yes.

Since this library can't deal with multiple windows I could change to

if (eventType == 'unload' && element == global) {

> Are there even distinct unload events for DOM objects (e.g. body) or
> are they all lumped together (as with body.onload?)

I think in the standard body and frameset have them.


> If an
> implementation does make a distinction, I would assume that the
> window's unload listeners would be the last to fire.
>
> > unloadListeners[unloadListeners.length] = listener;
>
> This is a good idea and something I need to add to mine. Otherwise,
> you can't clean up unload listeners (might remove them before they
> have run.) I think I just left the window's unload listeners alone in
> my most recent version. I am not sure whether listeners attached to
> the window object can create memory leaks in IE. (?)

Given the oddities of IE, could anyone say he is sure?


> > }
> > else {
> > element.attachEvent(
> > listener.eventType,
> > listener.wrappedHandler);
> > listeners[listeners.length] = listener;
> > }
> > };
>
> > global.attachEvent(
> > 'onunload',
> > function(e) {
> > e = e || window.event;
> > var i, listener;
> > for (i=unloadListeners.length; i--; ) {
> > listener = unloadListeners[i];
> > if (listener) {
> > // Uncomment try-catch if you do not need to support
> > // old browsers or some modern cell phones.
> > // The try-catch will ensure all handlers run.
> > // Also uncomment test for global.setTimeout above
> > //try {
> > listener.wrappedHandler(e);
>
> Shouldn't you call these?

When they are wrapped the "this" object is bound. The wrapping is like
"partial application".

[snip]

> > LIB.addDomReadyListener = function(handler, options) {
> > var options = options || {};
> > readyListeners[readyListeners.length] =
> > wrapHandler(
> > options.docNode || LIB.getDocumentElement(),
>
> This looks wrong. Should be:
>
> options.docNode || global.document

It is very wrong.

> However, as with my version, you only have one array for document
> ready listeners (and one ready flag), so you cannot support multiple
> documents here.

Indeed. I will also remove the options.docNode part.

I will have

wrapHandler(global.document, ...)


> > handler,
> > options);
> > }
>
> > var bReady = false;
> > var handleDomReady = function() {
> > if (!bReady) {
> > bReady = true;
> > for (var i = readyListeners.length; i--; ) {
> > readyListeners[i]();
> > }
> > }
> > };
> > LIB.fireDomReady = handleDomReady;
>
> I wish we could agree on the naming of the various methods (e.g.
> getDocumentElement vs. getHtmlElement

getHtmlElement would be ok if you never wanted to have the same API
with XML documents. I think that is why I use getDocumentElement.

> fireDomReady vs. whatever I
> called that, etc.)

"fire" is just so action packed I can't resist.


> > LIB.addListener(global, 'DOMContentLoaded', handleDomReady);
> > LIB.addListener(global, 'load', handleDomReady);
> > }
>
> > // clean up for memory leaks
> > // no known problems
>
> There could be a problem if an application attached an event to
> whatever anElement references. Your wrapped function's scope would
> include anElement. Of course, the cleanup code should clear things up
> on unload, but shouldn't be counted on. To this end, I made my
> "attachedListeners" array, as well as an array that stores contexts
> for each listener, properties of the global API object.
>
> That reminds me, both versions will grow infinitely long arrays if
> listeners are attached and detached repeatedly. A good example of a
> module that does this is drag and drop, which temporarily attaches
> mouseup, mousemove and keyup listeners (for keyboard-controlled drag
> operations) to the document object. Each time an element is dragged,
> the "cleanup queue" gets a little bigger. Adding a splice operation
> to prevent that is at the top of my list.

Before firing the event listeners, it is necessary to make a copy of
them and iterate over the copy. This is because if one of the
listeners removes itself then the listener that was to be next is
skipped. Well that is if you are iterating forward through the list.
If you are iterating backwards through the list other problems can
occur. A copy is essential. Then you need to decide if a listener that
hasn't run is removed by another, does the removed listener run or
not.

Thanks for the detailed read through.

One thing I realized about my Safari workaround is that by having it
on the documentElement, if a click listener on the window did prevent
default I wouldn't catch that. So I have moved the workaround up to
the window level and do something like the IE unload listener
workaround keeping the click and dblclick listeners in an array and
looping over them when they need to run. This is still ok because at
least these aren't the mousemove events that need to run so
repetitively.

I'll do another couple passes through the code to try and get the last
details and then I think it will be ready.

One thing I have been thinking about changing is the use of internal
references to the libraries own functions. You frequently have
something like

function isHostMethod(){}
API.isHostMethod = isHostMethod

and then in your library code directly use "isHostMethod". I am
thinking about using the public API.isHostMethod even internally
because if a developer wants to wrap it, then I want that to affect
the libraries use also. This makes the library API extensible for
"plugins". Why they are called "plugins" I have no idea. This could
get into a whole debate about efficiency and early binding vs
flexibility and late binding. JavaScript itself is a late binding
language and I buy into the concept that the performance hit is worth
it. It is very minor anyway.

Peter

David Mark

unread,
Feb 18, 2008, 12:41:57 AM2/18/08
to
On Feb 17, 11:44 pm, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 17, 5:37 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 17, 2:19 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > > David, I looked at some of your event code and noticed that you don't
> > > use DOMContentLoaded with IE. I think that if IE gets their act
> > > together they will probably have an 'onDOMContentLoaded' event so I
> > > have used it below.
>
> > Do you mean I don't use it with attachEvent?
>
> It looks like you do not. I see three in your code
>
> docNode.addEventListener('DOMContentLoaded', documentReadyListener,
> false);
> global.addEventListener('load', documentReadyListener, false);
> global.attachEvent('onload', documentReadyListener);
>
> > That sounds like an
> > oversight.  However, I am hopeful that IE8 will implement
> > addEventListener
>
> I don't think they will. It is in their best interest to make sure
> browsers are not interchangeable commodities. They have corporate
> environments with intranets for IE only locked in.

Let's hope they at least fix get/setAttribute.

>
> > > (function() {
>
> > >   if (Function.prototype.apply &&
> > >       LIB.getAnElement) {
>
> > >       var global = this,
> > >           anElement = LIB.getAnElement(),
> > >           listeners = [];
>
> > >       var wrapHandler = function(element, handler, options) {
> > >         options = options || {};
> > >         var thisObj = options.thisObj || element;
> > >         return function(e) {
> > >           return handler.apply(thisObj, [e || global.event]);
> > >         };
>
> > Why not use call here?
>
> Apply is older and works so there is I figured there was no need to

Older? IIRC, they were both introduced in JS 1.3.

If your array that holds references to the elements with attached
listeners shares a closure with the wrapped listener functions, you
will create a new circular reference every time.

>
> I don't have any unload cleanup for DOM2 as the only circular memory
> leak browser I know of is IE.

Yes, but apply this to the IE branch, which also stores element
references (two references each as I recall, one to keep track of the
listeners per element and one for cleanup.)

>
> I could add the IE unload listener cleanup just to be cautious.

That wasn't what I was getting at. I don't do the unload cleanup
unless attachEvent or DOM0 is in use. Attaching unload listeners is
best avoided when possible (disables fast history navigation in most
browsers.)

>
> [snip]
>
> > >     // BEGIN IE event model
> > >     else if (LIB.isHostMethod(global, 'attachEvent') &&
> > >              LIB.isHostMethod(anElement, 'attachEvent')
> > >              // uncomment next line if using
> > >              // the try-catch below
> > >              //&& global.setTimeout
> > >              ) {
>
> > >       // IE leaks memory and want to cleanup
> > >       // before when the document unloads
> > >       var unloadListeners = [];
>
> > >       LIB.addListener = function(element, eventType,
> > >                                                handler, options) {
> > >         var listener = createListener(element, 'on'+eventType,
> > >                                                 handler, options);
>
> > >         if (eventType == 'unload') {
>
> > Shouldn't this deal only with unload listeners on the window object?
>
> Yes.
>
> Since this library can't deal with multiple windows I could change to
>
> if (eventType == 'unload' && element == global) {

That is similar to what I did for this case.

>
> > Are there even distinct unload events for DOM objects (e.g. body) or
> > are they all lumped together (as with body.onload?)
>
> I think in the standard body and frameset have them.

But aren't they just mapped to the window unload event? An onload
attribute on the body can be overwritten by assigning something to
window.onload. Is this not the same for unload?

>
> > If an
> > implementation does make a distinction, I would assume that the
> > window's unload listeners would be the last to fire.
>
> > >           unloadListeners[unloadListeners.length] = listener;
>
> > This is a good idea and something I need to add to mine.  Otherwise,
> > you can't clean up unload listeners (might remove them before they
> > have run.)  I think I just left the window's unload listeners alone in
> > my most recent version.  I am not sure whether listeners attached to
> > the window object can create memory leaks in IE. (?)
>
> Given the oddities of IE, could anyone say he is sure?

Maybe Bill Gates can chime in on this. I have always heard that
references to DOM objects must be involved and the window object is
clearly not a DOM object. Regardless, I avoid having references to
the window (global) object inside closures.

>
> > >         }
> > >         else {
> > >           element.attachEvent(
> > >             listener.eventType,
> > >             listener.wrappedHandler);
> > >           listeners[listeners.length] = listener;
> > >         }
> > >       };
>
> > >       global.attachEvent(
> > >         'onunload',
> > >         function(e) {
> > >           e = e || window.event;
> > >           var i, listener;
> > >           for (i=unloadListeners.length; i--; ) {
> > >             listener = unloadListeners[i];
> > >             if (listener) {
> > >               // Uncomment try-catch if you do not need to support
> > >               // old browsers or some modern cell phones.
> > >               // The try-catch will ensure all handlers run.
> > >               // Also uncomment test for global.setTimeout above
> > >               //try {
> > >                 listener.wrappedHandler(e);
>
> > Shouldn't you call these?
>
> When they are wrapped the "this" object is bound. The wrapping is like
> "partial application".

Oops. I missed that these were "wrapped" listener functions.

>
> [snip]
>
> > >       LIB.addDomReadyListener = function(handler, options) {
> > >         var options = options || {};
> > >         readyListeners[readyListeners.length] =
> > >           wrapHandler(
> > >             options.docNode || LIB.getDocumentElement(),
>
> > This looks wrong.  Should be:
>
> > options.docNode || global.document
>
> It is very wrong.

I thought I was seeing things at first.

>
> > However, as with my version, you only have one array for document
> > ready listeners (and one ready flag), so you cannot support multiple
> > documents here.
>
> Indeed. I will also remove the options.docNode part.
>
> I will have
>
> wrapHandler(global.document, ...)
>
> > >             handler,
> > >             options);
> > >       }
>
> > >       var bReady = false;
> > >       var handleDomReady = function() {
> > >         if (!bReady) {
> > >           bReady = true;
> > >           for (var i = readyListeners.length; i--; ) {
> > >             readyListeners[i]();
> > >           }
> > >         }
> > >       };
> > >       LIB.fireDomReady = handleDomReady;
>
> > I wish we could agree on the naming of the various methods (e.g.
> > getDocumentElement vs. getHtmlElement
>
> getHtmlElement would be ok if you never wanted to have the same API
> with XML documents. I think that is why I use getDocumentElement.

Makes sense. I do support XML elements, but clearly getHtmlElement
has no part in that. I figured that callers would reference the
documentElement property directly for those. Remember, there are
alternate versions of getDocumentElement in CWR (at least I think they
made it into the repository.) The alternates use gEBTN or the all
property to find the HTML element.

>
> > fireDomReady vs. whatever I
> > called that, etc.)
>
> "fire" is just so action packed I can't resist.

It appears I called my variation "documentReadyListener." It really
isn't an action at all (at least not in this example.) Where it is
(optionally) called directly (from the tail end of the markup), the
"fire" prefix makes more sense.

>
> > >       LIB.addListener(global, 'DOMContentLoaded', handleDomReady);
> > >       LIB.addListener(global, 'load', handleDomReady);
> > >     }
>
> > >     // clean up for memory leaks
> > >     // no known problems
>
> > There could be a problem if an application attached an event to
> > whatever anElement references.  Your wrapped function's scope would
> > include anElement.  Of course, the cleanup code should clear things up
> > on unload, but shouldn't be counted on.  To this end, I made my
> > "attachedListeners" array, as well as an array that stores contexts
> > for each listener, properties of the global API object.
>
> > That reminds me, both versions will grow infinitely long arrays if
> > listeners are attached and detached repeatedly.  A good example of a
> > module that does this is drag and drop, which temporarily attaches
> > mouseup, mousemove and keyup listeners (for keyboard-controlled drag
> > operations) to the document object.  Each time an element is dragged,
> > the "cleanup queue" gets a little bigger.  Adding a splice operation
> > to prevent that is at the top of my list.
>
> Before firing the event listeners, it is necessary to make a copy of
> them and iterate over the copy. This is because if one of the

Is this in reference to the previously quoted bit? If so, you lost
me.

> listeners removes itself then the listener that was to be next is
> skipped. Well that is if you are iterating forward through the list.
> If you are iterating backwards through the list other problems can
> occur. A copy is essential. Then you need to decide if a listener that
> hasn't run is removed by another, does the removed listener run or
> not.

I don't think it is a good idea for listeners to remove themselves
during an event. In fact, I seem to remember some odd results with
code like that (can't remember the details, but I think it involved
NN6.2.) Nevertheless, my event scheme doesn't have this problem. At
first glance, I didn't think yours did either. You attach your
"wrapped" listeners as normal right (other than unload and DOM ready?)

>
> Thanks for the detailed read through.
>
> One thing I realized about my Safari workaround is that by having it
> on the documentElement, if a click listener on the window did prevent
> default I wouldn't catch that. So I have moved the workaround up to
> the window level and do something like the IE unload listener
> workaround keeping the click and dblclick listeners in an array and
> looping over them when they need to run. This is still ok because at
> least these aren't the mousemove events that need to run so
> repetitively.

I would be careful about assuming anything about the window object.
Can it really be expected to receive click events in all (or even
most) implementations?

>
> I'll do another couple passes through the code to try and get the last
> details and then I think it will be ready.
>
> One thing I have been thinking about changing is the use of internal
> references to the libraries own functions. You frequently have
> something like
>
> function isHostMethod(){}
> API.isHostMethod = isHostMethod
>
> and then in your library code directly use "isHostMethod". I am
> thinking about using the public API.isHostMethod even internally

I wouldn't do that for numerous reasons (performance for one.)

> because if a developer wants to wrap it, then I want that to affect

I have an AOP-like debugger that wraps the methods of the API object.
I like that it only reports on calls from the outside.

> the libraries use also. This makes the library API extensible for

It still could be extensible in this way, but it would be a less
flexible (and safer IMO) extensibility. Adding custom code inside the
library's closure to augment internal functions would allow what you
want (my higher-level modules do this repeatedly, see positionElement,
sizeElement, showElement, setElementHtml, changeImage, etc.) That is
the difference between a new module and an add-on (or plug-in or
whatever.)

> "plugins". Why they are called "plugins" I have no idea. This could
> get into a whole debate about efficiency and early binding vs
> flexibility and late binding. JavaScript itself is a late binding
> language and I buy into the concept that the performance hit is worth
> it. It is very minor anyway.

I would avoid using LIB.* or API.* or whatever in the internal code.
Doing so also makes the code less portable.

Peter Michaux

unread,
Feb 18, 2008, 1:31:51 AM2/18/08
to
On Feb 17, 9:41 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 17, 11:44 pm, Peter Michaux <petermich...@gmail.com> wrote:
> > On Feb 17, 5:37 pm, David Mark <dmark.cins...@gmail.com> wrote:
> > > On Feb 17, 2:19 pm, Peter Michaux <petermich...@gmail.com> wrote:

> > Apply is older and works so there is I figured there was no need to
>
> Older? IIRC, they were both introduced in JS 1.3.

and JScript 5.5 according to Thomas Lahn's page

<URL:http://pointedears.de/scripts/es-matrix/>

I really thought there was one browser I encountered where it had
apply and not call. Anyway apply is what I need in this case.

[snip]

> > > > LIB.addListener = function(element, eventType,
> > > > handler, options) {
> > > > var listener = createListener(element, eventType,
> > > > handler, options);
> > > > element.addEventListener(
> > > > listener.eventType,
> > > > listener.wrappedHandler,
> > > > false);
> > > > listeners[listeners.length] = listener;
>
> > > Make sure that the listeners array does not end up sharing a closure
> > > with this code (as it does here.) I assume that unload cleanup logic
> > > will follow at some point, but AIUI, such code doesn't do a bit of
> > > good for elements that have been removed from their documents (e.g. by
> > > innerHTML replacement.) IIRC, you covered this in your FORK library.
>
> > I don't really follow and these circular references make my head hurt.
>
> If your array that holds references to the elements with attached
> listeners shares a closure with the wrapped listener functions, you
> will create a new circular reference every time.

But for IE onunload I'm using detachEvent so the element no longer has
a reference to the functions involved. That should take care of it.

> > I don't have any unload cleanup for DOM2 as the only circular memory
> > leak browser I know of is IE.
>
> Yes, but apply this to the IE branch,

I use detachEvent on all the listeners in the IE branch.

> which also stores element
> references (two references each as I recall, one to keep track of the
> listeners per element and one for cleanup.)

As long as detachEvent is used, the problem should be solved
regardless of the number of places I have references to the the
handlers and wrapped handlers and if those have the element in the
closure. Just one break in the circle is enough, I thought.

[snip]

> > > Are there even distinct unload events for DOM objects (e.g. body) or
> > > are they all lumped together (as with body.onload?)
>
> > I think in the standard body and frameset have them.
>
> But aren't they just mapped to the window unload event? An onload
> attribute on the body can be overwritten by assigning something to
> window.onload. Is this not the same for unload?

I don't know.

[snip]


> > listeners removes itself then the listener that was to be next is
> > skipped. Well that is if you are iterating forward through the list.
> > If you are iterating backwards through the list other problems can
> > occur. A copy is essential. Then you need to decide if a listener that
> > hasn't run is removed by another, does the removed listener run or
> > not.
>
> I don't think it is a good idea for listeners to remove themselves
> during an event.

That is what happens at the end of a drag operation. When the mouseup
event occur both the mousemove and mouseup listeners are removed. At
least that is one way to do it.

[snip]

> I would be careful about assuming anything about the window object.
> Can it really be expected to receive click events in all (or even
> most) implementations?

Before using the Safari workaround I check that

typeof global.onclick == 'object'

before anything has been assigned to global.onclick. When a function
is assigned the typeof value is 'function'. That is true in at least
safari so it is a safe bet that the event will bubble up to the
window.

> I would avoid using LIB.* or API.* or whatever in the internal code.
> Doing so also makes the code less portable.

Portable to where?

Peter

David Mark

unread,
Feb 18, 2008, 1:55:20 AM2/18/08
to

From what I have heard, that doesn't help for elements that have been
removed from the document. I don't understand why it wouldn't help,
but it seems the justification for the "clean" functions that are
called when innerHTML replaces elements. IIRC, your FORK library does
that. Perhaps in this case the memory is leaked as soon as the
element is replaced? I really need to run some tests to confirm this
theory.

Regardless, I just don't think it is good practice to create circular
references involving DOM elements in the library's closure. I see the
unload cleanup as strictly a safeguard for applications that create
such references in closures outside of the library.

>
> > > I don't have any unload cleanup for DOM2 as the only circular memory
> > > leak browser I know of is IE.
>
> > Yes, but apply this to the IE branch,
>
> I use detachEvent on all the listeners in the IE branch.
>
> > which also stores element
> > references (two references each as I recall, one to keep track of the
> > listeners per element and one for cleanup.)
>
> As long as detachEvent is used, the problem should be solved
> regardless of the number of places I have references to the the
> handlers and wrapped handlers and if those have the element in the
> closure. Just one break in the circle is enough, I thought.

One break is enough, assuming it comes before the memory has been
irretrievably lost.

>
> [snip]
>
> > > > Are there even distinct unload events for DOM objects (e.g. body) or
> > > > are they all lumped together (as with body.onload?)
>
> > > I think in the standard body and frameset have them.
>
> > But aren't they just mapped to the window unload event?  An onload
> > attribute on the body can be overwritten by assigning something to
> > window.onload.  Is this not the same for unload?
>
> I don't know.

I don't know either, but I suspect it is the case. Again, I need to
run some tests so as not to rely on guesswork.

>
> [snip]
>
> > > listeners removes itself then the listener that was to be next is
> > > skipped. Well that is if you are iterating forward through the list.
> > > If you are iterating backwards through the list other problems can
> > > occur. A copy is essential. Then you need to decide if a listener that
> > > hasn't run is removed by another, does the removed listener run or
> > > not.
>
> > I don't think it is a good idea for listeners to remove themselves
> > during an event.
>
> That is what happens at the end of a drag operation. When the mouseup
> event occur both the mousemove and mouseup listeners are removed. At
> least that is one way to do it.

Good point. But getting back to the original issue, the "cleanup
queue" grows larger on each drag operation. This should be prevented
by splicing the array when listeners are removed. You should keep
this in mind when you add the code to remove listeners.

>
> [snip]
>
> > I would be careful about assuming anything about the window object.
> > Can it really be expected to receive click events in all (or even
> > most) implementations?
>
> Before using the Safari workaround I check that
>
> typeof global.onclick == 'object'

A null value I assume. This seems to be the norm in all but FF.

>
> before anything has been assigned to global.onclick. When a function
> is assigned the typeof value is 'function'. That is true in at least
> safari so it is a safe bet that the event will bubble up to the
> window.

Makes sense.

>
> > I would avoid using LIB.* or API.* or whatever in the internal code.
> > Doing so also makes the code less portable.
>
> Portable to where?
>

To other projects that do not involve the library.

Peter Michaux

unread,
Feb 18, 2008, 2:05:35 AM2/18/08
to
On Feb 17, 10:55 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 18, 1:31 am, Peter Michaux <petermich...@gmail.com> wrote:

[snip]

> > But for IE onunload I'm using detachEvent so the element no longer has
> > a reference to the functions involved. That should take care of it.
>
> From what I have heard, that doesn't help for elements that have been
> removed from the document. I don't understand why it wouldn't help,
> but it seems the justification for the "clean" functions that are
> called when innerHTML replaces elements. IIRC, your FORK library does
> that.

Yes. When I remove or replace elements in the DOM I call the
FORK.purgeElement with the deep flag to remove all listeners.

> Perhaps in this case the memory is leaked as soon as the
> element is replaced? I really need to run some tests to confirm this
> theory.
>
> Regardless, I just don't think it is good practice to create circular
> references involving DOM elements in the library's closure.

There is no choice when fixing the execution "this" object of a
handler. The handler has to have the element in it's closure.


> > > I don't think it is a good idea for listeners to remove themselves
> > > during an event.
>
> > That is what happens at the end of a drag operation. When the mouseup
> > event occur both the mousemove and mouseup listeners are removed. At
> > least that is one way to do it.
>
> Good point. But getting back to the original issue, the "cleanup
> queue" grows larger on each drag operation. This should be prevented
> by splicing the array when listeners are removed. You should keep
> this in mind when you add the code to remove listeners.

It just needs to be done with care. Making a copy of the list of
listeners is how I did it in FORK. The DOM does this also. It is
necessary to determine the possible set of handlers before calling
them. If not and one handler is adding itself to the end of the list
of handlers, it could be an infinite loop.

> > > I would be careful about assuming anything about the window object.
> > > Can it really be expected to receive click events in all (or even
> > > most) implementations?
>
> > Before using the Safari workaround I check that
>
> > typeof global.onclick == 'object'

The above is true in Safari before any function has been assigned to
global.onclick.

> A null value I assume. This seems to be the norm in all but FF.
>
>
>
> > before anything has been assigned to global.onclick. When a function
> > is assigned the typeof value is 'function'. That is true in at least
> > safari so it is a safe bet that the event will bubble up to the
> > window.
>
> Makes sense.


Peter

David Mark

unread,
Feb 18, 2008, 2:25:59 AM2/18/08
to
On Feb 18, 2:05 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 17, 10:55 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > On Feb 18, 1:31 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> [snip]
>
> > > But for IE onunload I'm using detachEvent so the element no longer has
> > > a reference to the functions involved. That should take care of it.
>
> > From what I have heard, that doesn't help for elements that have been
> > removed from the document.  I don't understand why it wouldn't help,
> > but it seems the justification for the "clean" functions that are
> > called when innerHTML replaces elements.  IIRC, your FORK library does
> > that.
>
> Yes. When I remove or replace elements in the DOM I call the
> FORK.purgeElement with the deep flag to remove all listeners.

But why if they will ultimately be purged on unload? Such practices
are what have led me to believe that the unload purge will be
ineffective in some cases. As mentioned, it would be a better idea to
test this directly than to rely on such deductions.

>
> > Perhaps in this case the memory is leaked as soon as the
> > element is replaced?  I really need to run some tests to confirm this
> > theory.
>
> > Regardless, I just don't think it is good practice to create circular
> > references involving DOM elements in the library's closure.
>
> There is no choice when fixing the execution "this" object of a
> handler. The handler has to have the element in it's closure.

Sure there is. Store the context references outside of the closure
(e.g. in an array that is a property of the API object.) The function
that adds a new reference returns the length of the array minus one as
a handle for the wrapped function to use. That's how I did it anyway.

>
> > > > I don't think it is a good idea for listeners to remove themselves
> > > > during an event.
>
> > > That is what happens at the end of a drag operation. When the mouseup
> > > event occur both the mousemove and mouseup listeners are removed. At
> > > least that is one way to do it.
>
> > Good point.  But getting back to the original issue, the "cleanup
> > queue" grows larger on each drag operation.  This should be prevented
> > by splicing the array when listeners are removed.  You should keep
> > this in mind when you add the code to remove listeners.
>
> It just needs to be done with care. Making a copy of the list of
> listeners is how I did it in FORK. The DOM does this also. It is
> necessary to determine the possible set of handlers before calling
> them. If not and one handler is adding itself to the end of the list
> of handlers, it could be an infinite loop.

Are you speaking specifically about the unload event? I don't see
where else in your code (other than the DOM Ready, which really
doesn't need an exposed detach function) that you call listeners
directly (i.e. the wrapped listener functions are attached and
subsequently called by the DOM.) I did the same thing for DOM0 using
what I think is a unique strategy. I will elaborate on that when we
get to that topic.

Peter Michaux

unread,
Feb 18, 2008, 3:03:30 AM2/18/08
to
On Feb 17, 11:25 pm, David Mark <dmark.cins...@gmail.com> wrote:
> On Feb 18, 2:05 am, Peter Michaux <petermich...@gmail.com> wrote:
>
>
>
> > On Feb 17, 10:55 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > On Feb 18, 1:31 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > [snip]
>
> > > > But for IE onunload I'm using detachEvent so the element no longer has
> > > > a reference to the functions involved. That should take care of it.
>
> > > From what I have heard, that doesn't help for elements that have been
> > > removed from the document. I don't understand why it wouldn't help,
> > > but it seems the justification for the "clean" functions that are
> > > called when innerHTML replaces elements. IIRC, your FORK library does
> > > that.
>
> > Yes. When I remove or replace elements in the DOM I call the
> > FORK.purgeElement with the deep flag to remove all listeners.
>
> But why if they will ultimately be purged on unload?

That is an excellent question. I would guess you could leave it until
onload and that being part of the DOM or not would not affect
collectablility. The reason I use the purgeElement is it is better to
clean up as soon as possible so that huge chunks of DOM can be garbage
collected before onunload. If the page is very long lived and the DOM
is heavily modified frequently you wouldn't want to let the garbage
build into a huge pile until onunload.


> > There is no choice when fixing the execution "this" object of a
> > handler. The handler has to have the element in it's closure.
>
> Sure there is. Store the context references outside of the closure
> (e.g. in an array that is a property of the API object.) The function
> that adds a new reference returns the length of the array minus one as
> a handle for the wrapped function to use. That's how I did it anyway.

That is very indirect and sounds like there is no real performance
hit. By avoiding creating the circular reference you wouldn't need the
purgeElement as I discussed above for the libraries behavior. However,
I think I would still use purgElement because I may have been sloppy
in my application code. I don't want to have to think that hard and
take such special care to avoid circular leaks when I'm writing
application code.


> > > > > I don't think it is a good idea for listeners to remove themselves
> > > > > during an event.
>
> > > > That is what happens at the end of a drag operation. When the mouseup
> > > > event occur both the mousemove and mouseup listeners are removed. At
> > > > least that is one way to do it.
>
> > > Good point. But getting back to the original issue, the "cleanup
> > > queue" grows larger on each drag operation. This should be prevented
> > > by splicing the array when listeners are removed. You should keep
> > > this in mind when you add the code to remove listeners.
>
> > It just needs to be done with care. Making a copy of the list of
> > listeners is how I did it in FORK. The DOM does this also. It is
> > necessary to determine the possible set of handlers before calling
> > them. If not and one handler is adding itself to the end of the list
> > of handlers, it could be an infinite loop.
>
> Are you speaking specifically about the unload event?

Yes

> I don't see
> where else in your code (other than the DOM Ready, which really
> doesn't need an exposed detach function) that you call listeners
> directly (i.e. the wrapped listener functions are attached and
> subsequently called by the DOM.)

For what I posted here that is the only place.

> I did the same thing for DOM0 using
> what I think is a unique strategy. I will elaborate on that when we
> get to that topic.

Here is where I did this in Fork using slice.

<URL: http://dev.forkjavascript.org/trac/browser/trunk/public/javascripts/fork/event.js#L113>

I need to add this same functionality to my code in this thread.

Peter

Peter Michaux

unread,
Feb 18, 2008, 3:11:15 AM2/18/08
to

Wait a second...

API is global. Every closure in your library has all the global
variables in it's closure. So the wrapped function has API in its
closure and API has a reference to the element to which the wrapped
function is attached. Isn't the circle still there but just bigger?

Peter

David Mark

unread,
Feb 18, 2008, 3:23:53 AM2/18/08
to
On Feb 18, 3:03 am, Peter Michaux <petermich...@gmail.com> wrote:
> On Feb 17, 11:25 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
>
>
>
>
> > On Feb 18, 2:05 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > On Feb 17, 10:55 pm, David Mark <dmark.cins...@gmail.com> wrote:
>
> > > > On Feb 18, 1:31 am, Peter Michaux <petermich...@gmail.com> wrote:
>
> > > [snip]
>
> > > > > But for IE onunload I'm using detachEvent so the element no longer has
> > > > > a reference to the functions involved. That should take care of it.
>
> > > > From what I have heard, that doesn't help for elements that have been
> > > > removed from the document.  I don't understand why it wouldn't help,
> > > > but it seems the justification for the "clean" functions that are
> > > > called when innerHTML replaces elements.  IIRC, your FORK library does
> > > > that.
>
> > > Yes. When I remove or replace elements in the DOM I call the
> > > FORK.purgeElement with the deep flag to remove all listeners.
>
> > But why if they will ultimately be purged on unload?
>
> That is an excellent question. I would guess you could leave it until
> onload and that being part of the DOM or not would not affect
> collectablility. The reason I use the purgeElement is it is better to
> clean up as soon as possible so that huge chunks of DOM can be garbage
> collected before onunload. If the page is very long lived and the DOM
> is heavily modified frequently you wouldn't want to let the garbage
> build into a huge pile until onunload.

That makes sense.

>
> > > There is no choice when fixing the execution "this" object of a
> > > handler. The handler has to have the element in it's closure.
>
> > Sure there is.  Store the context references outside of the closure
> > (e.g. in an array that is a property of the API object.)  The function
> > that adds a new reference returns the length of the array minus one as
> > a handle for the wrapped function to use.  That's how I did it anyway.
>
> That is very indirect and sounds like there is no real performance
> hit. By avoiding creating the circular reference you wouldn't need the
> purgeElement as I discussed above for the libraries behavior. However,

Yes you would still need it as there is no telling how many circular
references may have been created *outside* of the library (e.g. by the
calling code.)

> I think I would still use purgElement because I may have been sloppy
> in my application code. I don't want to have to think that hard and

Right.

> take such special care to avoid circular leaks when I'm writing
> application code.

That's what I was getting at.

[snip]

David Mark

unread,
Feb 18, 2008, 3:51:17 AM2/18/08
to

I assume you mean that properties of the Global Object are within the
scope of the closures (which is expected of course.)

> closure and API has a reference to the element to which the wrapped
> function is attached. Isn't the circle still there but just bigger?
>

If that were the case then there would be no way to avoid these sorts
of circular references as IE makes every element with an ID a property
of the Global Object and clearly every bit of code in a script can see
them. Thankfully, it is not the case.

I am sure that Richard could explain this better, but AIUI:

el => wrapped listener => Variable Object => listeners array => object
in the array => el

That pattern will clearly leak memory in IE unless the chain of
references is broken at some point (typically on unload or through a
listener purge when an element is replaced in the document.)

Your theory seems to indicate that you could swap the above "Variable
Object" for "Global Object", but that doesn't work as the wrapped
listener (a nested function created in the API's closure) does not
*reference* the Global Object. All properties of the Global Object
are clearly within its scope, but that is not an issue.

AKS

unread,
Feb 23, 2008, 7:11:46 AM2/23/08
to

Peter Michaux wrote:
> I'm writing a blog article about building a decent tabbed pane widget
> for a web page on the general web (anyone with a browser and internet
> connection has access)...

I have read the article today. Thank you, Peter - you have done huge
work!
I've noticed that new regular expressions have to be created every
time in functions named -hasClass- and -removeClass-. Why not to
create another free variable, as in the case of function named -
compile-, which does use -cache- variable?

var regExpCache = {};

LIB.hasClass = function(el, className) {
var re = regExpCache[className] || (regExpCache[className] =
new RegExp('(^|\\s+)' + className + '(\\s+|$)'));
return re.test(el.className);
};

Peter Michaux

unread,
Feb 23, 2008, 3:58:52 PM2/23/08
to
On Feb 23, 4:11 am, AKS <aksus...@yandex.ru> wrote:
> Peter Michaux wrote:
> > I'm writing a blog article about building a decent tabbed pane widget
> > for a web page on the general web (anyone with a browser and internet
> > connection has access)...
>
> I have read the article today. Thank you, Peter - you have done huge
> work!
> I've noticed that new regular expressions have to be created every
> time in functions named -hasClass- and -removeClass-. Why not to
> create another free variable, as in the case of function named -
> compile-, which does use -cache- variable?

I did think about doing something like this after I finished the
article but I wanted to run some performance tests to see what kind of
difference this would make. If the hasClass function is called many
times with the same class name (10000 times) then using the cache
makes a difference (30-50% time savings)

What offended me more in my code is the fact that I have the logic for
building the regular expression in three places. Using a cache allows
me to aggregate the regexp creation logic into one function without a
real performance hit.

Because I had a global flag on the className RegExp I ran into a
problem when I made this conversion because the regexp is being used
with test() multiple times. The problem is discussed in the following
post to the group.
<URL: http://groups.google.com/group/comp.lang.javascript/msg/a9523f0b06c4bdcc>


Thanks for taking a close look at the code and the feedback.


Peter

0 new messages