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