← servoy magazine

[Article] Accordion tab panels

  • Articles

by David Workman and Troy Elliott
Data Mosaic

Introduction

Accordion tabs can be the answer when you start running out of room to put secondary information. Compared to traditional tab panels, the "tabs" take up the y-axis of space instead of the x-axis making them great for long side columns.

While not as straight forward to code as traditional custom tab panels, the code in this sample solution will work in most situations with little to no modification.

Accordion tab panels are a cool addition to your UI bag of tricks that your users will like. Enjoy!

[image unavailable: Picture_1]

How it works

An accordion tab panel has three distinct objects: the tab panel, tab bars (the "tabs"), and arrow graphics. These are arranged with uniform width in a vertical space.

Each tab bar is linked to a specific tab panel tab. When clicked, that tab is selected in the tab panel. The tab panel is expanded to a default or specified height (see meta data) and moved to just below the selected tab bar. Subsequent tab bars are moved below the bottom of the tab panel. This little bit is where most of the coding complexity is involved.

You can click on a selected tab bar to "collapse" that tab. This will result in all tab bars in a "closed" or "unselected" state. No tab panel will show in this case.

Besides the positioning of the tab panel just below the selected tab bar, the "selected" state is further differentiated from the "unselected" state in three ways: the background color of the tab bar, the label color (foreground color) of the tab bar, and the direction the arrow graphic is pointing.

What you need: Objects

[image unavailable: Picture_2]

1) A tab panel named "tab_sidebar_a". You can have multiple accordion tabs on a form by using the naming convention "tab_sidebar_b", "tab_sidebar_c", etc.

2) A tab bar for each tab in the tab panel. Name "bar_a_1", "bar_a_2", etc. Specify a text property for each tab bar to give a label for the tab.

3) Arrow graphics for each tab bar. Name "bar_a_1_arrow", "bar_a_2_arrow", etc. Assign "arrow_right.png" to the imageMedia property.

What you need: Methods

1) An initialize method: "TAB_accordion_init". This should run on the onShow event of the form. You would think that the form onLoad event would be the appropriate place but this event triggers before the form objects are loaded. Which makes it a bit difficult to setup initial form object states

Even though the code is fired on the onShow event, it has logic to only fire once per form in memory.

2) The tab controller method: "TAB_change_accordion". Assign this to the onAction event of each tab bar.

Both methods are abstracted to work with an unlimited number of accordion tab panels throughout your solution using the naming conventions specified above.

/*
 *	TITLE    :	TAB_accordion_init
 *			  	
 *	ABOUT    :	creates form level variable that stores information about selected accordion
 *			  	
 *	INPUT    :	
 *			  	
 *	OUTPUT   :	
 *			  	
 *	REQUIRES :	tab panel named 'tab_sidebar_'+[a,b,...z]
 *			  	text label named 'bar_'+[a,b,...z]+[1,2,...n]
 *			  	expand/collapse arrow named 'bar_'+[a,b,...z]+[1,2,...n]+'_arrow'
 *			  	
 *			  	the same number of tabs as there are labeled bars and arrows
 *			  	
 *	MODIFIED :	May 2008 -- Troy Elliott, Data Mosaic
 *			  	
 */

var formName = application.getMethodTriggerFormName()

