1 /* jquery.aloha.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 /**
 28  * IMPORTANT!
 29  * Don't add any more custom jquery extensions here.
 30  * Instead use the define(...) mechanism to define a module and to
 31  * import it where you need it.
 32  */
 33 define([
 34 	'aloha/core',
 35 	'aloha/selection',
 36 	'jquery',
 37 	'aloha/console'
 38 ], function (
 39 	Aloha,
 40 	Selection,
 41 	jQuery,
 42 	console
 43 ) {
 44 	'use strict';
 45 
 46 	var XMLSerializer = window.XMLSerializer;
 47 
 48 	/**
 49 	 * jQuery between Extension
 50 	 *
 51 	 * insert either html code, a dom object OR a jQuery object inside of an existing text node.
 52 	 * if the chained jQuery object is not a text node, nothing will happen.
 53 	 *
 54 	 * @param content HTML Code, DOM object or jQuery object to be inserted
 55 	 * @param offset character offset from the start where the content should be inserted
 56 	 */
 57 	jQuery.fn.between = function (content, offset) {
 58 		var offSize, fullText;
 59 
 60 		if (this[0].nodeType !== 3) {
 61 			// we are not in a text node, just insert the element at the corresponding position
 62 			offSize = this.children().size();
 63 			if (offset > offSize) {
 64 				offset = offSize;
 65 			}
 66 			if (offset <= 0) {
 67 				this.prepend(content);
 68 			} else {
 69 				this.children().eq(offset - 1).after(content);
 70 			}
 71 		} else {
 72 			// we are in a text node so we have to split it at the correct position
 73 			if (offset <= 0) {
 74 				this.before(content);
 75 			} else if (offset >= this[0].length) {
 76 				this.after(content);
 77 			} else {
 78 				fullText = this[0].data;
 79 				this[0].data = fullText.substring(0, offset);
 80 				this.after(fullText.substring(offset, fullText.length));
 81 				this.after(content);
 82 			}
 83 		}
 84 	};
 85 
 86 	/**
 87 	 * Make the object contenteditable. Care about browser version (name of contenteditable attribute depends on it)
 88 	 */
 89 	jQuery.fn.contentEditable = function (b) {
 90 		// ie does not understand contenteditable but contentEditable
 91 		// contentEditable is not xhtml compatible.
 92 		var $el = jQuery(this);
 93 		var ce = 'contenteditable';
 94 
 95 		// Check
 96 		if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) == 7) {
 97 			ce = 'contentEditable';
 98 		}
 99 
100 		if (typeof b === 'undefined') {
101 
102 			// For chrome use this specific attribute. The old ce will only
103 			// return 'inherit' for nested elements of a contenteditable.
104 			// The isContentEditable is a w3c standard compliant property which works in IE7,8,FF36+, Chrome 12+
105 			if (typeof $el[0] === 'undefined') {
106 				console.warn('The jquery object did not contain any valid elements.'); // die silent
107 				return undefined;
108 			}
109 			if (typeof $el[0].isContentEditable === 'undefined') {
110 				console.warn('Could not determine whether the is editable or not. I assume it is.');
111 				return true;
112 			}
113 
114 			return $el[0].isContentEditable;
115 		}
116 
117 		if (b === '') {
118 			$el.removeAttr(ce);
119 		} else {
120 			if (b && b !== 'false') {
121 				b = 'true';
122 			} else {
123 				b = 'false';
124 			}
125 			$el.attr(ce, b);
126 		}
127 
128 		return $el;
129 	};
130 
131 	/**
132 	 * jQuery Aloha Plugin.
133 	 *
134 	 * Makes the elements in a jQuery selection set Aloha editables.
135 	 *
136 	 * @return jQuery container of holding DOM elements that have been
137 	 *         aloha()fied.
138 	 * @api
139 	 */
140 	jQuery.fn.aloha = function () {
141 		var $elements = this;
142 		Aloha.bind('aloha-plugins-loaded', function () {
143 			$elements.each(function (_, elem) {
144 				if (!Aloha.isEditable(elem)) {
145 					new Aloha.Editable(jQuery(elem)).init();
146 				}
147 			});
148 		});
149 		return $elements;
150 	};
151 
152 	/**
153 	 * jQuery destroy elements as editable
154 	 *
155 	 * destroy all mached elements editable capabilities
156 	 * @return	jQuery object for the matched elements
157 	 * @api
158 	 */
159 	jQuery.fn.mahalo = function () {
160 		return this.each(function () {
161 			if (Aloha.isEditable(this)) {
162 				Aloha.getEditableById(jQuery(this).attr('id')).destroy();
163 			}
164 		});
165 	};
166 
167 	/**
168 	 * jQuery Extension
169 	 * new Event which is triggered whenever a selection (length >= 0) is made in
170 	 * an Aloha Editable element
171 	 */
172 	jQuery.fn.contentEditableSelectionChange = function (callback) {
173 		var that = this;
174 
175 		// update selection when keys are pressed
176 		this.keyup(function (event) {
177 			var rangeObject = Selection.getRangeObject();
178 			callback(event);
179 		});
180 
181 		// update selection on doubleclick (especially important for the first automatic selection, when the Editable is not active yet, but is at the same time activated as the selection occurs
182 		this.dblclick(function (event) {
183 			callback(event);
184 		});
185 
186 		// update selection when text is selected
187 		this.mousedown(function (event) {
188 			// remember that a selection was started
189 			that.selectionStarted = true;
190 		});
191 
192 		jQuery(document).mouseup(function (event) {
193 			Selection.eventOriginalTarget = that;
194 			if (that.selectionStarted) {
195 				callback(event);
196 			}
197 			Selection.eventOriginalTarget = false;
198 			that.selectionStarted = false;
199 		});
200 
201 		return this;
202 	};
203 
204 	/**
205 	 * Fetch the outerHTML of an Element
206 	 * @version 1.0.0
207 	 * @date February 01, 2011
208 	 * @package jquery-sparkle {@link http://www.balupton/projects/jquery-sparkle}
209 	 * @author Benjamin Arthur Lupton {@link http://balupton.com}
210 	 * @copyright 2011 Benjamin Arthur Lupton {@link http://balupton.com}
211 	 * @license MIT License {@link http://creativecommons.org/licenses/MIT/}
212 	 * @return {String} outerHtml
213 	 */
214 	jQuery.fn.outerHtml = jQuery.fn.outerHtml || function () {
215 		var $el = jQuery(this),
216 			el = $el.get(0);
217 		if (typeof el.outerHTML != 'undefined') {
218 			return el.outerHTML;
219 		}
220 		try {
221 			// Gecko-based browsers, Safari, Opera.
222 			return (new XMLSerializer()).serializeToString(el);
223 		} catch (e) {
224 			try {
225 				// Internet Explorer.
226 				return el.xml;
227 			} catch (e2) {}
228 		}
229 	};
230 
231 	jQuery.fn.zap = function () {
232 		return this.each(function () {
233 			jQuery(this.childNodes).insertBefore(this);
234 		}).remove();
235 	};
236 
237 	jQuery.fn.textNodes = function (excludeBreaks, includeEmptyTextNodes) {
238 		var ret = [],
239 			doSomething = function (el) {
240 				var i, childLength;
241 				if ((el.nodeType === 3 && jQuery.trim(el.data) && !includeEmptyTextNodes) || (el.nodeType === 3 && includeEmptyTextNodes) || (el.nodeName == "BR" && !excludeBreaks)) {
242 					ret.push(el);
243 				} else {
244 					for (i = 0, childLength = el.childNodes.length; i < childLength; ++i) {
245 						doSomething(el.childNodes[i]);
246 					}
247 				}
248 			};
249 
250 		doSomething(this[0]);
251 
252 		return jQuery(ret);
253 	};
254 
255 	/**
256 	 * extendObjects is like jQuery.extend, but it does not extend arrays
257 	 */
258 	jQuery.extendObjects = jQuery.fn.extendObjects = function (arg1, arg2) {
259 		var options, name, src, copy, copyIsArray, clone,
260 		    start = 1,
261 		    target = arg1 || {},
262 			length = arguments.length,
263 		    deep = false,
264 		    i;
265 
266 
267 		// Handle a deep copy situation
268 		if (typeof target === "boolean") {
269 			deep = target;
270 			target = arg2 || {};
271 			// skip the boolean and the target
272 			start = 2;
273 		}
274 
275 		// Handle case when target is a string or something (possible in deep copy)
276 		if (typeof target !== "object" && !jQuery.isFunction(target)) {
277 			target = {};
278 		}
279 
280 		// extend jQuery itself if only one argument is passed
281 		if (length === start) {
282 			target = this;
283 			--start;
284 		}
285 
286 		for (i = start; i < length; i++) {
287 			// Only deal with non-null/undefined values
288 			if ((options = arguments[i]) != null) {
289 				// Extend the base object
290 				for (name in options) {
291 					if (options.hasOwnProperty(name)) {
292 
293 						src = target[name];
294 						copy = options[name];
295 
296 						// Prevent never-ending loop
297 						if (target === copy) {
298 							continue;
299 						}
300 
301 						// Recurse if we're merging plain objects or arrays
302 						if (deep && copy && (jQuery.isPlainObject(copy) || true === (copyIsArray = jQuery.isArray(copy)))) {
303 							if (copyIsArray) {
304 								copyIsArray = false;
305 								clone = src && jQuery.isArray(src) ? src : [];
306 
307 							} else {
308 								clone = src && jQuery.isPlainObject(src) ? src : {};
309 							}
310 
311 							// Never move original objects, clone them
312 							if (jQuery.isArray(copy)) {
313 								// don't extend arrays
314 								target[name] = copy;
315 							} else {
316 								target[name] = jQuery.extendObjects(deep, clone, copy);
317 							}
318 
319 							// Don't bring in undefined values
320 						} else if (copy !== undefined) {
321 							target[name] = copy;
322 						}
323 					}
324 				}
325 			}
326 		}
327 
328 		// Return the modified object
329 		return target;
330 	};
331 
332 	/*
333 	 * jQuery Hotkeys Plugin
334 	 * Copyright 2010, John Resig
335 	 * Dual licensed under the MIT or GPL Version 2 licenses.
336 	 *
337 	 * Based upon the plugin by Tzury Bar Yochay:
338 	 * http://github.com/tzuryby/hotkeys
339 	 *
340 	 * Original idea by:
341 	 * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
342 	 */
343 
344 	jQuery.hotkeys = {
345 		version: "0.8",
346 
347 		specialKeys: {
348 			8: "backspace",
349 			9: "tab",
350 			13: "return",
351 			16: "shift",
352 			17: "ctrl",
353 			18: "alt",
354 			19: "pause",
355 			20: "capslock",
356 			27: "esc",
357 			32: "space",
358 			33: "pageup",
359 			34: "pagedown",
360 			35: "end",
361 			36: "home",
362 			37: "left",
363 			38: "up",
364 			39: "right",
365 			40: "down",
366 			45: "insert",
367 			46: "del",
368 			96: "0",
369 			97: "1",
370 			98: "2",
371 			99: "3",
372 			100: "4",
373 			101: "5",
374 			102: "6",
375 			103: "7",
376 			104: "8",
377 			105: "9",
378 			106: "*",
379 			107: "+",
380 			109: "-",
381 			110: ".",
382 			111: "/",
383 			112: "f1",
384 			113: "f2",
385 			114: "f3",
386 			115: "f4",
387 			116: "f5",
388 			117: "f6",
389 			118: "f7",
390 			119: "f8",
391 			120: "f9",
392 			121: "f10",
393 			122: "f11",
394 			123: "f12",
395 			144: "numlock",
396 			145: "scroll",
397 			191: "/",
398 			224: "meta"
399 		},
400 
401 		shiftNums: {
402 			"`": "~",
403 			"1": "!",
404 			"2": "@",
405 			"3": "#",
406 			"4": "$",
407 			"5": "%",
408 			"6": "^",
409 			"7": "&",
410 			"8": "*",
411 			"9": "(",
412 			"0": ")",
413 			"-": "_",
414 			"=": "+",
415 			";": ": ",
416 			"'": "\"",
417 			",": "<",
418 			".": ">",
419 			"/": "?",
420 			"\\": "|"
421 		}
422 	};
423 
424 	function applyKeyHandler(handler, context, args, event) {
425 		// Don't fire in text-accepting inputs that we didn't directly bind to
426 		if (context !== event.target && (/textarea|input|select/i.test(event.target.nodeName) || event.target.type === "text")) {
427 			return;
428 		}
429 		return handler.apply(context, args);
430 	}
431 
432 	function keyHandler(handleObj) {
433 		var origHandler, keys, handle, i;
434 
435 		// Only care when a possible input has been specified
436 		if (typeof handleObj.data !== "string") {
437 			return;
438 		}
439 
440 		origHandler = handleObj.handler;
441 		keys = handleObj.data.toLowerCase().split(" ");
442 		handle = {};
443 
444 		for (i = 0; i < keys.length; i++) {
445 			handle[keys[i]] = true;
446 		}
447 
448 		handleObj.handler = function (event) {
449 			// The original comment that was added with this condition says:
450 			// "Don't fire in contentEditable true elements"
451 			// But this is incorrect.
452 			// What this condition does is it skips hotkey events for
453 			// any target unless it is directly bound.
454 			// The condition event.target.contentEditable !== true will
455 			// always be true, because contentEditable is a string
456 			// attribute that is never strictly equal true.
457 			//if (this !== event.target && event.target.contentEditable !== true) {
458 			//return;
459 			//}
460 			// Below is what this condition really does. Ideally, I'd
461 			// like to remove this condition since it was not there in
462 			// the original implementation by John Resig and it could
463 			// interfere with other plugins, but when I removed it, I
464 			// was unable to input any space characters into an
465 			// editable.
466 			// TODO figure out a way to safely remove this
467 			if (this !== event.target) {
468 				return;
469 			}
470 
471 			// Keypress represents characters, not special keys
472 			var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
473 				modif = "",
474 				character;
475 
476 			// check combinations (alt|ctrl|shift+anything)
477 			if (event.altKey && special !== "alt") {
478 				modif += "alt+";
479 			}
480 
481 			if (event.ctrlKey && special !== "ctrl") {
482 				modif += "ctrl+";
483 			}
484 
485 			// TODO: Need to make sure this works consistently across platforms
486 			if (event.metaKey && !event.ctrlKey && special !== "meta") {
487 				modif += "meta+";
488 			}
489 
490 			if (event.shiftKey && special !== "shift") {
491 				modif += "shift+";
492 			}
493 
494 			if (special) {
495 				if (handle[modif + special]) {
496 					return applyKeyHandler(origHandler, this, arguments, event);
497 				}
498 			} else {
499 				character = String.fromCharCode(event.which).toLowerCase();
500 
501 				if (handle[modif + character]) {
502 					return applyKeyHandler(origHandler, this, arguments, event);
503 				}
504 
505 				if (handle[modif + jQuery.hotkeys.shiftNums[character]]) {
506 					return applyKeyHandler(origHandler, this, arguments, event);
507 				}
508 
509 				// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
510 				if (modif === "shift+") {
511 					if (handle[jQuery.hotkeys.shiftNums[character]]) {
512 						return applyKeyHandler(origHandler, this, arguments, event);
513 					}
514 				}
515 			}
516 		};
517 	}
518 
519 	jQuery.each(['keydown', 'keyup', 'keypress'], function () {
520 		jQuery.event.special[this] = {
521 			add: keyHandler
522 		};
523 	});
524 
525 });
526