1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * Creates an empty tab group.
 27  * @constructor
 28  * @class
 29  * A tab group is used to manage keyboard focus among a group of related visual 
 30  * elements. It is a tree structure consisting of elements and other tab groups.
 31  * <p>
 32  * The root tab group is the only one without a parent tab group, and is the one
 33  * that the application interacts with. Focus listeners register with the root
 34  * tab group. The root tab group tracks where focus is.
 35  * 
 36  * @param {string}	name					the name of this tab group
 37  *
 38  * @author Ross Dargahi
 39  */
 40 DwtTabGroup = function(name) {
 41 
 42 	this.__members = new AjxVector();
 43 	this.__parent = null;
 44 	this.__name = name;
 45 	this.__currFocusMember = null;
 46 	this.__evtMgr = new AjxEventMgr();
 47 
 48     DwtTabGroup.BY_NAME[name] = this;
 49 };
 50 
 51 DwtTabGroup.prototype.isDwtTabGroup = true;
 52 DwtTabGroup.prototype.toString = function() { return "DwtTabGroup"; };
 53 
 54 
 55 
 56 /** 
 57  * Exception string that is thrown when an operation is attempted
 58  * on a non-root tab group.
 59  */
 60 DwtTabGroup.NOT_ROOT_TABGROUP = "NOT ROOT TAB GROUP";
 61 
 62 DwtTabGroup.__changeEvt = new DwtTabGroupEvent();
 63 
 64 // Allow static access to any tab group by its name
 65 DwtTabGroup.getByName = function(name) {
 66     return DwtTabGroup.BY_NAME[name];
 67 };
 68 DwtTabGroup.BY_NAME = {};
 69 
 70 /**
 71  * Gets the name of this tab group.
 72  * 
 73  * @return	{string}	the tab group name
 74  */
 75 DwtTabGroup.prototype.getName = function() {
 76 	return this.__name;
 77 };
 78 
 79 /**
 80  * Adds a focus change listener to the root tab group. The listener is called
 81  * when the focus member changes. Note that the focus member hasn't actually
 82  * been focused yet - only its status within the tab group has changed. It is
 83  * up to the listener to implement the appropriate focus action.
 84  * 
 85  * @param {AjxListener} listener	a listener
 86  * 
 87  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
 88  */
 89 DwtTabGroup.prototype.addFocusChangeListener = function(listener) {
 90 
 91 	this.__checkRoot();		
 92 	this.__evtMgr.addListener(DwtEvent.STATE_CHANGE, listener);
 93 };
 94 
 95 /**
 96  * Removes a focus change listener from the root tab group.
 97  * 
 98  * @param {AjxListener} listener	a listener
 99  * 
100  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
101  */
102 DwtTabGroup.prototype.removeFocusChangeListener = function(listener) {
103 
104 	this.__checkRoot();		
105 	this.__evtMgr.removeListener(DwtEvent.STATE_CHANGE, listener);
106 };
107 
108 /**
109  * Adds a member to the tab group.
110  * 
111  * @param {Array|DwtControl|DwtTabGroup|HTMLElement} member	the member(s) to be added
112  * @param {number} [index] 		the index at which to add the member. If omitted, the member
113  * 		will be added to the end of the tab group
114  */
115 DwtTabGroup.prototype.addMember = function(member, index) {
116 
117     index = (index != null) ? index : this.__members.size();
118     var members = AjxUtil.collapseList(AjxUtil.toArray(member));
119 
120 	for (var i = 0, len = members.length; i < len; i++) {
121         var member = members[i];
122         this.__members.add(member, index + i);
123         // If adding a tab group, register me as its parent
124         if (member.isDwtTabGroup) {
125             member.newParent(this);
126         }
127 	}
128 };
129 
130 /**
131  * Resets all members of the tab group to the given arguments.
132  * 
133  * @param {Array|DwtControl|DwtTabGroup|HTMLElement} members	the member(s) for the tab group
134  */
135 DwtTabGroup.prototype.setMembers = function(members) {
136 	this.removeAllMembers();
137 	this.addMember(members);
138 };
139 
140 /**
141  * Adds a member to the tab group, positioned after another member.
142  * 
143  * @param {DwtControl|DwtTabGroup|HTMLElement} member 		the member to be added
144  * @param {DwtControl|DwtTabGroup|HTMLElement} afterMember 	the member after which to add <code>member</code>
145  */
146 DwtTabGroup.prototype.addMemberAfter = function(newMember, afterMember) {
147 
148 	this.addMember(newMember, this.__indexOfMember(afterMember) + 1);
149 };
150 
151 /**
152  * Adds a member to the tab group, positioned before another member.
153  * 
154  * @param {DwtControl|DwtTabGroup|HTMLElement} member 		the member to be added
155  * @param {DwtControl|DwtTabGroup|HTMLElement} beforeMember 	the member before which to add <code>member</code>
156  */
157 DwtTabGroup.prototype.addMemberBefore = function(newMember, beforeMember) {
158 
159 	this.addMember(newMember, this.__indexOfMember(beforeMember));
160 };
161 
162 /**
163  * This method removes a member from the tab group. If the member being removed
164  * is currently the focus member, then we will try to set focus to the
165  * previous member. If that fails, we will try the next member.
166  * 
167  * @param {DwtControl|DwtTabGroup|HTMLElement} member 	the member to be removed
168  * @param {boolean} [checkEnabled] 		if <code>true</code>, then make sure that if we have a newly focused member it is enabled
169  * @param {boolean} [skipNotify] 		if <code>true</code>, notification is not fired. This flag typically set by Dwt tab management framework when it is calling into this method
170  * @return {DwtControl|DwtTabGroup|HTMLElement}	the removed member or <code>null</code> if <code>oldMember</code> is not in the tab groups hierarchy
171  */
172 DwtTabGroup.prototype.removeMember = function(member, checkEnabled, skipNotify) {
173 
174 	return this.replaceMember(member, null, checkEnabled, skipNotify);
175 };
176 
177 /**
178  * Removes all members.
179  * 
180  */
181 DwtTabGroup.prototype.removeAllMembers = function() {
182 
183 	this.__members.removeAll();
184 };
185 
186 /**
187  * This method replaces a member in the tab group with a new member. If the member being
188  * replaced is currently the focus member, then we will try to set focus to the
189  * previous member. If that fails, we will try the next member.
190  * 
191  * @param {DwtControl|DwtTabGroup|HTMLElement} oldMember 	the member to be replaced
192  * @param {DwtControl|DwtTabGroup|HTMLElement} newMember 	the replacing member
193  * 		If this parameter is <code>null</code>, then this method effectively removes <code>oldMember</code>
194  * @param {boolean} [checkEnabled] 	if <code>true</code>, then make sure that if we have a newly focused
195  * 		member it is enabled
196  * @param {boolean} [skipNotify] if <code>true</code>, notification is not fired. This flag is
197  * 		typically set by the tab management framework when it is calling into this method
198  * @return {DwtControl|DwtTabGroup|HTMLElement}	replaced member or <code>null></code> if <code>oldMember</code> is not in the tab group
199  */
200 DwtTabGroup.prototype.replaceMember = function(oldMember, newMember, checkEnabled, skipNotify, focusItem, noFocus) {
201 
202 	var tg = this.__getTabGroupForMember(oldMember);
203 	if (!tg) {
204 		this.addMember(newMember);
205 		return null;
206 	}
207 
208 	/* If we are removing the current focus member, then we need to adjust the focus
209 	 * member index. If the tab group is empty as a result of the removal
210 	 */
211 	var root = this.__getRootTabGroup();
212 	var newFocusMember;
213 	if (focusItem) {
214 		newFocusMember = focusItem;
215 	}
216     else if (root.__currFocusMember === oldMember || (oldMember && oldMember.isDwtTabGroup && oldMember.contains(root.__currFocusMember))) {
217 		if (newMember) {
218 			newFocusMember = (newMember.isDwtTabGroup) ? newMember.getFirstMember() : newMember;
219 		}
220         else {
221 			newFocusMember = this.__getPrevMember(oldMember, checkEnabled);
222 			if (!newFocusMember) {
223 				newFocusMember =  this.__getNextMember(oldMember, checkEnabled);
224 			}
225 		}
226 	}
227 
228 	if (newFocusMember && !noFocus) {
229 		root.__currFocusMember = newFocusMember;
230 		this.__showFocusedItem(this.__currFocusMember, "replaceMember");
231 		if (!skipNotify) {
232 			this.__notifyListeners(newFocusMember);
233 		}
234 	}
235 
236 	if (newMember && newMember.isDwtTabGroup) {
237 		newMember.newParent(this);
238 	}
239 		
240 	return newMember ? this.__members.replaceObject(oldMember, newMember) : this.__members.remove(oldMember);
241 };
242 
243 /**
244  * Returns true if this tab group contains <code>member</code>.
245  * 
246  * @param {DwtControl|DwtTabGroup|HTMLElement} member	the member for which to search
247  * 
248  * @return {boolean}	<code>true</code> if the tab group contains member
249  */
250 DwtTabGroup.prototype.contains = function(member) {
251 
252 	return !!this.__getTabGroupForMember(member);
253 };
254 
255 /**
256  * Sets a new parent for this tab group.
257  * 
258  * @param {DwtTabGroup} newParent 	the new parent. If the parent is <code>null</code>, then this tabGroup is the root tab group.
259  */
260 DwtTabGroup.prototype.newParent = function(newParent) {
261 
262 	this.__parent = newParent;
263 };
264 
265 /**
266  * Gets the first member of the tab group.
267  * 
268  * @param {boolean} [checkEnabled]		if <code>true</code>, then return first enabled member
269  *
270  * @return {DwtControl|HTMLElement}	the first member of the tab group
271  */
272 DwtTabGroup.prototype.getFirstMember = function(checkEnabled) {
273 
274 	return this.__getLeftMostMember(checkEnabled);
275 };
276 
277 /**
278  * Gets the last member of the tab group.
279  * 
280  * @param {boolean} [checkEnabled]		if <code>true</code>, then return last enabled member
281  *
282  * @return {DwtControl|HTMLElement}	the last member of the tab group
283  */
284 DwtTabGroup.prototype.getLastMember = function(checkEnabled) {
285 
286 	return this.__getRightMostMember(checkEnabled);
287 };
288  
289 /**
290  * Returns the current focus member.
291  * 
292  * @return {DwtControl|HTMLElement}	current focus member
293  * 
294  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
295  */
296 DwtTabGroup.prototype.getFocusMember = function(){
297 
298 	this.__checkRoot();
299 	return this.__currFocusMember;
300 };
301 
302 /**
303  * Sets the current focus member. 
304  * 
305  * @param {DwtControl|HTMLElement} member 		the member to which to set focus
306  * @param {boolean} [checkEnabled] 	if <code>true</code>, then make sure the member is enabled
307  * @param {boolean} [skipNotify] if <code>true</code>, notification is not fired. This flag
308  * 		typically set by Dwt tab management framework when it is calling into this method
309  * 
310  * @return {boolean}	<code>true</code> if member was part of the tab group hierarchy, else false
311  *
312  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
313  */
314 DwtTabGroup.prototype.setFocusMember = function(member, checkEnabled, skipNotify) {
315 
316     if (!member) {
317         return false;
318     }
319 
320     if (member.isDwtTabGroup) {
321         DBG.println(AjxDebug.FOCUS, "DwtTabGroup SETFOCUSMEMBER to a DwtTabGroup: " + member + " / " + member.getName());
322         member = member.getFocusMember() || member.getFirstMember();
323     }
324 	this.__checkRoot();	
325 	if (!this.__checkEnabled(member, checkEnabled)) {
326 		return false;
327 	}
328 
329 	if (this.contains(member)) {
330 		this.__currFocusMember = member;
331 		this.__showFocusedItem(this.__currFocusMember, "setFocusMember");
332 		if (!skipNotify) {
333 			this.__notifyListeners(this.__currFocusMember);
334 		}
335 		return true;	
336 	}
337 
338 	return false;
339 };
340 
341 /**
342  * This method sets and returns the next focus member in this tab group. If there is no next
343  * member, sets and returns the first member in the tab group.
344  * 
345  * @param {boolean} [checkEnabled] 	if <code>true</code>, get the next enabled member
346  * @param {boolean} [skipNotify] if <code>true</code>, notification is not fired. This flag
347  * 		typically set by {@link Dwt} tab management framework when it is calling into this method
348  * 
349  * @return {DwtControl|HTMLElement}	new focus member or <code>null</code> if there is no focus member or if the focus
350  * 		member has not changed (i.e. only one member in the tabgroup)
351  *
352  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
353  */
354 DwtTabGroup.prototype.getNextFocusMember = function(checkEnabled, skipNotify) {
355 
356 	this.__checkRoot();		
357 	return this.__setFocusMember(true, checkEnabled, skipNotify);
358 };
359 
360 /**
361  * This method sets and returns the previous focus member in this tab group. If there is no
362  * previous member, sets and returns the last member in the tab group.
363  * 
364  * @param {boolean} [checkEnabled] 	if <code>true</code>, get the previously enabled member
365  * @param {boolean} [skipNotify] if <code>true</code>, notification is not fired. This flag
366  * 		typically set by Dwt tab management framework when it is calling into this method
367  * 
368  * @return {DwtControl|HTMLElement}	new focus member or <code>null</code> if there is no focus member or if the focus
369  * 		member has not changed (i.e. only one member in the tabgroup)
370  *
371  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
372  */
373 DwtTabGroup.prototype.getPrevFocusMember = function(checkEnabled, skipNotify) {
374 
375 	this.__checkRoot();		
376 	return this.__setFocusMember(false, checkEnabled, skipNotify);
377 };
378 
379 /**
380  * Resets the the focus member to the first element in the tab group.
381  * 
382  * @param {boolean} [checkEnabled] 	if <code>true</code>, then pick a enabled member to which to set focus
383  * @param {boolean} [skipNotify] if <code>true</code>, notification is not fired. This flag
384  * 		typically set by Dwt tab management framework when it is calling into this method
385  * 
386  * @return {DwtControl|HTMLElement}	the new focus member
387  *
388  * @throws DwtTabGroup.NOT_ROOT_TABGROUP
389  */
390 DwtTabGroup.prototype.resetFocusMember = function(checkEnabled, skipNotify) {
391 
392 	this.__checkRoot();
393 	var focusMember = this.__getLeftMostMember(checkEnabled);
394 	if ((focusMember != this.__currFocusMember) && !skipNotify) {
395 		this.__notifyListeners(this.__currFocusMember);
396 	}
397 	this.__showFocusedItem(this.__currFocusMember, "resetFocusMember");
398     DBG.println(AjxDebug.FOCUS, "DwtTabGroup RESETFOCUSMEMBER: " + focusMember);
399 	this.__currFocusMember = focusMember;
400 	
401 	return this.__currFocusMember;
402 };
403 
404 /**
405  * Pretty-prints the contents of the tab group to the browser console or the
406  * debug window.
407  *
408  * @param {number} [debugLevel]     if specified, dump to the debug window
409  *                                  at the given level.
410  */
411 DwtTabGroup.prototype.dump = function(debugLevel) {
412 
413 	if (debugLevel) {
414 		if (!window.AjxDebug || !window.DBG) {
415 			return;
416 		}
417 
418 		var logger = function(s) {
419 			var s = AjxStringUtil.convertToHtml(s);
420 			DBG.println(debugLevel, s);
421 		}
422 
423 		DwtTabGroup.__dump(this, logger, 0);
424 	} else if (window.console && window.console.log) {
425 		var r = [];
426 		DwtTabGroup.__dump(this, r.push.bind(r), 0);
427 		console.log(r.join('\n'));
428 	}
429 };
430 
431 /**
432  * Gets the size of the group.
433  * 
434  * @return	{number}	the size
435  */
436 DwtTabGroup.prototype.size = function() {
437 
438 	return this.__members.size();
439 };
440 
441 /**
442  * Returns the previous member in the tag group.
443  * 
444  * @private
445  */
446 DwtTabGroup.prototype.__getPrevMember = function(member, checkEnabled) {
447 
448 	var a = this.__members.getArray();
449 
450 	// Start working from the member to the immediate left, then keep going left
451 	for (var i = this.__lastIndexOfMember(member) - 1; i > -1; i--) {
452 		var prevMember = a[i];
453 		/* if sibling is not a tab group, then it is the previous child. If the
454 		 * sibling is a tab group, get its rightmost member.*/
455 		if (!prevMember.isDwtTabGroup) {
456 			if (this.__checkEnabled(prevMember, checkEnabled)) {
457 				return prevMember;
458 			}
459 		} else {
460 			prevMember = prevMember.__getRightMostMember(checkEnabled);
461 			if (this.__checkEnabled(prevMember, checkEnabled)) {
462 				return prevMember;
463 			}
464 		}
465 	}
466 
467 	/* If we have fallen through to here it is because the tab group only has 
468 	 * one member. So we roll up to the parent, unless we are at the root in 
469 	 * which case we return null. */
470 	return this.__parent ? this.__parent.__getPrevMember(this, checkEnabled) : null;
471 };
472 
473 /**
474  * Returns true if the given member can accept focus, or if there is no need to check.
475  * If we are checking, the member must be enabled and visible if it is a control, and
476  * enabled otherwise. A member may also set the "noTab" flag to take itself out of the
477  * tab hierarchy.
478  * 
479  * @private
480  */
481 DwtTabGroup.prototype.__checkEnabled = function(member, checkEnabled) {
482 
483 	if (!checkEnabled) {
484 		return true;
485 	}
486 
487 	if (!member || member.noTab) {
488 		return false;
489 	}
490 
491 	if (member.isDwtControl ? !member.getEnabled() : member.disabled) {
492 		return false;
493 	}
494 
495 	if (member.isDwtControl) {
496 		member = member.getHtmlElement();
497 	}
498 
499 	var loc = Dwt.getLocation(member);
500 	if (loc.x === null || loc.y === null || loc.x === Dwt.LOC_NOWHERE || loc.y === Dwt.LOC_NOWHERE) {
501 		return false;
502 	}
503 
504 	var size = Dwt.getSize(member);
505 	if (!size || size.x === 0 || size.y === 0) {
506 		return false;
507 	}
508 
509 	if (member.nodeName && member.nodeName.toLowerCase() === "body") {
510 		return true;
511 	}
512 	return (Dwt.getZIndex(member, true) > Dwt.Z_HIDDEN &&
513 	        Dwt.getVisible(member) && Dwt.getVisibility(member));
514 };
515 
516 DwtTabGroup.prototype.__indexOfMember = function(member) {
517 
518     return this.__members.indexOf(member);
519 };
520 
521 DwtTabGroup.prototype.__lastIndexOfMember = function(member) {
522 
523     return this.__members.lastIndexOf(member);
524 };
525 
526 /**
527  * Sets and returns the next member in the tag group.
528  * 
529  * @private
530  */
531 DwtTabGroup.prototype.__getNextMember = function(member, checkEnabled) {
532 
533 	var a = this.__members.getArray();
534 	var sz = this.__members.size();
535 
536 	// Start working from the member rightwards
537 	for (var i = this.__indexOfMember(member) + 1; i < sz; i++) {
538 		var nextMember = a[i];
539 		/* if sibling is not a tab group, then it is the next child. If the
540 		 * sibling is a tab group, get its leftmost member.*/
541 		if (!nextMember.isDwtTabGroup) {
542 			if (this.__checkEnabled(nextMember, checkEnabled)) {
543 				return nextMember;
544 			}
545 		}
546         else {
547 			nextMember = nextMember.__getLeftMostMember(checkEnabled);
548 			if (this.__checkEnabled(nextMember, checkEnabled)) {
549 				return nextMember;
550 			}
551 		}
552 	}
553 
554 	/* If we have fallen through to here it is because the tab group only has 
555 	 * one member or we are at the end of the list. So we roll up to the parent, 
556 	 * unless we are at the root in which case we return null. */
557 	return this.__parent ? this.__parent.__getNextMember(this, checkEnabled) : null;
558 };
559 
560 /**
561  * Finds the rightmost member of the tab group. Will recurse down
562  * into contained tab groups if necessary.
563  * @private
564  */
565 DwtTabGroup.prototype.__getRightMostMember = function(checkEnabled) {
566 
567 	var a = this.__members.getArray();
568 	var member = null;
569 	
570 	/* Work backwards from the rightmost member. If the member is a tab group, then
571 	 * recurse into it. If member is not a tab group, return it as it is the 
572 	 * rightmost element. */
573 	for (var i = this.__members.size() - 1; i >= 0; i--) {
574 		member = a[i]
575 		if (!member.isDwtTabGroup) {
576 			if (this.__checkEnabled(member, checkEnabled)) {
577                 break;
578             }
579 		}
580         else {
581 			member = member.__getRightMostMember(checkEnabled);
582 			if (this.__checkEnabled(member, checkEnabled)) {
583                 break;
584             }
585 		}
586 	}
587 
588 	return this.__checkEnabled(member, checkEnabled) ? member : null;
589 };
590 
591 /**
592  *  Finds the leftmost member of the tab group. Will recurse down
593  * into contained tab groups if necessary.
594  * @private
595  */
596 DwtTabGroup.prototype.__getLeftMostMember = function(checkEnabled) {
597 
598 	var sz = this.__members.size();
599 	var a = this.__members.getArray();
600 	var member = null;
601 
602 	/* Work forwards from the leftmost member. If the member is a tabgroup, then
603 	 * recurse into it. If member is not a tabgroup, return it as it is the 
604 	 * rightmost element */
605 	for (var i = 0; i < sz; i++) {
606 		member = a[i]
607 		if (!member.isDwtTabGroup) {
608 			if  (this.__checkEnabled(member, checkEnabled)) {
609                 break;
610             }
611 		}
612         else {
613 			member = member.__getLeftMostMember(checkEnabled);
614 			if (this.__checkEnabled(member, checkEnabled)) {
615                 break;
616             }
617 		}
618 	}
619 
620 	return this.__checkEnabled(member, checkEnabled) ? member : null;
621 };
622 
623 /**
624  * Notifies focus change listeners.
625  * @private
626  */
627 DwtTabGroup.prototype.__notifyListeners = function(newFocusMember) {
628 
629 	// Only the root tab group will issue notifications
630 	var rootTg = this.__getRootTabGroup();
631 	if (rootTg.__evtMgr) {
632 		var evt = DwtTabGroup.__changeEvt;
633 		evt.reset();
634 		evt.tabGroup = this;
635 		evt.newFocusMember = newFocusMember;
636 		rootTg.__evtMgr.notifyListeners(DwtEvent.STATE_CHANGE, evt);
637 	}
638 };
639 
640 /**
641  * @private
642  */
643 DwtTabGroup.prototype.__getRootTabGroup = function() {
644 
645 	var root = this;
646 	while (root.__parent) {
647 		root = root.__parent;
648 	}
649 	
650 	return root;
651 }
652 
653 DwtTabGroup.DUMP_INDENT = '|\t';
654 
655 /**
656  * @private
657  */
658 DwtTabGroup.__dump = function(tg, logger, level) {
659 
660 	var myIndent = AjxStringUtil.repeat(DwtTabGroup.DUMP_INDENT, level);
661 
662 	logger(myIndent + "TABGROUP: " + tg.__name);
663 
664 	myIndent += DwtTabGroup.DUMP_INDENT;
665 
666 	var sz = tg.__members.size();
667 	var a = tg.__members.getArray();
668 	for (var i = 0; i < sz; i++) {
669         var m = a[i];
670 		if (m.isDwtTabGroup) {
671 			DwtTabGroup.__dump(m, logger, level + 1);
672 		}
673         else {
674 			var desc = m.nodeName ? [ m.nodeName, m.id, m.className ].join(' ') : [ String(m), m._htmlElId ].join(' ');
675 			if (m.noTab) {
676 				desc += ' - no tab!';
677 			}
678 			logger(myIndent + desc);
679 		}
680 	}
681 };
682 
683 /**
684  * Sets the next or previous focus member.
685  * @private
686  */
687 DwtTabGroup.prototype.__setFocusMember = function(next, checkEnabled, skipNotify) {
688 
689 	// If there is currently no focus member, then reset to the first member and return
690 	if (!this.__currFocusMember) {
691 		return this.resetFocusMember(checkEnabled, skipNotify);
692 	}
693 	
694 	var tabGroup = this.__getTabGroupForMember(this.__currFocusMember);
695 	if (!tabGroup) {
696 		DBG.println(AjxDebug.DBG1, "tab group not found for focus member: " + this.__currFocusMember);
697 		return null;
698 	}
699 	var m = next ? tabGroup.__getNextMember(this.__currFocusMember, checkEnabled)
700 				 : tabGroup.__getPrevMember(this.__currFocusMember, checkEnabled);
701 
702 	if (!m) {
703         // wrap around
704 		m = next ? this.__getLeftMostMember(checkEnabled)
705 				 : this.__getRightMostMember(checkEnabled);
706 
707 		// Test for the case where there is only one member in the tabgroup
708 		if (m == this.__currFocusMember) {
709 			return null;
710 		}
711 	}
712 
713 	this.__currFocusMember = m;
714 	
715 	this.__showFocusedItem(this.__currFocusMember, "__setFocusMember");
716 	if (!skipNotify) {
717 		this.__notifyListeners(this.__currFocusMember);
718 	}
719 	
720 	return this.__currFocusMember;
721 };
722 
723 /**
724  * Returns the tab group from within this tab group's hierarchy that contains the given member. Traverses the tree top-down.
725  *
726  * @private
727  */
728 DwtTabGroup.prototype.__getTabGroupForMember = function(member) {
729 
730 	if (!member) {
731         return null;
732     }
733 
734     var a = this.__members.getArray(),
735         ln = a.length, i, m;
736 
737 	for (i = 0; i < ln; i++) {
738 		m = a[i];
739 		if (m === member) {
740 			return this;
741 		}
742         else if (m.isDwtTabGroup && (m = m.__getTabGroupForMember(member))) {
743 			return m;
744 		}
745 	}
746 	return null;
747 };
748 
749 /**
750  * Throws an exception if this is not the root tab group.
751  * 
752  * @private
753  */
754 DwtTabGroup.prototype.__checkRoot = function() {
755 
756 	if (this.__parent) {
757         DBG.println(AjxDebug.DBG1, "DwtTabGroup NOT_ROOT_TABGROUP: " + this.getName());
758 //		throw DwtTabGroup.NOT_ROOT_TABGROUP;
759 	}
760 };
761 
762 // Prints out a debug line describing the currently focused member
763 DwtTabGroup.prototype.__showFocusedItem = function(item, caller) {
764 
765 	if (item && window.AjxDebug && window.DBG) {
766 		var callerText = caller ? "DwtTabGroup." + caller + ": " : "",
767 			idText = " [" + (item.isDwtControl ? item._htmlElId : item.id) + "] ",
768             itemText = (item.nodeName || item) + " " + idText,
769 			otherText = (item.getTitle && item.getTitle()) || (item.getText && item.getText()) || "",
770 			fullText = itemText + otherText;
771 
772 		DBG.println(AjxDebug.FOCUS, callerText + "current focus member is now " + itemText);
773 		DBG.println(AjxDebug.FOCUS1, "Focus: " + fullText);
774 	}
775 };
776