1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Singleton tooltip class.
 26  */
 27 DwtToolTip = function(shell, className, dialog) {
 28 
 29 	if (arguments.length == 0) { return; }
 30 
 31 	this.shell = shell;
 32 	this._dialog = dialog;
 33 	this._poppedUp = false;
 34 	this._div = document.createElement("div");
 35 	this._div.className = className || "DwtToolTip";
 36 	this._div.style.position = DwtControl.ABSOLUTE_STYLE;
 37 	this.shell.getHtmlElement().appendChild(this._div);
 38 	Dwt.setZIndex(this._div, Dwt.Z_HIDDEN);
 39 	Dwt.setLocation(this._div, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
 40 
 41 	this._eventMgr = new AjxEventMgr();
 42 
 43     // create html
 44     // NOTE: This id is ok because there's only ever one instance of a tooltip
 45     var templateId = "dwt.Widgets#DwtToolTip";
 46     this._div.innerHTML = AjxTemplate.expand(templateId);
 47 
 48     var params = AjxTemplate.getParams(templateId);
 49     this._offsetX = (params.width != null) ? Number(params.width) : DwtToolTip.POPUP_OFFSET_X;
 50     this._offsetY = (params.height != null) ? Number(params.height) : DwtToolTip.POPUP_OFFSET_Y;
 51 
 52     // save reference to content div
 53     this._contentDiv = document.getElementById("tooltipContents");
 54 
 55     Dwt.setHandler(this._div, DwtEvent.ONMOUSEOVER, AjxCallback.simpleClosure(this._mouseOverListener, this));
 56     Dwt.setHandler(this._div, DwtEvent.ONMOUSEOUT, AjxCallback.simpleClosure(this._mouseOutListener, this));
 57 
 58 	var events = [DwtEvent.ONCLICK,DwtEvent.ONDBLCLICK,DwtEvent.ONMOUSEDOWN,DwtEvent.ONMOUSEENTER,DwtEvent.ONMOUSELEAVE,DwtEvent.ONMOUSEMOVE,DwtEvent.ONMOUSEUP,DwtEvent.ONMOUSEWHEEL,DwtEvent.ONSCROLL];
 59 	for (var i=0; i<events.length; i++) {
 60 		var event = events[i];
 61     	Dwt.setHandler(this._div, event, AjxCallback.simpleClosure(this.notifyListeners, this, [event]));
 62 	}
 63 };
 64 
 65 DwtToolTip.prototype.isDwtToolTip = true;
 66 DwtToolTip.prototype.toString = function() { return "DwtToolTip"; };
 67 
 68 //
 69 // Constants
 70 //
 71 
 72 DwtToolTip.TOOLTIP_DELAY = 750;
 73 
 74 DwtToolTip.WINDOW_GUTTER = 5;	// space to leave between tooltip and edge of shell
 75 DwtToolTip.POPUP_OFFSET_X = 5;	// default horizontal offset from control
 76 DwtToolTip.POPUP_OFFSET_Y = 5;	// default vertical offset from control
 77 
 78 //
 79 // Data
 80 //
 81 
 82 //
 83 // Public methods
 84 //
 85 
 86 DwtToolTip.prototype.getContent =
 87 function() {
 88     return this._div.innerHTML;
 89 };
 90 
 91 DwtToolTip.prototype.setContent =
 92 function(content, setInnerHTML) {
 93 	this._content = content;
 94 	if(setInnerHTML) {
 95         this._contentDiv.innerHTML = this._content;
 96     }
 97 };
 98 
 99 /**
100  * Shows the tooltip. By default, its position will be relative to the location of the
101  * cursor when the mouseover event happened. Alternatively, the control that generated
102  * the tooltip can be passed in, and the tooltip will be positioned relative to it. If
103  * the control is a large composite control (eg a DwtListView), the hover event can be
104  * passed so that the actual target of the event can be found.
105  * 
106  * @param {number}			x					X-coordinate of cursor
107  * @param {number}			y					Y-coordinate of cursor
108  * @param {boolean}			skipInnerHTML		if true, do not copy content to DOM
109  * @param {boolean}			popdownOnMouseOver	if true, hide tooltip on mouseover
110  * @param {DwtControl}		obj					control that tooltip is for (optional)
111  * @param {DwtHoverEvent}	hoverEv				hover event (optional)
112  * @param {AjxCallback}		popdownListener		callback to run when tooltip pops down
113  */
114 DwtToolTip.prototype.popup = 
115 function(x, y, skipInnerHTML, popdownOnMouseOver, obj, hoverEv, popdownListener) {
116 	this._hovered = false;
117     if (this._popupAction) {
118         AjxTimedAction.cancelAction(this._popupAction);
119         this._popupAction = null;
120     }
121 	// popdownOnMouseOver may be true to pop down the tooltip if the mouse hovers over the tooltip. Optionally,
122 	// it can be an AjxCallback that will be called after popping the tooltip down.
123     this._popdownOnMouseOver = popdownOnMouseOver;
124 	// popdownListener is always called after popping the tooltip down, regardless of what called the popdown
125     this._popdownListener = popdownListener;
126     if (this._content != null) {
127 		if(!skipInnerHTML) {
128             this._contentDiv.innerHTML = this._content;
129         }
130 
131 		this._popupAction = new AjxTimedAction(this, this._positionElement, [x, y, obj, hoverEv]);
132 		AjxTimedAction.scheduleAction(this._popupAction, 5);
133 	}
134 };
135 
136 /*
137 * setSticky allows making the tooltip not to popdown. 
138 * IMPORTANT: Tooltip is singleton inside Zimbra i.e. only one instance of tooltip is reused by all objects. 
139 * So, it is very important for the code setting tooltip to sticky to have some mechanism to close the tooltip by itself. 
140 * Like have a close-button inside tooltip and when clicked, should set the setSticky(false) and then close the tooltip.
141 *
142 * If setSticky(true) is called, _poppedUp is set to false, which is essentially pretending the tooltip is not
143 * up. In that case, a call to popdown will not close the tooltip. And that means tooltip will stay up even if some other
144 * code path calls popdown on the singleton tooltip.
145 *
146 */
147 DwtToolTip.prototype.setSticky = 
148 function(bool) {
149 	this._poppedUp = !bool;
150 };
151 
152 DwtToolTip.prototype.popdown = 
153 function() {
154     this._popdownOnMouseOver = false;
155 	this._hovered = false;
156     if (this._popupAction) {
157         AjxTimedAction.cancelAction(this._popupAction);
158         this._popupAction = null;
159     }
160 	if (this._content != null && this._poppedUp) {
161 		Dwt.setLocation(this._div, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
162 		this._poppedUp = false;
163 		if (this._popdownListener instanceof AjxCallback) {
164 			this._popdownListener.run();
165 		}
166 		this._popdownListener = null;
167 	}
168 };
169 
170 //
171 // Protected methods
172 //
173 
174 // Positions the tooltip relative to the base element based on vertical and horizontal offsets.
175 DwtToolTip.prototype._positionElement = 
176 function(startX, startY, obj, hoverEv) {
177 	
178     this._popupAction = null;
179 	
180 	var wdSize = DwtShell.getShell(window).getSize();
181 	var wdWidth = wdSize.x, wdHeight = wdSize.y;
182 
183 	var tooltipX, tooltipY, baseLoc;
184 	var baseEl = obj && obj.getTooltipBase(hoverEv);
185 	if (baseEl) {
186 		baseLoc = Dwt.toWindow(baseEl);
187 		var baseSz = Dwt.getSize(baseEl);
188 		tooltipX = baseLoc.x + this._offsetX;
189 		tooltipY = baseLoc.y + baseSz.y + this._offsetY;
190 	}
191 	else {
192 		tooltipX = startX + this._offsetX;
193 		tooltipY = startY + this._offsetY;
194 	}
195 
196 	var popupSize = Dwt.getSize(this._div);
197 	var popupWidth = popupSize.x, popupHeight = popupSize.y;
198 
199 	// check for sufficient room to the right
200 	if (tooltipX + popupWidth > wdWidth - DwtToolTip.WINDOW_GUTTER) {
201 		tooltipX = wdWidth - DwtToolTip.WINDOW_GUTTER - popupWidth;
202 	}
203 	// check for sufficient room below
204 	if (tooltipY + popupHeight > wdHeight - DwtToolTip.WINDOW_GUTTER) {
205 		tooltipY = (baseLoc ? baseLoc.y : tooltipY) - this._offsetY - popupHeight;
206 	}
207 
208 	Dwt.setLocation(this._div, tooltipX, tooltipY);
209 	var zIndex = this._dialog ? this._dialog.getZIndex() + Dwt._Z_INC : Dwt.Z_TOOLTIP;
210 	Dwt.setZIndex(this._div, zIndex);
211     this._poppedUp = true;
212 };
213 
214 DwtToolTip.prototype._mouseOverListener = 
215 function(ev) {
216 	this._hovered = true;
217     if (this._popdownOnMouseOver && this._poppedUp) {
218         var callback = (this._popdownOnMouseOver.isAjxCallback || AjxUtil.isFunction(this._popdownOnMouseOver)) ? this._popdownOnMouseOver : null;
219         this.popdown();
220         if (callback) {
221             callback.run();
222 		}
223     }
224 	this.notifyListeners(DwtEvent.ONMOUSEOVER);
225 };
226 
227 DwtToolTip.prototype._mouseOutListener = 
228 function(ev) {
229 	ev = DwtUiEvent.getEvent(ev, this._div)
230 	var location = Dwt.toWindow(this._div);
231 	var size = Dwt.getSize(this._div);
232 	// We sometimes get mouseover events even though the cursor is inside the tooltip, so double-check before popping down
233 	if (ev.clientX <= location.x || ev.clientX >= (location.x + size.x) || ev.clientY <= location.y || ev.clientY >= (location.y + size.y)) {
234 		this.popdown();
235 		this.notifyListeners(DwtEvent.ONMOUSEOUT);
236 	}
237 };
238 
239 DwtToolTip.prototype.getHovered = 
240 function() {
241 	return this._hovered;
242 };
243 
244 
245 // The com_zimbra_email zimlet wants to put a listener on our mouseout event, but overwriting the existing handler is a no-no
246 // and we actually only want that event when the double-check above succeeds. Let API users add event listeners in a more clean way.
247 DwtToolTip.prototype.addListener =
248 function(eventType, listener, index) {
249 	return this._eventMgr.addListener(eventType, listener, index);
250 };
251 
252 DwtToolTip.prototype.setListener =
253 function(eventType, listener, index) {
254 	this.removeAllListeners(eventType);
255 	return this._eventMgr.addListener(eventType, listener, index);
256 };
257 
258 DwtToolTip.prototype.removeListener =
259 function(eventType, listener) {
260 	return this._eventMgr.removeListener(eventType, listener);
261 };
262 
263 DwtToolTip.prototype.removeAllListeners =
264 function(eventType) {
265 	return this._eventMgr.removeAll(eventType);
266 };
267 
268 DwtToolTip.prototype.isListenerRegistered =
269 function(eventType) {
270 	return this._eventMgr.isListenerRegistered(eventType);
271 };
272 
273 DwtToolTip.prototype.notifyListeners =
274 function(eventType, event) {
275 	return this._eventMgr.notifyListeners(eventType, event);
276 };
277 
278