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