Author: zhen
Date: Wed Jan 30 15:38:28 2008
New Revision: 616960
URL: http://svn.apache.org/viewvc?rev=616960&view=rev
Log:
Added implementation for the "tabs" feature.
Modified:
incubator/shindig/trunk/features/tabs/tabs.js
Modified: incubator/shindig/trunk/features/tabs/tabs.js
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/tabs/tabs.js?rev=616960&r1=616959&r2=616960&view=diff
==============================================================================
--- incubator/shindig/trunk/features/tabs/tabs.js (original)
+++ incubator/shindig/trunk/features/tabs/tabs.js Wed Jan 30 15:38:28 2008
@@ -51,7 +51,7 @@
/**
* Returns the label of the tab as a string (may contain HTML).
- * @return {String} The label of the tab.
+ * @return {String} Label of the tab.
*/
gadgets.Tab.prototype.getName = function() {
return this.td_.innerHTML;
@@ -109,7 +109,68 @@
* very top.
*/
gadgets.TabSet = function(opt_moduleId, opt_defaultTab, opt_container) {
- // TODO
+ this.moduleId_ = opt_moduleId || 0;
+ this.domIdFilter_ = new RegExp('^[A-Za-z]([0-9a-zA-Z_:.-]+)?$');
+ this.selectedTab_ = null;
+ this.tabs_ = [];
+ this.tabsAdded_ = 0;
+ this.defaultTabName_ = opt_defaultTab || '';
+ this.leftNavContainer_ = null;
+ this.rightNavContainer_ = null;
+ this.navTable_ = null;
+ this.tabsContainer_ = null;
+ this.rtl_ = document.body.dir == 'rtl';
+ this.mainContainer_ = this.createMainContainer_(opt_container);
+ this.tabTable_ = this.createTabTable_();
+ this.displayTabs(false);
+ gadgets.TabSet.addCSS_([
+ '.tablib_table {',
+ 'width: 100%;',
+ 'border-collapse: separate;',
+ 'border-spacing: 0px;',
+ 'empty-cells: show;',
+ 'font-size: 11px;',
+ 'text-align: center;',
+ '}',
+ '.tablib_emptyTab {',
+ 'border-bottom: 1px solid #676767;',
+ 'padding: 0px 1px;',
+ '}',
+ '.tablib_spacerTab {',
+ 'border-bottom: 1px solid #676767;',
+ 'padding: 0px 1px;',
+ 'width: 1px;',
+ '}',
+ '.tablib_selected {',
+ 'padding: 2px;',
+ 'background-color: #ffffff;',
+ 'border: 1px solid #676767;',
+ 'border-bottom-width: 0px;',
+ 'color: #3366cc;',
+ 'font-weight: bold;',
+ 'width: 80px;',
+ 'cursor: default;',
+ '}',
+ '.tablib_unselected {',
+ 'padding: 2px;',
+ 'background-color: #dddddd;',
+ 'border: 1px solid #aaaaaa;',
+ 'border-bottom-color: #676767;',
+ 'color: #000000;',
+ 'width: 80px;',
+ 'cursor: pointer;',
+ '}',
+ '.tablib_navContainer {',
+ 'width: 10px;',
+ 'vertical-align: middle;',
+ '}',
+ '.tablib_navContainer a:link, ',
+ '.tablib_navContainer a:visited, ',
+ '.tablib_navContainer a:hover {',
+ 'color: #3366aa;',
+ 'text-decoration: none;',
+ '}'
+ ].join(''));
};
/**
@@ -129,7 +190,49 @@
* @return {String} DOM id of the tab container.
*/
gadgets.TabSet.prototype.addTab = function(tabName, opt_params) {
- // TODO
+ var params = opt_params || {};
+
+ var tabIndex = -1;
+ if (params.index >= 0 && params.index < this.tabs_.length) {
+ tabIndex = params.index;
+ }
+ var tab = this.createTab_(tabName, {
+ contentContainer: params.contentContainer,
+ callback: params.callback,
+ tooltip: params.tooltip
+ });
+
+ var tr = this.tabTable_.rows[0];
+ if (this.tabs_.length > 0) {
+ var filler = document.createElement('td');
+ filler.className = this.cascade_('tablib_spacerTab');
+ filler.appendChild(document.createTextNode(' '));
+
+ var ref = tabIndex < 0 ? tr.cells[tr.cells.length - 1]
+ : this.tabs_[tabIndex].td_;
+ tr.insertBefore(filler, ref);
+ tr.insertBefore(tab.td_, tabIndex < 0 ? ref : filler);
+ } else {
+ tr.insertBefore(tab.td_, tr.cells[tr.cells.length - 1]);
+ }
+
+ if (tabIndex < 0) {
+ tabIndex = this.tabs_.length;
+ this.tabs_.push(tab);
+ } else {
+ this.tabs_.splice(tabIndex, 0, tab);
+ }
+
+ if (tabName == this.defaultTabName_ || (!this.defaultTabName_
+ && tabIndex == 0)) {
+ this.selectTab_(tab);
+ }
+
+ this.tabsAdded_++;
+ this.displayTabs(true);
+ this.adjustNavigation_();
+
+ return tab.contentContainer_.id;
};
/**
@@ -137,7 +240,29 @@
* @param {Number} tabIndex Index of the tab to remove.
*/
gadgets.TabSet.prototype.removeTab = function(tabIndex) {
- // TODO
+ var tab = this.tabs_[tabIndex];
+ if (tab) {
+ if (tab == this.selectedTab_) {
+ var maxIndex = this.tabs_.length - 1;
+ if (maxIndex > 0) {
+ this.selectTab_(tabIndex < maxIndex ?
+ this.tabs_[tabIndex + 1] :
+ this.tabs_[tabIndex - 1]);
+ }
+ }
+ var tr = this.tabTable_.rows[0];
+ if (this.tabs_.length > 1) {
+ tr.removeChild(tabIndex ? tab.td_.previousSibling : tab.td_.nextSibling);
+ }
+ tr.removeChild(tab.td_);
+ this.mainContainer_.removeChild(tab.contentContainer_);
+ this.tabs_.splice(tabIndex, 1);
+ this.adjustNavigation_();
+ if (this.tabs_.length == 0) {
+ this.displayTabs(false);
+ this.selectedTab_ = null;
+ }
+ }
};
/**
@@ -145,7 +270,7 @@
* @return {gadgets.Tab} The currently selected tab object.
*/
gadgets.TabSet.prototype.getSelectedTab = function() {
- // TODO
+ return this.selectedTab_;
};
/**
@@ -154,7 +279,9 @@
* @param {Number} tabIndex Index of the tab to select.
*/
gadgets.TabSet.prototype.setSelectedTab = function(tabIndex) {
- // TODO
+ if (this.tabs_[tabIndex]) {
+ this.selectTab_(this.tabs_[tabIndex]);
+ }
};
/**
@@ -164,7 +291,16 @@
* @param {Number} tabIndex2 Index of the secnod tab to swap.
*/
gadgets.TabSet.prototype.swapTabs = function(tabIndex1, tabIndex2) {
- // TODO
+ var tab1 = this.tabs_[tabIndex1];
+ var tab2 = this.tabs_[tabIndex2];
+ if (tab1 && tab2) {
+ var tr = tab1.td_.parentNode;
+ var slot = tab1.td_.nextSibling;
+ tr.insertBefore(tab1.td_, tab2.td_);
+ tr.insertBefore(tab2.td_, slot);
+ this.tabs_[tabIndex1] = tab2;
+ this.tabs_[tabIndex2] = tab1;
+ }
};
@@ -184,7 +320,17 @@
* value is 3px.
*/
gadgets.TabSet.prototype.alignTabs = function(align, opt_offset) {
- // TODO
+ var tr = this.tabTable_.rows[0];
+ var left = tr.cells[0];
+ var right = tr.cells[tr.cells.length - 1];
+ var offset = isNaN(opt_offset) ? '3px' : opt_offset + 'px';
+ left.style.width = align == 'left' ? offset : '';
+ right.style.width = align == 'right' ? offset : '';
+ // In Opera and potentially some other browsers, changes to the width of
+ // table cells aren't rendered. To fix this, we force to re-render the
+ // table by hiding and showing it again.
+ this.tabTable_.style.display = 'none';
+ this.tabTable_.style.display = '';
};
/**
@@ -192,7 +338,7 @@
* @param {Boolean} display true to show tabs; false to hide tabs.
*/
gadgets.TabSet.prototype.displayTabs = function(display) {
- // TODO
+ this.mainContainer_.style.display = display ? 'block' : 'none';
};
/**
@@ -200,13 +346,290 @@
* @return {Element} The tab headers container element.
*/
gadgets.TabSet.prototype.getHeaderContainer = function() {
- // TODO
+ return this.tabTable_;
+};
+
+/**
+ * Helper method that returns an HTML container element to which all
tab-related
+ * content will be appended.
+ * This container element is created and inserted as the first child of the
+ * gadget if opt_element is not specified.
+ * @param {Element} opt_element Optional HTML container element.
+ * @return {Element} HTML container element.
+ */
+gadgets.TabSet.prototype.createMainContainer_ = function(opt_element) {
+ var newId = 'tl_' + this.moduleId_;
+ var container = opt_element || document.getElementById(newId);
+
+ if (!container) {
+ container = document.createElement('div');
+ container.id = newId;
+ document.body.insertBefore(container, document.body.firstChild);
+ }
+
+ container.className = this.cascade_("tablib_main_container") + ' ' +
+ container.className;
+
+ return container;
+};
+
+/**
+ * Helper method that expands a class name into two class names.
+ * @param {String} label CSS class
+ * @return {String} Expanded class names.
+ */
+gadgets.TabSet.prototype.cascade_ = function(label) {
+ return label + ' ' + label + this.moduleId_;
+};
+
+/**
+ * Helper method that creates the tabs table and inserts it into the main
+ * container as the first child.
+ * @return {Element} HTML element of the tab container table.
+ */
+gadgets.TabSet.prototype.createTabTable_ = function() {
+ var table = document.createElement('table');
+ table.id = this.mainContainer_.id + '_header';
+ table.className = this.cascade_('tablib_table');
+ table.cellSpacing = '0';
+ table.cellPadding = '0';
+
+ var tbody = document.createElement('tbody');
+ var tr = document.createElement('tr');
+ tbody.appendChild(tr);
+ table.appendChild(tbody);
+
+ var emptyTd = document.createElement('td');
+ emptyTd.className = this.cascade_('tablib_emptyTab');
+ emptyTd.appendChild(document.createTextNode(' '));
+ tr.appendChild(emptyTd);
+ tr.appendChild(emptyTd.cloneNode(true));
+
+ // Construct a wrapper table around our tab table to house the navigation
+ // elements. These elements will appear if the tab table overflows.
+ var navTable = document.createElement('table');
+ navTable.id = this.mainContainer_.id + '_navTable';
+ navTable.style.width = '100%';
+ navTable.cellSpacing = '0';
+ navTable.cellPadding = '0';
+ navTable.style.tableLayout = 'fixed';
+ var navTbody = document.createElement('tbody');
+ var navTr = document.createElement('tr');
+ navTbody.appendChild(navTr);
+ navTable.appendChild(navTbody);
+
+ // Create the left navigation element.
+ var leftNavTd = document.createElement('td');
+ leftNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
+ this.cascade_('tablib_navContainer');
+ leftNavTd.style.textAlign = 'left';
+ leftNavTd.style.display = '';
+ var leftNav = document.createElement('a');
+ leftNav.href = 'javascript:void(0)';
+ leftNav.innerHTML = '«';
+ leftNavTd.appendChild(leftNav);
+ navTr.appendChild(leftNavTd);
+
+ // House the actual tab table in the middle, hiding any overflow.
+ var tabNavTd = document.createElement('td');
+ navTr.appendChild(tabNavTd);
+ var wrapper = document.createElement('div');
+ wrapper.style.width = '100%';
+ wrapper.style.overflow = 'hidden';
+ wrapper.appendChild(table);
+ tabNavTd.appendChild(wrapper);
+
+ // Create the right navigation element.
+ var rightNavTd = document.createElement('td');
+ rightNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
+ this.cascade_('tablib_navContainer');
+ rightNavTd.style.textAlign = 'right';
+ rightNavTd.style.display = '';
+ var rightNav = document.createElement('a');
+ rightNav.href = 'javascript:void(0)';
+ rightNav.innerHTML = '»';
+ rightNavTd.appendChild(rightNav);
+ navTr.appendChild(rightNavTd);
+
+ // Register onclick event handlers for smooth scrolling.
+ leftNav.onclick = function(event) {
+ this.smoothScroll_(wrapper, -120);
+ };
+ rightNav.onclick = function(event) {
+ this.smoothScroll_(wrapper, 120);
+ };
+
+ // Swap left and right scrolling if direction is RTL.
+ if (this.rtl_) {
+ var temp = leftNav.onclick;
+ leftNav.onclick = rightNav.onclick;
+ rightNav.onclick = temp;
+ }
+
+ // If we're already displaying tabs, then remove them.
+ if (this.navTable_) {
+ this.mainContainer_.replaceChild(navTable, this.navTable_);
+ } else {
+ this.mainContainer_.insertBefore(navTable, this.mainContainer_.firstChild);
+ var me = this;
+ var adjustNavigationFn = function() {
+ me.adjustNavigation_();
+ };
+ if (window.addEventListener) {
+ window.addEventListener('resize', adjustNavigationFn, false);
+ } else if (window.attachEvent) {
+ window.attachEvent('onresize', adjustNavigationFn);
+ }
+ }
+
+ this.navTable_ = navTable;
+ this.leftNavContainer_ = leftNavTd;
+ this.rightNavContainer_ = rightNavTd;
+ this.tabsContainer_ = wrapper;
+
+ return table;
+}
+
+/**
+ * Helper method that shows or hides the navigation elements.
+ */
+gadgets.TabSet.prototype.adjustNavigation_ = function() {
+ this.leftNavContainer_.style.display = 'none';
+ this.rightNavContainer_.style.display = 'none';
+ if (this.tabsContainer_.scrollWidth <= this.tabsContainer_.offsetWidth) {
+ this.tabsContainer_.scrollLeft = 0;
+ return;
+ }
+
+ this.leftNavContainer_.style.display = '';
+ this.rightNavContainer_.style.display = '';
+ if (this.tabsContainer_.scrollLeft + this.tabsContainer_.offsetWidth >
+ this.tabsContainer_.scrollWidth) {
+ this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth -
+ this.tabsContainer_.offsetWidth;
+ } else if (this.rtl_) {
+ this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth;
+ }
+};
+
+/**
+ * Helper method that smoothly scrolls the tabs container.
+ * @param {Element} container The tabs container element.
+ * @param {Number} distance The amount of pixels to scroll right.
+ */
+gadgets.TabSet.prototype.smoothScroll_ = function(container, distance) {
+ var scrollAmount = 10;
+ if (!distance) {
+ return;
+ } else {
+ container.scrollLeft += (distance < 0) ? -scrollAmount : scrollAmount;
+ }
+
+ var nextScroll = Math.min(scrollAmount, Math.abs(distance));
+ var me = this;
+ var timeoutFn = function() {
+ me.smoothScroll_(container, (distance < 0) ? distance + nextScroll :
+ distance - nextScroll);
+ };
+ setTimeout(timeoutFn, 10);
+};
+
+/**
+ * Helper function that dynamically inserts CSS rules to the page.
+ * @param {String} cssText CSS rules to inject
+ * @private
+ */
+gadgets.TabSet.addCSS_ = function(cssText) {
+ var head = document.getElementsByTagName('head')[0];
+ if (head) {
+ var styleElement = document.createElement('style');
+ styleElement.type = 'text/css';
+ if (styleElement.styleSheet) {
+ styleElement.styleSheet.cssText = cssText;
+ } else {
+ styleElement.appendChild(document.createTextNode(cssText));
+ }
+ head.insertBefore(styleElement, head.firstChild);
+ }
+};
+
+/**
+ * Helper method that creates a new gadgets.Tab object.
+ * @param {String} tabName Label of the tab to create.
+ * @param {Object} params Parameter object. The following properties
+ * are supported:
+ * .contentContainer An existing HTML element to be used as
+ * the tab content container. If omitted, the tabs
+ * library creates one.
+ * .callback A callback function to be executed when the tab
+ * is selected.
+ * .tooltip A tooltip description that pops up when user
moves
+ * the mouse cursor over the tab.
+ * @return {gadgets.Tab} A new gadgets.Tab object.
+ */
+gadgets.TabSet.prototype.createTab_ = function(tabName, params) {
+ var tab = new gadgets.Tab(this);
+ tab.contentContainer_ = params.contentContainer;
+ tab.callback_ = params.callback;
+ tab.td_ = document.createElement('td');
+ tab.td_.title = params.tooltip || '';
+ tab.td_.innerHTML = tabName;
+ tab.td_.className = this.cascade_('tablib_unselected');
+ tab.td_.onclick = this.setSelectedTabGenerator_(tab);
+
+ if (!tab.contentContainer_) {
+ tab.contentContainer_ = document.createElement('div');
+ tab.contentContainer_.id = this.mainContainer_.id + '_' + this.tabsAdded_;
+ this.mainContainer_.appendChild(tab.contentContainer_);
+ } else if (tab.contentContainer_.parentNode !== this.mainContainer_) {
+ this.mainContainer_.appendChild(tab.contentContainer_);
+ }
+ tab.contentContainer_.style.display = 'none';
+ tab.contentContainer_.className = this.cascade_('tablib_content_container') +
+ ' ' + tab.contentContainer_.className;
+ return tab;
+};
+
+/**
+ * Helper method that creates a function to select the specified tab.
+ * @param {gadgets.Tab} tab The tab to select.
+ * @return {Function} Callback function to select the tab.
+ */
+gadgets.TabSet.prototype.setSelectedTabGenerator_ = function(tab) {
+ return function() { tab.handle_.selectTab_(tab); }
+};
+
+/**
+ * Helper method that selects a tab and unselects the previously selected.
+ * If the tab is already selected, then callback is not executed.
+ * @param {gadgets.Tab} tab The tab to select.
+ */
+gadgets.TabSet.prototype.selectTab_ = function(tab) {
+ if (this.selectedTab_ == tab) {
+ return;
+ }
+
+ if (this.selectedTab_) {
+ this.selectedTab_.td_.className = this.cascade_('tablib_unselected');
+ this.selectedTab_.td_.onclick =
+ this.setSelectedTabGenerator_(this.selectedTab_);
+ this.selectedTab_.contentContainer_.style.display = 'none';
+ }
+
+ tab.td_.className = this.cascade_('tablib_selected');
+ tab.td_.onclick = null;
+ tab.contentContainer_.style.display = 'block';
+ this.selectedTab_ = tab;
+
+ if (typeof tab.callback_ == 'function') {
+ tab.callback_(tab.contentContainer_.id);
+ }
};
// Aliases for legacy code
var _IG_Tabs = gadgets.TabSet;
-_IG_Tabs.prototype.moveTabs = _IG_Tabs.prototype.swapTabs;
+_IG_Tabs.prototype.moveTab = _IG_Tabs.prototype.swapTabs;
_IG_Tabs.prototype.addDynamicTab = function(tabName, callback) {
return this.addTab(tabName, {callback: callback});
};