//check to see that form is defined in solution
if (forms[formName]) {
	
	//check that this form not already loaded
	if (!forms[formName].shown) {
		
		//set form as shown so that this method will not fire again
		forms[formName].shown = true
		
		//get foundset for configuration of sidebars
		var serverName = forms[formName].controller.getServerName()
		var tableName = 'sidebar_setup'
		var fsSidebar = databaseManager.getFoundSet(serverName, tableName)
		fsSidebar.clear()
		
		//hard coded for a maximum of 6 sidebars on one form (a-f)
		//a, b, c, d, e, f, false
		for (var numero = 'a' ; numero ; (numero == 'a') ? numero = 'b' : (numero == 'b') ? numero = 'c' : (numero == 'c') ? numero = 'd' : (numero == 'd') ? numero = 'e' : (numero == 'e') ? numero = 'f' : numero = false) {
			
			var tabPanelName = 'tab_sidebar_' + numero
			//if sidebar tab panel sidebar_x is defined, get information about its attributes
			if (forms[formName].elements[tabPanelName]) {
				//find sidebar_'numero' (a, b, etc.) specs for this form
				fsSidebar.find()
				fsSidebar.form_name = formName
				fsSidebar.sidebar = numero
				var results = fsSidebar.search()
				
				//array with info about this tab panel
				var sideBar = new Array()
				var maxY = forms[formName].elements[tabPanelName].getLocationY() + forms[formName].elements[tabPanelName].getHeight()
				
				//get number of tabs
				var tabTotal = forms[formName].elements[tabPanelName].getMaxTabIndex()
				
				//each sidebar can have an unlimited number of tabs
				//1,2,3,...false
				for (var numera = 1 ; numera, numera <= tabTotal ; numera++) {
					var tabBar = 'bar_' + numero + '_' + numera
					
					//try to find bar for tab in tab panel
					if (forms[formName].elements[tabBar]) {
						//info about this tab
						var tabInfo = new Object()
						
						//base y-location of tab
						var tabPosition = tabInfo.tabPosition = forms[formName].elements[tabBar].getLocationY()
						
						//default size is 0 which means maximum
						var tabSize = 0
						
						//default back/fore is black and white
						tabInfo.selectedForeground = '#FFFFFF'
						tabInfo.selectedBackground = '#000000'
						tabInfo.unSelectedForeground = '#000000'
						tabInfo.unSelectedBackground = '#FFFFFF'
						
						//there are specs for this form's sidebar
						if (results) {
							var record = fsSidebar.getRecord(1)
							
							//if not set, black and white
							tabInfo.selectedForeground = (record.selected_foreground) ? record.selected_foreground : '#FFFFFF'
							tabInfo.selectedBackground = (record.selected_background) ? record.selected_background : '#000000'
							tabInfo.unSelectedForeground = (record.unselected_foreground) ? record.unselected_foreground : '#000000'
							tabInfo.unSelectedBackground = (record.unselected_background) ? record.unselected_background : '#FFFFFF'
							
							//there are specs for this tab of the sidebar
							if (utils.hasRecords(fsSidebar.sidebar_setup_to_sidebar_setup_item) && fsSidebar.sidebar_setup_to_sidebar_setup_item.getSize() >= numera) {
								var record = fsSidebar.sidebar_setup_to_sidebar_setup_item.getRecord(numera)
								
								//get size of form if explicitly defined
								//if not auto, specify manual height
								if (!record.height_auto) {
									tabSize = record.height_manual
								}
							}
						}
						
						//space required below tab panel for remaining bars ((20 + 5) * remaining)
						var bottomOffset = 25 * (tabTotal - numera)
						//size of tab panel (bottom pixel we can use - (see above) - first pixel available - distance for tab panel below bar)
						var maxSize = maxY - bottomOffset - tabPosition - 25
						
						//form doesn't exist, isn't in record mode, or the set size is larger than what is available ==> set to max
						if (tabSize == 0 || tabSize > maxSize) {
							tabSize = maxSize
						}
						tabInfo.tabSize = tabSize
						
						sideBar[sideBar.length] = tabInfo
					}
				}
				
				//assign array to form with information about location and positioning of current accordion widget
				forms[formName]['sidebar_'+numero] = sideBar
				
				//flip to active tab
				globals.TAB_change_accordion(formName,'bar_'+numero+'_'+forms[formName].elements['tab_sidebar_'+numero].tabIndex)
			}
			//break out of loop because sidebar_x is not defined
			else {
				numero = false
			}
		}
	}
}

 

/*
 *	TITLE    :	TAB_change_accordion
 *			  	
 *	ABOUT    :	change tabIndex and all associated labels
 *			  		-finds the colors from swatch and then goes to work
 *			  	activates action buttons
 *			  	
 *	INPUT    :	ALL inputs are optional
 *			  	1)	form name with tab panel
 *			  	2)	tab to be activated eg. 'tab_d1'
 *			  	
 *	OUTPUT   :	
 *			  	
 *	REQUIRES :	tab panel named 'tab_sidebar_a', labels named 'bar_a_'+[1,2,...n]
 *			  	at least two tabs properly configured
 *			  	
 *	MODIFIED :	May 2008 -- Troy Elliott, Data Mosaic
 *			  	
 */

var formName = (arguments[0]) ? arguments[0] : application.getMethodTriggerFormName()
var buttonName = (arguments[1]) ? arguments[1] : application.getMethodTriggerElementName()
var expandSet = (arguments[1]) ? false : true

var prefixBar = 'bar_'
var suffixArrow = '_arrow'

var tabLetter = buttonName.charAt(4)
var tabPanelName = 'tab_sidebar_' + tabLetter
tabLetter += '_'

//get number of tabs
var tabTotal = forms[formName].elements[tabPanelName].getMaxTabIndex()

//get current tab
var currentTab = forms[formName].elements[tabPanelName].tabIndex

//get font string (font,normal/bold/italic/bolditalic,size)
//on a mac
if (application.getOSName().indexOf('Mac',0) >= 0) {
	var fontSelect = 'Verdana,1,11'
	var fontUnselect = 'Verdana,1,11'
}
//on windows, linux, etc.
else {
	var fontSelect = 'Tahoma,1,12'
	var fontUnselect = 'Tahoma,0,12'	
}

//the tab we are going to
var tabNum = buttonName.match(/[0-9]/)[0]

//x position of all elements to be moved around (tab panel, collapse/expand arrows, labels, and rounded rectangles)
var xPos = forms[formName].elements[buttonName].getLocationX()
var offsetArrow = 2
if (buttonName.indexOf(suffixArrow,0) >= 0) {
	xPos -= offsetArrow
}

//retrieve information about all tabs
var sideBar = forms[formName][tabPanelName.substring(4)]

