Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- From: David Mark <dmark.cinsoft@xxxxxxxxx>
- Date: Tue, 12 Feb 2008 22:28:42 -0800 (PST)
On Feb 12, 10:26 pm, Peter Michaux <petermich...@xxxxxxxxx> 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 ***.
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.[snip]
I look forward to any suggestions.
// 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
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 *** 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
*** 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="style***" 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 style*** 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
*** 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
***. 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.
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]:[]);' +
You really shouldn't reference LIB internally (other than to populate
it) for performance and style reasons.
((!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);
};
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.
.
- Follow-Ups:
- Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- From: Peter Michaux
- Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- From: Peter Michaux
- Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- From: Peter Michaux
- Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- References:
- RFC: Building the Perfect Tabbed Pane (an tutorial article)
- From: Peter Michaux
- RFC: Building the Perfect Tabbed Pane (an tutorial article)
- Prev by Date: Re: Problem with "this" keyword in event handler of body element
- Next by Date: Re: Problem with "this" keyword in event handler of body element
- Previous by thread: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- Next by thread: Re: RFC: Building the Perfect Tabbed Pane (an tutorial article)
- Index(es):