1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines a toolbar.
 27  *
 28  */
 29 
 30 /**
 31  * Creates a toolbar.
 32  * @constructor
 33  * @class
 34  * Creates a toolbar. Components must be added via the <code>add*()</code> functions.
 35  * A toolbar is a horizontal or vertical strip of widgets (usually buttons).
 36  *
 37  * @author Ross Dargahi
 38  * 
 39  * @param {hash}	params		a hash of parameters
 40  * @param	{DwtComposite}	params.parent	the parent widget
 41  * @param	{string}	params.className				the CSS class
 42  * @param	{DwtToolBar.HORIZ_STYLE|DwtToolBar.VERT_STYLE}	params.posStyle		the positioning style
 43  * @param	{constant}	params.style					the menu style
 44  * @param	{number}	params.index 				the index at which to add this control among parent's children
 45  * 
 46  * @extends	DwtComposite
 47  */
 48 DwtToolBar = function(params) {
 49 	if (arguments.length == 0) { return; }
 50 	params = Dwt.getParams(arguments, DwtToolBar.PARAMS);
 51 
 52 	params.className = params.className || "ZToolbar";
 53 	DwtComposite.call(this, params);
 54 
 55 	// since we attach event handlers at the toolbar level, make sure we don't double up on
 56 	// handlers when we have a toolbar within a toolbar
 57 	if (params.parent instanceof DwtToolBar) {
 58 		this._hasSetMouseEvents = params.parent._hasSetMouseEvents;
 59 	}
 60 	if (params.handleMouse !== false && !this._hasSetMouseEvents) {
 61 		var events = [DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEUP, DwtEvent.ONCLICK];
 62 		if (!AjxEnv.isIE) {
 63 			events.push(DwtEvent.ONMOUSEOVER, DwtEvent.ONMOUSEOUT);
 64 		}
 65 		this._setEventHdlrs(events);
 66 		this._hasSetMouseEvents = true;
 67 	}
 68 
 69 	this._style = params.style || DwtToolBar.HORIZ_STYLE;
 70     this._createHtml();
 71 
 72     this._numFillers = 0;
 73 	this._curFocusIndex = 0;
 74 
 75     // Let toolbar be a single tab stop, then manage focus among items using arrow keys
 76     this.tabGroupMember = this;
 77 
 78     this._keyMapName = (this._style == DwtToolBar.HORIZ_STYLE) ? DwtKeyMap.MAP_TOOLBAR_HORIZ : DwtKeyMap.MAP_TOOLBAR_VERT;
 79 };
 80 
 81 DwtToolBar.PARAMS = ["parent", "className", "posStyle", "style", "index", "id"];
 82 
 83 DwtToolBar.prototype = new DwtComposite;
 84 DwtToolBar.prototype.constructor = DwtToolBar;
 85 DwtToolBar.prototype.role = 'toolbar';
 86 
 87 DwtToolBar.prototype.isDwtToolBar = true;
 88 DwtToolBar.prototype.toString = function() { return "DwtToolBar"; };
 89 
 90 //
 91 // Constants
 92 //
 93 
 94 /**
 95  * Defines the "horizontal" style.
 96  */
 97 DwtToolBar.HORIZ_STYLE	= 1;
 98 /**
 99  * Defines the "vertical" style.
100  */
101 DwtToolBar.VERT_STYLE	= 2;
102 
103 DwtToolBar.FIRST_ITEM    = "ZFirstItem";
104 DwtToolBar.LAST_ITEM     = "ZLastItem";
105 DwtToolBar.SELECTED_NEXT = DwtControl.SELECTED + "Next";
106 DwtToolBar.SELECTED_PREV = DwtControl.SELECTED + "Prev";
107 DwtToolBar._NEXT_PREV_RE = new RegExp(
108     "\\b" +
109     [ DwtToolBar.SELECTED_NEXT, DwtToolBar.SELECTED_PREV ].join("|") +
110     "\\b", "g"
111 );
112 
113 //
114 // Data
115 //
116 
117 // main template
118 
119 DwtToolBar.prototype.TEMPLATE = "dwt.Widgets#ZToolbar";
120 
121 // item templates
122 
123 DwtToolBar.prototype.ITEM_TEMPLATE = "dwt.Widgets#ZToolbarItem";
124 DwtToolBar.prototype.SEPARATOR_TEMPLATE = "dwt.Widgets#ZToolbarSeparator";
125 DwtToolBar.prototype.SPACER_TEMPLATE = "dwt.Widgets#ZToolbarSpacer";
126 DwtToolBar.prototype.FILLER_TEMPLATE = "dwt.Widgets#ZToolbarFiller";
127 
128 // static data
129 
130 DwtToolBar.__itemCount = 0;
131 
132 //
133 // Public methods
134 //
135 
136 DwtToolBar.prototype.dispose =
137 function() {
138 	DwtComposite.prototype.dispose.call(this);
139 	this._itemsEl = null;
140 	this._prefixEl = null;
141 	this._suffixEl = null;
142 };
143 
144 /**
145  * Gets the item.
146  * 
147  * @param	{int}		index	the index
148  * @return	{Object}	the item
149  */
150 DwtToolBar.prototype.getItem =
151 function(index) {
152 	return this._children.get(index);
153 };
154 
155 /**
156  * Gets the item count.
157  * 
158  * @return	{number}	the size of the children items
159  */
160 DwtToolBar.prototype.getItemCount =
161 function() {
162 	return this._children.size();
163 };
164 
165 /**
166  * Gets the items.
167  * 
168  * @return	{array}	an array of children items
169  */
170 DwtToolBar.prototype.getItems =
171 function() {
172 	return this._children.getArray();
173 };
174 
175 // item creation
176 /**
177  * Adds a spacer.
178  * 
179  * @param	{string}	className	the spacer CSS class name
180  * @param	{number}	index		the index for the spacer
181  * @return	{Object}	the newly added element
182  */
183 DwtToolBar.prototype.addSpacer =
184 function(className, index) {
185 	var spacer = new DwtToolBarSpacer({
186 		parent: this,
187 		index: index,
188 		className: className,
189 		toolbarItemTemplate: this.SPACER_TEMPLATE,
190 		id: this._htmlElId + '_spacer' + DwtToolBar.__itemCount
191 	});
192 
193 	return spacer;
194 };
195 
196 /**
197  * Adds a separator.
198  * 
199  * @param	{string}	className	the separator CSS class name
200  * @param	{number}	index		the index for the separator
201  * @return	{Object}	the newly added element
202  */
203 DwtToolBar.prototype.addSeparator =
204 function(className, index) {
205 	var sep = new DwtToolBarSpacer({
206 		parent: this,
207 		index: index,
208 		className: className,
209 		toolbarItemTemplate: this.SEPARATOR_TEMPLATE,
210 		id: this._htmlElId + '_separator' + DwtToolBar.__itemCount
211 	});
212 
213 	return sep;
214 };
215 
216 /**
217  * Adds a filler.
218  * 
219  * @param	{string}	className	the CSS class name
220  * @param	{number}	index		the index for the filler
221  * @return	{Object}	the newly added element
222  */
223 DwtToolBar.prototype.addFiller =
224 function(className, index) {
225 	var filler = new DwtToolBarSpacer({
226 		parent: this,
227 		index: index,
228 		className: className,
229 		toolbarItemTemplate: this.FILLER_TEMPLATE,
230 		id: this._htmlElId + '_filler' + DwtToolBar.__itemCount
231 	});
232 
233 	return filler;
234 };
235 
236 // DwtComposite methods
237 
238 /**
239  * Adds a child item.
240  * 
241  * @param	{Object}	child	the child item
242  * @param	{number}	index		the index for the child
243  */
244 DwtToolBar.prototype.addChild = function(child, index) {
245 
246 	// get the reference element for insertion
247 	var placeControl = this.getChild(index);
248 	var placeEl = placeControl ?
249 		placeControl.getHtmlElement().parentNode : this._suffixEl;
250 
251 	// actually add the child
252 	DwtComposite.prototype.addChild.apply(this, arguments);
253 
254 	// create and insert the item element
255     if (this._itemsEl) {
256         var itemEl = this._createItemElement(child.toolbarItemTemplate);
257         this._itemsEl.insertBefore(itemEl, placeEl);
258     }
259 
260 	// finally, move the child to the item
261 	child.reparentHtmlElement(itemEl);
262 };
263 
264 DwtToolBar.prototype.removeChild = function(child) {
265 
266 	var item = child.getHtmlElement().parentNode;
267 
268 	DwtComposite.prototype.removeChild.apply(this, arguments);
269 
270 	if (item && item.parentNode) {
271 		item.parentNode.removeChild(item);
272 	}
273 };
274 
275 // keyboard nav
276 
277 /**
278  * Gets the key map name.
279  * 
280  * @return	{string}	the key map name
281  */
282 DwtToolBar.prototype.getKeyMapName =
283 function() {
284     return this._keyMapName;
285 };
286 
287 DwtToolBar.prototype.handleKeyAction = function(actionCode, ev) {
288 
289     // if the user typed a left or right arrow in an INPUT, only go to the previous/next item if the cursor is at the
290     // beginning or end of the text in the INPUT
291 	var curFocusIndex = this._curFocusIndex,
292 	    numItems = this.getItemCount(),
293         input = ev && ev.target && ev.target.nodeName.toLowerCase() === 'input' ? ev.target : null,
294         cursorPos = input && input.selectionStart,
295         valueLen = input && input.value && input.value.length;
296 
297 	if (numItems < 2) {
298 		return false;
299 	}
300 
301     DBG.println(AjxDebug.FOCUS, 'DwtToolBar HANDLEKEYACTION: cur focus index = ' + curFocusIndex);
302 
303 	switch (actionCode) {
304 
305 		case DwtKeyMap.PREV:
306             if (input && cursorPos > 0) {
307                 ev.forcePropagate = true;   // don't let subsequent handlers block propagation
308                 return false;
309             }
310 			else if (curFocusIndex > 0) {
311 				this._moveFocus(true, ev);
312 				return true;
313 			}
314 			break;
315 
316 		case DwtKeyMap.NEXT:
317             if (input && cursorPos < valueLen) {
318                 ev.forcePropagate = true;   // don't let subsequent handlers block propagation
319                 return false;
320             }
321 			else if (curFocusIndex < numItems - 1) {
322 				this._moveFocus(false);
323 				return true;
324 			}
325 			break;
326 
327 		default:
328 			// pass everything else to currently focused item
329             var item = this._getCurrentFocusItem();
330 			if (item) {
331 				return item.handleKeyAction(actionCode, ev);
332 			}
333 	}
334 
335 	return true;
336 };
337 
338 //
339 // Protected methods
340 //
341 
342 // utility
343 
344 /**
345  * @private
346  */
347 DwtToolBar.prototype._createItemId =
348 function(id) {
349     id = id || this._htmlElId;
350     var itemId = [id, "item", ++DwtToolBar.__itemCount].join("_");
351     return itemId;
352 };
353 
354 // html creation
355 
356 /**
357  * @private
358  */
359 DwtToolBar.prototype._createHtml = function() {
360 
361     var data = { id: this._htmlElId };
362     this._createHtmlFromTemplate(this.TEMPLATE, data);
363     this._itemsEl = document.getElementById(data.id + "_items");
364     this._prefixEl = document.getElementById(data.id + "_prefix");
365     this._suffixEl = document.getElementById(data.id + "_suffix");
366 };
367 
368 /**
369  * @private
370  */
371 DwtToolBar.prototype._createItemElement =
372 function(templateId) {
373         templateId = templateId || this.ITEM_TEMPLATE;
374         var data = { id: this._htmlElId, itemId: this._createItemId() };
375         var html = AjxTemplate.expand(templateId, data);
376 
377         // the following is like scratching your back with your heel:
378         //     var fragment = Dwt.toDocumentFragment(html, data.itemId);
379         //     return (AjxUtil.getFirstElement(fragment));
380 
381         var cont = AjxStringUtil.calcDIV();
382         cont.innerHTML = html;
383         return cont.firstChild.rows[0].cells[0]; // DIV->TABLE->TR->TD
384 };
385 
386 /**
387  * Focuses the current item.
388  *
389  * @param {DwtControl}  item    (optional) specific toolbar item to focus
390  */
391 DwtToolBar.prototype.focus = function(item) {
392 
393     DBG.println(AjxDebug.FOCUS, "DwtToolBar: FOCUS " + [this, this._htmlElId].join(' / '));
394 
395     this._setMenuKey();
396 
397 	item = item || this._getCurrentFocusItem();
398 	if (item && this._canFocusItem(item)) {
399         this._curFocusIndex = this.__getButtonIndex(item);
400 		return item.focus();
401 	}
402     else {
403 		// if current item isn't focusable, find first one that is
404 		return this._moveFocus(false);
405 	}
406 };
407 
408 /**
409  * Blurs the current item.
410  *
411  * @param {DwtControl}  item    (optional) specific toolbar item to blur
412  *
413  * @private
414  */
415 DwtToolBar.prototype.blur = function(item) {
416 
417     DBG.println(AjxDebug.FOCUS, "DwtToolBar: BLUR");
418 	item = item || this._getCurrentFocusItem();
419 	if (item && item.blur) {
420 		item.blur();
421 	}
422 };
423 
424 /**
425  * Returns the item at the given index, as long as it can accept focus.
426  * For now, we only move focus to simple components like buttons. Also,
427  * the item must be enabled and visible.
428  *
429  * @param {DwtControl}	item		an item within toolbar
430  * @return	{boolean}	true if the item can be focused
431  * 
432  * @private
433  */
434 DwtToolBar.prototype._canFocusItem = function(item) {
435 
436 	if (!item)									{ return false; }
437 	if (!item.focus)							{ return false; }
438 	if (item.isDwtToolBarSpacer)				{ return false; }
439 	if (item.getEnabled && !item.getEnabled())	{ return false; }
440 	if (item.getVisible && !item.getVisible())	{ return false; }
441 	if (item.isDwtText && !item.getText())		{ return false; }
442 
443 	return true;
444 };
445 
446 DwtToolBar.prototype._getCurrentFocusItem = function() {
447 
448     return this.getItem(this._curFocusIndex);
449 };
450 
451 DwtToolBar.prototype.getEnabled = function() {
452 	// toolbars delegate focus to their children, and so are only 'enabled' --
453 	// i.e. focusable -- when at least one child is
454 	return this._children.some(function(child) {
455 		return this._canFocusItem(child);
456 	}, this);
457 };
458 
459 /**
460  * Moves focus to next or previous item that can take focus.
461  *
462  * @param {boolean}	back		if <code>true</code>, move focus to previous item
463  * 
464  * @private
465  */
466 DwtToolBar.prototype._moveFocus = function(back) {
467 
468 	var index = this._curFocusIndex,
469 	    maxIndex = this.getItemCount() - 1,
470 	    item = null,
471         found = false;
472 
473     index = back ? index - 1 : index + 1;
474     while (!found && index >= 0 && index <= maxIndex) {
475         item = this.getItem(index);
476         if (this._canFocusItem(item)) {
477             found = true;
478         }
479         index = back ? index - 1 : index + 1;
480 	}
481 
482 	if (item && found) {
483 		this.blur();
484 		this.focus(item);
485 	}
486 
487     return item;
488 };
489 
490 // make sure the key for expanding a button submenu matches our style
491 DwtToolBar.prototype._setMenuKey = function() {
492 
493     if (!this._submenuKeySet) {
494         var kbm = this.shell.getKeyboardMgr();
495         if (kbm.isEnabled()) {
496             var kmm = kbm.__keyMapMgr;
497             if (kmm) {
498                 if (this._style == DwtToolBar.HORIZ_STYLE) {
499                     kmm.removeMapping(DwtKeyMap.MAP_BUTTON, "ArrowRight");
500                     kmm.setMapping(DwtKeyMap.MAP_BUTTON, "ArrowDown", DwtKeyMap.SUBMENU);
501                 } else {
502                     kmm.removeMapping(DwtKeyMap.MAP_BUTTON, "ArrowDown");
503                     kmm.setMapping(DwtKeyMap.MAP_BUTTON, "ArrowRight", DwtKeyMap.SUBMENU);
504                 }
505                 kmm.reloadMap(DwtKeyMap.MAP_BUTTON);
506             }
507         }
508         this._submenuKeySet = true;
509     }
510 };
511 
512 // Updates internal index when a child gets focus
513 DwtToolBar.prototype._childFocusListener = function(ev) {
514 
515     DBG.println(AjxDebug.FOCUS, "DwtToolBar CHILDFOCUSLISTENER: " + [ ev.dwtObj, ev.dwtObj._htmlElId ].join(' / '));
516     this._curFocusIndex = this.__getButtonIndex(ev.dwtObj);
517 };
518 
519 /**
520  * @private
521  */
522 DwtToolBar.prototype.__markPrevNext = function(id, opened) {
523 
524     var index = this.__getButtonIndex(id);
525     var prev = this.getChild(index - 1);
526     var next = this.getChild(index + 1);
527 
528     if (opened) {
529         if (prev) {
530             Dwt.delClass(prev.getHtmlElement(), DwtToolBar._NEXT_PREV_RE, DwtToolBar.SELECTED_PREV);
531         }
532         if (next) {
533             Dwt.delClass(next.getHtmlElement(), DwtToolBar._NEXT_PREV_RE, DwtToolBar.SELECTED_NEXT);
534         }
535     }
536     else {
537         if (prev) {
538             Dwt.delClass(prev.getHtmlElement(), DwtToolBar._NEXT_PREV_RE);
539         }
540         if (next) {
541             Dwt.delClass(next.getHtmlElement(), DwtToolBar._NEXT_PREV_RE);
542         }
543     }
544 
545     // hack: mark the first and last items so we can style them specially
546     //	MOW note: this should really not be here, as it only needs to be done once,
547     //				but I'm not sure where to put it otherwise
548     var first = this.getChild(0);
549     if (first) {
550         Dwt.addClass(first.getHtmlElement(), DwtToolBar.FIRST_ITEM);
551     }
552 
553     var last = this.getChild(this.getItemCount()-1);
554     if (last) {
555         Dwt.addClass(last.getHtmlElement(), DwtToolBar.LAST_ITEM);
556     }
557 };
558 
559 /**
560  * Find the array index of a toolbar button.
561  *
562  * @param id {String|DwtControl}    item ID, or item
563  *
564  * @return {number} Index of the id in the array, or -1 if the id does not exist.
565  * @private
566  */
567 DwtToolBar.prototype.__getButtonIndex = function(id) {
568 
569     var item = AjxUtil.isString(id) ? DwtControl.fromElementId(id) : id;
570 
571     for (var i = 0; i <= this.getItemCount() - 1; i++) {
572         if (item === this.getItem(i)) {
573             return i;
574         }
575     }
576 
577     return -1;
578 };
579 
580 //
581 // Classes
582 //
583 
584 /**
585  * Creates a tool bar button.
586  * @constructor
587  * @class
588  * This class represents a toolbar button.
589  * 
590  * @param	{hash}		params		a hash of parameters
591  * @param {DwtComposite}	parent		the parent widget
592  * @param {constant}	style				the menu style
593  * @param {string}	className				the CSS class
594  * @param {DwtToolBar.HORIZ_STYLE|DwtToolBar.VERT_STYLE}	posStyle		the positioning style
595  * @param {Object}	actionTiming 	the action timing
596  * @param {string}	id 	the id
597  * @param {number}	index 				the index at which to add this control among parent's children
598  *
599  * @extends	DwtButton
600  */
601 DwtToolBarButton = function(params) {
602 	if (arguments.length == 0) { return; }
603 	var params = Dwt.getParams(arguments, DwtToolBarButton.PARAMS);
604 	params.className = params.className || "ZToolbarButton";
605 	DwtButton.call(this, params);
606 };
607 
608 DwtToolBarButton.PARAMS = ["parent", "style", "className", "posStyle", "actionTiming", "id", "index"];
609 
610 DwtToolBarButton.prototype = new DwtButton;
611 DwtToolBarButton.prototype.constructor = DwtToolBarButton;
612 
613 DwtToolBarButton.prototype.isDwtToolBarButton = true;
614 DwtToolBarButton.prototype.toString = function() { return "DwtToolBarButton"; };
615 
616 // Data
617 DwtToolBarButton.prototype.TEMPLATE = "dwt.Widgets#ZToolbarButton";
618 
619 // Spacing controls (spacer, separator, filler)
620 DwtToolBarSpacer = function(params) {
621 	if (arguments.length == 0) { return; }
622 	this._noFocus = this.noTab = true;
623 	this.toolbarItemTemplate = params.toolbarItemTemplate;
624 	DwtControl.call(this, params);
625 };
626 
627 DwtToolBarSpacer.prototype = new DwtControl;
628 
629 DwtToolBarSpacer.prototype.constructor = DwtToolBarSpacer;
630 
631 DwtToolBarSpacer.prototype.isDwtToolBarSpacer = true;
632 DwtToolBarSpacer.prototype.toString = function() { return 'DwtToolBarSpacer'; };
633 
634 DwtToolBarSpacer.prototype.role = 'separator';
635