//position elements (only works if there are tabs and sidebar has been defined previously
for (var i = 1 ; i <= tabTotal && sideBar != undefined ; i++) {

	//get foreground/background color for banded bars
	var foreSelect = sideBar[i-1].selectedForeground
	var backSelect = sideBar[i-1].selectedBackground
	
	var foreUnselect = sideBar[i-1].unSelectedForeground
	var backUnselect = sideBar[i-1].unSelectedBackground
	
	//set selected
	if (tabNum == i) {
		//selected background color
		forms[formName].elements[prefixBar + tabLetter + i].bgcolor = backSelect
		forms[formName].elements[prefixBar + tabLetter + i].fgcolor = foreSelect
		
		//selected font
		forms[formName].elements[prefixBar + tabLetter + i].setFont(fontSelect)
		forms[formName].elements[prefixBar + tabLetter + i].text = forms[formName].elements[prefixBar + tabLetter + i].text.toUpperCase()

		//switch to tab if called from a method, if different than the one currently on, otherwise, collapse/expand toggle
		if (expandSet && forms[formName].elements[tabPanelName].tabIndex == tabNum && !(forms[formName][prefixBar + tabLetter] == (prefixBar + tabLetter + i))) {
			//selected graphic
			forms[formName].elements[prefixBar + tabLetter + i + suffixArrow].setImageURL("media:///arrow_right_rev.png")
			
			//hide tab panel
			forms[formName].elements[tabPanelName].visible = false
			//set form variable that this tab is hidden
			forms[formName][prefixBar + tabLetter] = prefixBar + tabLetter + i
		}
		else {
			//selected graphic
			forms[formName].elements[prefixBar + tabLetter + i + suffixArrow].setImageURL("media:///arrow_down_rev.png")

			//set tab, resize tab, and set tab location
			forms[formName].elements[tabPanelName].setLocation(xPos, eval(sideBar[i-1].tabPosition) + 25)
			forms[formName].elements[tabPanelName].setSize(forms[formName].elements[tabPanelName].getWidth(), eval(sideBar[i-1].tabSize))
			forms[formName].elements[tabPanelName].tabIndex = i
			
			//show tab panel
			forms[formName].elements[tabPanelName].visible = true
			//clear form variable that a tab is hidden
			forms[formName][prefixBar + tabLetter] = null
		}		
	}
	//set non-selected
	else {
		//default background color
		forms[formName].elements[prefixBar + tabLetter + i].bgcolor = backUnselect
		forms[formName].elements[prefixBar + tabLetter + i].fgcolor = foreUnselect
		
		//default font
		forms[formName].elements[prefixBar + tabLetter + i].setFont(fontUnselect)
		forms[formName].elements[prefixBar + tabLetter + i].text = globals.FX_initial_caps(forms[formName].elements[prefixBar + tabLetter + i].text)
		
		//default graphics
		forms[formName].elements[prefixBar + tabLetter + i + suffixArrow].setImageURL("media:///arrow_right.png")
	}
	
	//add additional shift if tab below tab panel and selected tab is not collapsed
	if (i > tabNum && !forms[formName][prefixBar + tabLetter]) {
		var tabBelow = utils.stringToNumber(sideBar[tabNum-1].tabSize) + 5
		
		forms[formName].elements[prefixBar + tabLetter + i].setLocation(xPos, eval(sideBar[i-1].tabPosition) + tabBelow)
		forms[formName].elements[prefixBar + tabLetter + i + suffixArrow].setLocation(xPos + offsetArrow, eval(sideBar[i-1].tabPosition) + tabBelow)
	}
	//set tab to be at default location
	else {
		forms[formName].elements[prefixBar + tabLetter + i].setLocation(xPos, eval(sideBar[i-1].tabPosition))
		forms[formName].elements[prefixBar + tabLetter + i + suffixArrow].setLocation(xPos + offsetArrow, eval(sideBar[i-1].tabPosition))
	}

}

Optional: Using meta data

There is default bar colors and a tab panel height specification in the methods. For each accordion tab panel in your solution, you can configure these properties by creating a meta data record. Our code will check to see if a meta data record exists before using the default properties.

[image unavailable: Picture_4]

Steps:

1) Create a meta data record. Specify the form name and the accordion tab panel name. Every accordion tab panel on a form will need it's own meta data record.

2) Specify the selected and unselected colors of the tab bars. Note that the foreground color is for the label of the tab bar.

3) You can specify the height of the tab panel for each individual tab bar (in pixels). The default will fill the vertical space of the accordion tab panel area.

[image unavailable: ]

Comments (3)

Keith Schuster
Newbie here trying to import this sample into 4.0. I'm getting an error on solution import Importing accordion.servoy... [info] Import contains XML version 12 and repository version 31. [error] Server 'data' does not exist. Import failed. [error] com.servoy.j2db.persistence.RepositoryException: Operation cancelled What do I need to do to make this work?
David Workman
Create a database connection called "data" first before importing. Also keep in mind that the sample solution was created in 3.5. In general we're getting good results with our 3.5 apps in 4.0 but this accordion stuff we haven't tested yet in 4.0.
Danny Camargo
I just read this article looking for an accordion tabpanel solution, I've not found a bean or plugin solution and I like to try this one, but I cannot download the .servoy file, link is broken. Can you put it available again? or if you have any idea of an accordion bean or plugin for servoy. Thanks in advance!