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