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