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