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