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