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