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)[\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 * @return void 212 * @hide 213 */ 214 init: function() { 215 var me = this; 216 217 // TODO make editables their own settings. 218 this.settings = Aloha.settings; 219 220 // smartContentChange settings 221 // @TODO move to new config when implemented in Aloha 222 if ( Aloha.settings && Aloha.settings.smartContentChange ) { 223 if ( Aloha.settings.smartContentChange.delimiters ) { 224 this.sccDelimiters = Aloha.settings.smartContentChange.delimiters; 225 } 226 227 if ( Aloha.settings.smartContentChange.idle ) { 228 this.sccIdle = Aloha.settings.smartContentChange.idle; 229 } 230 231 if ( Aloha.settings.smartContentChange.delay ) { 232 this.sccDelay = Aloha.settings.smartContentChange.delay; 233 } 234 } 235 236 // check if Aloha can handle the obj as Editable 237 if ( !this.check( this.obj ) ) { 238 //Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' ); 239 this.destroy(); 240 return; 241 } 242 243 // apply content handler to clean up content 244 if ( typeof Aloha.settings.contentHandler.getContents === 'undefined' ) { 245 Aloha.settings.contentHandler.getContents = Aloha.defaults.contentHandler.getContents; 246 } 247 248 // apply content handler to clean up content 249 if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) { 250 Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable; 251 } 252 253 var content = me.obj.html(); 254 content = ContentHandlerManager.handleContent( content, { 255 contenthandler: Aloha.settings.contentHandler.initEditable, 256 command: 'initEditable' 257 } ); 258 me.obj.html( content ); 259 260 // only initialize the editable when Aloha is fully ready (including plugins) 261 Aloha.bind( 'aloha-ready', function() { 262 // initialize the object 263 me.obj.addClass( 'aloha-editable' ).contentEditable( true ); 264 265 // add focus event to the object to activate 266 me.obj.mousedown( function( e ) { 267 // check whether the mousedown was already handled 268 if ( !Aloha.eventHandled ) { 269 Aloha.eventHandled = true; 270 return me.activate( e ); 271 } 272 } ); 273 274 me.obj.mouseup( function( e ) { 275 Aloha.eventHandled = false; 276 } ); 277 278 me.obj.focus( function( e ) { 279 return me.activate( e ); 280 } ); 281 282 // by catching the keydown we can prevent the browser from doing its own thing 283 // if it does not handle the keyStroke it returns true and therefore all other 284 // events (incl. browser's) continue 285 //me.obj.keydown( function( event ) { 286 //me.obj.add('.aloha-block', me.obj).live('keydown', function (event) { // live not working but would be usefull 287 me.obj.add('.aloha-block', me.obj).keydown(function (event) { 288 var letEventPass = Markup.preProcessKeyStrokes( event ); 289 me.keyCode = event.which; 290 291 if (!letEventPass) { 292 // the event will not proceed to key press, therefore trigger smartContentChange 293 me.smartContentChange( event ); 294 } 295 return letEventPass; 296 } ); 297 298 // handle keypress 299 me.obj.keypress( function( event ) { 300 // triggers a smartContentChange to get the right charcode 301 // To test try http://www.w3.org/2002/09/tests/keys.html 302 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 Aloha.trigger( 'aloha-editable-deactivated', { editable : this } ); 751 752 /** 753 * @event smartContentChanged 754 */ 755 Aloha.activeEditable.smartContentChange( { type : 'blur' }, null ); 756 }, 757 758 /** 759 * check if the string is empty 760 * used for zerowidth check 761 * @return true if empty or string is null, false otherwise 762 * @hide 763 */ 764 empty: function( str ) { 765 // br is needed for chrome 766 return ( null === str ) 767 || ( jQuery.trim( str ) === '' || str === '<br/>' ); 768 }, 769 770 /** 771 * Get the contents of this editable as a HTML string or child node DOM 772 * objects. 773 * 774 * @param {boolean} asObject Whether or not to retreive the contents of 775 * this editable as child node objects or as 776 * HTML string. 777 * @return {string|jQuery.<HTMLElement>} Contents of the editable as 778 * DOM objects or an HTML string. 779 */ 780 getContents: function (asObject) { 781 var raw = this.obj.html(); 782 var cache = editableContentCache[this.getId()]; 783 if (cache && raw === cache.raw) { 784 return asObject ? cache.elements : cache.clean; 785 } 786 787 var $clone = this.obj.clone(false); 788 $clone.find( '.aloha-cleanme' ).remove(); 789 this.removePlaceholder($clone); 790 PluginManager.makeClean($clone); 791 792 makeClean($clone); 793 794 $clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), { 795 contenthandler: Aloha.settings.contentHandler.getContents, 796 command: 'getContents' 797 }) + '</div>'); 798 799 cache = editableContentCache[this.getId()] = {}; 800 cache.raw = raw; 801 cache.clean = contentSerializer($clone[0]); 802 cache.elements = $clone.contents(); 803 804 return asObject ? cache.elements : cache.clean; 805 }, 806 807 /** 808 * Set the contents of this editable as a HTML string 809 * @param content as html 810 * @param return as object or html string 811 * @return contents of the editable 812 */ 813 setContents: function( content, asObject ) { 814 var reactivate = null; 815 816 if ( Aloha.getActiveEditable() === this ) { 817 Aloha.deactivateEditable(); 818 reactivate = this; 819 } 820 821 this.obj.html( content ); 822 823 if ( null !== reactivate ) { 824 reactivate.activate(); 825 } 826 827 this.smartContentChange({type : 'set-contents'}); 828 829 return asObject ? this.obj.contents() : contentSerializer(this.obj[0]); 830 }, 831 832 /** 833 * Get the id of this editable 834 * @method 835 * @return id of this editable 836 */ 837 getId: function() { 838 return this.obj.attr( 'id' ); 839 }, 840 841 /** 842 * Generates and signals a smartContentChange event. 843 * 844 * A smart content change occurs when a special editing action, or a 845 * combination of interactions are performed by the user during the 846 * course of editing within an editable. 847 * The smart content change event would therefore signal to any 848 * component that is listening to this event, that content has been 849 * inserted into the editable that may need to be prococessed in a 850 * special way 851 * This is used for smart actions within the content/while editing. 852 * @param {Event} event 853 * @hide 854 */ 855 smartContentChange: function( event ) { 856 var me = this, 857 uniChar = null, 858 re, 859 match; 860 861 // ignore meta keys like crtl+v or crtl+l and so on 862 if ( event && ( event.metaKey || event.crtlKey || event.altKey ) ) { 863 return false; 864 } 865 866 if ( event && event.originalEvent ) { 867 // regex to strip unicode 868 re = new RegExp( "U\\+(\\w{4})" ); 869 match = re.exec( event.originalEvent.keyIdentifier ); 870 871 // Use keyIdentifier if available 872 if ( event.originalEvent.keyIdentifier && 1 === 2 ) { 873 // @fixme: Because of "&& 1 === 2" above, this block is 874 // unreachable code 875 if ( match !== null ) { 876 uniChar = unescape( '%u' + match[1] ); 877 } 878 if ( uniChar === null ) { 879 uniChar = event.originalEvent.keyIdentifier; 880 } 881 882 // FF & Opera don't support keyIdentifier 883 } else { 884 // Use among browsers reliable which http://api.jquery.com/keypress 885 uniChar = ( this.keyCodeMap[ this.keyCode ] || 886 String.fromCharCode( event.which ) || 'unknown' ); 887 } 888 } 889 890 var snapshot = null; 891 function getSnapshotContent() { 892 if (null == snapshot) { 893 snapshot = me.getSnapshotContent(); 894 } 895 return snapshot; 896 } 897 898 // handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier" 899 // reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 900 if ( jQuery.inArray( uniChar, this.sccDelimiters ) >= 0 ) { 901 clearTimeout( this.sccTimerIdle ); 902 clearTimeout( this.sccTimerDelay ); 903 904 this.sccTimerDelay = window.setTimeout( function() { 905 Aloha.trigger( 'aloha-smart-content-changed', { 906 'editable' : me, 907 'keyIdentifier' : event.originalEvent.keyIdentifier, 908 'keyCode' : event.keyCode, 909 'char' : uniChar, 910 'triggerType' : 'keypress', // keypress, timer, blur, paste 911 'getSnapshotContent' : getSnapshotContent 912 } ); 913 914 console.debug( 'Aloha.Editable', 915 'smartContentChanged: event type keypress triggered' ); 916 }, this.sccDelay ); 917 } else if ( event && event.type === 'paste' ) { 918 Aloha.trigger( 'aloha-smart-content-changed', { 919 'editable' : me, 920 'keyIdentifier' : null, 921 'keyCode' : null, 922 'char' : null, 923 'triggerType' : 'paste', 924 'getSnapshotContent' : getSnapshotContent 925 } ); 926 927 } else if ( event && event.type === 'blur' ) { 928 Aloha.trigger( 'aloha-smart-content-changed', { 929 'editable' : me, 930 'keyIdentifier' : null, 931 'keyCode' : null, 932 'char' : null, 933 'triggerType' : 'blur', 934 'getSnapshotContent' : getSnapshotContent 935 } ); 936 937 } else if ( uniChar !== null ) { 938 // in the rare case idle time is lower then delay time 939 clearTimeout( this.sccTimerDelay ); 940 clearTimeout( this.sccTimerIdle ); 941 this.sccTimerIdle = window.setTimeout( function() { 942 Aloha.trigger( 'aloha-smart-content-changed', { 943 'editable' : me, 944 'keyIdentifier' : null, 945 'keyCode' : null, 946 'char' : null, 947 'triggerType' : 'idle', 948 'getSnapshotContent' : getSnapshotContent 949 } ); 950 }, this.sccIdle ); 951 } 952 }, 953 954 /** 955 * Get a snapshot of the active editable as a HTML string 956 * @hide 957 * @return snapshot of the editable 958 */ 959 getSnapshotContent: function() { 960 var ret = this.snapshotContent; 961 this.snapshotContent = this.getContents(); 962 return ret; 963 } 964 } ); 965 966 /** 967 * Sets the serializer function to be used for the contents of all editables. 968 * 969 * The default content serializer will just call the jQuery.html() 970 * function on the editable element (which gets the innerHTML property). 971 * 972 * This method is a static class method and will affect the result 973 * of editable.getContents() for all editables that have been or 974 * will be constructed. 975 * 976 * @param serializerFunction 977 * A function that accepts a DOM element and returns the serialized 978 * XHTML of the element contents (excluding the start and end tag of 979 * the passed element). 980 */ 981 Aloha.Editable.setContentSerializer = function( serializerFunction ) { 982 contentSerializer = serializerFunction; 983 }; 984 } ); 985