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