2 Scripts to create interactive tabs in SVG using ECMA script
3 Copyright (C) <2006> <Andreas Neumann>
4 Version 1.2, 2006-04-03
5 neumann@karto.baug.ethz.ch
7 http://www.carto.net/neumann/
14 Documentation: http://www.carto.net/papers/svg/gui/tabgroup/
18 current version: 1.2.1
25 added ".moveTo(x,y)" and ".resize(width,height)" methods, added an additional g-element to hold transform values
28 this.parentNode can now also be of type node reference (g or svg element); fixed a small bug when "hideContent" was set to true; changed this.parentGroup and introduced this.tabGroup
29 introduced id for group where content can be added, id name is: this.id+"__"+i+"_content"
32 fixed a bug for multiline tabs (dy attribute of the tab texts). This bug was apparent when having more than two lines of text in the tabs
37 This ECMA script library is free software; you can redistribute it and/or
38 modify it under the terms of the GNU Lesser General Public
39 License as published by the Free Software Foundation; either
40 version 2.1 of the License, or (at your option) any later version.
42 This library is distributed in the hope that it will be useful,
43 but WITHOUT ANY WARRANTY; without even the implied warranty of
44 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
45 Lesser General Public License for more details.
47 You should have received a copy of the GNU Lesser General Public
48 License along with this library (lesser_gpl.txt); if not, write to the Free Software
49 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
53 original document site: http://www.carto.net/papers/svg/gui/tabgroup/
54 Please contact the author in case you want to use code or ideas commercially.
55 If you use this code, please include this copyright header, the included full
56 LGPL 2.1 text and read the terms provided in the LGPL 2.1 license
57 (http://www.gnu.org/copyleft/lesser.txt)
59 -------------------------------
61 Please report bugs and send improvements to neumann@karto.baug.ethz.ch
62 If you use this control, please link to the original (http://www.carto.net/papers/svg/gui/tabgroup/)
63 somewhere in the source-code-comment or the "about" of your project and give credits, thanks!
67 function tabgroup(id,parentNode,transx,transy,width,height,tabheight,cornerLeft,cornerRight,tabmargins,spaceBetweenTabs,tabStyles,activetabBGColor,tabwindowStyles,tabtextStyles,tabTitles,activeTabindex,hideContent,functionToCall) {
69 var createTabgroup = true;
70 if (arguments.length == nrArguments) {
72 this.parentNode = parentNode; //can be of type string (id) or node reference (svg or g node)
79 this.tabheight = tabheight;
80 this.cornerLeft = cornerLeft; //values are "rect","round","triangle"
81 this.cornerRight = cornerRight; //values are "rect","round","triangle"
82 this.tabmargins = tabmargins;
83 this.spaceBetweenTabs = spaceBetweenTabs;
84 this.tabStyles = tabStyles;
85 if (!this.tabStyles["fill"]) {
86 this.tabStyles["fill"] = "lightgray";
88 this.activetabBGColor = activetabBGColor;
89 this.tabwindowStyles = tabwindowStyles;
90 this.tabtextStyles = tabtextStyles;
91 if (!this.tabtextStyles["font-size"]) {
92 this.tabtextStyles["font-size"] = 15;
94 this.tabTitles = tabTitles;
95 if (this.tabTitles instanceof Array) {
96 if (this.tabTitles.length == 0) {
97 createTabgroup = false;
98 alert("Error ("+this.id+"): the array 'tabTitles' has no elements!");
102 createTabgroup = false;
103 alert("Error ("+this.id+"): the array 'tabTitles' is not of type array!");
105 this.activeTabindex = activeTabindex;
106 if (this.activeTabindex >= this.tabTitles.length) {
107 createTabgroup = false;
108 this.outOfBoundMessage(this.activeTabindex);
110 this.hideContent = hideContent; //boolean, defines whether the display of a tab should be set to "none","inherit"
111 this.functionToCall = functionToCall;
112 if (!(typeof(this.functionToCall) == "function" || typeof(this.functionToCall) == "object" || typeof(this.functionToCall) == "undefined")) {
113 createTabgroup = false;
114 alert("Error ("+this.id+"): functionToCall is of type '"+typeof(this.functionToCall)+"' and not of type 'function', 'object' or 'undefined'");
118 createTabgroup = false;
119 alert("Error ("+id+"): wrong nr of arguments! You have to pass over "+nrArguments+" parameters.");
121 if (createTabgroup) {
122 this.parentGroup = null; //later a node reference to the parent group
123 this.tabGroup = null; //later a reference to the group within the parentGroup
124 this.tabwindows = new Array();
125 this.timer = new Timer(this); //a Timer instance for calling the functionToCall
126 this.timerMs = 200; //a constant of this object that is used in conjunction with the timer - functionToCall is called after 200 ms
127 this.createTabGroup();
130 alert("Could not create tabgroup with id '"+this.id+"' due to errors in the constructor parameters");
134 tabgroup.prototype.createTabGroup = function() {
135 var result = this.testParent();
137 this.tabGroup = document.createElementNS(svgNS,"g");
138 this.tabGroup.setAttributeNS(null,"transform","translate("+this.transx+","+this.transy+")");
139 this.parentGroup.appendChild(this.tabGroup);
140 //loop to create all tabs
141 var currentX = this.x;
142 for (var i=0;i<this.tabTitles.length;i++) {
143 currentLeft = currentX;
144 this.tabwindows[i] = new Array();
146 this.tabwindows[i]["group"] = document.createElementNS(svgNS,"g");
147 this.tabGroup.appendChild(this.tabwindows[i]["group"]);
149 var tabtitles = this.tabTitles[i].split("\n");
150 this.tabwindows[i]["tabTitle"] = document.createElementNS(svgNS,"text");
151 this.tabwindows[i]["tabTitle"].setAttributeNS(null,"y",this.y + this.tabtextStyles["font-size"]);
153 for (var attrib in this.tabtextStyles) {
154 value = this.tabtextStyles[attrib];
155 if (attrib == "font-size") {
158 this.tabwindows[i]["tabTitle"].setAttributeNS(null,attrib,value);
160 this.tabwindows[i]["tabTitle"].setAttributeNS(null,"pointer-events","none");
161 //create tspans and add text contents
162 for (var j=0;j<tabtitles.length;j++) {
163 var tspan = document.createElementNS(svgNS,"tspan");
164 tspan.setAttributeNS(null,"x",currentX+this.tabmargins);
165 var dy = this.tabtextStyles["font-size"]*1.1;
169 tspan.setAttributeNS(null,"dy",dy);
170 var textNode = document.createTextNode(tabtitles[j]);
171 tspan.appendChild(textNode);
172 this.tabwindows[i]["tabTitle"].appendChild(tspan);
174 this.tabwindows[i]["group"].appendChild(this.tabwindows[i]["tabTitle"]);
176 var bbox = this.tabwindows[i]["tabTitle"].getBBox();
177 currentX = Math.round(currentLeft + this.tabmargins * 2 + bbox.width);
178 //check if text-anchor is middle and shift the x-values accordingly
179 if (this.tabtextStyles["text-anchor"]) {
180 if (this.tabtextStyles["text-anchor"] == "middle") {
181 this.tabwindows[i]["tabTitle"].setAttributeNS(null,"transform","translate("+(bbox.width * 0.5)+" 0)");
184 //now draw tabwindow background
185 this.tabwindows[i]["tabbg"] = document.createElementNS(svgNS,"path");
186 for (var attrib in this.tabwindowStyles) {
187 this.tabwindows[i]["tabbg"].setAttributeNS(null,attrib,this.tabwindowStyles[attrib]);
189 //start the path for tab windows
192 if (this.cornerLeft == "rect") {
193 d += currentLeft+" "+this.y;
195 if (this.cornerLeft == "triangle") {
196 d += currentLeft+" "+(this.y+this.tabmargins)+"L"+(currentLeft+this.tabmargins)+" "+this.y;
198 if (this.cornerLeft == "round") {
199 d += currentLeft+" "+(this.y+this.tabmargins)+"a"+this.tabmargins+" "+this.tabmargins+" 0 0 1 "+this.tabmargins+" -"+this.tabmargins;
201 //right corner of tab
202 if (this.cornerRight == "rect") {
203 d += "L"+currentX+" "+this.y;
205 if (this.cornerRight == "triangle") {
206 d += "L"+(currentX-this.tabmargins)+" "+this.y+"L"+currentX+" "+(this.y+this.tabmargins);
208 if (this.cornerRight == "round") {
209 d += "L"+(currentX-this.tabmargins)+" "+this.y+"a"+this.tabmargins+" "+this.tabmargins+" 0 0 1 "+this.tabmargins+" "+this.tabmargins;
211 d += "L"+currentX+" "+(this.y+this.tabheight);
212 //complete the path for tab
213 var dtab = d + "L"+currentLeft+" "+(this.y+this.tabheight)+"z";
214 //complete the path for tab window
215 d += "L"+(this.x+this.width)+" "+(this.y+this.tabheight)+"L"+(this.x+this.width)+" "+(this.y+this.height);
216 d += "L"+this.x+" "+(this.y+this.height);
217 if (currentLeft == this.x) {
221 d += "L"+this.x+" "+(this.y+this.tabheight)+"L"+currentLeft+" "+(this.y+this.tabheight)+"z";
223 this.tabwindows[i]["tabbg"].setAttributeNS(null,"d",d);
224 this.tabwindows[i]["group"].insertBefore(this.tabwindows[i]["tabbg"],this.tabwindows[i]["tabTitle"]);
226 this.tabwindows[i]["tab"] = document.createElementNS(svgNS,"path");
227 for (var attrib in this.tabStyles) {
228 this.tabwindows[i]["tab"].setAttributeNS(null,attrib,this.tabStyles[attrib]);
230 this.tabwindows[i]["tab"].setAttributeNS(null,"d",dtab);
231 this.tabwindows[i]["tab"].setAttributeNS(null,"id",this.id+"__"+i);
232 this.tabwindows[i]["tab"].addEventListener("click",this,false);
233 this.tabwindows[i]["group"].insertBefore(this.tabwindows[i]["tab"],this.tabwindows[i]["tabTitle"]);
234 //create group for tab content
235 this.tabwindows[i]["content"] = document.createElementNS(svgNS,"g");
236 this.tabwindows[i]["content"].setAttributeNS(null,"id",this.id+"__"+i+"_content");
237 if (this.hideContent) {
238 this.tabwindows[i]["content"].setAttributeNS(null,"display","none");
240 this.tabwindows[i]["group"].appendChild(this.tabwindows[i]["content"]);
241 //set tab activate status
242 this.tabwindows[i].activeStatus = true;
243 //increment currentX with the space between tabs
244 currentX += this.spaceBetweenTabs;
247 this.tabwindows[this.activeTabindex]["tab"].setAttributeNS(null,"fill",this.activetabBGColor);
248 this.tabGroup.appendChild(this.tabwindows[this.activeTabindex]["group"]);
249 if (this.hideContent) {
250 this.tabwindows[this.activeTabindex]["content"].setAttributeNS(null,"display","inherit");
254 alert("could not create or reference 'parentNode' of tabgroup with id '"+this.id+"'");
258 //test if window group exists or create a new group at the end of the file
259 tabgroup.prototype.testParent = function() {
260 //test if of type object
261 var nodeValid = false;
262 if (typeof(this.parentNode) == "object") {
263 if (this.parentNode.nodeName == "svg" || this.parentNode.nodeName == "g") {
264 this.parentGroup = this.parentNode;
268 else if (typeof(this.parentNode) == "string") {
269 //first test if Windows group exists
270 if (!document.getElementById(this.parentNode)) {
271 this.parentGroup = document.createElementNS(svgNS,"g");
272 this.parentGroup.setAttributeNS(null,"id",this.parentNode);
273 document.documentElement.appendChild(this.parentGroup);
277 this.parentGroup = document.getElementById(this.parentNode);
284 tabgroup.prototype.handleEvent = function(evt) {
285 var tab = evt.target;
286 var id = tab.getAttributeNS(null,"id");
287 var idArray = id.split("__");
288 var index = parseInt(idArray[1]);
289 this.activateTabByIndex(index,false);
291 this.timer.setTimeout("fireFunction",this.timerMs);
294 tabgroup.prototype.fireFunction = function() {
295 if (typeof(this.functionToCall) == "function") {
296 this.functionToCall(this.id,this.tabTitles[this.activeTabindex],this.activeTabindex);
298 if (typeof(this.functionToCall) == "object") {
299 this.functionToCall.tabActivated(this.id,this.tabTitles[this.activeTabindex],this.activeTabindex);
301 if (typeof(this.functionToCall) == undefined) {
306 tabgroup.prototype.activateTabByIndex = function(tabindex,fireFunction) {
307 if (tabindex >= this.tabTitles.length) {
308 this.outOfBoundMessage(tabindex);
311 //set old active tab to inactive
312 this.tabwindows[this.activeTabindex]["tab"].setAttributeNS(null,"fill",this.tabStyles["fill"]);
313 if (this.hideContent) {
314 this.tabwindows[this.activeTabindex]["content"].setAttributeNS(null,"display","none");
317 this.activeTabindex = tabindex;
319 this.tabwindows[this.activeTabindex]["tab"].setAttributeNS(null,"fill",this.activetabBGColor);
321 this.tabGroup.appendChild(this.tabwindows[this.activeTabindex]["group"]);
323 if (this.hideContent) {
324 this.tabwindows[this.activeTabindex]["content"].setAttributeNS(null,"display","inherit");
327 this.timer.setTimeout("fireFunction",this.timerMs);
331 tabgroup.prototype.activateTabByTitle = function(title,fireFunction) {
333 for (var i=0;i<this.tabTitles.length;i++) {
334 if (this.tabTitles[i] == title) {
339 if (tabindex != -1) {
340 this.activateTabByIndex(tabindex,fireFunction);
343 alert("Error ("+this.id+"): Could not find title '"+title+"' in array tabTitles!");
347 //add content to this.tabwindows[tabindex]["content"]
348 tabgroup.prototype.addContent = function(node,tabindex,inheritDisplay) {
349 if (tabindex >= this.tabTitles.length) {
350 this.outOfBoundMessage(tabindex);
353 if (typeof(node) == "string") {
354 node = document.getElementById(node);
356 if (node != undefined || node != "null") {
357 if (inheritDisplay) {
358 node.setAttributeNS(null,"display","inherit");
360 this.tabwindows[tabindex]["content"].appendChild(node);
365 //remove content from this.tabwindows[tabindex]["content"]
366 tabgroup.prototype.removeContent = function(node,tabindex) {
367 var deletedNode = undefined;
368 if (tabindex >= this.tabTitles.length) {
369 this.outOfBoundMessage(tabindex);
372 if (typeof(node) == "string") {
373 node = document.getElementById(node);
375 if (node != undefined || node != "null") {
376 deletedNode = this.tabwindows[tabindex]["content"].removeChild(node);
382 //move the tab, reference is upper left corner
383 tabgroup.prototype.moveTo = function(x,y) {
386 this.tabGroup.setAttributeNS(null,"transform","translate("+this.transx+","+this.transy+")");
390 tabgroup.prototype.resize = function(width,height) {
392 this.height = height;
393 //loop to change all tab sizes
394 var currentX = this.x;
395 for (var i=0;i<this.tabTitles.length;i++) {
396 currentLeft = currentX;
398 var bbox = this.tabwindows[i]["tabTitle"].getBBox();
399 currentX = Math.round(currentLeft + this.tabmargins * 2 + bbox.width);
400 //start the path for tab windows
403 if (this.cornerLeft == "rect") {
404 d += currentLeft+" "+this.y;
406 if (this.cornerLeft == "triangle") {
407 d += currentLeft+" "+(this.y+this.tabmargins)+"L"+(currentLeft+this.tabmargins)+" "+this.y;
409 if (this.cornerLeft == "round") {
410 d += currentLeft+" "+(this.y+this.tabmargins)+"a"+this.tabmargins+" "+this.tabmargins+" 0 0 1 "+this.tabmargins+" -"+this.tabmargins;
412 //right corner of tab
413 if (this.cornerRight == "rect") {
414 d += "L"+currentX+" "+this.y;
416 if (this.cornerRight == "triangle") {
417 d += "L"+(currentX-this.tabmargins)+" "+this.y+"L"+currentX+" "+(this.y+this.tabmargins);
419 if (this.cornerRight == "round") {
420 d += "L"+(currentX-this.tabmargins)+" "+this.y+"a"+this.tabmargins+" "+this.tabmargins+" 0 0 1 "+this.tabmargins+" "+this.tabmargins;
422 d += "L"+currentX+" "+(this.y+this.tabheight);
423 //complete the path for tab window
424 d += "L"+(this.x+this.width)+" "+(this.y+this.tabheight)+"L"+(this.x+this.width)+" "+(this.y+this.height);
425 d += "L"+this.x+" "+(this.y+this.height);
426 if (currentLeft == this.x) {
430 d += "L"+this.x+" "+(this.y+this.tabheight)+"L"+currentLeft+" "+(this.y+this.tabheight)+"z";
432 //set modified path elements
433 this.tabwindows[i]["tabbg"].setAttributeNS(null,"d",d);
434 //increment currentX with the space between tabs
435 currentX += this.spaceBetweenTabs;
439 //deactivate a single tab
440 tabgroup.prototype.disableSingleTab = function(tabindex) {
441 if (tabindex >= this.tabTitles.length) {
442 this.outOfBoundMessage(tabindex);
445 this.tabwindows[tabindex]["activeStatus"] = false;
446 if (!this.tabwindows[tabindex]["tabClone"]) {
447 this.tabwindows[tabindex]["tabClone"] = this.tabwindows[tabindex]["tab"].cloneNode(false);
448 this.tabwindows[tabindex]["tabClone"].removeEventListener("click",this,false);
449 this.tabwindows[tabindex]["tabClone"].setAttributeNS(null,"fill-opacity",0.5);
450 if (this.tabwindows[tabindex]["tabClone"].hasAttributeNS(null,"cursor")){
451 this.tabwindows[tabindex]["tabClone"].removeAttributeNS(null,"cursor");
453 this.tabwindows[tabindex]["group"].appendChild(this.tabwindows[tabindex]["tabClone"]);
456 this.tabwindows[tabindex]["tabClone"].setAttributeNS(null,"display","inherit");
457 this.tabwindows[tabindex]["group"].appendChild(this.tabwindows[tabindex]["tabClone"]);
462 //deactivate all tabs
463 tabgroup.prototype.disableAllTabs = function() {
464 for (var i=0;i<this.tabTitles.length;i++) {
465 this.disableSingleTab(i);
469 //deactivate all tabs
470 tabgroup.prototype.enableAllTabs = function() {
471 for (var i=0;i<this.tabTitles.length;i++) {
472 this.enableSingleTab(i);
476 //activate a single tab
477 tabgroup.prototype.enableSingleTab = function(tabindex) {
478 if (tabindex >= this.tabTitles.length) {
479 this.outOfBoundMessage(tabindex);
482 this.tabwindows[tabindex]["activeStatus"] = true;
483 if (this.tabwindows[tabindex]["tabClone"]) {
484 this.tabwindows[tabindex]["tabClone"].setAttributeNS(null,"display","none");
489 //out of bound error message
490 tabgroup.prototype.outOfBoundMessage = function(tabindex) {
491 alert("Error ("+this.id+"): the 'tabindex' (value: "+tabindex+") is out of bounds!\nThe index nr is bigger than the number of tabs.");