1 /*core.js is part of Aloha Editor project http://aloha-editor.org
  2  *
  3  * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
  4  * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
  5  * Contributors http://aloha-editor.org/contribution.php
  6  *
  7  * Aloha Editor is free software; you can redistribute it and/or
  8  * modify it under the terms of the GNU General Public License
  9  * as published by the Free Software Foundation; either version 2
 10  * of the License, or any later version.
 11  *
 12  * Aloha Editor is distributed in the hope that it will be useful,
 13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU General Public License
 18  * along with this program; if not, write to the Free Software
 19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 20  *
 21  * As an additional permission to the GNU GPL version 2, you may distribute
 22  * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
 23  * source code without the copy of the GNU GPL normally required,
 24  * provided you include this license notice and a URL through which
 25  * recipients can access the Corresponding Source.
 26  */
 27 define([
 28 	'jquery',
 29 	'aloha/pluginmanager'
 30 ], function (
 31 	$,
 32 	PluginManager
 33 ) {
 34 	'use strict';
 35  36 
	var Aloha = window.Aloha;
 37 
 38 	/**
 39 	 * Checks whether the current user agent is supported.
 40 	 *
 41 	 * @return {boolean} True if Aloha supports the current browser.
 42 	 */
 43 	function isBrowserSupported() {
 44 		var browser = $.browser;
 45 		var version = browser.version;
 46 		return !(
 47 			// Chrome/Safari 4
 48 			(browser.webkit && parseFloat(version) < 532.5) ||
 49 			// FF 3.5
 50 			(browser.mozilla && parseFloat(version) < 1.9) ||
 51 			// IE 7
 52 			(browser.msie && version < 7) ||
 53 			// Right now Opera needs some work
 54 			(browser.opera && version < 11)
 55 		);
 56 	}
 57 
 58 	/**
 59 	 * Checks whether the given jQuery event originates from an Aloha dialog
 60 	 * element.
 61 	 *
 62 	 * This is used to facilitate a hackish way of preventing blurring
 63 	 * editables when interacting with Aloha UI modals.
 64 	 *
 65 	 * @param {jQuery<Event>} $event
 66 	 * @return {boolean} True if $event is initiated from within an Aloha
 67 	 *                   dialog element.
 68 	 */
 69 	function originatesFromDialog($event) {
 70 		var $target = $($event.target);
 71 		return $target.is('.aloha-dialog')
 72 			|| $target.closest('.aloha').length;
 73 	}
 74 
 75 	/**
 76 	 * Registers events on the documents to cause editables to be blurred when
 77 	 * clicking outside of editables.
 78 	 *
 79 	 * Hack: Except when the click originates from a modal dialog.
 80 	 */
 81 	function registerEvents() {
 82 		$('html').mousedown(function ($event) {
 83 			if (Aloha.activeEditable && !Aloha.eventHandled
 84 					&& !originatesFromDialog($event)) {
 85 				Aloha.deactivateEditable();
 86 			}
 87 		}).mouseup(function () {
 88 			Aloha.eventHandled = false;
 89 		});
 90 	}
 91 
 92 	/**
 93 	 * Initialize Aloha.
 94 	 *
 95 	 * @param {function} event Event to trigger after completing tasks.
 96 	 * @param {function} next Function to call after completing tasks.
 97 	 */
 98 	function initAloha(event, next) {
 99 		if (!isBrowserSupported()) {
100 			var console = window.console;
101 			if (console) {
102 				var fn = console.error ? 'error' : console.log ? 'log' : null;
103 				if (fn) {
104 					console[fn]('This browser is not supported');
105 				}
106 			}
107 			return;
108 		}
109 
110 		// Because different css is to be applied based on what the user-agent
111 		// supports.  For example: outlines do not render in IE7.
112 		if ($.browser.webkit) {
113 			$('html').addClass('aloha-webkit');
114 		} else if ($.browser.opera) {
115 			$('html').addClass('aloha-opera');
116 		} else if ($.browser.msie) {
117 			$('html').addClass('aloha-ie' + parseInt($.browser.version, 10));
118 		} else if ($.browser.mozilla) {
119 			$('html').addClass('aloha-mozilla');
120 		}
121 
122 		if (navigator.appVersion.indexOf('Win') !== -1) {
123 124 			Aloha.OSName = 'Win';
125 		} else if (navigator.appVersion.indexOf('Mac') !== -1) {
126 			Aloha.OSName = 'Mac';
127 		} else if (navigator.appVersion.indexOf('X11') !== -1) {
128 			Aloha.OSName = 'Unix';
129 		} else if (navigator.appVersion.indexOf('Linux') !== -1) {
130 			Aloha.OSName = 'Linux';
131 		}
132 
133 		registerEvents();
134 		Aloha.settings.base = Aloha.getAlohaUrl();
135 		Aloha.Log.init();
136 
137 		// Initialize error handler for general javascript errors.
138 		if (Aloha.settings.errorhandling) {
139 			window.onerror = function (msg, url, line) {
140 				Aloha.Log.error(Aloha, 'Error message: ' + msg + '\nURL: ' +
141 				                       url + '\nLine Number: ' + line);
142 				return true;
143 			};
144 		}
145 
146 		event();
147 		next();
148 	}
149 
150 	/**
151 	 * Initialize managers
152 	 *
153 	 * @param {function} event Event to trigger after completing tasks.
154 	 * @param {function} next Function to call after completing tasks.
155 	 */
156 	function initRepositoryManager(event, next) {
157 		Aloha.RepositoryManager.init();
158 		event();
159 		next();
160 	}
161 
162 	/**
163 	 * Initialize Aloha plugins.
164 	 *
165 	 *
166 	 * @param {function} event Event to trigger after completing tasks.
167 	 * @param {function} next Callback that will be invoked after all plugins
168 	 *                        have been initialized.  Whereas plugins are loaded
169 	 *                        synchronously, plugins may initialize
170 	 *                        asynchronously.
171 	 */
172 	function initPluginManager(event, next) {
173 		// Because if there are no loadedPlugins specified, then the default is
174 		// to initialized all available plugins.
175 		if (0 === Aloha.loadedPlugins.length) {
176 			var plugins = PluginManager.plugins;
177 			var plugin;
178 			for (plugin in plugins) {
179 				if (plugins.hasOwnProperty(plugin)) {
180 					Aloha.loadedPlugins.push(plugin);
181 				}
182 			}
183 		}
184 
185 		var fired = false;
186 
187 		PluginManager.init(function () {
188 			if (!fired) {
189 				event();
190 				fired = true;
191 			}
192 			next();
193 		}, Aloha.loadedPlugins);
194 195 
		if (!fired) {
196 			event();
197 			fired = true;
198 		}
199 	}
200 
201 	/**
202 	 * Begin initialize editables.
203 	 *
204 	 * @param {function} event Event to trigger after completing tasks.
205 	 * @param {function} next Function to call after completing tasks.
206 	 */
207 	function initEditables(event, next) {
208 		var i;
209 		for (i = 0; i < Aloha.editables.length; i++) {
210 			if (!Aloha.editables[i].ready) {
211 				Aloha.editables[i].init();
212 			}
213 		}
214 		event();
215 		next();
216 	}
217 
218 	/**
219 	 * Initialization phases.
220 	 *
221 	 * These stages denote the initialization states which Aloha will go
222 	 * through from loading to ready.
223 	 *
224 	 * Each phase object contains the following properties:
225 	 *        fn : The process that is to be invoked during this phase.
226 	 *             Will take two functions: event() and next().
227 	 *     event : The event name, which if provided, will be fired after the
228 	 *             phase has been started (optional).
229 	 *  deferred : A $.Deferred() object to hold event handlers until that
230 	 *             initialization phase has been done (optional).
231 	 *
232 	 * @type {Array.<phase>}
233 	 */
234 	var phases = [
235 		// Phase 0: Waiting for initialization to begin.  This is the state at
236 		//          load-time.
237 		{
238 			fn: null,
239 			event: null,
240 			deferred: null
241 		},
242 
243 		// Phase 1: DOM is ready; performing compatibility checks, registering
244 		//          basic events, and initializing logging.
245 		{
246 			fn: initAloha,
247 			event: null,
248 			deferred: null
249 		},
250 
251 		// Phase 2: Initial checks have passed; Initializing repository manger.
252 		{
253 			fn: initRepositoryManager,
254 			event: null,
255 			deferred: null
256 		},
257 
258 		// Phase 3: Repository manager is ready for use; commence
259 		//          initialization of configured or default plugins.
260 		{
261 			fn: initPluginManager,
262 			event: 'aloha-plugins-loaded',
263 			deferred: null
264 		},
265 
266 		// Phase 4: Plugins have all begun their initialization process, but it
267 		//          is not necessary that they have completed.  Start
268 		//          initializing editable, along with their scaffolding UI.
269 		//          Editables will not be fully initialized however, until
270 		//          plugins have finished initialization.
271 		{
272 			fn: initEditables,
273 			event: null,
274 			deferred: null
275 		},
276 
277 		// Phase 5: The "ready" state.  Notify the system that Aloha is fully
278 		//          loaded, and ready for use.
279 		{
280 			fn: null,
281 			event: 'aloha-ready',
282 			deferred: null
283 		}
284 	];
285 
286 
287 	/**
288 	 * Base Aloha Object
289 	 * @namespace Aloha
290 	 * @class Aloha The Aloha base object, which contains all the core functionality
291 	 * @singleton
292 	 */
293 	$.extend(true, Aloha, {
294 
295 		/**
296 		 * The Aloha Editor Version we are using
297 		 * It should be set by us and updated for the particular branch
298 		 * @property
299 		 */
300 		version: '${version}',
301 
302 		/**
303 		 * Array of editables that are managed by Aloha
304 		 * @property
305 		 * @type Array
306 		 */
307 		editables: [],
308 
309 		/**
310 		 * The currently active editable is referenced here
311 		 * @property
312 		 * @type Aloha.Editable
313 		 */
314 		activeEditable: null,
315 
316 		/**
317 		 * settings object, which will contain all Aloha settings
318 		 * @cfg {Object} object Aloha's settings
319 		 */
320 		settings: {},
321 
322 		/**
323 		 * defaults object, which will contain all Aloha defaults
324 		 * @cfg {Object} object Aloha's settings
325 		 */
326 		defaults: {},
327 
328 		/**
329 		 * Namespace for ui components
330 		 */
331 		ui: {},
332 
333 		/**
334 		 * This represents the name of the users OS. Could be:
335 		 * 'Mac', 'Linux', 'Win', 'Unix', 'Unknown'
336 		 * @property
337 		 * @type string
338 		 */
339 		OSName: 'Unknown',
340 
341 		/**
342 		 * A list of loaded plugin names, available after the STAGES.PLUGINS
343 		 * initialization phase.
344 		 *
345 		 * @type {Array.<string>}
346 		 * @internal
347 		 */
348 		loadedPlugins: [],
349 
350 		/**
351 		 * Maps names of plugins (link) to the base URL (../plugins/common/link).
352 		 */
353 		_pluginBaseUrlByName: {},
354 
355 		/**
356 		 * Start the initialization process.
357 		 */
358 		init: function () {
359 			Aloha.initialize(phases);
360 		},
361 
362 		/**
363 		 * Returns list of loaded plugins (without Bundle name)
364 		 *
365 		 * @return array
366 		 */
367 		getLoadedPlugins: function () {
368 			return this.loadedPlugins;
369 		},
370 
371 		/**
372 		 * Returns true if a certain plugin is loaded, false otherwise.
373 		 *
374 		 * @param {string} plugin Name of plugin
375 		 * @return {boolean} True if plugin with given name is load.
376 		 */
377 		isPluginLoaded: function (name) {
378 			var loaded = false;
379 			$.each(this.loadedPlugins, function (i, plugin) {
380 				if (name === plugin.toString()) {
381 					loaded = true;
382 					return false;
383 				}
384 			});
385 			return loaded;
386 		},
387 
388 		/**
389 		 * Activates editable and deactivates all other Editables.
390 		 *
391 		 * @param {Editable} editable the Editable to be activated
392 		 */
393 		activateEditable: function (editable) {
394 			// Because editables may be removed on blur, Aloha.editables.length
395 			// is not cached.
396 			var editables = Aloha.editables;
397 			var i;
398 			for (i = 0; i < editables.length; i++) {
399 				if (editables[i] !== editable && editables[i].isActive) {
400 					editables[i].blur();
401 				}
402 			}
403 			Aloha.activeEditable = editable;
404 		},
405 
406 		/**
407 		 * Returns the current Editable
408 		 * @return {Editable} returns the active Editable
409 		 */
410 		getActiveEditable: function () {
411 			return Aloha.activeEditable;
412 		},
413 
414 		/**
415 		 * Deactivates the active Editable.
416 		 *
417 		 * TODO: Would be better named "deactivateActiveEditable".
418 		 */
419 		deactivateEditable: function () {
420 			if (Aloha.activeEditable) {
421 				Aloha.activeEditable.blur();
422 				Aloha.activeEditable = null;
423 			}
424 		},
425 
426 		/**
427 		 * Gets an editable by an ID or null if no Editable with that ID
428 		 * registered.
429 		 *
430 		 * @param {string} id The element id to look for.
431 		 * @return {Aloha.Editable|null} An editable, or null if none if found
432 		 *                               for the given id.
433 		 */
434 		getEditableById: function (id) {
435 			// Because if the element is a textarea, then it's necessary to
436 			// route to the editable div.
437 			var $editable = $('#' + id);
438 			if ($editable.length
439 					&& 'textarea' === $editable[0].nodeName.toLowerCase()) {
440 				id = id + '-aloha';
441 			}
442 			var i;
443 			for (i = 0; i < Aloha.editables.length; i++) {
444 				if (Aloha.editables[i].getId() === id) {
445 					return Aloha.editables[i];
446 				}
447 			}
448 			return null;
449 		},
450 
451 		/**
452 		 * Checks whether an object is a registered Aloha Editable.
453 		 * @param {jQuery} obj the jQuery object to be checked.
454 		 * @return {boolean}
455 		 */
456 		isEditable: function (obj) {
457 			var i, editablesLength;
458 
459 			for (i = 0, editablesLength = Aloha.editables.length; i < editablesLength; i++) {
460 				if (Aloha.editables[i].originalObj.get(0) === obj) {
461 					return true;
462 				}
463 			}
464 			return false;
465 		},
466 
467 		/**
468 		 * Gets the nearest editable parent of the DOM element contained in the
469 		 * given jQuery object.
470 		 *
471 		 * @param {jQuery} $element jQuery unit set containing DOM element.
472 		 * @return {Aloha.Editable} Editable, or null if none found.
473 		 */
474 		getEditableHost: (function () {
475 			var getEditableOf = function (editable) {
476 				var i;
477 				for (i = 0; i < Aloha.editables.length; i++) {
478 					if (Aloha.editables[i].originalObj[0] === editable) {
479 						return Aloha.editables[i];
480 					}
481 				}
482 				return null;
483 			};
484 
485 			return function ($element) {
486 				if (!$element || 0 === $element.length) {
487 					return null;
488 				}
489 				var editable = getEditableOf($element[0]);
490 				if (!editable) {
491 					$element.parents().each(function (__unused__, node) {
492 						editable = getEditableOf(node);
493 						if (editable) {
494 							return false;
495 						}
496 					});
497 				}
498 				return editable;
499 			};
500 		}()),
501 
502 		/**
503 		 * Logs a message to the console.
504 		 *
505 		 * @param {string} level Level of the log
506 		 *                       ("error", "warn" or "info", "debug").
507 		 * @param {object} component Component that calls the log.
508 		 * @param {string} message Log message.
509 		 * @hide
510 		 */
511 		log: function (level, component, message) {
512 			if (typeof Aloha.Log !== 'undefined') {
513 				Aloha.Log.log(level, component, message);
514 			}
515 		},
516 
517 		/**
518 		 * Register the given editable.
519 		 *
520 		 * @param {Editable} editable to register.
521 		 * @hide
522 		 */
523 		registerEditable: function (editable) {
524 			Aloha.editables.push(editable);
525 		},
526 
527 		/**
528 		 * Unregister the given editable. It will be deactivated and removed
529 		 * from editables.
530 		 *
531 		 * @param {Editable} editable The editable to unregister.
532 		 * @hide
533 		 */
534 		unregisterEditable: function (editable) {
535 			var index = $.inArray(editable, Aloha.editables);
536 			if (index !== -1) {
537 				Aloha.editables.splice(index, 1);
538 			}
539 		},
540 
541 		/**
542 		 * Check whether at least one editable was modified.
543 		 *
544 		 * @return {boolean} True when at least one editable was modified,
545 		 *                   false otherwise.
546 		 */
547 		isModified: function () {
548 			var i;
549 			for (i = 0; i < Aloha.editables.length; i++) {
550 				if (Aloha.editables[i].isModified
551 						&& Aloha.editables[i].isModified()) {
552 					return true;
553 				}
554 			}
555 			return false;
556 		},
557 
558 		/**
559 		 * Determines the Aloha Url.
560 		 *
561 		 * @return {String} Aloha's baseUrl setting or "" if not set.
562 		 */
563 		getAlohaUrl: function (suffix) {
564 			return Aloha.settings.baseUrl || '';
565 		},
566 
567 		/**
568 		 * Gets the plugin's url.
569 		 *
570 		 * @param {string} name The name with which the plugin was registered
571 		 *                      with.
572 		 * @return {string} The fully qualified url of this plugin.
573 		 */
574 		getPluginUrl: function (name) {
575 			if (!name) {
576 				return null;
577 			}
578 			var url = Aloha.settings._pluginBaseUrlByName[name];
579 			if (url) {
580 				// Check if url is absolute and attach base url if it is not.
581 				if (!url.match("^(\/|http[s]?:).*")) {
582 					url = Aloha.getAlohaUrl() + '/' + url;
583 				}
584 			}
585 			return url;
586 		},
587 
588 		/**
589 		 * Disable object resizing by executing command 'enableObjectResizing',
590 		 * if the browser supports this.
591 		 */
592 		disableObjectResizing: function () {
593 			try {
594 				// This will disable browsers image resizing facilities in
595 				// order disable resize handles.
596 				var supported;
597 				try {
598 					supported = document.queryCommandSupported('enableObjectResizing');
599 				} catch (e) {
600 					supported = false;
601 					Aloha.Log.log('enableObjectResizing is not supported.');
602 				}
603 				if (supported) {
604 					document.execCommand('enableObjectResizing', false, false);
605 					Aloha.Log.log('enableObjectResizing disabled.');
606 				}
607 			} catch (e2) {
608 				Aloha.Log.error(e2, 'Could not disable enableObjectResizing');
609 				// this is just for others, who will not support disabling enableObjectResizing
610 			}
611 		},
612 
613 		/**
614 		 * Human-readable string representation of this.
615 		 *
616 		 * @hide
617 		 */
618 		toString: function () {
619 			return 'Aloha';
620 		}
621 	});
622 
623 	return Aloha;
624 });
625