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 }, 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 this.isActive = false; 765 this.initPlaceholder(); 766 this.obj.removeClass('aloha-editable-active'); 767 768 /** 769 * @event editableDeactivated fires after the editable has been activated by clicking on it. 770 * This event is triggered in Aloha's global scope Aloha 771 * @param {Event} e the event object 772 * @param {Array} a an array which contains a reference to this editable 773 */ 774 Aloha.trigger('aloha-editable-deactivated', { 775 editable: this 776 }); 777 778 /** 779 * @event smartContentChanged 780 */ 781 Aloha.activeEditable.smartContentChange({ 782 type: 'blur' 783 }, null); 784 }, 785 786 /** 787 * check if the string is empty 788 * used for zerowidth check 789 * @return true if empty or string is null, false otherwise 790 * @hide 791 */ 792 empty: function (str) { 793 // br is needed for chrome 794 return (null === str) || (jQuery.trim(str) === '' || str === '<br/>'); 795 }, 796 797 /** 798 * Get the contents of this editable as a HTML string or child node DOM 799 * objects. 800 * 801 * @param {boolean} asObject Whether or not to retreive the contents of 802 * this editable as child node objects or as 803 * HTML string. 804 * @return {string|jQuery.<HTMLElement>} Contents of the editable as 805 * DOM objects or an HTML string. 806 */ 807 getContents: function (asObject) { 808 var raw = this.obj.html(); 809 var cache = editableContentCache[this.getId()]; 810 811 if (!cache || raw !== cache.raw) { 812 813 BlockJump.removeZeroWidthTextNodeFix(); 814 815 var $clone = this.obj.clone(false); 816 this.removePlaceholder($clone); 817 $clone = jQuery(Ephemera.prune($clone[0])); 818 PluginManager.makeClean($clone); 819 820 // TODO rewrite ContentHandlerManager to accept DOM trees instead of strings 821 $clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), { 822 contenthandler: Aloha.settings.contentHandler.getContents, 823 command: 'getContents' 824 }, this) + '</div>'); 825 826 cache = editableContentCache[this.getId()] = {}; 827 cache.raw = raw; 828 cache.element = $clone; 829 } 830 831 if (asObject) { 832 return cache.element.clone().contents(); 833 } 834 835 if (null == cache.serialized) { 836 cache.serialized = contentSerializer(cache.element[0]); 837 } 838 return cache.serialized; 839 }, 840 841 /** 842 * Set the contents of this editable as a HTML string 843 * @param content as html 844 * @param return as object or html string 845 * @return contents of the editable 846 */ 847 setContents: function (content, asObject) { 848 var reactivate = null; 849 850 if (Aloha.getActiveEditable() === this) { 851 Aloha.deactivateEditable(); 852 reactivate = this; 853 } 854 855 this.obj.html(content); 856 857 if (null !== reactivate) { 858 reactivate.activate(); 859 } 860 861 this.smartContentChange({ 862 type: 'set-contents' 863 }); 864 865 return asObject ? this.obj.contents() : contentSerializer(this.obj[0]); 866 867 }, 868 869 /** 870 * Get the id of this editable 871 * @method 872 * @return id of this editable 873 */ 874 getId: function () { 875 return this.obj.attr('id'); 876 }, 877 878 /** 879 * Generates and signals a smartContentChange event. 880 * 881 * A smart content change occurs when a special editing action, or a 882 * combination of interactions are performed by the user during the 883 * course of editing within an editable. 884 * The smart content change event would therefore signal to any 885 * component that is listening to this event, that content has been 886 * inserted into the editable that may need to be prococessed in a 887 * special way 888 * This is used for smart actions within the content/while editing. 889 * @param {Event} event 890 * @hide 891 */ 892 smartContentChange: function (event) { 893 var me = this, 894 uniChar = null, 895 re, 896 match; 897 898 // ignore meta keys like crtl+v or crtl+l and so on 899 if (event && (event.metaKey || event.crtlKey || event.altKey)) { 900 return false; 901 } 902 903 if (event && event.originalEvent) { 904 // regex to strip unicode 905 re = new RegExp("U\\+(\\w{4})"); 906 match = re.exec(event.originalEvent.keyIdentifier); 907 908 // Use among browsers reliable which http://api.jquery.com/keypress 909 uniChar = (this.keyCodeMap[this.keyCode] || String.fromCharCode(event.which) || 'unknown'); 910 } 911 912 var snapshot = null; 913 914 function getSnapshotContent() { 915 if (null == snapshot) { 916 snapshot = me.getSnapshotContent(); 917 } 918 return snapshot; 919 } 920 921 // handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier" 922 // reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html 923 if (jQuery.inArray(uniChar, this.sccDelimiters) >= 0) { 924 clearTimeout(this.sccTimerIdle); 925 clearTimeout(this.sccTimerDelay); 926 927 this.sccTimerDelay = window.setTimeout(function () { 928 Aloha.trigger('aloha-smart-content-changed', { 929 'editable': me, 930 'keyIdentifier': event.originalEvent.keyIdentifier, 931 'keyCode': event.keyCode, 932 'char': uniChar, 933 'triggerType': 'keypress', // keypress, timer, blur, paste 934 'getSnapshotContent': getSnapshotContent 935 }); 936 handleSmartContentChange(me); 937 938 console.debug('Aloha.Editable', 939 'smartContentChanged: event type keypress triggered'); 940 }, this.sccDelay); 941 942 } else if (event && event.type === 'paste') { 943 Aloha.trigger('aloha-smart-content-changed', { 944 'editable': me, 945 'keyIdentifier': null, 946 'keyCode': null, 947 'char': null, 948 'triggerType': 'paste', 949 'getSnapshotContent': getSnapshotContent 950 }); 951 handleSmartContentChange(me); 952 953 } else if (event && event.type === 'blur') { 954 Aloha.trigger('aloha-smart-content-changed', { 955 'editable': me, 956 'keyIdentifier': null, 957 'keyCode': null, 958 'char': null, 959 'triggerType': 'blur', 960 'getSnapshotContent': getSnapshotContent 961 }); 962 handleSmartContentChange(me); 963 964 } else if (event && event.type === 'block-change') { 965 Aloha.trigger('aloha-smart-content-changed', { 966 'editable': me, 967 'keyIdentifier': null, 968 'keyCode': null, 969 'char': null, 970 'triggerType': 'block-change', 971 'getSnapshotContent': getSnapshotContent 972 }); 973 handleSmartContentChange(me); 974 975 } else if (uniChar !== null) { 976 // in the rare case idle time is lower then delay time 977 clearTimeout(this.sccTimerDelay); 978 clearTimeout(this.sccTimerIdle); 979 this.sccTimerIdle = window.setTimeout(function () { 980 Aloha.trigger('aloha-smart-content-changed', { 981 'editable': me, 982 'keyIdentifier': null, 983 'keyCode': null, 984 'char': null, 985 'triggerType': 'idle', 986 'getSnapshotContent': getSnapshotContent 987 }); 988 handleSmartContentChange(me); 989 }, this.sccIdle); 990 } 991 }, 992 993 /** 994 * Get a snapshot of the active editable as a HTML string 995 * @hide 996 * @return snapshot of the editable 997 */ 998 getSnapshotContent: function () { 999 var ret = this.snapshotContent; 1000 this.snapshotContent = this.getContents(); 1001 return ret; 1002 } 1003 }); 1004 1005 /** 1006 * Sets the content serializer function. 1007 * 1008 * The default content serializer will just call the jQuery.html() 1009 * function on the editable element (which gets the innerHTML property). 1010 * 1011 * This method is a static class method and will affect the result 1012 * of editable.getContents() for all editables that have been or 1013 * will be constructed. 1014 * 1015 * @param {!Function} serializerFunction 1016 * A function that accepts a DOM element and returns the serialized 1017 * XHTML of the element contents (excluding the start and end tag of 1018 * the passed element). 1019 * @api 1020 */ 1021 Aloha.Editable.setContentSerializer = function (serializerFunction) { 1022 contentSerializer = serializerFunction; 1023 }; 1024 1025 /** 1026 * Gets the content serializer function. 1027 * 1028 * @see Aloha.Editable.setContentSerializer() 1029 * @api 1030 * @return {!Function} 1031 * The serializer function. 1032 */ 1033 Aloha.Editable.getContentSerializer = function () { 1034 return contentSerializer; 1035 }; 1036 }); 1037