I have to mention that “Custom HTML” is really an oxymoron. When you start adding tags that aren’t part of the official standard, then you really have an XML document that the browser attempts to interpret as HTML. Think of this as a technical exercise. Two things led me to this exercise.
New HTML 5 tags
I’ve been doing a lot of reading about HTML5 and some of the new tags that are specified (my favorite such article is here). In this article, Mark Pilgrim suggests going ahead and using the new tags now, even though browser support is limited. Most of the new tags don’t display as anything special, and are only semantic (such as the header
, footer
, article
, or nav
tags) and will render the same as a normal span
tag. And just like a span
tag, you can apply CSS to them as you wish. Except for Internet Explorer, which requires a little JavaScript tricker to make it work:
1 |
document.createElement('header'); |
John Resig goes over this trick in detail in one of his excellent blog postings.
Evented Programming
The second piece of this puzzle was an article/video that came along with the 1.4 release of jQuery, where Yehuda Katz discusses his approach to creating widgets on your web pages. He reasons we should make these widgets to function as if they were built into the browser in the first place. For example, say you’d like a tabbed control on your page. The usual approach would be to draw the individual tabs and panels as div
s or span
s, and attach code to each tab to hide and show their associated panel when clicked. Something like this:
1 2 3 4 5 6 7 8 |
<ul> <li onclick="setTab(this, 'panel1')">First Tab</li> <li onclick="setTab(this, 'panel2')">Second Tab</li> <li onclick="setTab(this, 'panel3')">Third Tab</li> </ul> <div id="panel3">Stuff in first tab</div> <div id="panel3">Stuff in second tab</div> <div id="panel3">Stuff in third tab</div> |
If a tab control was built in to the browser, you might just create a tabpanel
tag, and fill it with tab
and pane
tags. Maybe you’d then give the tabpanel
an onchange
event to do something special when a different tab was clicked. Maybe like this:
1 2 3 4 5 6 7 8 |
<tabpanel onchange="doSomething()"> <tab selected="selected">First Tab</tab> <tab>Second Tab</tab> <tab>Third Tab</tab> <pane>Stuff for first tab</pane> <pane>Stuff for second tab</pane> <pane>Stuff for third tab</pane> </tabpanel> |
A shame we don’t have those tabs. Unless we create them, taking advantage of the knowledge at the beginning on this post. Let’s start, as Yehuda might suggest, with…
How We’d Like It to Be
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <script type="text/javascript" src="js/tabs.js"></script> </head> <body> <tabpanel> <tab selected="selected">Tab 1</tab> <tab>Tab 2</tab> <tab>Tab 3</tab> <pane>Just a test pane.</pane> <pane>A second test pane.</pane> <pane>A third test pane.</pane> </tabpanel> </body> </html> |
The magic is in tabs.js
. Here’s what it has to do:
- Do the createElement trickery for Internet Explorer
- Connect click events to the
tab
elements - Have those click events display the appropriate
pane
element and trigger anychange
event attached to thetabpanel
- Give these tags approriate default styles
First, we have to make it so Internet Explorer will see our new tags:
1 2 3 4 5 |
(function() { // doing this inside a closure to avoid polluting the global namespace if(/*@cc_on!@*/0) { var els = "tabpanel,tab,pane,blink".split(","), i = els.length; while(i--)document.createElement(els[i]); } |
That funky bit inside the if
statement is a conditional compliation trigger, which will cause these lines to run only for Internet Explorer.
Next we set default styles for the new elements:
1 2 3 4 5 6 7 |
// default styles document.write('<style type="text/css">' +'tabpanel{display:inline-block;margin:0.15em;}' +'tab{cursor:pointer;border:0.15em outset buttonface;background:buttonshadow;padding:0 0.2em;margin:0;display:inline-block;border-bottom:0.15em outset buttonface}' +'tab.selected{background:buttonface;border-bottom:0.15em solid buttonface}' +'pane{display:block;border:0.15em outset buttonface;background:buttonface;border-top:0px;padding:0.5em;margin:0}' +'</style>'); |
The following addClass
and removeClass
just handle adding and removing classes from elements. Toggling a “selector” class allows us to style the selected tab differently:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var removeClass = function(el, className) { var cs = el.className.split(' '); for(var i=0;i<cs.length;i++) { if(cs[i]==className) { cs.splice(i, 1); el.className = cs.join(' '); return; } } }, addClass = function(el, className) { var cs = el.className.split(' '); for(var i=0;i<cs.length;i++) { if(cs[i]==className) { return; } } cs.push(className); el.className = cs.join(' '); }, |
Now we get to the meatiest part (the comments should explain it pretty well):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
ready = function(){ var panels = document.getElementsByTagName('tabpanel'); for(var i = 0; i<panels.length; i++) { // set up each tabpanel var tabs = panels[i].getElementsByTagName('tab'), panes = panels[i].getElementsByTagName('pane'), idx = -1; for(var j=0;j<tabs.length;j++) { if(tabs[j].getAttribute('selected')) idx = j; // get currently selected tab index removeClass(tabs[j], 'selected'); // deselect all tabs tabs[j].setAttribute('data-index', j); // set the tab's index for future use tabs[j].onclick=function(){ var tc = this.parentNode, ix = this.getAttribute('data-index'), oldix = tc.getAttribute('selectedIndex'); if(ix == oldix) return; // if the same tab, do nothing removeClass(tabs[oldix], 'selected'); panes[oldix].style.display = 'none'; // hide the old pane tc.setAttribute('selectedIndex', ix); // update selectedIndex of the tabpanel addClass(this, 'selected'); panes[ix].style.display = 'block'; // show the new pane if(tc.onchange) tc.onchange(); // fire the tabpanel's onchange event if it exists }; } // hide all panes except the "selected" one for this first rendering for(var j=0;j<panes.length;j++) panes[j].style.display = 'none'; addClass(tabs[idx], 'selected'); panes[idx].style.display='block'; // some browsers may not see an onchange attribute as a function, so we force it if(typeof panels[i].getAttribute('onchange') === 'string') { panels[i].onchange = new Function("event", panels[i].getAttribute('onchange')); } panels[i].setAttribute('selectedIndex', idx); } }; |
Finally, this last piece hooks everything up when the page loads.
1 2 3 4 5 6 |
if(document.addEventListener) { window.addEventListener("load", ready, false); } else if (document.attachEvent) { window.attachEvent("onload", ready); } })(); |
The Final Result
This is how it looks in Chrome (it’s very similar in IE7):
Clicking on the tabs shows the associated panel and triggers any onchange event on the tabpanel
element. The default styles are fairly plain (intentionally so), but just like any other element can be overridden by a style sheet or inline style declarations.
The propriety of adding custom elements to an HTML document like this may be debatable, but there are places where this technique could certainly be extremely useful.