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

P

Peter Michaux

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>

<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 == '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 != 'undefined' && f(a)) {
rs[rs.length] = a;
}
}
return rs;
};
LIB.forEach = function(a, f) {
for (var i=0, ilen=a.length; i<ilen; i++) {
if (typeof a != 'undefined') {
f(a);
}
}
};

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

// 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() {
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) {
return cache;
}

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;' +
'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 = f;
return f;
}

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

}

})();

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

(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);
}
};

}

})();
 
D

David Mark

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.
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>

  <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 == '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.
    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.)
        // 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') &&

Use isRealObjectProperty. I think the distinction is important. Each
of the three feature test functions serves a specific purpose.
          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">');

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.
    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;

}

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.
// 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 != 'undefined' && f(a)) {
        rs[rs.length] = a;
      }
  }
  return rs;};

LIB.forEach = function(a, f) {
  for (var i=0, ilen=a.length; i<ilen; i++) {
    if (typeof a != 'undefined') {
      f(a);
    }
  }

};

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

// 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;
  }

};


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.

    var cache = {};
    var compile = function(s) {
      if (cache) {
        return cache;
      }

      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;' +
               '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 + '}');


What is this about?
      f = new Function('d', f);
      cache = f;
      return f;
    }

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

  }

})();

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

(function() {

  if (typeof LIB.getAnElement != 'undefined' &&


Testing (LIB.getAnElement) is enough here.
 
D

dhtml

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.

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

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]

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.

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.

The library for the example only has the code necessary to make the
example run.
That is true.
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
 
P

Peter Michaux

[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.

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.

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.



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?

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.

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.


Looks weird but it is correct.

What is this about?

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


Peter
 
D

dhtml

On Feb 13, 11:34 pm, Peter Michaux <[email protected]> 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
 
D

David Mark

[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.

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.
I typed some more about this farther down the reply.









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.







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

Yes. This happens to me all the time. That's why I use JSLint.
first-class nature of functions.


Good idea.







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.

No problem.

elPane.style.display = '';
I could query for all the class="sections" elements and add a
"disabledTabbedPane" class.


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.
And here I think it makes it clearer and actually more future proof.



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]) { 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.
Looks weird but it is correct.

I haven't encountered that term before.
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.
 
D

David Mark


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.
 
D

dhtml

[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?
 
D

David Mark

[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.
You never know with host objects.
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'tbrowser-specific.  I think in this example

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.
 
H

Henry

On Feb 13, 9:16 pm, David Mark <[email protected]> wrote:
if (!(isHostObject(this, 'document'))) {


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.
 
P

Peter Michaux

[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
 
P

Peter Michaux

[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
 
D

David Mark

On Feb 13, 11:34 pm, Peter Michaux <[email protected]> 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.

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.)
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.)
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.
 
D

David Mark

[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.)
 
P

Peter Michaux

On Feb 14, 12:01 am, David Mark <[email protected]> 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]
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
 
D

David Mark

On Feb 14, 12:01 am, David Mark <[email protected]> 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]
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.
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.)
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.
 
P

Peter Michaux

[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]

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

Peter
 
D

David Mark

[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]

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.
 
P

Peter Michaux

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
 
D

David Mark

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.
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]
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,091
Messages
2,570,605
Members
47,225
Latest member
DarrinWhit

Latest Threads

Top