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