1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2011, 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) 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a singleton that manages outside mouse clicks for a given widget.
 26  * @constructor
 27  * @class
 28  * This class is designed to make it easy for a widget to detect mouse events
 29  * that happen somewhere outside its HTML structure. The typical use case is
 30  * so that a menu can pop down when a user clicks outside of it, but there are
 31  * others. For the most part, we care about mousedown events.
 32  *
 33  * There are several ways to detecting outside mouse events:
 34  *
 35  * 1. Set the body element to capture all mouse events, using DwtMouseEventCapture.
 36  *    That is all that's needed for non-IE browsers.
 37  * 2. Any control that receives an event calls the global listener through
 38  *    DwtEventManager.
 39  * 3. The shell listens for mouse events.
 40  * 4. Listen for window blur events.
 41  *
 42  * 2 and 3 are used by IE only. Note that the controls and the shell must be handling
 43  * those events in order to trigger the listeners.
 44  *
 45  * 4 is not used by IE, since window.onblur does not work correctly on IE. It's possible to
 46  * use document.onfocusout, but that is triggered even on clicks within the document, so
 47  * you need to check activeElement and cross your fingers. It's not worth the risk.
 48  *
 49  * We also have classes that create elements in another document (IFRAME) forward
 50  * their mouse events to us so that we can notify a client object if appropriate.
 51  * The one class that does that is DwtIframe.
 52  *
 53  * The framework can support multiple simultaneous clients. For example, a context
 54  * menu and toast could both be listening for outside mouse clicks at the same time.
 55  * Each will be notified as appropriate. If the toast is clicked, the context menu
 56  * will be notified of an outside click.
 57  *
 58  * A client may also add an element to its defined "inside" area by calling
 59  * startListening() with the same ID. One use case is a menu that pops up a submenu.
 60  * The two are considered together when determining if a mouse click was "outside".
 61  * If the submenu pops down, its element is removed from the area to check.
 62  */
 63 DwtOutsideMouseEventMgr = function() {
 64 
 65 	this._reset();
 66 	this._mouseEventListener = DwtOutsideMouseEventMgr._mouseEventHdlr.bind();
 67 	DwtOutsideMouseEventMgr.INSTANCE = this;
 68 	this.id = "DwtOutsideMouseEventMgr";
 69 };
 70 
 71 DwtOutsideMouseEventMgr.prototype.isDwtOutsideMouseEventMgr = true;
 72 DwtOutsideMouseEventMgr.prototype.toString = function() { return "DwtOutsideMouseEventMgr"; };
 73 
 74 DwtOutsideMouseEventMgr.EVENTS = [DwtEvent.ONMOUSEDOWN];
 75 DwtOutsideMouseEventMgr.EVENTS_HASH = AjxUtil.arrayAsHash(DwtOutsideMouseEventMgr.EVENTS);
 76 
 77 /**
 78  * Start listening for outside mouse events on behalf of the given object.
 79  *
 80  * @param {hash}		params					hash of params:
 81  * @param {string}		params.id				unique ID for this listening session
 82  * @param {DwtControl}	params.obj				control on behalf of whom we're listening
 83  * @param {string}		params.elementId		ID of reference element, if other than control's HTML element
 84  * @param {AjxListener}	params.outsideListener	listener to call when we get an outside mouse event
 85  * @param {boolean}		params.noWindowBlur		if true, don't listen for window blur events; useful for dev
 86  */
 87 DwtOutsideMouseEventMgr.prototype.startListening =
 88 function(params) {
 89 
 90 	DBG.println("out", "start listening: " + params.id);
 91 
 92 	if (!(params && params.outsideListener)) { return; }
 93 	var id = params.id;
 94 
 95 	if (!this._menuCapObj) {
 96 		// we only need a single menu capture object, create it lazily
 97 		var mecParams = {
 98 			id:				this.id,
 99 			hardCapture:	false,
100 			mouseDownHdlr:	DwtOutsideMouseEventMgr._mouseEventHdlr
101 		}
102 		this._menuCapObj = new DwtMouseEventCapture(mecParams);
103 	}
104 
105 	var elementId = params.elementId || (params.obj && params.obj.getHTMLElId && params.obj.getHTMLElId());
106 	DBG.println("out", "add element ID " + elementId + " for ID " + id);
107 
108 	var context = this._byId[id];
109 	if (context) {
110 		// second and subsequent calls with same ID will just add element IDs; typical case is submenu
111 		if (elementId) {
112 			context.elementIds.push(elementId);
113 		}
114 		DBG.println("out", "element IDs: " + context.elementIds);
115 		return;
116 	}
117 	else {
118 		context = this._byId[id] = {
119 			id:					id,
120 			obj:				params.obj,
121 			elementIds:			[elementId],
122 			outsideListener:	params.outsideListener
123 		}
124 	}
125 
126 	// add various event listeners when we get our first client
127 	if (this._numIds == 0) {
128 		if (AjxEnv.isIE) {
129 			var shell = DwtShell.getShell(window);
130 			var events = DwtOutsideMouseEventMgr.EVENTS;
131 			shell._setEventHdlrs(events);
132 			for (var i = 0; i < events.length; i++) {
133 				var ev = events[i];
134 				shell.addListener(ev, this._mouseEventListener);
135 				DwtEventManager.addListener(ev, this._mouseEventListener);
136 			}
137 		}
138 
139 		if (!AjxEnv.isIE && !params.noWindowBlur) {
140 			this._savedWindowBlurHandler = window.onblur;
141 			window.onblur = DwtOutsideMouseEventMgr._mouseEventHdlr;
142 		}
143 
144 		// start a new capture session
145 		DBG.println("out", "capture");
146 		this._menuCapObj.capture();
147 		this._capturing = true;
148 	}
149 
150 	this._numIds++;
151 };
152 
153 /**
154  * Stop listening for outside mouse events. Listening is stopped for the element
155  * provided, if any, or for the element indicated by the context ID. Outside
156  * listeners are removed once there are no more elements in the context.
157  *
158  * @param {string|hash}	params				ID, if string, otherwise hash of params:
159  * @param {string}		params.id			unique ID for this listening session
160  * @param {DwtControl}	params.obj			control on behalf of whom we're listening
161  * @param {string}		params.elementId	ID of element to remove from listening context
162  * @param {boolean}		params.noWindowBlur	if true, don't listen for window blur events; useful for dev
163  */
164 DwtOutsideMouseEventMgr.prototype.stopListening =
165 function(params) {
166 
167 	if (typeof params == "string") {
168 		params = {id:params};
169 	}
170 	var id = params.id;
171 	var context = this._byId[id];
172 	if (!context) { return; }
173 	DBG.println("out", "stop listening: " + id);
174 
175 	var elIds = context.elementIds;
176 	var elementId = params.elementId || (params.obj && params.obj.getHTMLElId());
177 	if (elementId) {
178 		AjxUtil.arrayRemove(elIds, elementId);
179 		if (elIds.length > 0) {
180 			// still at least one element to check against
181 			return;
182 		}
183 	}
184 
185 	// no more elements in this context, remove listeners
186 	delete this._byId[id];
187 	this._numIds--;
188 
189 	if (this._numIds == 0) {
190 		if (AjxEnv.isIE) {
191 			var shell = DwtShell.getShell(window);
192 			var events = DwtOutsideMouseEventMgr.EVENTS;
193 			shell._setEventHdlrs(events, true);
194 			for (var i = 0; i < events.length; i++) {
195 				var ev = events[i];
196 				shell.removeListener(ev, this._mouseEventListener);
197 				DwtEventManager.removeListener(ev, this._mouseEventListener);
198 			}
199 		}
200 
201 		if (!AjxEnv.isIE && !params.noWindowBlur) {
202 			window.onblur = this._savedWindowBlurHandler;
203 		}
204 
205 		this._reset();
206 	}
207 };
208 
209 DwtOutsideMouseEventMgr.prototype._reset =
210 function() {
211 
212 	if (this._capturing && (DwtMouseEventCapture.getId() == this.id)) {
213 		DBG.println("out", "release");
214 		this._menuCapObj.release();
215 		this._capturing = false;
216 	}
217 	this._byId		= {};
218 	this._numIds	= 0;
219 };
220 
221 /**
222  * If the event is one we're listening for, check its target to see if
223  * we should pass it along.
224  *
225  * @param {Event}	ev
226  */
227 DwtOutsideMouseEventMgr.forwardEvent =
228 function(ev) {
229 
230 	if (!ev) { return; }
231 	var omem = DwtOutsideMouseEventMgr.INSTANCE;
232 	if (!omem._numIds) { return; }
233 
234 	var type = "on" + ev.type;
235 	if (DwtOutsideMouseEventMgr.EVENTS_HASH[type]) {
236 		DwtOutsideMouseEventMgr._mouseEventHdlr(ev);
237 	}
238 };
239 
240 /**
241  * Call the client's outside listener if the event happened outside of the elements
242  * defined by the client's context. Note that the event that gets passed in might be
243  * a DOM event, or a DwtMouseEvent. That's okay, since both have a "target" property.
244  *
245  * @param {Event|DwtMouseEvent}	ev		event
246  */
247 DwtOutsideMouseEventMgr._mouseEventHdlr =
248 function(ev) {
249 
250     ev = DwtUiEvent.getEvent(ev);
251     if (!ev)
252         return;
253 
254     var omem = DwtOutsideMouseEventMgr.INSTANCE;
255 	var targetEl = DwtUiEvent.getTarget(ev);
256 	DBG.println("out", "event type: " + ev.type);
257 	DBG.println("out", "target: " + targetEl.id);
258 	// bug 59782 - FF issues mysterious window.blur event that we should ignore
259 	if (AjxEnv.isGeckoBased && ev && (ev.type == "blur") && ev.target && ev.explicitOriginalTarget &&
260 		(ev.target != ev.explicitOriginalTarget)) {
261 		
262 		DwtUiEvent.setBehaviour(ev, false, true);
263 		return true;
264 	}
265 	
266 	for (var id in omem._byId) {
267 		var runListener = true;
268 		var context = omem._byId[id];
269 		var elementIds = context.elementIds;
270 		DBG.println("out", "element IDs for " + id + ": " + context.elementIds);
271 		for (var i = 0; i < elementIds.length; i++) {
272 			DBG.println("out", "check: " + elementIds[i]);
273 			var el = document.getElementById(elementIds[i]);
274 			if (el && targetEl && Dwt.isAncestor(el, targetEl)) {
275 				runListener = false;
276 				break;
277 			}
278 		}
279 		if (runListener) {
280 			DBG.println("out", "run listener for: " + context.id);
281 			context.outsideListener.run(ev, context);
282 		}
283 	}
284 
285 	DwtUiEvent.setBehaviour(ev, false, true);
286 	return true;
287 };
288 
289 // create our singleton instance
290 new DwtOutsideMouseEventMgr();
291