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