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 (Aloha,
 39              Selection,
 40 			 jQuery,
 41 			 console) {
 42 	'use strict';
 43 
 44 	var XMLSerializer = window.XMLSerializer;
 45 
 46 	/**
 47 	 * jQuery between Extension
 48 	 *
 49 	 * insert either html code, a dom object OR a jQuery object inside of an existing text node.
 50 	 * if the chained jQuery object is not a text node, nothing will happen.
 51 	 *
 52 	 * @param content HTML Code, DOM object or jQuery object to be inserted
 53  54 	 * @param offset character offset from the start where the content should be inserted
 55 	 */
 56 	jQuery.fn.between = function (content, offset) {
 57 		var
 58 			offSize,
 59 			fullText;
 60 
 61 		if (this[0].nodeType !== 3) {
 62 			// we are not in a text node, just insert the element at the corresponding position
 63 			offSize = this.children().size();
 64 			if (offset > offSize) {
 65 				offset = offSize;
 66 			}
 67 			if (offset <= 0) {
 68 				this.prepend(content);
 69 			} else {
 70 				this.children().eq(offset -1).after(content);
 71 			}
 72 		} else {
 73 			// we are in a text node so we have to split it at the correct position
 74 			if (offset <= 0) {
 75 				this.before(content);
 76 			} else if (offset >= this[0].length) {
 77 				this.after(content);
 78 			} else {
 79 				fullText = this[0].data;
 80 				this[0].data = fullText.substring(0, offset);
 81 				this.after(fullText.substring(offset, fullText.length));
 82 				this.after(content);
 83 			}
 84 		}
 85 	};
 86 
 87 	/**
 88 	 * Make the object contenteditable. Care about browser version (name of contenteditable attribute depends on it)
 89 	 */
 90 	jQuery.fn.contentEditable = function ( b ) {
 91 		// ie does not understand contenteditable but contentEditable
 92 		// contentEditable is not xhtml compatible.
 93 		var	$el = jQuery(this);
 94 		var	ce = 'contenteditable';
 95 
 96 		// Check
 97 		if (jQuery.browser.msie && parseInt(jQuery.browser.version,10) == 7 ) {
 98 			ce = 'contentEditable';
 99 		}
100 
101 		if (typeof b === 'undefined' ) {
102 
103 			// For chrome use this specific attribute. The old ce will only
104 			// return 'inherit' for nested elements of a contenteditable.
105 			// The isContentEditable is a w3c standard compliant property which works in IE7,8,FF36+, Chrome 12+
106 			if (typeof $el[0] === 'undefined' ) {
107 				console.warn('The jquery object did not contain any valid elements.'); // die silent
108 				return undefined;
109 			}
110 			if (typeof $el[0].isContentEditable === 'undefined') {
111 				console.warn('Could not determine whether the is editable or not. I assume it is.');
112 				return true;
113 			} else {
114 				return $el[0].isContentEditable;
115 			}
116 		} else if (b === '') {
117 			$el.removeAttr(ce);
118 		} else {
119 			if (b && b !== 'false') {
120 				b = 'true';
121 			} else {
122 				b = 'false';
123 			}
124 			$el.attr(ce, b);
125 		}
126 
127 		return $el;
128 	};
129 
130 	/**
131 	 * jQuery Aloha Plugin
132 	 *
133 	 * turn all dom elements to continous text
134 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 ) );
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
218 			$el = jQuery(this),
219 			el = $el.get(0);
220 			if (typeof el.outerHTML != 'undefined') {
221 				return el.outerHTML;
222 			} else {
223 				try {
224 					// Gecko-based browsers, Safari, Opera.
225 					return (new XMLSerializer()).serializeToString(el);
226 				 } catch (e) {
227 228 					try {
229 					  // Internet Explorer.
230 					  return el.xml;
231 					} catch (e) {}
232 				}
233 			}
234 
235 	};
236 
237 	jQuery.fn.zap = function () {
238 		return this.each(function (){ jQuery(this.childNodes).insertBefore(this); }).remove();
239 	};
240 
241 	jQuery.fn.textNodes = function (excludeBreaks, includeEmptyTextNodes) {
242 		var
243 			ret = [],
244 			doSomething = function (el) {
245 				if (
246 					(el.nodeType === 3 && jQuery.trim(el.data) && !includeEmptyTextNodes) ||
247 					(el.nodeType === 3 && includeEmptyTextNodes) ||
248 					(el.nodeName =="BR" && !excludeBreaks)) {
249 					ret.push(el);
250 				} else {
251 					for (var i=0, childLength = el.childNodes.length; i < childLength; ++i) {
252 						doSomething(el.childNodes[i]);
253 					}
254 				}
255 			};
256 
257 		doSomething(this[0]);
258 
259 		return jQuery(ret);
260 	};
261 
262 	/**
263 	 * extendObjects is like jQuery.extend, but it does not extend arrays
264 	 */
265 	jQuery.extendObjects = jQuery.fn.extendObjects = function () {
266 		var options, name, src, copy, copyIsArray, clone,
267 			target = arguments[0] || {},
268 			i = 1,
269 			length = arguments.length,
270 			deep = false;
271 
272 		// Handle a deep copy situation
273 		if ( typeof target === "boolean" ) {
274 			deep = target;
275 			target = arguments[1] || {};
276 			// skip the boolean and the target
277 			i = 2;
278 		}
279 
280 		// Handle case when target is a string or something (possible in deep copy)
281 		if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
282 			target = {};
283 		}
284 
285 		// extend jQuery itself if only one argument is passed
286 		if ( length === i ) {
287 			target = this;
288 			--i;
289 		}
290 
291 		for ( ; i < length; i++ ) {
292 			// Only deal with non-null/undefined values
293 			if ( (options = arguments[ i ]) != null ) {
294 				// Extend the base object
295 				for ( name in options ) {
296 					src = target[ name ];
297 					copy = options[ name ];
298 
299 					// Prevent never-ending loop
300 					if ( target === copy ) {
301 						continue;
302 					}
303 
304 					// Recurse if we're merging plain objects or arrays
305 					if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
306 						if ( copyIsArray ) {
307 							copyIsArray = false;
308 							clone = src && jQuery.isArray(src) ? src : [];
309 
310 						} else {
311 							clone = src && jQuery.isPlainObject(src) ? src : {};
312 						}
313 
314 						// Never move original objects, clone them
315 						if (jQuery.isArray(copy)) {
316 							// don't extend arrays
317 							target[ name ] = copy;
318 						} else {
319 							target[ name ] = jQuery.extendObjects( deep, clone, copy );
320 						}
321 
322 					// Don't bring in undefined values
323 					} else if ( copy !== undefined ) {
324 						target[ name ] = copy;
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", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
351 			20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
352 			37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
353 			96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
354 			104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
355 			112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
356 			120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
357 		},
358 
359 		shiftNums: {
360 			"`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
361 			"8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
362 			".": ">",  "/": "?",  "\\": "|"
363 		}
364 	};
365 
366 	function applyKeyHandler(handler, context, args, event) {
367 		// Don't fire in text-accepting inputs that we didn't directly bind to
368 		if ( context !== event.target
369 			 && (/textarea|input|select/i.test( event.target.nodeName )
370 				 || event.target.type === "text") ) {
371 			return;
372 		}
373 		return handler.apply(context, args);
374 	}
375 
376 	function keyHandler( handleObj ) {
377 		// Only care when a possible input has been specified
378 		if ( typeof handleObj.data !== "string" ) {
379 			return;
380 		}
381 
382 		var origHandler = handleObj.handler,
383 		    keys = handleObj.data.toLowerCase().split(" "),
384 		    handle = {};
385 
386 		for (var i = 0; i < keys.length; i++) {
387 			handle[keys[i]] = true;
388 		}
389 
390 		handleObj.handler = function(event) {
391 			// The original comment that was added with this condition says:
392 			// "Don't fire in contentEditable true elements"
393 			// But this is incorrect.
394 			// What this condition does is it skips hotkey events for
395 			// any target unless it is directly bound.
396 397 			// The condition event.target.contentEditable !== true will
398 			// always be true, because contentEditable is a string
399 			// attribute that is never strictly equal true.
400 			//if (this !== event.target && event.target.contentEditable !== true) {
401 			//return;
402 			//}
403 			// Below is what this condition really does. Ideally, I'd
404 			// like to remove this condition since it was not there in
405 			// the original implementation by John Resig and it could
406 			// interfere with other plugins, but when I removed it, I
407 			// was unable to input any space characters into an
408 			// editable.
409 			// TODO figure out a way to safely remove this
410 			if (this !== event.target) {
411 				return;
412 			}
413 
414 			// Keypress represents characters, not special keys
415 			var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
416 			    modif = "",
417 			    character;
418 
419 			// check combinations (alt|ctrl|shift+anything)
420 			if ( event.altKey && special !== "alt" ) {
421 				modif += "alt+";
422 			}
423 
424 			if ( event.ctrlKey && special !== "ctrl" ) {
425 				modif += "ctrl+";
426 			}
427 
428 			// TODO: Need to make sure this works consistently across platforms
429 			if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
430 				modif += "meta+";
431 			}
432 
433 			if ( event.shiftKey && special !== "shift" ) {
434 				modif += "shift+";
435 			}
436 
437 			if ( special ) {
438 				if (handle[modif + special]) {
439 					return applyKeyHandler(origHandler, this, arguments, event);
440 				}
441 			} else {
442 				character = String.fromCharCode(event.which).toLowerCase();
443 
444 				if (handle[modif + character]) {
445 					return applyKeyHandler(origHandler, this, arguments, event);
446 				}
447 
448 				if (handle[modif + jQuery.hotkeys.shiftNums[character]]) {
449 					return applyKeyHandler(origHandler, this, arguments, event);
450 				}
451 
452 				// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
453 				if (modif === "shift+") {
454 					if (handle[jQuery.hotkeys.shiftNums[character]]) {
455 						return applyKeyHandler(origHandler, this, arguments, event);
456 					}
457 				}
458 			}
459 		};
460 	}
461 
462 	jQuery.each(['keydown', 'keyup', 'keypress'], function () {
463 		jQuery.event.special[this] = {add: keyHandler};
464 	});
465 
466 });
467