/* tabmanager.js - provides a simple API for tabs on a webpage Copyright (C) 2004 Guido Wesdorp email johnny@debris.demon.nl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA The TabManager is a simple, fully automated tab manager, all you need to do is provide a bunch of HTML, instantiate the TabManager and use it (tell it which tabs to render). Usage (for a working example see 'test.html'): var tm = new TabManager(); // This will get all top-level (non-text) nodes from the element with id // 'tabs', each of the elements is considered a tab. They must have an // id, that will be used as the tab id, and can have a 'tabname' // attribute with the name that will be displayed (id is used as a // fallback. The HTML in the page can optionally be parsed by a function // before it is displayed. var tabsel = document.getElementById('tabsel'); tm.initialize(document, tabsel); // set up a set of static tabs, the ids passed in are the ids of the // top-level elements // add tab containing contents of element with id 'tab1' without parsing tm.addTab('tab1'); // add content of 'tab2', parse it with function 'parseTab2' tm.addTab('tab2', parseTab2); // render the tabs tm.render(); // unrender the tabs again, then render only tab1 tm.unrender(); tm.addTab('tab1'); tm.render(); Public roperties: TabManager: active_tabs an array containing the current active tabs current_active_tab_index the index (id attribute of the content element) of the tab that is currently displayed tabs a mapping (object, 'associative array') of tabid to Tab object container a reference to the element containing the tabs tabrow the element containing the tab labels (buttons) contentel the element containing the tab content TabManager: id the id of the tab, the id of the HTML node containing the tab's content is used for this name the 'tabname' attribute of the tab content node, is displayed in the tab button (label) doc the document containing the tab table tabmanager a reference to the TabManager that controls the tab template a reference to the (unparsed) template element containing the tab's content rendered a reference to the (parsed if a prepareFunction was used) template content (without the help bit), if you want to modify the contents of an already rendered tab (so *not* from the prepareFunction but rather from an event handler or something) you will need to modify this attribute use_rendered whether to re-use the rendered template contents when the tab is shown after the first time it was rendered Public methods: TabManager: initialize(doc, tabsel) initialize the tab manager, must be called after instantiation doc is a reference to the document containing the tabsel, tabsel is a node containing a set of nodes that contain tab data, each tabsel must have an id, can have an attribute 'tabname' and should contain some content render() render the current active tabs unrender() unrender the current active tabs so a new set can be created activateTab(index, reload) show a tab, index is the id attribute of the tab element and reload is passed to Tab.getContents() (if called) to control caching (getContents() is only called when the tab is shown the first time after TabManager.rendered() is called, and the 'reload' arg can be used to cache *external* data to store in the tab (so to tell getContents() we want un-cached data between TabManager.render() calls) activateTab() will not be of much interest to you if you use a simple setup, it's called when a tab button is clicked addTab(id, prepareFunction) show a tab (actually add it to the list of current tabs before calling TabManager.render()), id is the id attribute of the node containing the tab content, prepareFunction is an (optional) function to parse the tab contents before it is displayed prepareFunction will be called with as arguments a reference to the tab, a clone of the tab contents (don't ever try to change anything in HTML stored on tab itself, always modify this clone!) and the 'reload' argument mentioned in the activeTab() bit above usedTabs() get a list of ids of the current used tabs reload() reload the contents of all the tabs, call any prepareFunctions() Tab: initialize(doc, tabmanager, templateel) initialize the tab, is called from the TabManager on object instantiation (you should not have to instantiate Tab objects yourself, TabManager will do that on initialization) beforeShow() hook that will be called just before the tab is displayed afterShow() hook that will be called just when the tab is displayed beforeHide() hook that will be called just before the tab is removed from view afterHide() hook that will be called just whenn the tab is removed from view reload() reload the tabs contents, calling prepareFunction() if registered displayHelp() is called when the user presses the help button, displays an optional piece of the tab's content that is a direct child of the tab node and has a 'type' attribute set to 'help' in a pop-up window */ var global = this; global.tabmanager = new function tabmanager() { var tabmanager = this; //------------------------------------------------------------------------ // TabManager class //------------------------------------------------------------------------ function TabManager() { /* generates and manages the tabs */ }; this.TabManager = TabManager; TabManager.prototype.initialize = function(doc, tabsel) { /* load the contents of elid into the tabs */ this.doc = doc; this.active_tabs = []; this.current_active_tab_index = null; var tc = this.tabscontainer = tabsel; // put all children of tc in a list var tabs = this.tabs = {}; for (var i=0; i < tc.childNodes.length; i++) { var child = tc.childNodes[i]; if (child.nodeType != 1) { continue; }; var tab = new Tab(); var id = tab.initialize(doc, this, child); tabs[id] = tab; }; // and unrender them while (tc.hasChildNodes()) { tc.removeChild(tc.childNodes[0]); }; }; TabManager.prototype.render = function() { /* sets up the tabs */ var doc = this.doc; var container = this.tabcontainer = doc.createElement('div'); container.className = 'tabmanager-container'; var numcols = this.active_tabs.length; var tabrow = this.tabrow = this.doc.createElement('div'); tabrow.className = 'tabmanager-tabrow'; for (var i=0; i < this.active_tabs.length; i++) { var tab = this.active_tabs[i]; var tabel = doc.createElement('div'); tabel.className = 'tabmanager-tab'; var tabtext = doc.createTextNode(tab.name); tabel.appendChild(tabtext); misclib.addEventHandler(tabel, 'click', this.clickTab, this, i); // place a reference to the tab th on the tab tab.button = tabel; if (tab.helpcontent) { var helplink = doc.createElement('span'); helplink.appendChild(doc.createTextNode('\xa0?\xa0')); helplink.className = 'helplink'; tabel.appendChild(helplink); misclib.addEventHandler(helplink, 'click', tab.displayHelp, tab); }; tabrow.appendChild(tabel); }; var filler = this.doc.createElement('div'); filler.className = 'tabmanager-filler'; filler.appendChild(this.doc.createTextNode('\xa0')); tabrow.appendChild(filler); container.appendChild(tabrow); var contentel = this.contentel = doc.createElement('div'); contentel.className = 'tabmanager-content'; container.appendChild(contentel); // render the contents of the first tab into the body this.activateTab(0); this.tabscontainer.appendChild(container); }; TabManager.prototype.unrender = function() { /* remove all tabs */ for (var i=0; i < this.active_tabs.length; i++) { this.active_tabs[i].beforeHide(); }; while (this.tabscontainer.hasChildNodes()) { this.tabscontainer.removeChild(this.tabscontainer.childNodes[0]); }; this.current_active_tab = null; for (var i=0; i < this.active_tabs.length; i++) { var tab = this.active_tabs[i]; var rendered = tab.rendered; tab.rendered = null; delete rendered; tab.afterHide(); }; this.active_tabs = []; }; TabManager.prototype.clickTab = function(event, tabindex) { /* handle a tab click */ if (this.current_active_tab_index != null) { var tab = this.active_tabs[this.current_active_tab_index]; tab.beforeHide(); this.contentel.removeChild(this.contentel.firstChild); tab.afterHide(); }; this.activateTab(tabindex); }; TabManager.prototype.activateTab = function(index, reload) { this.current_active_tab_index = index; var tab = this.active_tabs[index]; // use rendered version if already available var bodycontents = (tab.rendered && tab.use_rendered) ? tab.rendered : tab.getContents(reload); tab.beforeShow(); this.contentel.appendChild(bodycontents); for (var i=0; i < this.tabrow.childNodes.length; i++) { var child = this.tabrow.childNodes[i]; if (child.className == 'tabmanager-filler') { continue; }; var classname = 'tabmanager-tab'; var islast = (i == this.tabrow.childNodes.length - 2); if (i == index && islast) { classname = 'tabmanager-tab tabmanager-last ' + 'tabmanager-selected'; } else if (islast) { classname = 'tabmanager-tab tabmanager-last'; } else if (i == index) { classname = 'tabmanager-tab tabmanager-selected'; }; child.className = classname; }; tab.afterShow(); }; TabManager.prototype.addTab = function(tabid, prepareFunction) { /* add a tab to the current layout */ var tab = this.tabs[tabid]; tab.prepareFunction = prepareFunction; this.active_tabs.push(this.tabs[tabid]); }; TabManager.prototype.usedTabs = function() { /* return a list of ids of all tabs that are currently in use */ var ids = []; for (var i=0; i < this.active_tabs.length; i++) { ids.push(this.active_tabs[i].id); }; return ids; }; TabManager.prototype.reload = function() { while (this.tabscontainer.hasChildNodes()) { this.tabscontainer.removeChild(this.tabscontainer.childNodes[0]); }; for (var i=0; i < this.active_tabs.length; i++) { this.active_tabs[i].reload(); }; this.render(); }; //------------------------------------------------------------------------ // Tab class //------------------------------------------------------------------------ function Tab() { /* manages the contents of a single tab */ }; this.Tab = Tab; Tab.prototype.initialize = function(doc, tabmanager, templateel) { this.doc = doc; this.tabmanager = tabmanager; // see if it contains a help section for (var i=0; i < templateel.childNodes.length; i++) { var child = templateel.childNodes[i]; if (child.nodeType == 1 && child.getAttribute('type') == 'help') { this.helpcontent = child; templateel.removeChild(child); }; }; // create a copy of the template and save that this.template = templateel.cloneNode(true); // cache this.rendered = null; // whether the cached version should be used this.use_rendered = true; this._parseTemplateEl(templateel); return this.id; }; Tab.prototype._parseTemplateEl = function(el) { /* get data from the template el and store it */ this.id = el.getAttribute('id'); this.name = el.getAttribute('tabname'); }; Tab.prototype.beforeShow = function() { /* will be called just before this tab is shown */ }; Tab.prototype.afterShow = function() { /* will be called just after the tab is shown */ }; Tab.prototype.beforeHide = function() { /* will be called just before the tab is hidden */ }; Tab.prototype.afterHide = function() { /* will be called just after the tab is hidden */ }; Tab.prototype.getContents = function(reload) { var content = this.template; if (this.prepareFunction) { content = content.cloneNode(true); this.rendered = content; this.prepareFunction(this, content, reload); content = this.rendered; // allow prepareFunction to mess with it }; return this.rendered; }; Tab.prototype.reload = function() { this.rendered = null; while (this.tabmanager.contentel.hasChildNodes()) { this.tabmanager.contentel.removeChild( this.tabmanager.contentel.firstChild); }; this.tabmanager.activateTab(this.tabmanager.current_active_tab_index, true); }; Tab.prototype.displayHelp = function(event) { var win = window.open('about:blank', 'win', 'width=500,height=350,scrollbars=yes,status=no,toolbar=no'); var doc = this.doc; // create the full html content for the window, then serialize and // write to the doc (Mozilla seems to not like appendChild for this // purpose) var html = doc.createElement('html'); var head = doc.createElement('head'); html.appendChild(head); var title = doc.createElement('title'); head.appendChild(title); title.appendChild(doc.createTextNode('Help text for ' + this.name)); // create link elements var stylesheets = []; if (this.helpcontent.getAttribute('stylesheets')) { stylesheets = this.helpcontent.getAttribute('stylesheets').split(' '); } else { var parentlinks = this.doc.getElementsByTagName('link'); for (var i=0; i < parentlinks.length; i++) { var parentlink = parentlinks[i]; if (parentlink.getAttribute('rel') == 'stylesheet' && parentlink.getAttribute('type') == 'text/css') { stylesheets.push(parentlink.href); }; }; }; for (var i=0; i < stylesheets.length; i++) { var link = doc.createElement('link'); link.setAttribute('type', 'text/css'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('href', stylesheets[i]); head.appendChild(link); }; var body = doc.createElement('body'); html.appendChild(body); body.appendChild(this.helpcontent); var html = '' + html.innerHTML + ''; win.document.write(html); win.document.close(); event.cancelBubble = true; if (event.preventDefaut) { event.preventDefault(); } else { event.returnValue = false; }; win.focus(); delete html; }; }();