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