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