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 = '&laquo;';
+  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 = '&raquo;';
+  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});
 };


Reply via email to