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