1 /* editable.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 define([ 28 'aloha/core', 29 'util/class', 30 'jquery', 31 'aloha/pluginmanager', 32 'aloha/selection', 33 'aloha/markup', 34 'aloha/contenthandlermanager', 35 'aloha/console', 36 'aloha/block-jump', 37 'aloha/ephemera', 38 'util/dom2', 39 'PubSub', 40 'aloha/copypaste' 41 ], function ( 42 Aloha, 43 Class, 44 $, 45 PluginManager, 46 Selection, 47 Markup, 48 ContentHandlerManager, 49 console, 50 BlockJump, 51 Ephemera, 52 Dom, 53 PubSub, 54 CopyPaste 55 ) { 56 'use strict'; 57 58 var jQuery = $; 59 var unescape = window.unescape, 60 GENTICS = window.GENTICS, 61 62 // True, if the next editable activate event should not be handled 63 ignoreNextActivateEvent = false; 64 65 /** 66 * A cache to hold information derived, and used in getContents(). 67 * @type {object<string,(string|jQuery.<HTMLElement>)>} 68 * @private 69 */ 70 var editableContentCache = {}; 71 72 // default supported and custom content handler settings 73 // @TODO move to new config when implemented in Aloha 74 Aloha.defaults.contentHandler = {}; 75 Aloha.defaults.contentHandler.initEditable = ['blockelement', 'sanitize']; 76 Aloha.defaults.contentHandler.getContents = ['blockelement', 'sanitize', 'basic']; 77 78 // The insertHtml contenthandler ( paste ) will, by default, use all 79 // registered content handlers. 80 //Aloha.defaults.contentHandler.insertHtml = void 0; 81 82 if (typeof Aloha.settings.contentHandler === 'undefined') { 83 Aloha.settings.contentHandler = {}; 84 } 85 86 var defaultContentSerializer = function (editableElement) { 87 return jQuery(editableElement).html(); 88 }; 89 90 var contentSerializer = defaultContentSerializer; 91 92 /** 93 * Triggers smartContentChange handlers. 94 * 95 * @param {Aloha.Editable} 96 * @return {string} Content that has been processed by getContent handlers 97 * and smartContentChange handlers. 98 */ 99 function handleSmartContentChange(editable) { 100 return ContentHandlerManager.handleContent(editable.getContents(), { 101 contenthandler: Aloha.settings.contentHandler.smartContentChange 102 }, editable); 103 } 104 105 /** 106 * List of observed key, mapped against their keycodes. 107 * 108 * @type {object<number, string>} 109 * @const 110 */ 111 var KEYCODES = { 112 65: 'a' 113 }; 114 115 /** 116 * Handlers for various key combos. 117 * Each handler ought to return false if they do not want the event to 118 * continue propagating. 119 */ 120 var keyBindings = { 121 'ctrl+a': function () { 122 var editable = CopyPaste.getEditableAt(CopyPaste.getRange()); 123 if (editable) { 124 CopyPaste.selectAllOf(editable.obj[0]); 125 return false; 126 } 127 } 128 } 129 130 /** 131 * Gets the name of the modifier key if is in effect for the given event. 132 * 133 * eg: <Ctrl>+c 134 * 135 * @param {jQuery.Event} $event 136 * @return {string|null} Modifier string or null if no modifier is in 137 * effect. 138 * 139 */ 140 function keyModifier($event) { 141 return $event.altKey ? 'alt' : 142 $event.ctrlKey ? 'ctrl' : 143 $event.shiftKey ? 'shift' : null; 144 } 145 146 /** 147 * Handles keydown events that are fired on the page's document. 148 * 149 * @param {jQuery.Event) $event 150 * @return {boolean} Returns false to stop propagation; undefined otherwise. 151 */ 152 function onKeydown($event) { 153 if (!Aloha.activeEditable) { 154 return; 155 } 156 var key = KEYCODES[$event.which]; 157 if (key) { 158 var modifier = keyModifier($event); 159 var combo = (modifier ? modifier + '+' : '') + key; 160 if (keyBindings[combo]) { 161 return keyBindings[combo]($event); 162 } 163 } 164 } 165 166 $(document).keydown(onKeydown); 167 168 /** 169 * Editable object 170 * @namespace Aloha 171 * @class Editable 172 * @method 173 * @constructor 174 * @param {Object} obj jQuery object reference to the object 175 */ 176 Aloha.Editable = Class.extend({ 177 178 _constructor: function (obj) { 179 // check wheter the object has an ID otherwise generate and set 180 // globally unique ID 181 if (!obj.attr('id')) { 182 obj.attr('id', GENTICS.Utils.guid()); 183 } 184 185 // store object reference 186 this.obj = obj; 187 this.originalObj = obj; 188 this.ready = false; 189 190 // delimiters, timer and idle for smartContentChange 191 // smartContentChange triggers -- tab: '\u0009' - space: '\u0020' - enter: 'Enter' 192 // backspace: U+0008 - delete: U+007F 193 this.sccDelimiters = [':', ';', '.', '!', '?', ',', 194 unescape('%u0009'), unescape('%u0020'), unescape('%u0008'), unescape('%u007F'), 'Enter']; 195 this.sccIdle = 5000; 196 this.sccDelay = 500; 197 this.sccTimerIdle = false; 198 this.sccTimerDelay = false; 199 200 // see keyset http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 201 this.keyCodeMap = { 202 93: 'Apps', // The Application key 203 18: 'Alt', // The Alt ( Menu ) key. 204 20: 'CapsLock', // The Caps Lock ( Capital ) key. 205 17: 'Control', // The Control ( Ctrl ) key. 206 40: 'Down', // The Down Arrow key. 207 35: 'End', // The End key. 208 13: 'Enter', // The Enter key. 209 112: 'F1', // The F1 key. 210 113: 'F2', // The F2 key. 211 114: 'F3', // The F3 key. 212 115: 'F4', // The F4 key. 213 116: 'F5', // The F5 key. 214 117: 'F6', // The F6 key. 215 118: 'F7', // The F7 key. 216 119: 'F8', // The F8 key. 217 120: 'F9', // The F9 key. 218 121: 'F10', // The F10 key. 219 122: 'F11', // The F11 key. 220 123: 'F12', // The F12 key. 221 222 // Anybody knows the keycode for F13-F24? 223 36: 'Home', // The Home key. 224 45: 'Insert', // The Insert ( Ins ) key. 225 37: 'Left', // The Left Arrow key. 226 224: 'Meta', // The Meta key. 227 34: 'PageDown', // The Page Down ( Next ) key. 228 33: 'PageUp', // The Page Up key. 229 19: 'Pause', // The Pause key. 230 44: 'PrintScreen', // The Print Screen ( PrintScrn, SnapShot ) key. 231 39: 'Right', // The Right Arrow key. 232 145: 'Scroll', // The scroll lock key 233 16: 'Shift', // The Shift key. 234 38: 'Up', // The Up Arrow key. 235 91: 'Win', // The left Windows Logo key. 236 92: 'Win' // The right Windows Logo key. 237 }; 238 239 this.placeholderClass = 'aloha-placeholder'; 240 241 Aloha.registerEditable(this); 242 }, 243 244 /** 245 * Initialize the editable 246 * @return void 247 * @hide 248 */ 249 init: function () { 250 var me = this; 251 252 // TODO make editables their own settings. 253 this.settings = Aloha.settings; 254 255 // smartContentChange settings 256 // @TODO move to new config when implemented in Aloha 257 if (Aloha.settings && Aloha.settings.smartContentChange) { 258 if (Aloha.settings.smartContentChange.delimiters) { 259 this.sccDelimiters = Aloha.settings.smartContentChange.delimiters; 260 } 261 262 if (Aloha.settings.smartContentChange.idle) { 263 this.sccIdle = Aloha.settings.smartContentChange.idle; 264 } 265 266 if (Aloha.settings.smartContentChange.delay) { 267 this.sccDelay = Aloha.settings.smartContentChange.delay; 268 } 269 } 270 271 // check if Aloha can handle the obj as Editable 272 if (!this.check(this.obj)) { 273 //Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' ); 274 this.destroy(); 275 return; 276 } 277 278 // apply content handler to clean up content 279 if (typeof Aloha.settings.contentHandler.getContents === 'undefined') { 280 Aloha.settings.contentHandler.getContents = Aloha.defaults.contentHandler.getContents; 281 } 282 283 // apply content handler to clean up content 284 if (typeof Aloha.settings.contentHandler.initEditable === 'undefined') { 285 Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable; 286 } 287 288 var content = me.obj.html(); 289 content = ContentHandlerManager.handleContent(content, { 290 contenthandler: Aloha.settings.contentHandler.initEditable, 291 command: 'initEditable' 292 }, me); 293 me.obj.html(content); 294 295 // Because editables can only properly be initialized when Aloha 296 // plugins are loaded. 297 Aloha.bind('aloha-plugins-loaded', function () { 298 me.obj.addClass('aloha-editable').contentEditable(true); 299 300 me.obj.mousedown(function (e) { 301 if (!Aloha.eventHandled) { 302 Aloha.eventHandled = true; 303 return me.activate(e); 304 } 305 }); 306 307 me.obj.mouseup(function (e) { 308 Aloha.eventHandled = false; 309 }); 310 311 me.obj.focus(function (e) { 312 return me.activate(e); 313 }); 314 315 // by catching the keydown we can prevent the browser from doing its own thing 316 // if it does not handle the keyStroke it returns true and therefore all other 317 // events (incl. browser's) continue 318 //me.obj.keydown( function( event ) { 319 //me.obj.add('.aloha-block', me.obj).live('keydown', function (event) { // live not working but would be usefull 320 me.obj.add('.aloha-block', me.obj).keydown(function (event) { 321 var letEventPass = Markup.preProcessKeyStrokes(event); 322 me.keyCode = event.which; 323 324 if (!letEventPass) { 325 // the event will not proceed to key press, therefore trigger smartContentChange 326 me.smartContentChange(event); 327 } 328 return letEventPass; 329 }); 330 331 // handle keypress 332 me.obj.keypress(function (event) { 333 // triggers a smartContentChange to get the right charcode 334 // To test try http://www.w3.org/2002/09/tests/keys.html 335 Aloha.activeEditable.smartContentChange(event); 336 }); 337 338 // handle shortcut keys 339 me.obj.keyup(function (event) { 340 if (event.keyCode === 27) { 341 Aloha.deactivateEditable(); 342 return false; 343 } 344 }); 345 346 // register the onSelectionChange Event with the Editable field 347 me.obj.contentEditableSelectionChange(function (event) { 348 Selection.onChange(me.obj, event); 349 return me.obj; 350 }); 351 352 // mark the editable as unmodified 353 me.setUnmodified(); 354 355 // we don't do the sanitizing on aloha ready, since some plugins add elements into the content and bind 356 // events to it. If we sanitize by replacing the html, all events would get lost. TODO: think about a 357 // better solution for the sanitizing, without destroying the events apply content handler to clean up content 358 // var content = me.obj.html(); 359 // if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) { 360 // Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable; 361 // } 362 // content = ContentHandlerManager.handleContent( content, { 363 // contenthandler: Aloha.settings.contentHandler.initEditable 364 // } ); 365 // me.obj.html( content ); 366 367 me.snapshotContent = me.getContents(); 368 369 // FF bug: check for empty editable contents ( no <br>; no whitespace ) 370 if (jQuery.browser.mozilla) { 371 me.initEmptyEditable(); 372 } 373 374 me.initPlaceholder(); 375 376 me.ready = true; 377 378 // disable object resizing. 379 // we do this in here and with a slight delay, because 380 // starting with FF 15, this would cause a JS error 381 // if done before the first DOM object is made contentEditable. 382 window.setTimeout(function () { 383 Aloha.disableObjectResizing(); 384 }, 20); 385 386 // throw a new event when the editable has been created 387 /** 388 * @event editableCreated fires after a new editable has been created, eg. via $( '#editme' ).aloha() 389 * The event is triggered in Aloha's global scope Aloha 390 * @param {Event} e the event object 391 * @param {Array} a an array which contains a reference to the currently created editable on its first position 392 */ 393 Aloha.trigger('aloha-editable-created', [me]); 394 PubSub.pub('aloha.editable.created', {data: me}); 395 }); 396 }, 397 398 /** 399 * True, if this editable is active for editing 400 * @property 401 * @type boolean 402 */ 403 isActive: false, 404 405 /** 406 * stores the original content to determine if it has been modified 407 * @hide 408 */ 409 originalContent: null, 410 411 /** 412 * every time a selection is made in the current editable the selection has to 413 * be saved for further use 414 * @hide 415 */ 416 range: undefined, 417 418 /** 419 * Check if object can be edited by Aloha Editor 420 * @return {boolean } editable true if Aloha Editor can handle else false 421 * @hide 422 */ 423 check: function () { 424 /* TODO check those elements 425 'map', 'meter', 'object', 'output', 'progress', 'samp', 426 'time', 'area', 'datalist', 'figure', 'kbd', 'keygen', 427 'mark', 'math', 'wbr', 'area', 428 */ 429 430 // Extract El 431 var me = this, 432 obj = this.obj, 433 el = obj.get(0), 434 nodeName = el.nodeName.toLowerCase(), 435 436 // supported elements 437 textElements = ['a', 'abbr', 'address', 'article', 'aside', 'b', 'bdo', 'blockquote', 'cite', 'code', 'command', 'del', 'details', 'dfn', 'div', 'dl', 'em', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'i', 'ins', 'menu', 'nav', 'p', 'pre', 'q', 'ruby', 'section', 'small', 'span', 'strong', 'sub', 'sup', 'var'], 438 i, 439 div; 440 441 for (i = 0; i < textElements.length; ++i) { 442 if (nodeName === textElements[i]) { 443 return true; 444 } 445 } 446 447 // special handled elements 448 switch (nodeName) { 449 case 'label': 450 case 'button': 451 // TODO need some special handling. 452 break; 453 case 'textarea': 454 case 'input': 455 // Create a div alongside the textarea 456 div = jQuery('<div id="' + this.getId() + '-aloha" class="aloha-' + nodeName + '" />').insertAfter(obj); 457 458 // Resize the div to the textarea and 459 // Populate the div with the value of the textarea 460 // Then, hide the textarea 461 div.height(obj.height()).width(obj.width()).html(obj.val()); 462 463 obj.hide(); 464 465 // Attach a onsubmit to the form to place the HTML of the 466 // div back into the textarea 467 obj.parents('form:first').submit(function () { 468 obj.val(me.getContents()); 469 }); 470 471 // Swap textarea reference with the new div 472 this.obj = div; 473 474 // Supported 475 return true; 476 default: 477 break; 478 } 479 480 // the following elements are not supported 481 /* 482 'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr', 483 'iframe', 'img', 'input', 'map', 'script', 'select', 'style', 484 'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript', 485 */ 486 return false; 487 }, 488 489 /** 490 * Init Placeholder 491 * 492 * @return void 493 */ 494 initPlaceholder: function () { 495 if (Aloha.settings.placeholder && this.isEmpty()) { 496 this.addPlaceholder(); 497 } 498 }, 499 500 /** 501 * Check if the conteneditable is empty. 502 * 503 * @return {Boolean} 504 */ 505 isEmpty: function () { 506 var editableTrimedContent = jQuery.trim(this.getContents()), 507 onlyBrTag = (editableTrimedContent === '<br>') ? true : false; 508 return (editableTrimedContent.length === 0 || onlyBrTag); 509 }, 510 511 /** 512 * Check if the editable div is not empty. Fixes a FF browser bug 513 * see issue: https://github.com/alohaeditor/Aloha-Editor/issues/269 514 * 515 * @return {undefined} 516 */ 517 initEmptyEditable: function () { 518 var obj = this.obj; 519 if (this.empty(this.getContents())) { 520 jQuery(obj).prepend('<br class="aloha-cleanme" />'); 521 } 522 }, 523 524 /** 525 * Add placeholder in editable 526 * 527 * @return void 528 */ 529 addPlaceholder: function () { 530 var div = jQuery('<div>'), 531 span = jQuery('<span>'), 532 el, 533 obj = this.obj; 534 535 if (GENTICS.Utils.Dom.allowsNesting(obj[0], div[0])) { 536 el = div; 537 } else { 538 el = span; 539 } 540 if (jQuery("." + this.placeholderClass, obj).length !== 0) { 541 return; 542 } 543 jQuery(obj).append(el.addClass(this.placeholderClass)); 544 jQuery.each(Aloha.settings.placeholder, function (selector, selectorConfig) { 545 if (obj.is(selector)) { 546 el.html(selectorConfig); 547 } 548 }); 549 550 // remove browser br 551 jQuery('br', obj).remove(); 552 553 // delete div, span, el; 554 }, 555 556 /** 557 * remove placeholder from contenteditable. If setCursor is true, 558 * will also set the cursor to the start of the selection. However, 559 * this will be ASYNCHRONOUS, so if you rely on the fact that 560 * the placeholder is removed after calling this method, setCursor 561 * should be false ( or not set ) 562 * 563 * @return void 564 */ 565 removePlaceholder: function (obj, setCursor) { 566 var placeholderClass = this.placeholderClass, 567 range; 568 if (jQuery("." + this.placeholderClass, obj).length === 0) { 569 return; 570 } 571 // set the cursor // remove placeholder 572 if (setCursor === true) { 573 window.setTimeout(function () { 574 range = new Selection.SelectionRange(); 575 range.startContainer = range.endContainer = obj.get(0); 576 range.startOffset = range.endOffset = 0; 577 jQuery('.' + placeholderClass, obj).remove(); 578 range.select(); 579 580 }, 100); 581 } else { 582 jQuery('.' + placeholderClass, obj).remove(); 583 } 584 }, 585 586 /** 587 * destroy the editable 588 * @return void 589 */ 590 destroy: function () { 591 // leave the element just to get sure 592 if (this === Aloha.getActiveEditable()) { 593 this.blur(); 594 } 595 596 // special handled elements 597 switch (this.originalObj.get(0).nodeName.toLowerCase()) { 598 case 'label': 599 case 'button': 600 // TODO need some special handling. 601 break; 602 case 'textarea': 603 case 'input': 604 // restore content to original textarea 605 this.originalObj.val(this.getContents()); 606 this.obj.remove(); 607 this.originalObj.show(); 608 break; 609 default: 610 break; 611 } 612 613 // now the editable is not ready any more 614 this.ready = false; 615 616 // remove the placeholder if needed. 617 this.removePlaceholder(this.obj); 618 619 // initialize the object and disable contentEditable 620 // unbind all events 621 // TODO should only unbind the specific handlers. 622 this.obj.removeClass('aloha-editable').contentEditable(false).unbind('mousedown click dblclick focus keydown keypress keyup'); 623 624 /* TODO remove this event, it should implemented as bind and unbind 625 // register the onSelectionChange Event with the Editable field 626 this.obj.contentEditableSelectionChange( function( event ) { 627 Aloha.Selection.onChange( me.obj, event ); 628 return me.obj; 629 } ); 630 */ 631 632 // throw a new event when the editable has been created 633 /** 634 * @event editableCreated fires after a new editable has been destroyes, eg. via $( '#editme' ).mahalo() 635 * The event is triggered in Aloha's global scope Aloha 636 * @param {Event} e the event object 637 * @param {Array} a an array which contains a reference to the currently created editable on its first position 638 */ 639 Aloha.trigger('aloha-editable-destroyed', [this]); 640 PubSub.pub('aloha.editable.destroyed', {data: this}); 641 642 // finally register the editable with Aloha 643 Aloha.unregisterEditable(this); 644 }, 645 646 /** 647 * marks the editables current state as unmodified. Use this method to inform the editable 648 * that it's contents have been saved 649 * @method 650 */ 651 setUnmodified: function () { 652 this.originalContent = this.getContents(); 653 }, 654 655 /** 656 * check if the editable has been modified during the edit process# 657 * @method 658 * @return boolean true if the editable has been modified, false otherwise 659 */ 660 isModified: function () { 661 return this.originalContent !== this.getContents(); 662 }, 663 664 /** 665 * String representation of the object 666 * @method 667 * @return Aloha.Editable 668 */ 669 toString: function () { 670 return 'Aloha.Editable'; 671 }, 672 673 /** 674 * check whether the editable has been disabled 675 */ 676 isDisabled: function () { 677 return !this.obj.contentEditable() || this.obj.contentEditable() === 'false'; 678 }, 679 680 /** 681 * disable this editable 682 * a disabled editable cannot be written on by keyboard 683 */ 684 disable: function () { 685 return this.isDisabled() || this.obj.contentEditable(false); 686 }, 687 688 /** 689 * enable this editable 690 * reenables a disabled editable to be writteable again 691 */ 692 enable: function () { 693 return this.isDisabled() && this.obj.contentEditable(true); 694 }, 695 696 697 /** 698 * activates an Editable for editing 699 * disables all other active items 700 * @method 701 */ 702 activate: function (e) { 703 // get active Editable before setting the new one. 704 var oldActive = Aloha.getActiveEditable(); 705 706 // We need to ommit this call when this flag is set to true. 707 // This flag will only be set to true before the removePlaceholder method 708 // is called since that method invokes a focus event which will again trigger 709 // this method. We want to avoid double invokation of this method. 710 if (ignoreNextActivateEvent) { 711 ignoreNextActivateEvent = false; 712 return; 713 } 714 715 // handle special case in which a nested editable is focused by a click 716 // in this case the "focus" event would be triggered on the parent element 717 // which actually shifts the focus away to it's parent. this if is here to 718 // prevent this situation 719 if (e && e.type === 'focus' && oldActive !== null && oldActive.obj.parent().get(0) === e.currentTarget) { 720 return; 721 } 722 723 // leave immediately if this is already the active editable 724 if (this.isActive || this.isDisabled()) { 725 // we don't want parent editables to be triggered as well, so return false 726 return; 727 } 728 729 this.obj.addClass('aloha-editable-active'); 730 731 Aloha.activateEditable(this); 732 733 ignoreNextActivateEvent = true; 734 this.removePlaceholder(this.obj, true); 735 ignoreNextActivateEvent = false; 736 737 this.isActive = true; 738 739 /** 740 * @event editableActivated fires after the editable has been activated by clicking on it. 741 * This event is triggered in Aloha's global scope Aloha 742 * @param {Event} e the event object 743 * @param {Array} a an array which contains a reference to last active editable on its first position, as well 744 * as the currently active editable on it's second position 745 */ 746 // trigger a 'general' editableActivated event 747 Aloha.trigger('aloha-editable-activated', { 748 'oldActive': oldActive, 749 'editable': this 750 }); 751 }, 752 753 /** 754 * handle the blur event 755 * this must not be attached to the blur event, which will trigger far too often 756 * eg. when a table within an editable is selected 757 * @hide 758 759 */ 760 blur: function () { 761 this.obj.blur(); 762 this.isActive = false; 763 this.initPlaceholder(); 764 this.obj.removeClass('aloha-editable-active'); 765 766 /** 767 * @event editableDeactivated fires after the editable has been activated by clicking on it. 768 * This event is triggered in Aloha's global scope Aloha 769 * @param {Event} e the event object 770 * @param {Array} a an array which contains a reference to this editable 771 */ 772 Aloha.trigger('aloha-editable-deactivated', { 773 editable: this 774 }); 775 776 /** 777 * @event smartContentChanged 778 */ 779 Aloha.activeEditable.smartContentChange({ 780 type: 'blur' 781 }, null); 782 }, 783 784 /** 785 * check if the string is empty 786 * used for zerowidth check 787 * @return true if empty or string is null, false otherwise 788 * @hide 789 */ 790 empty: function (str) { 791 // br is needed for chrome 792 return (null === str) || (jQuery.trim(str) === '' || str === '<br/>'); 793 }, 794 795 /** 796 * Get the contents of this editable as a HTML string or child node DOM 797 * objects. 798 * 799 * @param {boolean} asObject Whether or not to retreive the contents of 800 * this editable as child node objects or as 801 * HTML string. 802 * @return {string|jQuery.<HTMLElement>} Contents of the editable as 803 * DOM objects or an HTML string. 804 */ 805 getContents: function (asObject) { 806 var raw = this.obj.html(); 807 var cache = editableContentCache[this.getId()]; 808 809 if (!cache || raw !== cache.raw) { 810 811 BlockJump.removeZeroWidthTextNodeFix(); 812 813 var $clone = this.obj.clone(false); 814 this.removePlaceholder($clone); 815 $clone = jQuery(Ephemera.prune($clone[0])); 816 PluginManager.makeClean($clone); 817 818 // TODO rewrite ContentHandlerManager to accept DOM trees instead of strings 819 $clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), { 820 contenthandler: Aloha.settings.contentHandler.getContents, 821 command: 'getContents' 822 }, this) + '</div>'); 823 824 cache = editableContentCache[this.getId()] = {}; 825 cache.raw = raw; 826 cache.element = $clone; 827 } 828 829 if (asObject) { 830 return cache.element.clone().contents(); 831 } 832 833 if (null == cache.serialized) { 834 cache.serialized = contentSerializer(cache.element[0]); 835 } 836 return cache.serialized; 837 }, 838 839 /** 840 * Set the contents of this editable as a HTML string 841 * @param content as html 842 * @param return as object or html string 843 * @return contents of the editable 844 */ 845 setContents: function (content, asObject) { 846 var reactivate = null; 847 848 if (Aloha.getActiveEditable() === this) { 849 Aloha.deactivateEditable(); 850 reactivate = this; 851 } 852 853 this.obj.html(content); 854 855 if (null !== reactivate) { 856 reactivate.activate(); 857 } 858 859 this.smartContentChange({ 860 type: 'set-contents' 861 }); 862 863 return asObject ? this.obj.contents() : contentSerializer(this.obj[0]); 864 }, 865 866 /** 867 * Get the id of this editable 868 * @method 869 * @return id of this editable 870 */ 871 getId: function () { 872 return this.obj.attr('id'); 873 }, 874 875 /** 876 * Generates and signals a smartContentChange event. 877 * 878 * A smart content change occurs when a special editing action, or a 879 * combination of interactions are performed by the user during the 880 * course of editing within an editable. 881 * The smart content change event would therefore signal to any 882 * component that is listening to this event, that content has been 883 * inserted into the editable that may need to be prococessed in a 884 * special way 885 * This is used for smart actions within the content/while editing. 886 * @param {Event} event 887 * @hide 888 */ 889 smartContentChange: function (event) { 890 var me = this, 891 uniChar = null, 892 re, 893 match; 894 895 // ignore meta keys like crtl+v or crtl+l and so on 896 if (event && (event.metaKey || event.crtlKey || event.altKey)) { 897 return false; 898 } 899 900 if (event && event.originalEvent) { 901 // regex to strip unicode 902 re = new RegExp("U\\+(\\w{4})"); 903 match = re.exec(event.originalEvent.keyIdentifier); 904 905 // Use among browsers reliable which http://api.jquery.com/keypress 906 uniChar = (this.keyCodeMap[this.keyCode] || String.fromCharCode(event.which) || 'unknown'); 907 } 908 909 var snapshot = null; 910 911 function getSnapshotContent() { 912 if (null == snapshot) { 913 snapshot = me.getSnapshotContent(); 914 } 915 return snapshot; 916 } 917 918 // handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier" 919 // reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 920 if (jQuery.inArray(uniChar, this.sccDelimiters) >= 0) { 921 clearTimeout(this.sccTimerIdle); 922 clearTimeout(this.sccTimerDelay); 923 924 this.sccTimerDelay = window.setTimeout(function () { 925 Aloha.trigger('aloha-smart-content-changed', { 926 'editable': me, 927 'keyIdentifier': event.originalEvent.keyIdentifier, 928 'keyCode': event.keyCode, 929 'char': uniChar, 930 'triggerType': 'keypress', // keypress, timer, blur, paste 931 'getSnapshotContent': getSnapshotContent 932 }); 933 handleSmartContentChange(me); 934 935 console.debug('Aloha.Editable', 936 'smartContentChanged: event type keypress triggered'); 937 }, this.sccDelay); 938 939 } else if (event && event.type === 'paste') { 940 Aloha.trigger('aloha-smart-content-changed', { 941 'editable': me, 942 'keyIdentifier': null, 943 'keyCode': null, 944 'char': null, 945 'triggerType': 'paste', 946 'getSnapshotContent': getSnapshotContent 947 }); 948 handleSmartContentChange(me); 949 950 } else if (event && event.type === 'blur') { 951 Aloha.trigger('aloha-smart-content-changed', { 952 'editable': me, 953 'keyIdentifier': null, 954 'keyCode': null, 955 'char': null, 956 'triggerType': 'blur', 957 'getSnapshotContent': getSnapshotContent 958 }); 959 handleSmartContentChange(me); 960 961 } else if (event && event.type === 'block-change') { 962 Aloha.trigger('aloha-smart-content-changed', { 963 'editable': me, 964 'keyIdentifier': null, 965 'keyCode': null, 966 'char': null, 967 'triggerType': 'block-change', 968 'getSnapshotContent': getSnapshotContent 969 }); 970 handleSmartContentChange(me); 971 972 } else if (uniChar !== null) { 973 // in the rare case idle time is lower then delay time 974 clearTimeout(this.sccTimerDelay); 975 clearTimeout(this.sccTimerIdle); 976 this.sccTimerIdle = window.setTimeout(function () { 977 Aloha.trigger('aloha-smart-content-changed', { 978 'editable': me, 979 'keyIdentifier': null, 980 'keyCode': null, 981 'char': null, 982 'triggerType': 'idle', 983 'getSnapshotContent': getSnapshotContent 984 }); 985 handleSmartContentChange(me); 986 }, this.sccIdle); 987 } 988 }, 989 990 /** 991 * Get a snapshot of the active editable as a HTML string 992 * @hide 993 * @return snapshot of the editable 994 */ 995 getSnapshotContent: function () { 996 var ret = this.snapshotContent; 997 this.snapshotContent = this.getContents(); 998 return ret; 999 } 1000 }); 1001 1002 /** 1003 * Sets the content serializer function. 1004 * 1005 * The default content serializer will just call the jQuery.html() 1006 * function on the editable element (which gets the innerHTML property). 1007 * 1008 * This method is a static class method and will affect the result 1009 * of editable.getContents() for all editables that have been or 1010 * will be constructed. 1011 * 1012 * @param {!Function} serializerFunction 1013 * A function that accepts a DOM element and returns the serialized 1014 * XHTML of the element contents (excluding the start and end tag of 1015 * the passed element). 1016 * @api 1017 */ 1018 Aloha.Editable.setContentSerializer = function (serializerFunction) { 1019 contentSerializer = serializerFunction; 1020 }; 1021 1022 /** 1023 * Gets the content serializer function. 1024 * 1025 * @see Aloha.Editable.setContentSerializer() 1026 * @api 1027 * @return {!Function} 1028 * The serializer function. 1029 */ 1030 Aloha.Editable.getContentSerializer = function () { 1031 return contentSerializer; 1032 }; 1033 }); 1034