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 alohaText gets contents for an Aloha Editor editable
169 	 *
170 	 * getContents forall editable
171 	 * @return	jQuery object for the matched elements
172 	 * @api
173 	 */
174 	jQuery.fn.alohaText = function () {
175 		return this.each(function () {
176 			if (Aloha.isEditable(this)) {
177 				Aloha.getEditableById(jQuery(this).attr('id')).getContents();
178 			}
179 		});
180 	};
181 
182 	/**
183 	 * jQuery Extension
184 	 * new Event which is triggered whenever a selection (length >= 0) is made in
185 	 * an Aloha Editable element
186 	 */
187 	jQuery.fn.contentEditableSelectionChange = function (callback) {
188 		var that = this;
189 
190 		// update selection when keys are pressed
191 		this.keyup(function (event) {
192 			var rangeObject = Selection.getRangeObject();
193 			callback(event);
194 		});
195 
196 		// 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
197 		this.dblclick(function (event) {
198 			callback(event);
199 		});
200 
201 		// update selection when text is selected
202 		this.mousedown(function (event) {
203 			// remember that a selection was started
204 			that.selectionStarted = true;
205 		});
206 
207 		jQuery(document).mouseup(function (event) {
208 			Selection.eventOriginalTarget = that;
209 			if (that.selectionStarted) {
210 				callback(event);
211 			}
212 			Selection.eventOriginalTarget = false;
213 			that.selectionStarted = false;
214 		});
215 
216 		return this;
217 	};
218 
219 	/**
220 	 * Fetch the outerHTML of an Element
221 	 * @version 1.0.0
222 	 * @date February 01, 2011
223 	 * @package jquery-sparkle {@link http://www.balupton/projects/jquery-sparkle}
224 	 * @author Benjamin Arthur Lupton {@link http://balupton.com}
225 	 * @copyright 2011 Benjamin Arthur Lupton {@link http://balupton.com}
226 	 * @license MIT License {@link http://creativecommons.org/licenses/MIT/}
227 	 * @return {String} outerHtml
228 	 */
229 	jQuery.fn.outerHtml = jQuery.fn.outerHtml || function () {
230 		var $el = jQuery(this),
231 			el = $el.get(0);
232 		if (typeof el.outerHTML != 'undefined') {
233 			return el.outerHTML;
234 		}
235 		try {
236 			// Gecko-based browsers, Safari, Opera.
237 			return (new XMLSerializer()).serializeToString(el);
238 		} catch (e) {
239 			try {
240 				// Internet Explorer.
241 				return el.xml;
242 			} catch (e2) {}
243 		}
244 	};
245 
246 	jQuery.fn.zap = function () {
247 		return this.each(function () {
248 			jQuery(this.childNodes).insertBefore(this);
249 		}).remove();
250 	};
251 
252 	jQuery.fn.textNodes = function (excludeBreaks, includeEmptyTextNodes) {
253 		var ret = [],
254 			doSomething = function (el) {
255 				var i, childLength;
256 				if ((el.nodeType === 3 && jQuery.trim(el.data) && !includeEmptyTextNodes) || (el.nodeType === 3 && includeEmptyTextNodes) || (el.nodeName == "BR" && !excludeBreaks)) {
257 					ret.push(el);
258 				} else {
259 					for (i = 0, childLength = el.childNodes.length; i < childLength; ++i) {
260 						doSomething(el.childNodes[i]);
261 					}
262 				}
263 			};
264 
265 		doSomething(this[0]);
266 
267 		return jQuery(ret);
268 	};
269 
270 	/**
271 	 * extendObjects is like jQuery.extend, but it does not extend arrays
272 	 */
273 	jQuery.extendObjects = jQuery.fn.extendObjects = function (arg1, arg2) {
274 		var options, name, src, copy, copyIsArray, clone,
275 		    start = 1,
276 		    target = arg1 || {},
277 			length = arguments.length,
278 		    deep = false,
279 		    i;
280 
281 
282 		// Handle a deep copy situation
283 		if (typeof target === "boolean") {
284 			deep = target;
285 			target = arg2 || {};
286 			// skip the boolean and the target
287 			start = 2;
288 		}
289 
290 		// Handle case when target is a string or something (possible in deep copy)
291 		if (typeof target !== "object" && !jQuery.isFunction(target)) {
292 			target = {};
293 		}
294 
295 		// extend jQuery itself if only one argument is passed
296 		if (length === start) {
297 			target = this;
298 			--start;
299 		}
300 
301 		for (i = start; i < length; i++) {
302 			// Only deal with non-null/undefined values
303 			if ((options = arguments[i]) != null) {
304 				// Extend the base object
305 				for (name in options) {
306 					if (options.hasOwnProperty(name)) {
307 
308 						src = target[name];
309 						copy = options[name];
310 
311 						// Prevent never-ending loop
312 						if (target === copy) {
313 							continue;
314 						}
315 
316 						// Recurse if we're merging plain objects or arrays
317 						if (deep && copy && (jQuery.isPlainObject(copy) || true === (copyIsArray = jQuery.isArray(copy)))) {
318 							if (copyIsArray) {
319 								copyIsArray = false;
320 								clone = src && jQuery.isArray(src) ? src : [];
321 
322 							} else {
323 								clone = src && jQuery.isPlainObject(src) ? src : {};
324 							}
325 
326 							// Never move original objects, clone them
327 							if (jQuery.isArray(copy)) {
328 								// don't extend arrays
329 								target[name] = copy;
330 							} else {
331 								target[name] = jQuery.extendObjects(deep, clone, copy);
332 							}
333 
334 							// Don't bring in undefined values
335 						} else if (copy !== undefined) {
336 							target[name] = copy;
337 						}
338 					}
339 				}
340 			}
341 		}
342 
343 		// Return the modified object
344 		return target;
345 	};
346 
347 	/*
348 	 * jQuery Hotkeys Plugin
349 	 * Copyright 2010, John Resig
350 	 * Dual licensed under the MIT or GPL Version 2 licenses.
351 	 *
352 	 * Based upon the plugin by Tzury Bar Yochay:
353 	 * http://github.com/tzuryby/hotkeys
354 	 *
355 	 * Original idea by:
356 	 * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
357 	 */
358 
359 	jQuery.hotkeys = {
360 		version: "0.8",
361 
362 		specialKeys: {
363 			8: "backspace",
364 			9: "tab",
365 			13: "return",
366 			16: "shift",
367 			17: "ctrl",
368 			18: "alt",
369 			19: "pause",
370 			20: "capslock",
371 			27: "esc",
372 			32: "space",
373 			33: "pageup",
374 			34: "pagedown",
375 			35: "end",
376 			36: "home",
377 			37: "left",
378 			38: "up",
379 			39: "right",
380 			40: "down",
381 			45: "insert",
382 			46: "del",
383 			96: "0",
384 			97: "1",
385 			98: "2",
386 			99: "3",
387 			100: "4",
388 			101: "5",
389 			102: "6",
390 			103: "7",
391 			104: "8",
392 			105: "9",
393 			106: "*",
394 			107: "+",
395 			109: "-",
396 			110: ".",
397 			111: "/",
398 			112: "f1",
399 			113: "f2",
400 			114: "f3",
401 			115: "f4",
402 			116: "f5",
403 			117: "f6",
404 			118: "f7",
405 			119: "f8",
406 			120: "f9",
407 			121: "f10",
408 			122: "f11",
409 			123: "f12",
410 			144: "numlock",
411 			145: "scroll",
412 			188: ",",
413 			190: ".",
414 			191: "/",
415 			224: "meta"
416 		},
417 
418 		shiftNums: {
419 			"`": "~",
420 			"1": "!",
421 			"2": "@",
422 			"3": "#",
423 			"4": "$",
424 			"5": "%",
425 			"6": "^",
426 			"7": "&",
427 			"8": "*",
428 			"9": "(",
429 			"0": ")",
430 			"-": "_",
431 			"=": "+",
432 			";": ": ",
433 			"'": "\"",
434 			",": "<",
435 			".": ">",
436 			"/": "?",
437 			"\\": "|"
438 		}
439 	};
440 
441 	function applyKeyHandler(handler, context, args, event) {
442 		// Don't fire in text-accepting inputs that we didn't directly bind to
443 		if (context !== event.target && (/textarea|input|select/i.test(event.target.nodeName) || event.target.type === "text")) {
444 			return;
445 		}
446 		return handler.apply(context, args);
447 	}
448 
449 	function keyHandler(handleObj) {
450 		var origHandler, keys, handle, i;
451 
452 		// Only care when a possible input has been specified
453 		if (typeof handleObj.data !== "string") {
454 			return;
455 		}
456 
457 		origHandler = handleObj.handler;
458 		keys = handleObj.data.toLowerCase().split(" ");
459 		handle = {};
460 
461 		for (i = 0; i < keys.length; i++) {
462 			handle[keys[i]] = true;
463 		}
464 
465 		handleObj.handler = function (event) {
466 			// The original comment that was added with this condition says:
467 			// "Don't fire in contentEditable true elements"
468 			// But this is incorrect.
469 			// What this condition does is it skips hotkey events for
470 			// any target unless it is directly bound.
471 			// The condition event.target.contentEditable !== true will
472 			// always be true, because contentEditable is a string
473 			// attribute that is never strictly equal true.
474 			//if (this !== event.target && event.target.contentEditable !== true) {
475 			//return;
476 			//}
477 			// Below is what this condition really does. Ideally, I'd
478 			// like to remove this condition since it was not there in
479 			// the original implementation by John Resig and it could
480 			// interfere with other plugins, but when I removed it, I
481 			// was unable to input any space characters into an
482 			// editable.
483 			// TODO figure out a way to safely remove this
484 			if (this !== event.target) {
485 				return;
486 			}
487 
488 			// Keypress represents characters, not special keys
489 			var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
490 				modif = "",
491 				character;
492 
493 			// check combinations (alt|ctrl|shift+anything)
494 			if (event.altKey && special !== "alt") {
495 				modif += "alt+";
496 			}
497 
498 			if (event.ctrlKey && special !== "ctrl") {
499 				modif += "ctrl+";
500 			}
501 
502 			// TODO: Need to make sure this works consistently across platforms
503 			if (event.metaKey && !event.ctrlKey && special !== "meta") {
504 				modif += "meta+";
505 			}
506 
507 			if (event.shiftKey && special !== "shift") {
508 				modif += "shift+";
509 			}
510 
511 			if (special) {
512 				if (handle[modif + special]) {
513 					return applyKeyHandler(origHandler, this, arguments, event);
514 				}
515 			} else {
516 				character = String.fromCharCode(event.which).toLowerCase();
517 
518 				if (handle[modif + character]) {
519 					return applyKeyHandler(origHandler, this, arguments, event);
520 				}
521 
522 				if (handle[modif + jQuery.hotkeys.shiftNums[character]]) {
523 					return applyKeyHandler(origHandler, this, arguments, event);
524 				}
525 
526 				// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
527 				if (modif === "shift+") {
528 					if (handle[jQuery.hotkeys.shiftNums[character]]) {
529 						return applyKeyHandler(origHandler, this, arguments, event);
530 					}
531 				}
532 			}
533 		};
534 	}
535 
536 	jQuery.each(['keydown', 'keyup', 'keypress'], function () {
537 538 		jQuery.event.special[this] = {
539 			add: keyHandler
540 		};
541 	});
542 
543 });
544