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