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 if (Aloha.activeEditable == null || typeof Aloha.activeEditable === 'undefined' || $editable[0] !== Aloha.activeEditable.obj[0]) { 183 Aloha.mouseEventChangedEditable = true; 184 } 185 return editable.activate(event); 186 } 187 }); 188 $editable.mouseup(function (event) { 189 Aloha.eventHandled = false; 190 }); 191 192 $editable.focus(function (event) { 193 return editable.activate(event); 194 }); 195 196 $editable.keydown(function (event) { 197 var letEventPass = Markup.preProcessKeyStrokes(event); 198 editable.keyCode = event.which; 199 if (!letEventPass) { 200 // the event will not proceed to key press, therefore trigger 201 // smartContentChange 202 editable.smartContentChange(event); 203 } 204 return letEventPass; 205 }); 206 207 $editable.keypress(StateOverride.keyPressHandler); 208 $editable.keypress(function (event) { 209 // triggers a smartContentChange to get the right charcode 210 // To test try http://www.w3.org/2002/09/tests/keys.html 211 Aloha.activeEditable.smartContentChange(event); 212 }); 213 214 $editable.keyup(function (event) { 215 if (event.keyCode === 27) { 216 Aloha.deactivateEditable(); 217 return false; 218 } 219 }); 220 221 $editable.contentEditableSelectionChange(function (event) { 222 Selection.onChange($editable, event, 0, Aloha.mouseEventChangedEditable); 223 if (Aloha.mouseEventChangedEditable) { 224 Aloha.mouseEventChangedEditable = false; 225 } 226 return $editable; 227 }); 228 } 229 230 $(document).keydown(onKeydown); 231 232 233 /** 234 * Editable object 235 * @namespace Aloha 236 * @class Editable 237 * @method 238 * @constructor 239 * @param {Object} obj jQuery object reference to the object 240 */ 241 Aloha.Editable = Class.extend({ 242 243 _constructor: function (obj) { 244 // check wheter the object has an ID otherwise generate and set 245 // globally unique ID 246 if (!obj.attr('id')) { 247 obj.attr('id', GENTICS.Utils.guid()); 248 } 249 250 // store object reference 251 this.obj = obj; 252 this.originalObj = obj; 253 this.ready = false; 254 255 // delimiters, timer and idle for smartContentChange 256 // smartContentChange triggers -- tab: '\u0009' - space: '\u0020' - enter: 'Enter' 257 // backspace: U+0008 - delete: U+007F 258 this.sccDelimiters = [':', ';', '.', '!', '?', ',', 259 unescape('%u0009'), unescape('%u0020'), unescape('%u0008'), unescape('%u007F'), 'Enter']; 260 this.sccIdle = 5000; 261 this.sccDelay = 500; 262 this.sccTimerIdle = false; 263 this.sccTimerDelay = false; 264 265 // see keyset http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 266 this.keyCodeMap = { 267 93: 'Apps', // The Application key 268 18: 'Alt', // The Alt ( Menu ) key. 269 20: 'CapsLock', // The Caps Lock ( Capital ) key. 270 17: 'Control', // The Control ( Ctrl ) key. 271 40: 'Down', // The Down Arrow key. 272 35: 'End', // The End key. 273 13: 'Enter', // The Enter key. 274 112: 'F1', // The F1 key. 275 113: 'F2', // The F2 key. 276 114: 'F3', // The F3 key. 277 115: 'F4', // The F4 key. 278 116: 'F5', // The F5 key. 279 117: 'F6', // The F6 key. 280 118: 'F7', // The F7 key. 281 119: 'F8', // The F8 key. 282 120: 'F9', // The F9 key. 283 121: 'F10', // The F10 key. 284 122: 'F11', // The F11 key. 285 123: 'F12', // The F12 key. 286 287 // Anybody knows the keycode for F13-F24? 288 36: 'Home', // The Home key. 289 45: 'Insert', // The Insert ( Ins ) key. 290 37: 'Left', // The Left Arrow key. 291 224: 'Meta', // The Meta key. 292 34: 'PageDown', // The Page Down ( Next ) key. 293 33: 'PageUp', // The Page Up key. 294 19: 'Pause', // The Pause key. 295 44: 'PrintScreen', // The Print Screen ( PrintScrn, SnapShot ) key. 296 39: 'Right', // The Right Arrow key. 297 145: 'Scroll', // The scroll lock key 298 16: 'Shift', // The Shift key. 299 38: 'Up', // The Up Arrow key. 300 91: 'Win', // The left Windows Logo key. 301 92: 'Win' // The right Windows Logo key. 302 }; 303 304 this.placeholderClass = 'aloha-placeholder'; 305 306 Aloha.registerEditable(this); 307 }, 308 309 /** 310 * Initialize the editable 311 * @return void 312 * @hide 313 */ 314 init: function () { 315 var me = this; 316 317 // TODO make editables their own settings. 318 this.settings = Aloha.settings; 319 320 // smartContentChange settings 321 // @TODO move to new config when implemented in Aloha 322 if (Aloha.settings && Aloha.settings.smartContentChange) { 323 if (Aloha.settings.smartContentChange.delimiters) { 324 this.sccDelimiters = Aloha.settings.smartContentChange.delimiters; 325 } 326 327 if (Aloha.settings.smartContentChange.idle) { 328 this.sccIdle = Aloha.settings.smartContentChange.idle; 329 } 330 331 if (Aloha.settings.smartContentChange.delay) { 332 this.sccDelay = Aloha.settings.smartContentChange.delay; 333 } 334 } 335 336 // check if Aloha can handle the obj as Editable 337 if (!this.check(this.obj)) { 338 //Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' ); 339 this.destroy(); 340 return; 341 } 342 343 // apply content handler to clean up content 344 if (typeof Aloha.settings.contentHandler.getContents === 'undefined') { 345 Aloha.settings.contentHandler.getContents = Aloha.defaults.contentHandler.getContents; 346 } 347 348 // apply content handler to clean up content 349 if (typeof Aloha.settings.contentHandler.initEditable === 'undefined') { 350 Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable; 351 } 352 353 var content = me.obj.html(); 354 content = ContentHandlerManager.handleContent(content, { 355 contenthandler: Aloha.settings.contentHandler.initEditable, 356 command: 'initEditable' 357 }, me); 358 me.obj.html(content); 359 360 // Because editables can only properly be initialized when Aloha 361 // plugins are loaded. 362 Aloha.bind('aloha-plugins-loaded', function () { 363 me.obj.addClass('aloha-editable').contentEditable(true); 364 365 registerEvents(me); 366 367 // mark the editable as unmodified 368 me.setUnmodified(); 369 370 // we don't do the sanitizing on aloha ready, since some plugins add elements into the content and bind 371 // events to it. If we sanitize by replacing the html, all events would get lost. TODO: think about a 372 // better solution for the sanitizing, without destroying the events apply content handler to clean up content 373 // var content = me.obj.html(); 374 // if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) { 375 // Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable; 376 // } 377 // content = ContentHandlerManager.handleContent( content, { 378 // contenthandler: Aloha.settings.contentHandler.initEditable 379 // } ); 380 // me.obj.html( content ); 381 382 me.snapshotContent = me.getContents(); 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 401 * The event is triggered in Aloha's global scope Aloha 402 * @param {Event} e the event object 403 * @param {Array} a an array which contains a reference to the currently created editable on its first position 404 */ 405 Aloha.trigger('aloha-editable-created', [me]); 406 PubSub.pub('aloha.editable.created', {data: me}); 407 }); 408 }, 409 410 /** 411 * True, if this editable is active for editing 412 * @property 413 * @type boolean 414 */ 415 isActive: false, 416 417 /** 418 * stores the original content to determine if it has been modified 419 * @hide 420 */ 421 originalContent: null, 422 423 /** 424 * every time a selection is made in the current editable the selection has to 425 * be saved for further use 426 * @hide 427 */ 428 range: undefined, 429 430 /** 431 * Check if object can be edited by Aloha Editor 432 * @return {boolean } editable true if Aloha Editor can handle else false 433 * @hide 434 */ 435 check: function () { 436 /* TODO check those elements 437 'map', 'meter', 'object', 'output', 'progress', 'samp', 438 'time', 'area', 'datalist', 'figure', 'kbd', 'keygen', 439 'mark', 'math', 'wbr', 'area', 440 */ 441 442 // Extract El 443 var me = this, 444 obj = this.obj, 445 el = obj.get(0), 446 nodeName = el.nodeName.toLowerCase(), 447 448 // supported elements 449 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'], 450 i, 451 div; 452 453 for (i = 0; i < textElements.length; ++i) { 454 if (nodeName === textElements[i]) { 455 return true; 456 } 457 } 458 459 // special handled elements 460 switch (nodeName) { 461 case 'label': 462 case 'button': 463 // TODO need some special handling. 464 break; 465 case 'textarea': 466 case 'input': 467 // Create a div alongside the textarea 468 div = jQuery('<div id="' + this.getId() + '-aloha" class="aloha-' + nodeName + '" />').insertAfter(obj); 469 470 // Resize the div to the textarea and 471 // Populate the div with the value of the textarea 472 // Then, hide the textarea 473 div.height(obj.height()).width(obj.width()).html(obj.val()); 474 475 obj.hide(); 476 477 // Attach a onsubmit to the form to place the HTML of the 478 // div back into the textarea 479 obj.parents('form:first').submit(function () { 480 obj.val(me.getContents()); 481 }); 482 483 // Swap textarea reference with the new div 484 this.obj = div; 485 486 // Supported 487 return true; 488 default: 489 break; 490 } 491 492 // the following elements are not supported 493 /* 494 'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr', 495 'iframe', 'img', 'input', 'map', 'script', 'select', 'style', 496 'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript', 497 */ 498 return false; 499 }, 500 501 /** 502 * Init Placeholder 503 * 504 * @return void 505 */ 506 initPlaceholder: function () { 507 if (Aloha.settings.placeholder && this.isEmpty()) { 508 this.addPlaceholder(); 509 } 510 }, 511 512 /** 513 * Check if the conteneditable is empty. 514 * 515 * @return {Boolean} 516 */ 517 isEmpty: function () { 518 var editableTrimedContent = jQuery.trim(this.getContents()), 519 onlyBrTag = (editableTrimedContent === '<br>') ? true : false; 520 return (editableTrimedContent.length === 0 || onlyBrTag); 521 }, 522 523 /** 524 * Add placeholder in editable 525 * 526 * @return void 527 */ 528 addPlaceholder: function () { 529 var div = jQuery('<div>'), 530 span = jQuery('<span>'), 531 el, 532 obj = this.obj; 533 if (GENTICS.Utils.Dom.allowsNesting(obj[0], div[0])) { 534 el = div; 535 } else { 536 el = span; 537 } 538 if (jQuery("." + this.placeholderClass, obj).length !== 0) { 539 return; 540 } 541 jQuery.each(Aloha.settings.placeholder, function (selector, selectorConfig) { 542 if (obj.is(selector)) { 543 el.html(selectorConfig); 544 } 545 }); 546 if (!el.is(':empty')) { 547 el.addClass(this.placeholderClass).addClass('aloha-ephemera'); 548 jQuery(obj).append(el); 549 } 550 jQuery('br', obj).remove(); 551 }, 552 553 /** 554 * remove placeholder from contenteditable. If setCursor is true, 555 * will also set the cursor to the start of the selection. However, 556 * this will be ASYNCHRONOUS, so if you rely on the fact that 557 * the placeholder is removed after calling this method, setCursor 558 * should be false ( or not set ) 559 * 560 * @return void 561 */ 562 removePlaceholder: function (obj, setCursor) { 563 var placeholderClass = this.placeholderClass, 564 range; 565 if (jQuery("." + this.placeholderClass, obj).length === 0) { 566 return; 567 } 568 // set the cursor // remove placeholder 569 if (setCursor === true) { 570 window.setTimeout(function () { 571 range = new Selection.SelectionRange(); 572 range.startContainer = range.endContainer = obj.get(0); 573 range.startOffset = range.endOffset = 0; 574 jQuery('.' + placeholderClass, obj).remove(); 575 range.select(); 576 577 }, 100); 578 } else { 579 jQuery('.' + placeholderClass, obj).remove(); 580 } 581 }, 582 583 /** 584 * destroy the editable 585 * @return void 586 */ 587 destroy: function () { 588 // leave the element just to get sure 589 if (this === Aloha.getActiveEditable()) { 590 this.blur(); 591 } 592 593 // special handled elements 594 switch (this.originalObj.get(0).nodeName.toLowerCase()) { 595 case 'label': 596 case 'button': 597 // TODO need some special handling. 598 break; 599 case 'textarea': 600 case 'input': 601 // restore content to original textarea 602 this.originalObj.val(this.getContents()); 603 this.obj.remove(); 604 this.originalObj.show(); 605 break; 606 default: 607 break; 608 } 609 610 // now the editable is not ready any more 611 this.ready = false; 612 613 // remove the placeholder if needed. 614 this.removePlaceholder(this.obj); 615 616 // initialize the object and disable contentEditable 617 // unbind all events 618 // TODO should only unbind the specific handlers. 619 this.obj.removeClass('aloha-editable').contentEditable(false).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 PubSub.pub('aloha.editable.destroyed', {data: this}); 638 639 // finally register the editable with Aloha 640 Aloha.unregisterEditable(this); 641 }, 642 643 /** 644 * marks the editables current state as unmodified. Use this method to inform the editable 645 * that it's contents have been saved 646 * @method 647 */ 648 setUnmodified: function () { 649 this.originalContent = this.getContents(); 650 }, 651 652 /** 653 * check if the editable has been modified during the edit process# 654 * @method 655 * @return boolean true if the editable has been modified, false otherwise 656 */ 657 isModified: function () { 658 return this.originalContent !== this.getContents(); 659 }, 660 661 /** 662 * String representation of the object 663 * @method 664 * @return Aloha.Editable 665 */ 666 toString: function () { 667 return 'Aloha.Editable'; 668 }, 669 670 /** 671 * check whether the editable has been disabled 672 */ 673 isDisabled: function () { 674 return !this.obj.contentEditable() || 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 && oldActive.obj.parent().get(0) === e.currentTarget) { 717 return; 718 } 719 720 // leave immediately if this is already the active editable 721 if (this.isActive || this.isDisabled()) { 722 // we don't want parent editables to be triggered as well, so return false 723 return; 724 } 725 726 this.obj.addClass('aloha-editable-active'); 727 728 Aloha.activateEditable(this); 729 730 ignoreNextActivateEvent = true; 731 this.removePlaceholder(this.obj, true); 732 ignoreNextActivateEvent = false; 733 734 this.isActive = true; 735 736 /** 737 * @event editableActivated fires after the editable has been activated by clicking on it. 738 * This event is triggered in Aloha's global scope Aloha 739 * @param {Event} e the event object 740 * @param {Array} a an array which contains a reference to last active editable on its first position, as well 741 * as the currently active editable on it's second position 742 */ 743 // trigger a 'general' editableActivated event 744 Aloha.trigger('aloha-editable-activated', { 745 'oldActive': oldActive, 746 'editable': this 747 }); 748 PubSub.pub('aloha.editable.activated', { 749 data: { 750 old: oldActive, 751 editable: this 752 } 753 }); 754 }, 755 756 /** 757 * handle the blur event 758 * this must not be attached to the blur event, which will trigger far too often 759 * eg. when a table within an editable is selected 760 * @hide 761 */ 762 blur: function () { 763 this.obj.blur(); 764 765 this.isActive = false; 766 this.initPlaceholder(); 767 this.obj.removeClass('aloha-editable-active'); 768 769 /** 770 * @event editableDeactivated fires after the editable has been activated by clicking on it. 771 * This event is triggered in Aloha's global scope Aloha 772 * @param {Event} e the event object 773 * @param {Array} a an array which contains a reference to this editable 774 */ 775 Aloha.trigger('aloha-editable-deactivated', { 776 editable: this 777 }); 778 PubSub.pub('aloha.editable.deactivated', { 779 data: { 780 editable: this 781 } 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 handleSmartContentChange(me); 942 943 console.debug('Aloha.Editable', 944 'smartContentChanged: event type keypress triggered'); 945 }, this.sccDelay); 946 947 } else if (event && event.type === 'paste') { 948 Aloha.trigger('aloha-smart-content-changed', { 949 'editable': me, 950 'keyIdentifier': null, 951 'keyCode': null, 952 'char': null, 953 'triggerType': 'paste', 954 'getSnapshotContent': getSnapshotContent 955 }); 956 handleSmartContentChange(me); 957 958 } else if (event && event.type === 'blur') { 959 Aloha.trigger('aloha-smart-content-changed', { 960 'editable': me, 961 'keyIdentifier': null, 962 'keyCode': null, 963 'char': null, 964 'triggerType': 'blur', 965 'getSnapshotContent': getSnapshotContent 966 }); 967 handleSmartContentChange(me); 968 969 } else if (event && event.type === 'block-change') { 970 Aloha.trigger('aloha-smart-content-changed', { 971 'editable': me, 972 'keyIdentifier': null, 973 'keyCode': null, 974 'char': null, 975 'triggerType': 'block-change', 976 'getSnapshotContent': getSnapshotContent 977 }); 978 handleSmartContentChange(me); 979 980 } else if (uniChar !== null) { 981 // in the rare case idle time is lower then delay time 982 clearTimeout(this.sccTimerDelay); 983 clearTimeout(this.sccTimerIdle); 984 this.sccTimerIdle = window.setTimeout(function () { 985 Aloha.trigger('aloha-smart-content-changed', { 986 'editable': me, 987 'keyIdentifier': null, 988 'keyCode': null, 989 'char': null, 990 'triggerType': 'idle', 991 'getSnapshotContent': getSnapshotContent 992 }); 993 handleSmartContentChange(me); 994 }, this.sccIdle); 995 } 996 }, 997 998 /** 999 * Get a snapshot of the active editable as a HTML string 1000 * @hide 1001 * @return snapshot of the editable 1002 */ 1003 getSnapshotContent: function () { 1004 var ret = this.snapshotContent; 1005 this.snapshotContent = this.getContents(); 1006 return ret; 1007 } 1008 }); 1009 1010 /** 1011 * Sets the content serializer function. 1012 * 1013 * The default content serializer will just call the jQuery.html() 1014 * function on the editable element (which gets the innerHTML property). 1015 * 1016 * This method is a static class method and will affect the result 1017 * of editable.getContents() for all editables that have been or 1018 * will be constructed. 1019 * 1020 * @param {!Function} serializerFunction 1021 * A function that accepts a DOM element and returns the serialized 1022 * XHTML of the element contents (excluding the start and end tag of 1023 * the passed element). 1024 * @api 1025 */ 1026 Aloha.Editable.setContentSerializer = function (serializerFunction) { 1027 contentSerializer = serializerFunction; 1028 }; 1029 1030 /** 1031 * Gets the content serializer function. 1032 * 1033 * @see Aloha.Editable.setContentSerializer() 1034 * @api 1035 * @return {!Function} 1036 * The serializer function. 1037 */ 1038 Aloha.Editable.getContentSerializer = function () { 1039 return contentSerializer; 1040 }; 1041 1042 Aloha.Editable.registerEvents = registerEvents; 1043 1044 1045 }); 1046