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