1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 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, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains a base Dwt dialog control.
 27  * 
 28  */
 29 
 30 /**
 31  * @class
 32  * This is a base class for dialogs. Given content, this class will take care of 
 33  * showing and hiding the dialog, as well as dragging it.
 34  * <p>
 35  * Dialogs always hang off the main shell since their stacking order is managed through z-index.
 36  *
 37  * @author Ross Dargahi
 38  * @author Conrad Damon
 39  * 
 40  * @param {hash}	params		a hash of parameters
 41  * @param	{DwtComposite}	params.parent	the parent widget (the shell)
 42  * @param	{string}	params.className		the CSS class
 43  * @param	{string}	params.title		the title
 44  * @param	{number}	[params.zIndex=Dwt.Z_DIALOG]		the z-index to set for this dialog when it is visible
 45  * @param	{DwtBaseDialog.MODAL|DwtBaseDialog.MODELESS}	[params.mode=DwtBaseDialog.MODAL] 		the modality of the dialog
 46  * @param	{DwtPoint}	params.loc			the location at which to popup the dialog. Defaults to being centered within its parent
 47  * @param	{boolean}		params.disposeOnPopDown		    destroy the content of dialog on popdown, Defaults to false
 48  * @param	{DwtControl}	params.view 		the the control whose element is to be re-parented
 49  * @param	{string}	params.dragHandleId 		the the ID of element used as drag handle
 50  * 
 51  * @extends	DwtComposite
 52  */
 53 DwtBaseDialog = function(params) {
 54 	if (arguments.length == 0) { return; }
 55 
 56 	params = Dwt.getParams(arguments, DwtBaseDialog.PARAMS);
 57 	var parent = params.parent;
 58 	if (!(parent instanceof DwtShell)) {
 59 		throw new DwtException("DwtBaseDialog parent must be a DwtShell", DwtException.INVALIDPARENT, "DwtDialog");
 60 	}
 61 	params.className = params.className || "DwtBaseDialog";
 62 	params.posStyle = DwtControl.ABSOLUTE_STYLE;
 63 	params.isFocusable = false;
 64 
 65 	this._title = params.title || "";
 66 
 67 	DwtComposite.call(this, params);
 68     this._disposeOnPopDown = params.disposeOnPopDown || false;
 69 	this._shell = parent;
 70 	this._zIndex = params.zIndex || Dwt.Z_DIALOG;
 71 	this._mode = params.mode || DwtBaseDialog.MODAL;
 72 	
 73 	this._loc = new DwtPoint();
 74 	if (params.loc) {
 75 		this._loc.x = params.loc.x;
 76 		this._loc.y = params.loc.y
 77 	} else {
 78 		this._loc.x = this._loc.y = Dwt.LOC_NOWHERE;
 79 	}
 80 
 81 	// Default dialog tab group.
 82 	this._tabGroup = new DwtTabGroup(this.toString());
 83 
 84     this._dragHandleId = params.dragHandleId || this._htmlElId + "_handle";
 85 	this._createHtml();
 86     this._initializeDragging(this._dragHandleId);
 87 
 88 	if (params.view) {
 89 		this.setView(params.view);
 90     }
 91 
 92 	// reset tab index
 93     this.setZIndex(Dwt.Z_HIDDEN); // not displayed until popup() called
 94 
 95     // Set visible to true now to allow for getting metrics. ZIndex hidden will prevent it
 96     // from actually being visible until the dialog is popped up.
 97 	this.setVisible(true);
 98 
 99 	this._position(DwtBaseDialog.__nowhereLoc);
100 
101 	// Make sure mouse clicks propagate to the DwtDraggable handler (document.onMouseMove and onMouseUp)
102 	this._propagateEvent[DwtEvent.ONMOUSEUP] = true;
103 };
104 
105 /**
106  * @private
107  */
108 DwtBaseDialog.PARAMS = ["parent", "className", "title", "zIndex", "mode", "loc", "view", "dragHandleId", "id"];
109 
110 DwtBaseDialog.prototype = new DwtComposite;
111 DwtBaseDialog.prototype.constructor = DwtBaseDialog;
112 
113 DwtBaseDialog.prototype.toString = function() { return "DwtBaseDialog"; };
114 DwtBaseDialog.prototype.isDwtBaseDialog = true;
115 
116 DwtBaseDialog.prototype.role = 'dialog';
117 DwtBaseDialog.prototype.isFocusable = true;
118 
119 
120 //
121 // Constants
122 //
123 
124 // modes
125 
126 /**
127  * Defines a "modeless" dialog.
128  */
129 DwtBaseDialog.MODELESS = 1;
130 
131 /**
132  * Defines a "modal" dialog.
133  */
134 DwtBaseDialog.MODAL = 2;
135 
136 /**
137  * @private
138  */
139 DwtBaseDialog.__nowhereLoc = new DwtPoint(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
140 
141 //
142 // Data
143 //
144 
145 /**
146  * @private
147  */
148 DwtBaseDialog.prototype.TEMPLATE = "dwt.Widgets#DwtBaseDialog";
149 
150 /**
151  * <strong>Note:</strong>
152  * This member variable will be set by sub-classes that want a control bar
153  * to appear below the dialog contents.
154  * 
155  * @private
156  */
157 DwtBaseDialog.prototype.CONTROLS_TEMPLATE = null;
158 
159 //
160 // Public methods
161 //
162 
163 /**
164  * Adds a popup listener.
165  * 
166  * @param		{AjxListener}	listener		the listener to add
167  */
168 DwtBaseDialog.prototype.addPopupListener =
169 function(listener) {
170 	this.addListener(DwtEvent.POPUP, listener);
171 }
172 
173 /**
174  * Removes a popup listener.
175  * 
176  * @param		{AjxListener}	listener		the listener to remove
177  */
178 DwtBaseDialog.prototype.removePopupListener = 
179 function(listener) {
180 	this.removeListener(DwtEvent.POPUP, listener);
181 }
182 
183 /**
184  * Adds a popdown listener.
185  * 
186  * @param		{AjxListener}	listener		the listener to add
187  */
188 DwtBaseDialog.prototype.addPopdownListener = 
189 function(listener) {
190 	this.addListener(DwtEvent.POPDOWN, listener);
191 }
192 
193 /**
194  * Removes a popdown listener.
195  * 
196  * @param		{AjxListener}	listener		the listener to remove
197  */
198 DwtBaseDialog.prototype.removePopdownListener = 
199 function(listener) {
200 	this.removeListener(DwtEvent.POPDOWN, listener);
201 }
202 
203 /**
204  * Pops-up the dialog, makes the dialog visible in places. Everything under the dialog will
205  * become veiled if we are modal. Note: popping up a dialog will block
206  * keyboard actions from being delivered to the global key action handler (if one
207  * is registered).
208  *
209  * @param {DwtPoint}		loc		the desired location
210  */
211 DwtBaseDialog.prototype.popup =
212 function(loc) {
213 	if (this._poppedUp) { return; }
214 
215 	this.cleanup(true);
216 	var thisZ = this._zIndex;
217 
218 	// if we're modal, setup the veil effect, and track which dialogs are open
219 	if (this._mode == DwtBaseDialog.MODAL) {
220 		thisZ = this._setModalEffect(thisZ);
221 	}
222 
223 	this._shell._veilOverlay.activeDialogs.push(this);
224 	this.setVisible(true);
225 	
226 	// use whichever has a value, local has precedence
227 	if (loc) {
228 		this._loc.x = loc.x;
229 		this._loc.y = loc.y;
230 	}
231 	this._position(loc);
232 
233     //reset TAB focus before popup of dialog.
234     //method be over-written to focus a different member.
235     this._resetTabFocus();
236 
237 	this.setZIndex(thisZ);
238 	this._poppedUp = true;
239 
240 	// Push our tab group
241 	var kbMgr = this._shell.getKeyboardMgr();
242 	kbMgr.pushTabGroup(this._tabGroup);
243 	kbMgr.pushDefaultHandler(this);
244 
245 	DwtShell.getShell().addListener(DwtEvent.CONTROL, this._resizeHdlr.bind(this));
246 
247 	this.notifyListeners(DwtEvent.POPUP, this);
248 };
249 
250 /**
251  * @private
252  */
253 DwtBaseDialog.prototype._resetTabFocus =
254 function(){
255     this._tabGroup.resetFocusMember(true);
256 };
257 
258 DwtBaseDialog.prototype.focus = 
259 function () {
260 	// if someone is listening for the focus to happen, give control to them,
261 	// otherwise focus on this dialog.
262 	if (this.isListenerRegistered(DwtEvent.ONFOCUS)) {
263 		this.notifyListeners(DwtEvent.ONFOCUS);
264 	} else if (this._focusElementId){
265 		var focEl = document.getElementById(this._focusElementId);
266 		if (focEl) {
267 			focEl.focus();
268             return focEl;
269 		}
270 	}
271 };
272 
273 /**
274  * Checks if the dialog is popped-up.
275  * 
276  * @return	{boolean}	<code>true</code> if the dialog is popped-up; <code>false</code> otherwise
277  */
278 DwtBaseDialog.prototype.isPoppedUp =
279 function () {
280 	return this._poppedUp;
281 };
282 
283 /**
284  * Pops-down and hides the dialog.
285  * 
286  */
287 DwtBaseDialog.prototype.popdown =
288 function() {
289 
290 	if (this._poppedUp) {
291 		this._poppedUp = false;
292 		this.cleanup(false);
293 	
294 		//var myZIndex = this.getZIndex();
295 	    var myZIndex = this._zIndex;
296 		this.setZIndex(Dwt.Z_HIDDEN);
297 		this.setVisible(false);
298 		//TODO we should not create an object everytime we popdown a dialog (ditto w/popup)
299 		this._position(DwtBaseDialog.__nowhereLoc);
300 		if (this._mode == DwtBaseDialog.MODAL) {
301 			this._undoModality(myZIndex);
302 		} else {
303 			this._shell._veilOverlay.activeDialogs.pop();
304 		}
305 		//this.removeKeyListeners();
306 
307         //Dispose the dialog if _disposeOnPopDown is set to true
308 		if (this._disposeOnPopDown === true) {
309             this.dispose();
310         }
311 
312 		// Pop our tab group
313 		var kbMgr = this._shell.getKeyboardMgr();
314 		kbMgr.popTabGroup(this._tabGroup);
315 		kbMgr.popDefaultHandler();
316 
317 		DwtShell.getShell().removeListener(DwtEvent.CONTROL, this._resizeHdlr.bind(this));
318 
319 		this.notifyListeners(DwtEvent.POPDOWN, this);
320 	}
321 };
322 
323 /**
324  * Sets the content of the dialog to a new view. Essentially re-parents
325  * the supplied control's HTML element to the dialogs HTML element
326  * 
327  * @param {DwtControl} newView		the control whose element is to be re-parented
328  */
329 DwtBaseDialog.prototype.setView =
330 function(newView) {
331 	this.reset();
332 	if (newView) {
333 		this._getContentDiv().appendChild(newView.getHtmlElement());
334 	}
335 };
336 
337 /**
338  * Resets the dialog back to its original state. Subclasses should override this method
339  * to add any additional behavior, but should still call up into this method.
340  * 
341  */
342 DwtBaseDialog.prototype.reset =
343 function() {
344 	this._loc.x = this._loc.y = Dwt.LOC_NOWHERE;
345 }
346 
347 /**
348 * Cleans up the dialog so it can be used again later.
349 * 
350 * @param	{boolean}		bPoppedUp		if <code>true</code>, the dialog is popped-up; <code>false</code> otherwise
351 */
352 DwtBaseDialog.prototype.cleanup =
353 function(bPoppedUp) {
354 	//TODO handle different types of input fields e.g. checkboxes etc
355 	var inputFields = this._getInputFields();
356 	
357 	if (inputFields) {
358 		var len = inputFields.length;
359 		for (var i = 0; i < len; i++) {
360 			inputFields[i].disabled = !bPoppedUp;
361 			if (bPoppedUp)
362 				inputFields[i].value = "";
363 		}
364 	}
365 }
366 
367 /**
368  * Sets the title.
369  * 
370  * @param	{string}		title		the title
371  */
372 DwtBaseDialog.prototype.setTitle =
373 function(title) {
374     if (this._titleEl) {
375         this._titleEl.innerHTML = title || "";
376     }
377 
378     this._title = title;
379 };
380 
381 /**
382 * Sets the dialog content (below the title, above the buttons).
383 *
384 * @param {string}		text		the dialog content
385 */
386 DwtBaseDialog.prototype.setContent =
387 function(text) {
388 	var d = this._getContentDiv();
389 	if (d) {
390 		d.innerHTML = text || "";
391 	}
392 }
393 
394 /**
395  * @private
396  */
397 DwtBaseDialog.prototype._getContentDiv =
398 function() {
399 	return this._contentEl;
400 };
401 
402 /**
403  * Adds an enter listener.
404  * 
405  * @param	{AjxListener}	listener		the listener to add
406  */
407 DwtBaseDialog.prototype.addEnterListener =
408 function(listener) {
409 	this.addListener(DwtEvent.ENTER, listener);
410 };
411 
412 /**
413  * Gets the active dialog.
414  * 
415  * @return	{DwtBaseDialog}		the active dialog
416  */
417 DwtBaseDialog.getActiveDialog = 
418 function() {
419 	var dialog = null;
420 	var shellObj = DwtShell.getShell(window);
421 	if (shellObj) {
422 		var len = shellObj._veilOverlay.activeDialogs.length;
423 		if (len > 0) {
424 			dialog = shellObj._veilOverlay.activeDialogs[len - 1];
425 		}
426 	}
427 	return dialog;
428 };
429 
430 //
431 // Protected methods
432 //
433 
434 DwtBaseDialog.prototype.getTabGroupMember = function() {
435 	return this._tabGroup;
436 };
437 
438 /**
439  * @private
440  */
441 DwtBaseDialog.prototype._initializeDragging =
442 function(dragHandleId) {
443 	var dragHandle = document.getElementById(dragHandleId);
444 	if (dragHandle) {
445 		var control = DwtControl.fromElementId(window._dwtShellId);
446 		if (control) {
447 			var p = Dwt.getSize(control.getHtmlElement());
448 			var dragObj = document.getElementById(this._htmlElId);
449 			var size = this.getSize();
450 			var dragEndCb = new AjxCallback(this, this._dragEnd);
451 			var dragCb = new AjxCallback(this, this._duringDrag);
452 			var dragStartCb = new AjxCallback(this, this._dragStart);
453 
454 			DwtDraggable.init(dragHandle, dragObj, 0,
455 							  document.body.offsetWidth - 10, 0, document.body.offsetHeight - 10, dragStartCb, dragCb, dragEndCb);
456 		}
457 	}
458 };
459 
460 /**
461  * @private
462  */
463 DwtBaseDialog.prototype._getContentHtml =
464 function() {
465     return "";
466 };
467 
468 /**
469  * @private
470  */
471 DwtBaseDialog.prototype._createHtml = function(templateId) {
472     var data = { id: this._htmlElId };
473     this._createHtmlFromTemplate(templateId || this.TEMPLATE, data);
474 };
475 
476 /**
477  * @private
478  */
479 DwtBaseDialog.prototype._createHtmlFromTemplate = function(templateId, data) {
480     // set default params
481     data.dragId = this._dragHandleId;
482     data.title = this._title;
483     data.icon = "";
484     data.closeIcon1 = "";
485     data.closeIcon2 = "";
486     data.controlsTemplateId = this.CONTROLS_TEMPLATE;
487 
488     // expand template
489     DwtComposite.prototype._createHtmlFromTemplate.call(this, templateId, data);
490 
491     // remember elements
492     this._titleBarEl = document.getElementById(data.id+"_titlebar");
493     this._titleEl = document.getElementById(data.id+"_title");
494     this._contentEl = document.getElementById(data.id+"_content");
495 
496 	if (this._titleEl) {
497 		this.setAttribute('aria-labelledby', this._titleEl.id);
498 		this._titleEl.setAttribute('role', 'heading');
499 		this._titleEl.setAttribute('aria-level', '2');
500 	}
501 
502     // NOTE: This is for backwards compatibility. There are just
503     //       too many sub-classes of dialog that expect to return
504     //       the dialog contents via the _getContentHtml method.
505     this.setContent(this._getContentHtml());
506 };
507 
508 /**
509  * @private
510  */
511 DwtBaseDialog.prototype._setModalEffect =
512 function() {
513 	// place veil under this dialog
514 	var dialogZ = this._shell._veilOverlay.dialogZ;
515 	var currentDialogZ = null;
516 	var thisZ, veilZ;
517 	if (dialogZ.length)
518 		currentDialogZ = dialogZ[dialogZ.length - 1];
519 	if (currentDialogZ) {
520 		thisZ = currentDialogZ + 2;
521 		veilZ = currentDialogZ + 1;
522 	} else {
523 		thisZ = this._zIndex;
524 		veilZ = Dwt.Z_VEIL;
525 	}
526 	this._shell._veilOverlay.veilZ.push(veilZ);
527 	this._shell._veilOverlay.dialogZ.push(thisZ);
528 	Dwt.setZIndex(this._shell._veilOverlay, veilZ);
529 	return thisZ;
530 };
531 
532 /**
533  * @private
534  */
535 DwtBaseDialog.prototype._undoModality =
536 function (myZIndex) {
537 	var veilZ = this._shell._veilOverlay.veilZ;
538 	veilZ.pop();
539 	var newVeilZ = veilZ[veilZ.length - 1];
540 	Dwt.setZIndex(this._shell._veilOverlay, newVeilZ);
541 	this._shell._veilOverlay.dialogZ.pop();
542 	this._shell._veilOverlay.activeDialogs.pop();
543 	if (this._shell._veilOverlay.activeDialogs.length > 0 ) {
544 		this._shell._veilOverlay.activeDialogs[0].focus();
545 	}
546 };
547 
548 /**
549  * Subclasses should implement this method to return an array of input fields that
550  * they want to be cleaned up between instances of the dialog being popped up and
551  * down
552  * 
553  * @return An array of the input fields to be reset
554  */
555 DwtBaseDialog.prototype._getInputFields = 
556 function() {
557 	// overload me
558 }
559 
560 DwtBaseDialog.prototype._resizeHdlr =
561 function(ev) {
562 	if (this._loc.x === Dwt.LOC_NOWHERE && this._loc.y === Dwt.LOC_NOWHERE) {
563 		this._position();
564 	}
565 };
566 
567 /**
568  * @private
569  */
570 DwtBaseDialog.prototype._dragStart =
571 function (x, y){
572     // fix for bug 3177
573     if (AjxEnv.isNav && !this._ignoreSetDragBoundries) {
574         this._currSize = this.getSize();
575         var control = DwtControl.fromElementId(window._dwtShellId);
576         if (control) {
577             var p = Dwt.getSize(control.getHtmlElement());
578             DwtDraggable.setDragBoundaries(DwtDraggable.dragEl, 0, p.x - this._currSize.x, 0, p.y - this._currSize.y);
579         }
580     }
581 };
582 
583 /**
584  * @private
585  */
586 DwtBaseDialog.prototype._dragEnd =
587 function(x, y) {
588  	// save dropped position so popup(null) will not re-center dialog box
589 	this._loc.x = x;
590 	this._loc.y = y;
591 }
592 
593 /**
594  * @private
595  */
596 DwtBaseDialog.prototype._duringDrag =
597 function(x, y) {
598 	// overload me
599 };
600 
601 /**
602  * @private
603  */
604 DwtBaseDialog.prototype._doesContainElement = 
605 function (element) {
606 	return Dwt.contains(this.getHtmlElement(), element);
607 };