1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2007, 2008, 2009, 2010, 2013, 2014, 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) 2007, 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 /**
 24  * Creates an empty accordion widget.
 25  * @constructor
 26  * @class
 27  * This class implements an accordion widget, which is a stack of expandable
 28  * accordion headers. Clicking on an accordion header's button expands it in
 29  * place.
 30  *
 31  * @author Parag Shah
 32  *
 33  * @param {DwtControl}	parent	the parent widget
 34  * @param {string}	className	the CSS class
 35  * @param {Dwt.STATIC_STYLE|Dwt.ABSOLUTE_STYLE|Dwt.RELATIVE_STYLE}	posStyle	the positioning style
 36  * 
 37  * @extends		DwtComposite
 38  */
 39 DwtAccordion = function(parent, className, posStyle) {
 40 
 41 	if (arguments.length == 0) return;
 42 	DwtComposite.call(this, {parent:parent, className:className, posStyle:(posStyle || Dwt.ABSOLUTE_STYLE)});
 43 
 44 	this._initialize(className);
 45 };
 46 
 47 DwtAccordion.prototype = new DwtComposite;
 48 DwtAccordion.prototype.constructor = DwtAccordion;
 49 
 50 
 51 // Public Methods
 52 
 53 /**
 54  * Returns a string representation of the object.
 55  * 
 56  * @return		{string}		a string representation of the object
 57  */
 58 DwtAccordion.prototype.toString =
 59 function() {
 60 	return "DwtAccordion";
 61 };
 62 
 63 /**
 64  * Adds an item to the accordion, in the form of a table row.
 65  *
 66  * @param {hash}	params		a hash of parameters
 67  * @param {string}      params.title			the text for accordion header
 68  * @param {hash}      params.data			the item data
 69  * @param {string}      params.icon			the icon
 70  * @param {boolean}      params.hideHeader	if <code>true</code>, do not show header (ideal when there's only one visible header item)
 71  */
 72 DwtAccordion.prototype.addAccordionItem =
 73 function(params) {
 74 
 75 	if (!this.isListenerRegistered(DwtEvent.CONTROL)) {
 76 		this.addControlListener(new AjxListener(this, this._controlListener));
 77 	}
 78 
 79 	var itemNum = this.__ITEMCOUNT++;
 80 	var item = new DwtAccordionItem(itemNum, params.title, params.data, this);
 81 	var subs = {
 82 		id: this._htmlElId,
 83 		itemNum: itemNum,
 84 		title: params.title,
 85 		icon: params.icon
 86 	};
 87 
 88 	// append new accordion item
 89 	var row = this._table.insertRow(-1);
 90 	var cell = row.insertCell(-1);
 91 	cell.id = this._htmlElId + "_cell_" + itemNum;
 92 	cell.className = "ZAccordionCell";
 93 	cell.innerHTML = AjxTemplate.expand("dwt.Widgets#ZAccordionItem", subs);
 94 
 95 	// add onclick event handler to header DIV
 96 	var headerDiv = document.getElementById(this._htmlElId + "_header_" + itemNum);
 97 	if (params.hideHeader) {
 98 		Dwt.setVisible(headerDiv, false);
 99 	} else {
100 		headerDiv.onclick = AjxCallback.simpleClosure(this._handleOnClickHeader, this, item);
101 		headerDiv.oncontextmenu = AjxCallback.simpleClosure(this._handleOnRightClickHeader, this, item);
102 		headerDiv.onmouseover = AjxCallback.simpleClosure(this._handleOnMouseoverHeader, this, item);
103 		headerDiv.onmouseout = AjxCallback.simpleClosure(this._handleOnMouseoutHeader, this, item);
104 	}
105 	this._items.push(item);
106 
107 	return item;
108 };
109 
110 /**
111  * Gets the ordered list of accordion items.
112  * 
113  * @return	{array}	an array of {@link DwtAccordionItem} objects
114  */
115 DwtAccordion.prototype.getItems =
116 function() {
117 	return this._items;
118 };
119 
120 /**
121  * Gets the accordion item with the given ID.
122  *
123  * @param {number}	id	the accordion item ID
124  * @return	{DwtAccordionItem}		the item or <code>null</code> if not found
125  */
126 DwtAccordion.prototype.getItem =
127 function(id) {
128 	for (var i = 0; i < this._items.length; i++) {
129 		if (this._items[i].id == id) {
130 			return this._items[i];
131 		}
132 	}
133 	return null;
134 };
135 
136 /**
137  * Gets the item by index.
138  * 
139  * @param {number}	id	the accordion item index
140  * @return	{DwtAccordionItem}		the item or <code>null</code> if not found
141  */
142 DwtAccordion.prototype.getItemByIndex =
143 function(index) {
144 	return (index >=0 && index < this._items.length) ? this._items[index] : null;
145 }
146 
147 /**
148  * Hides all accordion items.
149  */
150 DwtAccordion.prototype.hideAccordionItems =
151 function() {
152 	for (var i = 0; i < this._items.length; i++) {
153 		var header = document.getElementById(this._htmlElId + "_header_" + this._items[i].id);
154 		if (header) {
155 			Dwt.setVisible(header, false);
156 		}
157 	}
158 };
159 
160 /**
161  * Shows single accordion item based on given id.
162  *
163  * @param {number}	id	the accordion item ID
164  */
165 DwtAccordion.prototype.showAccordionItem =
166 function(id) {
167 	var header = document.getElementById(this._htmlElId + "_header_" + id);
168 	if (header) {
169 		Dwt.setVisible(header, true);
170 	}
171 };
172 
173 /**
174  * Allows the accordion items to be clickable or not. If disabled, the label of
175  * each accordion item will be grayed out.
176  *
177  * @param {boolean}	enabled			if <code>true</code>, enabled.
178  */
179 DwtAccordion.prototype.setEnabled =
180 function(enabled) {
181 	if (enabled == this._enabled) { return; }
182 
183 	this._enabled = enabled;
184 
185 	for (var i in this._items) {
186 		var item = this._items[i];
187 		if (this._currentItemId != item.id) {
188 			item._setEnabled(enabled);
189 		}
190 	}
191 };
192 
193 /**
194  * Resizes the accordion. This override applies accordion size changes to accordion items as well.
195  *
196  * @param {number}	width		the new width for accordion
197  * @param {number}	height		the new height for accordion
198  */
199 DwtAccordion.prototype.resize =
200 function(width, height) {
201 	if (width) {
202 		// if width changed, resize all header items
203 		for (var i = 0; i < this._items.length; i++) {
204 			var id = this._items[i].id;
205 			var title = document.getElementById(this._htmlElId + "_title_" + id);
206 			var fudge = 30;
207 
208 			var iconCell = document.getElementById(this._htmlElId + "_icon_" + id);
209 			if (iconCell && iconCell.className && iconCell.className != "") {
210 				fudge += 16; // the default width of an icon
211 			}
212 			Dwt.setSize(title, width - fudge);
213 
214 		}
215 	}
216 
217 	var newHeight;
218 	if (height) {
219 		var hdr = document.getElementById(this._htmlElId + "_header_" + this._currentItemId);
220 		if (hdr) {
221 			var hdrHeightSum = Dwt.getSize(hdr).y * this._getVisibleHeaderCount();
222 			newHeight = (height - hdrHeightSum); // force min. height of 100px?
223 		}
224 	}
225 
226 	var body = document.getElementById(this._htmlElId + "_body_" + this._currentItemId);
227 	if (body) {
228 		Dwt.setSize(body, width, newHeight);
229 		if (body.firstChild) {
230 			Dwt.setSize(body.firstChild, width, newHeight);
231 		}
232 	}
233 };
234 
235 DwtAccordion.prototype.setBounds =
236 function(x, y, width, height) {
237 	DwtComposite.prototype.setBounds.call(this, x, y, width, height);
238 
239 	this.resize(width, height);
240 };
241 
242 /**
243  * Gets the expanded accordion item.
244  * 
245  * @return	{DwtAccordionItem}		the item
246  */
247 DwtAccordion.prototype.getExpandedItem =
248 function() {
249 	return this._items[this._currentItemId || 0];
250 };
251 
252 /**
253  * Expands the accordion item with the given ID by making its body visible. The bodies of
254  * other items are hidden.
255  *
256  * @param {number}	id	the accordion item ID
257  * @param {boolean}	notify	if <code>true</code>, selection listeners are to be notified
258  */
259 DwtAccordion.prototype.expandItem =
260 function(id, notify) {
261 	var selectedItem;
262 	for (var i = 0; i < this._items.length; i++) {
263 		var itemId = this._items[i].id;
264 		var header = document.getElementById(this._htmlElId + "_header_" + itemId);
265 		var body = document.getElementById(this._htmlElId + "_body_" + itemId);
266 		var cell = document.getElementById(this._htmlElId + "_cell_" + itemId);
267 		var status = document.getElementById(this._htmlElId + "_status_" + itemId);
268 
269 		if (id == itemId) {
270 			Dwt.setVisible(body, true);
271 			header.className = "ZAccordionHeader ZWidget ZSelected";
272 			status.className = "ImgAccordionOpened";
273 			cell.style.height = "100%";
274 			this._currentItemId = id;
275 			selectedItem = this._items[i];
276 		} else {
277 			Dwt.setVisible(body, false);
278 			header.className = "ZAccordionHeader ZWidget";
279 			status.className = "ImgAccordionClosed";
280 			cell.style.height = "0px";
281 		}
282 	}
283 
284 	if (selectedItem && notify && this.isListenerRegistered(DwtEvent.SELECTION)) {
285 		this.notifySelectionListeners(selectedItem);
286 	}
287 };
288 
289 DwtAccordion.prototype.notifySelectionListeners =
290 function(selectedItem) {
291 	var selEv = DwtShell.selectionEvent;
292 	selEv.item = this;
293 	selEv.detail = selectedItem;
294 	this.notifyListeners(DwtEvent.SELECTION, selEv);
295 };
296 
297 /**
298  * Attaches the HTML content of the given control to the accordion item with
299  * the given ID.
300  *
301  * @param {number}	id				the accordion item ID
302  * @param {DwtControl}	contentObject		the control that contains this item's content
303  */
304 DwtAccordion.prototype.setItemContent =
305 function(id, contentObject) {
306 	var aiBody = this.getBody(id);
307 	if (aiBody) {
308 		this._items[id].control = contentObject;
309 		contentObject.reparentHtmlElement(aiBody);
310 		var size = contentObject.getSize();
311 		this.resize(size.x, size.y);
312 	}
313 };
314 
315 /**
316  * Gets the <code><body></code> element of the accordion item with the given ID.
317  *
318  * @param {number}		id	the accordion item ID
319  * @return	{Element}		the element
320  */
321 DwtAccordion.prototype.getBody =
322 function(id) {
323 	return document.getElementById(this._htmlElId + "_body_" + id);
324 };
325 
326 /**
327  * Gets the <code><header></code> element of the accordion item with the given ID.
328  *
329  * @param {number}		id	the accordion item ID
330  * @return	{Element}		the element
331  */
332 DwtAccordion.prototype.getHeader =
333 function(id) {
334 	return document.getElementById(this._htmlElId + "_header_" + id);
335 };
336 
337 /**
338  * Shows or hides the accordion.
339  *
340  * @param {boolean}	show	if <code>true</code>, show the accordion; otherwise hide it
341  */
342 DwtAccordion.prototype.show =
343 function(show) {
344 	var div = document.getElementById(this._htmlElId + "_div");
345 	if (div) {
346 		Dwt.setVisible(div, show);
347 	}
348 };
349 
350 /**
351  * Adds a listener to be notified when the button is pressed.
352  *
353  * @param {AjxListener}	listener	a listener
354  */
355 DwtAccordion.prototype.addSelectionListener =
356 function(listener) {
357 	this.addListener(DwtEvent.SELECTION, listener);
358 };
359 
360 /**
361  * Shows or hides an alert (aka orange background) on the accordion header
362  *
363  * @param {number}	id	the accordion item ID
364  * @param {boolean}	show	if <code>true</code>, show the alert
365  */
366 DwtAccordion.prototype.showAlert =
367 function(id, show) {
368 	var header = document.getElementById(this._htmlElId + "_header_" + id);
369 	if (show) {
370 		Dwt.delClass(header, null, "ZAlert");
371 	} else {
372 		Dwt.delClass(header, "ZAlert", null);
373 	}
374 };
375 
376 // Private Methods
377 
378 /**
379  * Creates the HTML skeleton for the accordion.
380  * 
381  * @private
382  */
383 DwtAccordion.prototype._initialize =
384 function() {
385 	this._items = [];
386 	this.__ITEMCOUNT = 0;
387 
388 	this.getHtmlElement().innerHTML = AjxTemplate.expand("dwt.Widgets#ZAccordion", {id: this._htmlElId});
389 	this._table = document.getElementById(this._htmlElId + "_accordion_table");
390 
391 	this._setMouseEventHdlrs();
392 };
393 
394 /**
395  * Returns the number of accordion items which have visible headers.
396  * 
397  * @private
398  */
399 DwtAccordion.prototype._getVisibleHeaderCount =
400 function() {
401 	var count = 0;
402 	for (var i = 0; i < this._items.length; i++) {
403 		var hdr = document.getElementById(this._htmlElId + "_header_" + this._items[i].id);
404 		if (hdr && Dwt.getVisible(hdr)) {
405 			count++;
406 		}
407 	}
408 	return count;
409 };
410 
411 
412 // Listeners
413 
414 /**
415  * When a header button is clicked, the item is expanded. Also, any listeners
416  * are notified.
417  *
418  * @param {DwtAccordionItem}	item		the accordion item whose header was clicked
419  * @param {DwtUiEvent}	ev		the click event
420  * 
421  * @private
422  */
423 DwtAccordion.prototype._handleOnClickHeader =
424 function(item, ev) {
425 	if (!this._enabled) { return; }
426 
427 	this.expandItem(item.id, true);
428 };
429 
430 /**
431  * When a header button is right-clicked, any listeners will be notified so a
432  * context menu can be shown, for example.
433  *
434  * @param {DwtAccordionItem}	item		the accordion item whose header was clicked
435  * @param {DwtUiEvent}	ev		the click event
436  * 
437  * @private
438  */
439 DwtAccordion.prototype._handleOnRightClickHeader =
440 function(item, ev) {
441 	ev = ev || window.event;
442 
443 	if (this.isListenerRegistered(DwtEvent.ONCONTEXTMENU)) {
444 		var selEv = DwtShell.selectionEvent;
445 		DwtUiEvent.copy(selEv, ev);
446 		selEv.item = this;
447 		selEv.detail = item;
448 		this.notifyListeners(DwtEvent.ONCONTEXTMENU, selEv);
449 	}
450 };
451 
452 DwtAccordion.prototype._handleOnMouseoverHeader =
453 function(item, ev) {
454 	ev = ev || window.event;
455 
456 	if (this.isListenerRegistered(DwtEvent.ONMOUSEOVER)) {
457 		var selEv = DwtShell.selectionEvent;
458 		DwtUiEvent.copy(selEv, ev);
459 		selEv.item = this;
460 		selEv.detail = item;
461 		this.notifyListeners(DwtEvent.ONMOUSEOVER, selEv);
462 	}
463 };
464 
465 DwtAccordion.prototype._handleOnMouseoutHeader =
466 function(ev) {
467 	this.setToolTipContent(null);
468 };
469 
470 /**
471  * Handles a resize event.
472  *
473  * @param {DwtEvent}	ev	the control event
474  * 
475  * @private
476  */
477 DwtAccordion.prototype._controlListener =
478 function(ev) {
479 	if (this.getScrollStyle() != Dwt.CLIP) { return; }
480 
481 	var newWidth = (ev.oldWidth != ev.newWidth) ? ev.newWidth : null;
482 	var newHeight = (ev.oldHeight != ev.newHeight) ? ev.newHeight : null;
483 
484 	if ((!newWidth && !newHeight) || ev.newWidth < 0 || ev.newHeight < 0) { return;	}
485 
486 	this.resize(newWidth, newHeight);
487 };
488 
489 /**
490  * Creates an accordion item.
491  * @class
492  * This class represents a single expandable accordion item.
493  *
494  * @param {string}	id		the unique ID for this item
495  * @param {string}	title		the text for the item header
496  * @param {hash}	data		a hash of arbitrary data for this item
497  * @param {DwtAccordion}	accordion	the owning accordion
498  */
499 DwtAccordionItem = function(id, title, data, accordion) {
500 	this.id = id;
501 	this.title = title;
502 	this.data = data;
503 	this.accordion = accordion;
504 };
505 
506 /**
507  * Returns a string representation of the object.
508  * 
509  * @return		{string}		a string representation of the object
510  */
511 DwtAccordionItem.prototype.toString =
512 function() {
513 	return "DwtAccordionItem";
514 };
515 
516 /**
517  * Sets the icon.
518  * 
519  * @param	{string}	icon		the icon
520  */
521 DwtAccordionItem.prototype.setIcon =
522 function(icon) {
523 	var iconCell = document.getElementById(this.accordion._htmlElId + "_icon_" + this.id);
524 	if (iconCell) {
525 		iconCell.className = icon;
526 	}
527 };
528 
529 /**
530  * Gets the icon cell.
531  * 
532  * @return	{Element}		the cell
533  */
534 DwtAccordionItem.prototype.getIconCell =
535 function() {
536 	return document.getElementById(this.accordion._htmlElId + "_icon_" + this.id);
537 };
538 
539 /**
540  * Sets the title.
541  * 
542  * @param	{string}	title		the title
543  */
544 DwtAccordionItem.prototype.setTitle =
545 function(title) {
546 	var titleCell = document.getElementById(this.accordion._htmlElId + "_title_" + this.id);
547 	if (titleCell) {
548 		titleCell.innerHTML = title;
549 	}
550 };
551 
552 /**
553  * @private
554  */
555 DwtAccordionItem.prototype._setEnabled =
556 function(enabled) {
557 	var titleCell = document.getElementById(this.accordion._htmlElId + "_title_" + this.id);
558 	if (titleCell) {
559 		if (enabled) {
560 			Dwt.delClass(titleCell, "ZDisabled");
561 		} else {
562 			Dwt.addClass(titleCell, "ZDisabled");
563 		}
564 	}
565 
566 	var status = document.getElementById(this.accordion._htmlElId + "_status_" + this.id);
567 	if (status) {
568 		if (enabled) {
569 			Dwt.delClass(status, "ZDisabledImage ZDisabled");
570 		} else {
571 			Dwt.addClass(status, "ZDisabledImage ZDisabled");
572 		}
573 	}
574 };
575