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