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