1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 2010, 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) 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @class
 26  * This static class serves as a central location for registering and calling
 27  * public API methods. For example, the registration of a public API method for
 28  * pulling up the compose form might look something like this:
 29  * 
 30  * <pre>
 31  * AjxDispatcher.register("Compose", "Mail", new AjxCallback(this, this.doAction));
 32  * </pre>
 33  * 
 34  * and a client call of it like this:
 35  * 
 36  * <pre>
 37  * AjxDispatcher.run("Compose", {action:ZmOperation.NEW_MESSAGE, inNewWindow:false});
 38  * </pre>
 39  * 
 40  * Registration will most likely need to happen in a constructor, when 'this' is
 41  * available, since you'll most likely want the function call to happen in that
 42  * object's context.
 43  * <p>
 44  * A package can also register a callback to be run once the package has loaded. One use case for
 45  * that is to register newly-defined classes as drop targets. There is a wrapper method around
 46  * AjxPackage.require() that will call that method and then run the post-load callback (if any):
 47  *
 48  * <pre>
 49  * AjxDispatcher.require("Calendar");
 50  * </pre>
 51  * 
 52  * @author Conrad Damon
 53  */
 54 AjxDispatcher = function() {}
 55 
 56 // Table of API names, packages, and associated function calls
 57 AjxDispatcher._registry = {};
 58 
 59 // Table of package names and callbacks to run after loading (optional)
 60 AjxDispatcher._package = {};
 61 
 62 AjxDispatcher._preLoad	= [];
 63 AjxDispatcher._postLoad	= [];
 64 AjxDispatcher._loadFunctionsEnabled	= false;
 65 AjxDispatcher._timedAction = null;
 66 
 67 /**
 68  * Adds a function to be called after the given package has been loaded.
 69  * 
 70  * @param {string}	pkg		the name of package
 71  * @param {AjxCallback}	callback	the callback to run after package has loaded
 72  */
 73 AjxDispatcher.addPackageLoadFunction =
 74 function(pkg, callback) {
 75 	var pkgData = AjxDispatcher._getPackageData(pkg);
 76 	if (!pkgData._loaded && !AjxPackage.isDefined(pkg)) {
 77 		pkgData.callback.push(callback);
 78 	} else {
 79 		AjxTimedAction.scheduleAction(new AjxTimedAction(callback, callback.run), 0);
 80 	}
 81 };
 82 
 83 /**
 84  * Adds a function to be called while a package is being loaded. A typical use
 85  * is to display a "Loading..." screen.
 86  * 
 87  * @param {AjxCallback}	callback	the callback to run after package has loaded
 88  */
 89 AjxDispatcher.addPreLoadFunction =
 90 function(callback) {
 91 	AjxDispatcher._preLoad.push(callback);
 92 };
 93 
 94 /**
 95  * Adds a function to be called after a package has been loaded. A typical use
 96  * is to clear a "Loading..." screen.
 97  * 
 98  * @param {AjxCallback}	callback	the callback to run after package has loaded
 99  */
