/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of "Scrivo" nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * $Id: TabPanel.js 616 2013-04-22 23:48:38Z geert $ */ "use strict"; SUI.TabPanel = SUI.defineClass( /** @lends SUI.TabPanel.prototype */{ /** @ignore */ baseClass: SUI.Box, /** * @class * SUI.TabPanel is a component that helps you to save space by stacking a * number of panels/boxes and let the user swap them by clicking on the * tabs. A SUI.TapPanel can host a number of SUI boxes, each of them has * a tab with a title for the user to identify and select them. * TODO: create a separate Tab class * * @augments SUI.Box * * @description * Construct a SUI.TabPanel object. The titles of the tabs and (optional) * the client boxes are given as argument to this constructor. * It is also possible to select the initial tab. * * @constructs * @param see base class * @param {object[]} items An object array containing objects with the * following members: * @param {String} items[].title Tab title * @param {SUI.Box} items[].box A client box (optional) * @param {int} selected Index of tab that should be selected initially * (optional) * @exception {String} If there is no valid initial selection for the * default client area */ initializer: function(arg) { // anchors default to all sides if (!arg.anchor) { arg.anchor = {left:true,right:true,top:true,bottom:true}; } SUI.TabPanel.initializeBase(this, arg); if (arg.onSelectTab) { this.addListener("onSelectTab", arg.onSelectTab); } // create the boxes for the tab panel and add the event handlers this._buildControl(arg); }, /** * Padding of the content on the tab. */ PANEL_PADDING: 4, /** * Border width of the line under the tab strip. */ TAB_BORDER_BOTTOM_WIDTH: 1, /** * Border width of the lines of the tab. */ TAB_BORDER_WIDTH: 1, /** * Height of the highlight strip on top of a selected tab. */ TAB_HILIGHT_HEIGHT: 5, /** * Top margin of unselected tabs. */ TAB_MARGIN_TOP: 3, /** * Height of the tab bar (including bottom border). */ TABBAR_HEIGHT : 29, /** * Top padding of the text in the tabs. */ TABTEXT_PADDING_TOP: 5, /** * Left padding of the text in the tabs. */ TABTEXT_PADDING_LEFT: 7, /** * Left position of the image in a scroller button. */ SCROLL_IMG_PADDING_LEFT: 1, /** * Top position of the image in a scroller button. */ SCROLL_IMG_PADDING_TOP: 5, /** * Width of a the scroller button. */ SCROLLER_WIDTH: 20, /** * Margin top of the selected tab. */ SELTAB_MARGIN_TOP: 0, /** * Add a box to one of the client areas of the control. * @param {SUI.Box} child The box to add to the control * @param {int} i Index position of the client container to at the box to */ add: function(child, i) { this._tabs[i].content.add(child); }, /** * Display the tab panel control. Set the CSS size and position of the tabs * and the currently displayed content panel. */ display: function() { if (this.width() <=0 || this.height() <=0) { return; } this.setDim(); // set the CSS dimensions of the header this.tabstripvpt.setDim(); this.tabstrip.setDim(); // set the CSS dimensions of the tabs for (var i=0; i<this._tabs.length; i++) { this._tabs[i].tab.setDim(); this._tabs[i].tabtext.setDim(); } // set the CSS dimensions of the client area and its contents this.clientArea.setDim(); this._selectedTab.content.display(); // set the CSS dimensions of the tab highlight and the scrollers this.highlight.setDim(); this.scrollRight.setDim(); this.scrollLeft.setDim(); }, /** * Lay out the tab panel control. Calculate the size and position of the * tabs and client area. */ layOut: function() { // set the size of the tabstrip viewport this.tabstripvpt.setRect(0, 0, this.width(), this.TABBAR_HEIGHT); // set the size of the client area this.clientArea.setRect(this.TABBAR_HEIGHT, 0, this.width(), this.height()-this.TABBAR_HEIGHT); for (var i=0,l=0; i<this._tabs.length; i++) { // set the left and width of the tab this._tabs[i].tab.left(l); this._tabs[i].tab.width(this._tabs[i].textLength + 2 * this.TABTEXT_PADDING_LEFT + 2 * this.TAB_BORDER_WIDTH); // set the size and position of the text box this._tabs[i].tabtext.setRect( this.TABTEXT_PADDING_TOP, this.TABTEXT_PADDING_LEFT, this._tabs[i].textLength, this._tabHeight() - this.TABTEXT_PADDING_TOP - this.TAB_BORDER_WIDTH); // set the size and position of the content container this._tabs[i].content.setRect(0,0, this.clientArea.clientWidth(), this.clientArea.clientHeight()); // it this is the currently selected tab ... if (this._selectedTab === this._tabs[i]) { // ... layout the selected tab ... this._layOutSelectedTab(this._selectedTab); // ... and layout the the tab's contents this._tabs[i].content.layOut(); } else { // ... else do the normal tab layout this._layOutNormalTab(this._tabs[i]); } // get the left of the next tab l += this._tabs[i].tab.width()-this.TAB_BORDER_WIDTH; } // set the width of the tab strop this.tabstrip.setRect(0, 0, l+this.TAB_BORDER_WIDTH, this.TABBAR_HEIGHT); // check if we need to draw scroller buttons ... if (this.tabstripvpt.width() < this.tabstrip.width()) { // ... yes: show the scrollers ... this.scrollRight.el().style.display = "block"; this.scrollLeft.el().style.display = "block"; // ... set their sizes and positions ... this.scrollRight.setRect(this.TAB_MARGIN_TOP, this.width() - this.SCROLLER_WIDTH, this.SCROLLER_WIDTH, this.TABBAR_HEIGHT - this.TAB_MARGIN_TOP); this.scrollLeft.setRect(this.scrollRight); this.scrollLeft.left(this.scrollLeft.left() - this.SCROLLER_WIDTH + this.TAB_BORDER_WIDTH); // ... add some extra width for the scrollers ... this.tabstrip.width(this.tabstrip.width() + this.SCROLLER_WIDTH * 2); // ... and enable the scrollers. this._enableScrollers(); } else { // ... no: hide the scrollers ... this.scrollRight.el().style.display = "none"; this.scrollLeft.el().style.display = "none"; // ... and the width of the tabstrip to its viewport this.tabstrip.width(this.tabstripvpt.width()); } }, /** * onSelectTab event handler: is executed when the user clicks on a tab. */ onSelectTab: function() { }, /** * Get the top, left, right and bottom offset of the client area * relative to the outer dimensions of the tab panel. */ clientAreaPosition: function() { return { top: this.TABBAR_HEIGHT + this.clientArea.border().top + this.clientArea.padding().top, left: this.clientArea.border().left + this.clientArea.padding().left, right: this.clientArea.border().right + this.clientArea.padding().right, bottom: this.clientArea.border().bottom + this.clientArea.padding().bottom }; }, /** * Set or get the selected tab. * @param {Object} tab (optional) the tab to set the selected tab to. * @return {Object} the selected tab (null if method was used as setter) */ selectedTab: function(tab) { return tab !== undefined ? (this._selectedTab = tab) && null : this._selectedTab; }, /** * Set or get the index of the selected tab. * @param {int} i (optional) the index to set the index of the selected * tab to. * @return {int} the index of the selected tab (null if method was used * as setter) */ selectedTabIndex: function(i) { return i !== undefined ? (this._selectedTab = this._tabs[i]) && null : this._tabs.indexOf(this._selectedTab); }, /** * Select a tab. Set the selected tab call the onSelectTab listener and * draw it. Note: this method is probably most usefull when overriding, not * to call it on a tab object directly. * @param {Object} tab Tab to select */ selectTab: function(tab) { this._selectedTab = tab; this.callListener("onSelectTab", tab); this.draw(); }, // reference to the the selected tab _selectedTab: null, /* Add the onclick event handler on the tab */ _addOnClickTab: function(tab) { var that = this; // 'that' and 'tab' are two closure variables SUI.browser.addEventListener(tab.tab.el(), "click", function(e) { if (!that.selectTab(tab)) { SUI.browser.noPropagation(e); } } ); }, /* Make all required boxes for the control, set the CSS styles and add * the event handlers. */ _buildControl: function(arg) { // start with an empty tab list this._tabs = []; // add the CSS class to the main box this.addClass("sui-tp-tabpanel"); // create a viewport to scroll the tab strip this.tabstripvpt = new SUI.Box({parent: this}); this.tabstripvpt.el().style.overflow = "hidden"; this.tabstripvpt.addClass("sui-tp-tabstripvpt"); // create the tap strip and add it to the viewport this.tabstrip = new SUI.Box({parent: this.tabstripvpt}); this.tabstrip.border( new SUI.Border(0, 0, this.TAB_BORDER_BOTTOM_WIDTH, 0)); this.tabstrip.addClass("sui-tp-tabstrip"); // create the content area to host the tab containers this.clientArea = new SUI.Box({parent: this}); this.clientArea.addClass("sui-tp-border"); this.clientArea.border( new SUI.Border(0, this.TAB_BORDER_WIDTH, this.TAB_BORDER_WIDTH)); this.clientArea.padding( new SUI.Padding(arg.panelMargin || this.PANEL_PADDING)); // read in the items form the arguments object and store them in // a standardized way in the tabs list for (var i=0; i<arg.tabs.length; i++) { // start with a default profile var tab = { title: "Item "+i, box: null }; for (var prop in arg.tabs[i]) { // and overwrite the tabault profile with the entries set // in the arguments if (arg.tabs[i].hasOwnProperty(prop)) { tab[prop] = arg.tabs[i][prop]; } } // create boxes for the tab and set the event handler this._createTab(tab); // is this the selected tab ... if (i === arg.selected) { // ... yes, then select it this._selectedTab = tab; } this._tabs.push(tab); // if there is already content, then add it to the container if (tab.box) { this.add(tab.box, i); } } // create a little box on top of the tab to serve as highlight this.highlight = new SUI.Box({parent: this.tabstripvpt}); this.highlight.border( new SUI.Border(this.TAB_BORDER_WIDTH)); this.highlight.addClass("sui-tp-tabhighlight"); // if no selected tab was given in the arguments ... if (!this._selectedTab && this._tabs.length) { // ... set the currently selected tab to first this._selectedTab = this._tabs[0]; } if (!this._selectedTab) { throw "SUI.TabPanel: index for selected tab out of range"; } // add the to scrollers that are needed of there are too many tabs // for the available width this.scrollLeft = this._createScroller( SUI.resource.tpScrollLeft, this._scrollLeft); this.scrollRight = this._createScroller( SUI.resource.tpScrollRight, this._scrollRight); }, /* Create boxes for the tab and set the event handlers. */ _createTab: function(tab) { // create the tab tab.tab = new SUI.Box({parent: this.tabstripvpt}); tab.tab.addClass("sui-tp-tab"); tab.tab.border(new SUI.Border(this.TAB_BORDER_WIDTH, this.TAB_BORDER_WIDTH, 0, this.TAB_BORDER_WIDTH)); // store the length of the text on the tab tab.textLength = SUI.style.textLength(tab.title); // create a box for the tab text tab.tabtext = new SUI.TextBox({ parent: tab.tab, text: tab.title }); // create the content container for the tab tab.content = new SUI.AnchorLayout({parent: this.clientArea}); tab.content.addClass("sui-tp-tabcontent"); tab.content.el().style.overflow = "auto"; tab.content.el().style.backgroundColor = "transparent"; tab.content.el().style.display = "none"; // add the onclick event handler for the tab this._addOnClickTab(tab); }, /* Create a scroller button that is needed to scroll the tap strip * if there are too many tabs for the available space. */ _createScroller: function(icon, fn) { // 'that' and 'fn' are the two closure variables var that = this; // create a box for the scroller var scroller = new SUI.Box({parent: this}); scroller.addClass("sui-tp-scroller"); scroller.border(new SUI.Border(this.TAB_BORDER_WIDTH, this.TAB_BORDER_WIDTH, this.TAB_BORDER_BOTTOM_WIDTH)); // and append an icon to it var img = document.createElement("IMG"); img.src = SUI.imgDir + "/" + icon; img.style.marginTop = this.SCROLL_IMG_PADDING_TOP + "px"; img.style.marginLeft = this.SCROLL_IMG_PADDING_LEFT + "px"; scroller.el().appendChild(img); // add a handler to the onclick event of the box SUI.browser.addEventListener(scroller.el(), "click", function(e) { if (!fn.call(that)) { SUI.browser.noPropagation(e); } } ); return scroller; }, /* Enable the scrollers depending on the current scroll position * of the tab strip in its viewport. */ _enableScrollers: function() { // if there is a left scroll offset enable the left scroller if (this.tabstripvpt.el().scrollLeft) { this.scrollLeft.removeClass("sui-tp-scroller-disabled"); } else { this.scrollLeft.addClass("sui-tp-scroller-disabled"); } // compare the distance of the left scroller with the right of // the last tab. If it is smaller enable the right scroller if (this._tabRight(this._tabs[this._tabs.length-1]) < this._leftScrollerLeft()) { // ... disable the scroller this.scrollRight.addClass("sui-tp-scroller-disabled"); } else { this.scrollRight.removeClass("sui-tp-scroller-disabled"); } }, /* Lay out a normal tab */ _layOutNormalTab: function(tab) { // Remove CSS class tab.tab.removeClass("sui-tp-tabselected"); // set the dimensions tab.tab.setRect(this.TAB_MARGIN_TOP, tab.tab.left(), tab.tab.width(), this._tabHeight()); tab.tabtext.top(this.TABTEXT_PADDING_TOP); // hide the tab content and highlight bar tab.content.el().style.display = "none"; }, /* Lay out a a selected tab */ _layOutSelectedTab: function(tab) { // Add CSS class tab.tab.addClass("sui-tp-tabselected"); // set the dimensions tab.tab.setRect(this.SELTAB_MARGIN_TOP, tab.tab.left(), tab.tab.width(), this.TABBAR_HEIGHT - this.SELTAB_MARGIN_TOP); tab.tabtext.top(this.TABTEXT_PADDING_TOP + this.TAB_MARGIN_TOP - this.SELTAB_MARGIN_TOP); // show the tab content and highlight bar tab.content.el().style.display = "block"; // set the tab highlight this.highlight.setRect(tab.tab); this.highlight.height(this.TAB_HILIGHT_HEIGHT); }, /* Get the distance of the left scroller to the left side of the component. */ _leftScrollerLeft: function() { return this.tabstripvpt.el().scrollLeft + this.scrollLeft.left(); }, /* Scroll one tab to the left. */ _scrollLeft: function() { // find the tab of which the right side is larger or equeal to the left // side of the left scroller for (var i=0; i<this._tabs.length; i++) { if (this._tabRight(this._tabs[i]) >= this._leftScrollerLeft()) { break; } } // set the scroll-left of the vieport so that found tab's left // equals the left of the scroller this.tabstripvpt.el().scrollLeft = this._tabs[i].tab.left() - this.scrollLeft.left() + this.TAB_BORDER_WIDTH; this._enableScrollers(); }, /* Scroll one tab to the right. */ _scrollRight: function() { // find the tab of which the right side is further that the left // side of the left scroller for (var i=0; i<this._tabs.length-1; i++) { if (this._tabRight(this._tabs[i]) > this._leftScrollerLeft()) { break; } } // set the scroll-left of the viewport so that the found tab will // be adjacent to the left scroller this.tabstripvpt.el().scrollLeft = this._tabRight(this._tabs[i]) - this.scrollLeft.left(); this._enableScrollers(); }, /* Height of an unselected tab (including top border but excluding bottom * border) */ _tabHeight: function() { return this.TABBAR_HEIGHT - this.TAB_BORDER_BOTTOM_WIDTH - this.TAB_MARGIN_TOP; }, /* Get the position of the right side of a tab. */ _tabRight: function(tab) { return tab.tab.left() + tab.tab.width(); } });