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