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 // disable object resizing. 362 // we do this in here and with a slight delay, because 363 // starting with FF 15, this would cause a JS error 364 // if done before the first DOM object is made contentEditable. 365 window.setTimeout( function() { 366 Aloha.disableObjectResizing(); 367 }, 20 ); 368 369 // throw a new event when the editable has been created 370 /** 371 * @event editableCreated fires after a new editable has been created, eg. via $( '#editme' ).aloha() 372 * The event is triggered in Aloha's global scope Aloha 373 * @param {Event} e the event object 374 * @param {Array} a an array which contains a reference to the currently created editable on its first position 375 */ 376 Aloha.trigger( 'aloha-editable-created', [ me ] ); 377 } ); 378 }, 379 380 /** 381 * True, if this editable is active for editing 382 * @property 383 * @type boolean 384 */ 385 isActive: false, 386 387 /** 388 * stores the original content to determine if it has been modified 389 * @hide 390 */ 391 originalContent: null, 392 393 /** 394 * every time a selection is made in the current editable the selection has to 395 * be saved for further use 396 * @hide 397 */ 398 range: undefined, 399 400 /** 401 * Check if object can be edited by Aloha Editor 402 * @return {boolean } editable true if Aloha Editor can handle else false 403 * @hide 404 */ 405 check: function() { 406 /* TODO check those elements 407 'map', 'meter', 'object', 'output', 'progress', 'samp', 408 'time', 'area', 'datalist', 'figure', 'kbd', 'keygen', 409 'mark', 'math', 'wbr', 'area', 410 */ 411 412 // Extract El 413 var me = this, 414 obj = this.obj, 415 el = obj.get( 0 ), 416 nodeName = el.nodeName.toLowerCase(), 417 418 // supported elements 419 textElements = [ 'a', 'abbr', 'address', 'article', 'aside', 420 'b', 'bdo', 'blockquote', 'cite', 'code', 'command', 421 'del', 'details', 'dfn', 'div', 'dl', 'em', 'footer', 422 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'i', 423 'ins', 'menu', 'nav', 'p', 'pre', 'q', 'ruby', 424 'section', 'small', 'span', 'strong', 'sub', 'sup', 425 'var' ], 426 i, div; 427 428 for ( i = 0; i < textElements.length; ++i ) { 429 if ( nodeName === textElements[ i ] ) { 430 return true; 431 } 432 } 433 434 // special handled elements 435 switch ( nodeName ) { 436 case 'label': 437 case 'button': 438 // TODO need some special handling. 439 break; 440 case 'textarea': 441 case 'input': 442 // Create a div alongside the textarea 443 div = jQuery( '<div id="' + this.getId() + 444 '-aloha" class="aloha-' + nodeName + '" />' ) 445 .insertAfter( obj ); 446 447 // Resize the div to the textarea and 448 // Populate the div with the value of the textarea 449 // Then, hide the textarea 450 div.height( obj.height() ) 451 .width( obj.width() ) 452 .html( obj.val() ); 453 454 obj.hide(); 455 456 // Attach a onsubmit to the form to place the HTML of the 457 // div back into the textarea 458 obj.parents( 'form:first' ).submit( function() { 459 obj.val( me.getContents() ); 460 } ); 461 462 // Swap textarea reference with the new div 463 this.obj = div; 464 465 // Supported 466 return true; 467 default: 468 break; 469 } 470 471 // the following elements are not supported 472 /* 473 'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr', 474 'iframe', 'img', 'input', 'map', 'script', 'select', 'style', 475 'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript', 476 */ 477 return false; 478 }, 479 480 /** 481 482 * Init Placeholder 483 * 484 * @return void 485 */ 486 initPlaceholder: function() { 487 if ( Aloha.settings.placeholder && this.isEmpty() ) { 488 this.addPlaceholder(); 489 } 490 }, 491 492 /** 493 * Check if the conteneditable is empty. 494 * 495 * @return {Boolean} 496 */ 497 isEmpty: function() { 498 var editableTrimedContent = jQuery.trim( this.getContents() ), 499 onlyBrTag = ( editableTrimedContent === '<br>' ) ? true : false; 500 return ( editableTrimedContent.length === 0 || onlyBrTag ); 501 }, 502 503 /** 504 * Check if the editable div is not empty. Fixes a FF browser bug 505 * see issue: https://github.com/alohaeditor/Aloha-Editor/issues/269 506 * 507 * @return {undefined} 508 */ 509 initEmptyEditable: function( ) { 510 var obj = this.obj; 511 if ( this.empty( this.getContents() ) ) { 512 jQuery( obj ).prepend( '<br class="aloha-cleanme" />' ); 513 } 514 }, 515 516 /** 517 * Add placeholder in editable 518 * 519 * @return void 520 */ 521 addPlaceholder: function() { 522 var div = jQuery( '<div>' ), 523 span = jQuery( '<span>' ), 524 el, 525 obj = this.obj; 526 527 if ( GENTICS.Utils.Dom.allowsNesting( obj[0], div[0] ) ) { 528 el = div; 529 } else { 530 el = span; 531 } 532 if ( jQuery( "." + this.placeholderClass, obj ).length !== 0 ) { 533 return; 534 } 535 jQuery( obj ).append( el.addClass( this.placeholderClass ) ); 536 jQuery.each( 537 Aloha.settings.placeholder, 538 function( selector, selectorConfig ) { 539 if ( obj.is( selector ) ) { 540 el.html( selectorConfig ); 541 } 542 } 543 ); 544 545 // remove browser br 546 jQuery( 'br', obj ).remove(); 547 548 // delete div, span, el; 549 }, 550 551 /** 552 * remove placeholder from contenteditable. If setCursor is true, 553 * will also set the cursor to the start of the selection. However, 554 * this will be ASYNCHRONOUS, so if you rely on the fact that 555 * the placeholder is removed after calling this method, setCursor 556 * should be false ( or not set ) 557 * 558 * @return void 559 */ 560 removePlaceholder: function( obj, setCursor ) { 561 var placeholderClass = this.placeholderClass, 562 range; 563 if ( jQuery("." + this.placeholderClass, obj ).length === 0 ) { 564 return; 565 } 566 // set the cursor // remove placeholder 567 if ( setCursor === true ) { 568 window.setTimeout( function() { 569 range = new Selection.SelectionRange(); 570 range.startContainer = range.endContainer = obj.get(0); 571 range.startOffset = range.endOffset = 0; 572 jQuery( '.' + placeholderClass, obj ).remove(); 573 range.select(); 574 575 }, 100 ); 576 } else { 577 jQuery( '.' + placeholderClass, obj ).remove(); 578 } 579 }, 580 581 /** 582 * destroy the editable 583 * @return void 584 */ 585 destroy: function() { 586 // leave the element just to get sure 587 if ( this === Aloha.getActiveEditable() ) { 588 this.blur(); 589 } 590 591 // special handled elements 592 switch ( this.originalObj.get(0).nodeName.toLowerCase() ) { 593 case 'label': 594 case 'button': 595 // TODO need some special handling. 596 break; 597 case 'textarea': 598 case 'input': 599 // restore content to original textarea 600 this.originalObj.val( this.getContents() ); 601 this.obj.remove(); 602 this.originalObj.show(); 603 break; 604 default: 605 break; 606 } 607 608 // now the editable is not ready any more 609 this.ready = false; 610 611 // remove the placeholder if needed. 612 this.removePlaceholder( this.obj ); 613 614 // initialize the object and disable contentEditable 615 // unbind all events 616 // TODO should only unbind the specific handlers. 617 this.obj.removeClass( 'aloha-editable' ) 618 .contentEditable( false ) 619 .unbind( 'mousedown click dblclick focus keydown keypress keyup' ); 620 621 /* TODO remove this event, it should implemented as bind and unbind 622 // register the onSelectionChange Event with the Editable field 623 this.obj.contentEditableSelectionChange( function( event ) { 624 Aloha.Selection.onChange( me.obj, event ); 625 return me.obj; 626 } ); 627 */ 628 629 // throw a new event when the editable has been created 630 /** 631 * @event editableCreated fires after a new editable has been destroyes, eg. via $( '#editme' ).mahalo() 632 * The event is triggered in Aloha's global scope Aloha 633 * @param {Event} e the event object 634 * @param {Array} a an array which contains a reference to the currently created editable on its first position 635 */ 636 Aloha.trigger( 'aloha-editable-destroyed', [ this ] ); 637 638 // finally register the editable with Aloha 639 Aloha.unregisterEditable( this ); 640 }, 641 642 /** 643 * marks the editables current state as unmodified. Use this method to inform the editable 644 * that it's contents have been saved 645 * @method 646 */ 647 setUnmodified: function() { 648 this.originalContent = this.getContents(); 649 }, 650 651 /** 652 * check if the editable has been modified during the edit process# 653 * @method 654 * @return boolean true if the editable has been modified, false otherwise 655 */ 656 isModified: function() { 657 return this.originalContent !== this.getContents(); 658 }, 659 660 /** 661 * String representation of the object 662 * @method 663 * @return Aloha.Editable 664 */ 665 toString: function() { 666 return 'Aloha.Editable'; 667 }, 668 669 /** 670 * check whether the editable has been disabled 671 */ 672 isDisabled: function() { 673 return !this.obj.contentEditable() 674 || this.obj.contentEditable() === 'false'; 675 }, 676 677 /** 678 * disable this editable 679 * a disabled editable cannot be written on by keyboard 680 */ 681 disable: function() { 682 return this.isDisabled() || this.obj.contentEditable( false ); 683 }, 684 685 /** 686 * enable this editable 687 * reenables a disabled editable to be writteable again 688 */ 689 enable: function() { 690 return this.isDisabled() && this.obj.contentEditable( true ); 691 }, 692 693 694 /** 695 * activates an Editable for editing 696 * disables all other active items 697 * @method 698 */ 699 activate: function( e ) { 700 // get active Editable before setting the new one. 701 var oldActive = Aloha.getActiveEditable(); 702 703 // We need to ommit this call when this flag is set to true. 704 // This flag will only be set to true before the removePlaceholder method 705 // is called since that method invokes a focus event which will again trigger 706 // this method. We want to avoid double invokation of this method. 707 if ( ignoreNextActivateEvent ) { 708 ignoreNextActivateEvent = false; 709 return; 710 } 711 712 // handle special case in which a nested editable is focused by a click 713 // in this case the "focus" event would be triggered on the parent element 714 // which actually shifts the focus away to it's parent. this if is here to 715 // prevent this situation 716 if ( e && e.type === 'focus' && oldActive !== null 717 && oldActive.obj.parent().get( 0 ) === e.currentTarget ) { 718 return; 719 } 720 721 // leave immediately if this is already the active editable 722 if ( this.isActive || this.isDisabled() ) { 723 // we don't want parent editables to be triggered as well, so return false 724 return; 725 } 726 727 this.obj.addClass( 'aloha-editable-active' ); 728 729 Aloha.activateEditable( this ); 730 731 ignoreNextActivateEvent = true; 732 this.removePlaceholder ( this.obj, true ); 733 ignoreNextActivateEvent = false; 734 735 this.isActive = true; 736 737 /** 738 * @event editableActivated fires after the editable has been activated by clicking on it. 739 * This event is triggered in Aloha's global scope Aloha 740 * @param {Event} e the event object 741 * @param {Array} a an array which contains a reference to last active editable on its first position, as well 742 * as the currently active editable on it's second position 743 */ 744 // trigger a 'general' editableActivated event 745 Aloha.trigger( 'aloha-editable-activated', { 746 'oldActive' : oldActive, 747 'editable' : this 748 } ); 749 }, 750 751 /** 752 * handle the blur event 753 * this must not be attached to the blur event, which will trigger far too often 754 * eg. when a table within an editable is selected 755 * @hide 756 */ 757 blur: function() { 758 this.obj.blur(); 759 this.isActive = false; 760 this.initPlaceholder(); 761 this.obj.removeClass( 'aloha-editable-active' ); 762 763 /** 764 * @event editableDeactivated fires after the editable has been activated by clicking on it. 765 * This event is triggered in Aloha's global scope Aloha 766 * @param {Event} e the event object 767 * @param {Array} a an array which contains a reference to this editable 768 */ 769 Aloha.trigger( 'aloha-editable-deactivated', { editable : this } ); 770 771 /** 772 * @event smartContentChanged 773 */ 774 Aloha.activeEditable.smartContentChange( { type : 'blur' }, null ); 775 }, 776 777 /** 778 * check if the string is empty 779 * used for zerowidth check 780 * @return true if empty or string is null, false otherwise 781 * @hide 782 */ 783 empty: function( str ) { 784 // br is needed for chrome 785 return ( null === str ) 786 || ( jQuery.trim( str ) === '' || str === '<br/>' ); 787 }, 788 789 /** 790 * Get the contents of this editable as a HTML string or child node DOM 791 * objects. 792 * 793 * @param {boolean} asObject Whether or not to retreive the contents of 794 * this editable as child node objects or as 795 * HTML string. 796 * @return {string|jQuery.<HTMLElement>} Contents of the editable as 797 * DOM objects or an HTML string. 798 */ 799 getContents: function (asObject) { 800 var raw = this.obj.html(); 801 var cache = editableContentCache[this.getId()]; 802 803 if (!cache || raw !== cache.raw) { 804 805 BlockJump.removeZeroWidthTextNodeFix(); 806 807 var $clone = this.obj.clone(false); 808 $clone.find( '.aloha-cleanme' ).remove(); 809 this.removePlaceholder($clone); 810 PluginManager.makeClean($clone); 811 makeClean($clone); 812 $clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), { 813 contenthandler: Aloha.settings.contentHandler.getContents, 814 command: 'getContents' 815 }) + '</div>'); 816 cache = editableContentCache[this.getId()] = {}; 817 cache.raw = raw; 818 cache.element = $clone; 819 } 820 821 if (asObject) { 822 return cache.element.clone().contents(); 823 } else { 824 if (null == cache.serialized) { 825 cache.serialized = contentSerializer(cache.element[0]); 826 } 827 return cache.serialized; 828 } 829 }, 830 831 /** 832 * Set the contents of this editable as a HTML string 833 * @param content as html 834 * @param return as object or html string 835 * @return contents of the editable 836 */ 837 setContents: function( content, asObject ) { 838 var reactivate = null; 839 840 if ( Aloha.getActiveEditable() === this ) { 841 Aloha.deactivateEditable(); 842 reactivate = this; 843 } 844 845 this.obj.html( content ); 846 847 if ( null !== reactivate ) { 848 reactivate.activate(); 849 } 850 851 this.smartContentChange({type : 'set-contents'}); 852 853 return asObject ? this.obj.contents() : contentSerializer(this.obj[0]); 854 }, 855 856 /** 857 * Get the id of this editable 858 * @method 859 * @return id of this editable 860 */ 861 getId: function() { 862 return this.obj.attr( 'id' ); 863 }, 864 865 /** 866 * Generates and signals a smartContentChange event. 867 * 868 * A smart content change occurs when a special editing action, or a 869 * combination of interactions are performed by the user during the 870 * course of editing within an editable. 871 * The smart content change event would therefore signal to any 872 * component that is listening to this event, that content has been 873 * inserted into the editable that may need to be prococessed in a 874 * special way 875 * This is used for smart actions within the content/while editing. 876 * @param {Event} event 877 * @hide 878 */ 879 smartContentChange: function( event ) { 880 var me = this, 881 uniChar = null, 882 re, 883 match; 884 885 // ignore meta keys like crtl+v or crtl+l and so on 886 if ( event && ( event.metaKey || event.crtlKey || event.altKey ) ) { 887 return false; 888 } 889 890 if ( event && event.originalEvent ) { 891 // regex to strip unicode 892 re = new RegExp( "U\\+(\\w{4})" ); 893 match = re.exec( event.originalEvent.keyIdentifier ); 894 895 // Use keyIdentifier if available 896 if ( event.originalEvent.keyIdentifier && 1 === 2 ) { 897 // @fixme: Because of "&& 1 === 2" above, this block is 898 // unreachable code 899 if ( match !== null ) { 900 uniChar = unescape( '%u' + match[1] ); 901 } 902 if ( uniChar === null ) { 903 uniChar = event.originalEvent.keyIdentifier; 904 } 905 906 // FF & Opera don't support keyIdentifier 907 } else { 908 // Use among browsers reliable which http://api.jquery.com/keypress 909 uniChar = ( this.keyCodeMap[ this.keyCode ] || 910 String.fromCharCode( event.which ) || 'unknown' ); 911 } 912 } 913 914 var snapshot = null; 915 function getSnapshotContent() { 916 if (null == snapshot) { 917 snapshot = me.getSnapshotContent(); 918 } 919 return snapshot; 920 } 921 922 // handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier" 923 // reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 924 if ( jQuery.inArray( uniChar, this.sccDelimiters ) >= 0 ) { 925 clearTimeout( this.sccTimerIdle ); 926 clearTimeout( this.sccTimerDelay ); 927 928 this.sccTimerDelay = window.setTimeout( function() { 929 Aloha.trigger( 'aloha-smart-content-changed', { 930 'editable' : me, 931 'keyIdentifier' : event.originalEvent.keyIdentifier, 932 'keyCode' : event.keyCode, 933 'char' : uniChar, 934 'triggerType' : 'keypress', // keypress, timer, blur, paste 935 'getSnapshotContent' : getSnapshotContent 936 } ); 937 938 console.debug( 'Aloha.Editable', 939 'smartContentChanged: event type keypress triggered' ); 940 }, this.sccDelay ); 941 } else if ( event && event.type === 'paste' ) { 942 Aloha.trigger( 'aloha-smart-content-changed', { 943 'editable' : me, 944 'keyIdentifier' : null, 945 'keyCode' : null, 946 'char' : null, 947 'triggerType' : 'paste', 948 'getSnapshotContent' : getSnapshotContent 949 } ); 950 951 } else if ( event && event.type === 'blur' ) { 952 Aloha.trigger( 'aloha-smart-content-changed', { 953 'editable' : me, 954 'keyIdentifier' : null, 955 'keyCode' : null, 956 'char' : null, 957 'triggerType' : 'blur', 958 'getSnapshotContent' : getSnapshotContent 959 } ); 960 961 } else if ( uniChar !== null ) { 962 // in the rare case idle time is lower then delay time 963 clearTimeout( this.sccTimerDelay ); 964 clearTimeout( this.sccTimerIdle ); 965 this.sccTimerIdle = window.setTimeout( function() { 966 Aloha.trigger( 'aloha-smart-content-changed', { 967 'editable' : me, 968 'keyIdentifier' : null, 969 'keyCode' : null, 970 'char' : null, 971 'triggerType' : 'idle', 972 'getSnapshotContent' : getSnapshotContent 973 } ); 974 }, this.sccIdle ); 975 } 976 }, 977 978 /** 979 * Get a snapshot of the active editable as a HTML string 980 * @hide 981 * @return snapshot of the editable 982 */ 983 getSnapshotContent: function() { 984 var ret = this.snapshotContent; 985 this.snapshotContent = this.getContents(); 986 return ret; 987 } 988 } ); 989 990 /** 991 * Sets the content serializer function. 992 * 993 * The default content serializer will just call the jQuery.html() 994 * function on the editable element (which gets the innerHTML property). 995 * 996 * This method is a static class method and will affect the result 997 * of editable.getContents() for all editables that have been or 998 * will be constructed. 999 * 1000 * @param {!Function} serializerFunction 1001 * A function that accepts a DOM element and returns the serialized 1002 * XHTML of the element contents (excluding the start and end tag of 1003 * the passed element). 1004 * @api 1005 */ 1006 Aloha.Editable.setContentSerializer = function (serializerFunction) { 1007 contentSerializer = serializerFunction; 1008 }; 1009 1010 /** 1011 * Gets the content serializer function. 1012 * 1013 * @see Aloha.Editable.setContentSerializer() 1014 * @api 1015 * @return {!Function} 1016 * The serializer function. 1017 */ 1018 Aloha.Editable.getContentSerializer = function () { 1019 return contentSerializer; 1020 }; 1021 }); 1022