100 AjxDispatcher.addPostLoadFunction =
101 function(callback) {
102 	AjxDispatcher._postLoad.push(callback);
103 };
104 
105 /**
106  * @deprecated Use addPackageLoadFunction instead.
107  */
108 AjxDispatcher.setPackageLoadFunction = AjxDispatcher.addPackageLoadFunction;
109 
110 /**
111  * @deprecated Use addPreLoadFunction instead.
112  */
113 AjxDispatcher.setPreLoadFunction = AjxDispatcher.addPreLoadFunction;
114 
115 /**
116  * @deprecated Use addPostLoadFunction instead.
117  */
118 AjxDispatcher.setPostLoadFunction = AjxDispatcher.addPostLoadFunction;
119 
120 /**
121  * Enables/disables the running of the pre/post load functions.
122  * 
123  * @param {boolean}	enabled		if <code>true</code>, run pre/post load functions
124  */
125 AjxDispatcher.enableLoadFunctions =
126 function(enable) {
127 	AjxDispatcher._loadFunctionsEnabled = enable;
128 };
129 
130 /**
131  * Checks if the given package has been loaded.
132  * 
133  * @param	{string}	pkg		the name of package
134  * @return	{boolean}	<code>true</code> if the package is loaded
135  */
136 AjxDispatcher.loaded =
137 function(pkg) {
138     var pkgData = AjxDispatcher._getPackageData(pkg);
139 	return (pkgData && pkgData._loaded) || AjxPackage.isDefined(pkg);
140 };
141 
142 /**
143  * Programmatically sets whether the given packages has been loaded. Use with care!
144  * 
145  * @param {string}	pkg		the name of package
146  * @return	{boolean}	if <code>true</code>, the package is loaded
147  */
148 AjxDispatcher.setLoaded =
149 function(pkg, loaded) {
150 	var pkgData = AjxDispatcher._getPackageData(pkg);
151 	pkgData._loaded = loaded;
152 	if (loaded) {
153 		var callbacks = pkgData.callback || [];
154 		for (var i = 0; i < callbacks.length; i++) {
155 			callbacks[i].run();
156 		}
157 	}
158 };
159 
160 /**
161  * Registers an API method so that it may be called.
162  * 
163  * @param {string}	method	the name of the API method
164  * @param {string}	pkg		the name of required package(s)
165  * @param {AjxCallback}	callback	the callback to run for this API call
166  */
167 AjxDispatcher.registerMethod =
168 function(method, pkg, callback) {
169 	AjxDispatcher._registry[method] = {pkg:pkg, callback:callback};
170 };
171 
172 /**
173  * Calls the given API method with the given arguments. It can be passed any
174  * number of arguments (provided after the API name), and they will be passed
175  * to the function that gets called.
176  * 
177  * PS: You are in a maze of twisty callbacks, all alike.
178  * 
179  * @param {string}	method		the name of the API method
180  * @param {boolean}	async			if <code>true</code>, load package asynchronously
181  * @param {AjxCallback}	callback		the callback to run with results (must be present
182  * 										if there are pre- or post-load functions)
183  * @param {boolean}		preLoadOk		if <code>true</code>, okay to run registered pre-load function
184  * 
185  * @private
186  */
187 AjxDispatcher.run =
188 function(params /*, arg1 ... argN */) {
189 	if (!params) { return; }
190 	var method, noLoad, async, callback, preLoadOk;
191 	if (typeof(params) == "string") {
192 		method = params;
193         async = false;
194 		preLoadOk = false;
195 	} else {
196 		method = params.method;
197 		noLoad = params.noLoad;
198 		callback = params.callback;
199         async = params.async != null ? params.async : Boolean(callback);
200 		preLoadOk = params.preLoadOk != null ? params.preLoadOk : (callback != null);
201 	}
202 	var item = AjxDispatcher._registry[method];
203 	if (!item) {
204 		// method hasn't been registered
205 		AjxPackage.__log("API method '" + method + "' not found");
206 		return;
207 	}
208 	AjxPackage.__log("Run method: " + method);
209 	var pkg = item.pkg;
210 	var args = [];
211 	for (var i = 1; i < arguments.length; ++i) {
212 		args.push(arguments[i]);
213 	}
214 	if (callback) {
215 		args.push(callback);
216 	}
217 	
218 	return AjxDispatcher.require(pkg, async, item.callback, args, preLoadOk);
219 };
220 
221 /**
222  * Loads the given package, and runs its requested post-load callback. Clients should
223  * be careful not to mix async and sync calls for the same package, in order to avoid
224  * race conditions.
225  * 
226  * @param {string}	pkg				the name of the API method
227  * @param {boolean}	async				if <code>true</code>, load package asynchronously
228  * @param {AjxCallback}	callback			the callback to run after pkg load
229  * @param {array}	args				the args to pass to callback
230  * @param {boolean}	preLoadOk			if <code>true</code>, okay to run registered pre-load function
231  */
232 AjxDispatcher.require =
233 function(pkg, async, callback, args, preLoadOk) {
234 	if (!pkg) { return; }
235 	
236 	if (typeof(pkg) == "string") {
237 		pkg = [pkg];
238 	}
239 	var unloaded = [];
240 	for (var i = 0; i < pkg.length; i++) {
241 		var p = pkg[i];
242 		if (!AjxDispatcher._getPackageData(p)._loaded) {
243 			unloaded.push(p);
244 		}
245 	}
246 	if (unloaded.length == 0) {
247 		return AjxDispatcher._postLoadCallback(pkg, false, callback, args);
248 	} else {
249 		// need callback in order to run pre-load function
250 		var preLoad = AjxDispatcher._preLoad;
251 		if (preLoadOk && AjxDispatcher._loadFunctionsEnabled && preLoad.length) {
252 			AjxPackage.__log("pre-load function");
253 			AjxDispatcher._timedAction = new AjxCallback(null, AjxDispatcher._continueRequire, [unloaded, async, callback, args]);
254 			for (var i = 0; i < preLoad.length; i++) {
255 				preLoad[i].run();
256 			}
257 			window.setTimeout('AjxDispatcher._timedAction.run()', 0);
258 		} else {
259 			return AjxDispatcher._continueRequire(unloaded, async, callback, args);
260 		}
261 	}
262 };
263 
264 AjxDispatcher._continueRequire =
265 function(pkg, async, callback, args) {
266 	var pkgString = pkg.join(", ");
267 	AjxPackage.__log("------------------------------------- Loading package: " + pkgString);
268 	if (window.console && window.console.log) { console.log("------------------------------------- Loading package: " + pkgString); }
269 	if (async && callback) {
270 		var postLoadCallback = new AjxCallback(null, AjxDispatcher._postLoadCallback, [pkg, true, callback, args]);
271 		AjxPackage.require({name:pkg, callback:postLoadCallback});
272 	} else {
273 		var _st = new Date();
274 		for (var i = 0; i < pkg.length; i++) {
275 			AjxPackage.require(pkg[i]);
276 		}
277 		var _en = new Date();
278 		var t = _en.getTime() - _st.getTime();
279 		AjxPackage.__log("LOAD TIME for " + pkgString + ": " + t);
280 
281 		return AjxDispatcher._postLoadCallback(pkg, true, callback, args);
282 	}
283 };
284 
285 AjxDispatcher._postLoadCallback =
286 function(pkg, pkgWasLoaded, callback, args) {
287     for (var i = 0; i < pkg.length; i++) {
288         AjxDispatcher._getPackageData(pkg[i])._loaded = true;
289     }
290     for (var i = 0; i < pkg.length; i++) {
291 		var pkgData = AjxDispatcher._getPackageData(pkg[i]);
292 		if (pkgWasLoaded && pkgData.callback.length && !pkgData.callbackDone) {
293 			pkgData.callbackDone = true;
294 			AjxPackage.__log("Running post-load package function for " + pkg[i]);
295 			var callbacks = pkgData.callback;
296 			for (var j = 0; j < callbacks.length; j++) {
297 				callbacks[j].run();
298 			}
299 			pkgData.callback.length = 0;
300 		}
301 	}
302 	if (pkgWasLoaded) {
303 		var postLoad = AjxDispatcher._postLoad;
304 		if (AjxDispatcher._loadFunctionsEnabled && postLoad.length) {
305 			for (var i = 0; i < postLoad.length; i++) {
306 				postLoad[i].run();
307 			}
308 		}
309 	}
310 	
311 	if (callback) {
312  		return callback.isAjxCallback ? callback.run1(args) : callback(args);
313 	}
314 };
315 
316 AjxDispatcher._getPackageData = function(pkg) {
317 	if (!AjxDispatcher._package[pkg]) {
318 		AjxDispatcher._package[pkg] = { callback: [] };
319 	}
320 	return AjxDispatcher._package[pkg];
321 };
322