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