1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * HTML editor which wraps TinyMCE 26 * 27 * @param {Hash} params a hash of parameters: 28 * @param {constant} posStyle new message, reply, forward, or an invite action 29 * @param {Object} content 30 * @param {constant} mode 31 * @param {Boolean} withAce 32 * @param {Boolean} parentElement 33 * @param {String} textAreaId 34 * @param {Function} attachmentCallback callback to create image attachment 35 * @param {Function} pasteCallback callback invoked when data is pasted and uploaded to the server 36 * @param {Function} initCallback callback invoked when the editor is fully initialized 37 * 38 * @author Satish S 39 * @private 40 */ 41 ZmHtmlEditor = function() { 42 if (arguments.length == 0) { return; } 43 44 var params = Dwt.getParams(arguments, ZmHtmlEditor.PARAMS); 45 46 if (!params.className) { 47 params.className = 'ZmHtmlEditor'; 48 } 49 50 if (!params.id) { 51 params.id = Dwt.getNextId('ZmHtmlEditor'); 52 } 53 54 DwtControl.call(this, params); 55 56 this.isTinyMCE = window.isTinyMCE; 57 this._mode = params.mode; 58 this._hasFocus = {}; 59 this._bodyTextAreaId = params.textAreaId || this.getHTMLElId() + '_body'; 60 this._iFrameId = this._bodyTextAreaId + "_ifr"; 61 this._initCallbacks = []; 62 this._attachmentCallback = params.attachmentCallback; 63 this._pasteCallback = params.pasteCallback; 64 this._onContentInitializeCallbacks = [] 65 this.initTinyMCEEditor(params); 66 this._ignoreWords = {}; 67 this._classCount = 0; 68 69 if (params.initCallback) 70 this._initCallbacks.push(params.initCallback); 71 72 var settings = appCtxt.getSettings(); 73 var listener = new AjxListener(this, this._settingChangeListener); 74 settings.getSetting(ZmSetting.COMPOSE_INIT_FONT_COLOR).addChangeListener(listener); 75 settings.getSetting(ZmSetting.COMPOSE_INIT_FONT_FAMILY).addChangeListener(listener); 76 settings.getSetting(ZmSetting.COMPOSE_INIT_FONT_SIZE).addChangeListener(listener); 77 settings.getSetting(ZmSetting.COMPOSE_INIT_DIRECTION).addChangeListener(listener); 78 settings.getSetting(ZmSetting.SHOW_COMPOSE_DIRECTION_BUTTONS).addChangeListener(listener); 79 80 this.addControlListener(this._resetSize.bind(this)); 81 82 this.addListener(DwtEvent.ONFOCUS, this._onFocus.bind(this)); 83 this.addListener(DwtEvent.ONBLUR, this._onBlur.bind(this)); 84 }; 85 86 ZmHtmlEditor.PARAMS = [ 87 'parent', 88 'posStyle', 89 'content', 90 'mode', 91 'withAce', 92 'parentElement', 93 'textAreaId', 94 'attachmentCallback', 95 'initCallback' 96 ]; 97 98 ZmHtmlEditor.prototype = new DwtControl(); 99 ZmHtmlEditor.prototype.constructor = ZmHtmlEditor; 100 101 ZmHtmlEditor.prototype.isZmHtmlEditor = true; 102 ZmHtmlEditor.prototype.isInputControl = true; 103 ZmHtmlEditor.prototype.toString = function() { return "ZmHtmlEditor"; }; 104 105 ZmHtmlEditor.TINY_MCE_PATH = "/js/ajax/3rdparty/tinymce"; 106 107 // used as a data key (mostly for menu items) 108 ZmHtmlEditor.VALUE = "value"; 109 110 ZmHtmlEditor._INITDELAY = 50; 111 112 ZmHtmlEditor._containerDivId = "zimbraEditorContainer"; 113 114 ZmHtmlEditor.prototype.getEditor = 115 function() { 116 return (window.tinyMCE) ? tinyMCE.get(this._bodyTextAreaId) : null; 117 }; 118 119 ZmHtmlEditor.prototype.getBodyFieldId = 120 function() { 121 if (this._mode == Dwt.HTML) { 122 var editor = this.getEditor(); 123 return editor ? this._iFrameId : this._bodyTextAreaId; 124 } 125 126 return this._bodyTextAreaId; 127 }; 128 129 ZmHtmlEditor.prototype.getBodyField = 130 function() { 131 return document.getElementById(this.getBodyFieldId()); 132 }; 133 134 ZmHtmlEditor.prototype._resetSize = 135 function() { 136 var field = this.getContentField(); 137 138 if (this._resetSizeAction) { 139 clearTimeout(this._resetSizeAction); 140 this._resetSizeAction = null; 141 } 142 143 if (field) { 144 var bounds = this.boundsForChild(field); 145 Dwt.setSize(field, bounds.width, bounds.height); 146 } 147 148 var editor = this.getEditor(); 149 150 if (!editor || !editor.getContentAreaContainer() || !editor.getBody()) { 151 if (this.getVisible()) { 152 this._resetSizeAction = 153 setTimeout(ZmHtmlEditor.prototype._resetSize.bind(this), 100); 154 } 155 return; 156 } 157 158 var iframe = Dwt.byId(this._iFrameId); 159 var bounds = this.boundsForChild(iframe); 160 var x = bounds.width, y = bounds.height; 161 162 //Subtracting editor toolbar heights 163 AjxUtil.foreach(Dwt.byClassName('mce-toolbar-grp', 164 editor.getContainer()), 165 function(elem) { 166 y -= Dwt.getSize(elem).y; 167 }); 168 169 // on Firefox, the toolbar is detected as unreasonably large during load; 170 // so start the timer for small sizes -- even in small windows, the toolbar 171 // should never be more than ~110px tall 172 if (bounds.height - y > 200) { 173 this._resetSizeAction = 174 setTimeout(ZmHtmlEditor.prototype._resetSize.bind(this), 100); 175 return; 176 } 177 178 //Subtracting spellcheckmodediv height 179 var spellCheckModeDiv = this._spellCheckModeDivId && document.getElementById(this._spellCheckModeDivId); 180 if (spellCheckModeDiv && spellCheckModeDiv.style.display !== "none") { 181 y = y - Dwt.getSize(spellCheckModeDiv).y; 182 } 183 184 if (isNaN(x) || x < 0 || isNaN(y) || y < 0) { 185 if (this.getVisible()) { 186 this._resetSizeAction = 187 setTimeout(ZmHtmlEditor.prototype._resetSize.bind(this), 100); 188 } 189 return; 190 } 191 192 Dwt.setSize(iframe, Math.max(0, x), Math.max(0, y)); 193 194 var body = editor.getBody(); 195 var bounds = 196 Dwt.insetBounds(Dwt.insetBounds({x: 0, y: 0, width: x, height: y}, 197 Dwt.getMargins(body)), 198 Dwt.getInsets(body)); 199 200 Dwt.setSize(body, Math.max(0, bounds.width), Math.max(0, bounds.height)); 201 }; 202 203 ZmHtmlEditor.prototype.focus = 204 function(editor) { 205 var currentObj = this, 206 bodyField; 207 208 if (currentObj._mode === Dwt.HTML) { 209 editor = editor || currentObj.getEditor(); 210 if (currentObj._editorInitialized && editor) { 211 editor.focus(); 212 currentObj.setFocusStatus(true); 213 editor.getWin().scrollTo(0,0); 214 } 215 } 216 else { 217 bodyField = currentObj.getContentField(); 218 if (bodyField) { 219 bodyField.focus(); 220 currentObj.setFocusStatus(true, true); 221 } 222 } 223 }; 224 225 /** 226 * @param {Boolean} keepModeDiv if <code>true</code>, _spellCheckModeDiv is not removed 227 */ 228 ZmHtmlEditor.prototype.getTextVersion = function (convertor, keepModeDiv) { 229 this.discardMisspelledWords(keepModeDiv); 230 return this._mode === Dwt.HTML 231 ? this._convertHtml2Text(convertor) 232 : this.getContentField().value; 233 }; 234 235 ZmHtmlEditor.prototype._focus = function() { 236 if (this._mode === Dwt.HTML && this.getEditor()) { 237 this.getEditor().focus(); 238 } 239 }; 240 241 /** 242 * Returns the content of the editor. 243 * 244 * @param {boolean} insertFontStyle if true, add surrounding DIV with font settings 245 * @param {boolean} onlyInnerContent if true, do not surround with HTML and BODY 246 */ 247 ZmHtmlEditor.prototype.getContent = 248 function(addDivContainer, onlyInnerContent) { 249 250 this.discardMisspelledWords(); 251 252 var field = this.getContentField(); 253 254 var content = ""; 255 if (this._mode == Dwt.HTML) { 256 var editor = this.getEditor(), 257 content1 = ""; 258 if (editor) { 259 content1 = editor.save({ format:"raw", set_dirty: false }); 260 } 261 else { 262 content1 = field.value || ""; 263 } 264 if (content1 && (/\S+/.test(AjxStringUtil.convertHtml2Text(content1)) || content1.match(/<img/i)) ) { 265 content = this._embedHtmlContent(content1, addDivContainer, onlyInnerContent, this._classCount); 266 } 267 } 268 else { 269 if (/\S+/.test(field.value)) { 270 content = field.value; 271 } 272 } 273 274 return content; 275 }; 276 277 ZmHtmlEditor.prototype._embedHtmlContent = 278 function(html, addDivContainer, onlyInnerContent, classCount) { 279 280 html = html || ""; 281 if (addDivContainer) { 282 if (classCount) { 283 var editor = this.getEditor(); 284 var document = editor.getDoc(); 285 var containerEl = document.getElementById(ZmHtmlEditor._containerDivId); 286 if (containerEl) { 287 // Leave the previous container in place and update its 288 // class (used for classCount) 289 containerEl.setAttribute("class", classCount.toString()); 290 // Set to zero, so an additional classCount is not added in the new container 291 classCount = 0; 292 } 293 } 294 html = ZmHtmlEditor._addDivContainer(html, classCount); 295 } 296 return onlyInnerContent ? html : [ "<html><body>", html, "</body></html>" ].join(""); 297 }; 298 ZmHtmlEditor._embedHtmlContent = ZmHtmlEditor.prototype._embedHtmlContent; 299 300 ZmHtmlEditor._addDivContainer = 301 function(html, classCount) { 302 return ZmHtmlEditor._getDivContainerPrefix(classCount) + html + ZmHtmlEditor._getDivContainerSuffix(); 303 }; 304 305 ZmHtmlEditor._getDivContainerPrefix = 306 function(classCount) { 307 var recordClassCount = !!classCount; 308 var a = [], i = 0; 309 a[i++] = '<div '; 310 if (recordClassCount) { 311 a[i++] = 'id="' + ZmHtmlEditor._containerDivId + '" '; 312 } 313 a[i++] = 'style="font-family: '; 314 a[i++] = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_FAMILY); 315 a[i++] = '; font-size: '; 316 a[i++] = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_SIZE); 317 a[i++] = '; color: '; 318 a[i++] = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_COLOR); 319 a[i++] = '"'; 320 if (appCtxt.get(ZmSetting.COMPOSE_INIT_DIRECTION) === ZmSetting.RTL) { 321 a[i++] = ' dir="' + ZmSetting.RTL + '"'; 322 } 323 // Cheat; Store the classCount (used for mapping excel classes to unique ids) in a class attribute. 324 // Otherwise, if stored in a non-standard attribute, it gets stripped by the server defanger. 325 if (recordClassCount) { 326 a[i++] = ' class=' + classCount.toString() + ' ' 327 } 328 a[i++] = ">"; 329 return a.join(""); 330 }; 331 332 ZmHtmlEditor._getDivContainerSuffix = 333 function() { 334 return "</div>"; 335 }; 336 337 /* 338 If editor is not initialized and mode is HTML, tinymce will automatically initialize the editor with the content in textarea 339 */ 340 ZmHtmlEditor.prototype.setContent = function (content) { 341 if (this._mode === Dwt.HTML && this._editorInitialized) { 342 var ed = this.getEditor(); 343 ed.setContent(content, {format:'raw'}); 344 this._setContentStyles(ed); 345 } else { 346 this.getContentField().value = content; 347 } 348 this._ignoreWords = {}; 349 }; 350 351 ZmHtmlEditor.prototype._setContentStyles = function(ed) { 352 var document = ed.getDoc(); 353 354 // First, get the number of classes already added via paste; Only exists if this was retrieved from the server 355 // (otherwise, use the in-memory this._classCount). This is used to create unique class names for styles 356 // imported on an Excel paste 357 var containerDiv = document.getElementById(ZmHtmlEditor._containerDivId); 358 if (containerDiv && containerDiv.hasAttribute("class")) { 359 // Cheated - stored classCount in class, since non-standard attributes will be stripped by the 360 // server html defanger. 361 this._classCount = parseInt(containerDiv.getAttribute("class")); 362 if (isNaN(this._classCount)) { 363 this._classCount = 0; 364 } 365 } 366 367 // Next, move all style nodes to be children of the body, otherwise when adding a style to the body, any subnode 368 // style nodes will be deleted! 369 var dom = ed.dom; 370 var body = document.body; 371 var styles = dom.select("style", body); 372 var parentNode; 373 for (var i = 0; i < styles.length; i++) { 374 parentNode = styles[i].parentNode; 375 if (parentNode.tagName.toLowerCase() != 'body') { 376 parentNode.removeChild(styles[i]); 377 body.insertBefore(styles[i], body.childNodes[0]); 378 } 379 } 380 } 381 382 ZmHtmlEditor.prototype.reEnableDesignMode = 383 function() { 384 // tinyMCE doesn't need to handle this 385 }; 386 387 ZmHtmlEditor.prototype.getMode = 388 function() { 389 return this._mode; 390 }; 391 392 ZmHtmlEditor.prototype.isHtmlModeInited = 393 function() { 394 return Boolean(this.getEditor()); 395 }; 396 397 ZmHtmlEditor.prototype._convertHtml2Text = function (convertor) { 398 var editor = this.getEditor(), 399 body; 400 if (editor) { 401 body = editor.getBody(); 402 if (body) { 403 return (AjxStringUtil.convertHtml2Text(body, convertor, true)); 404 } 405 } 406 return ""; 407 }; 408 409 ZmHtmlEditor.prototype.moveCaretToTop = 410 function(offset) { 411 if (this._mode == Dwt.TEXT) { 412 var control = this.getContentField(); 413 control.scrollTop = 0; 414 if (control.createTextRange) { // IE 415 var range = control.createTextRange(); 416 if (offset) { 417 range.move('character', offset); 418 } 419 else { 420 range.collapse(true); 421 } 422 range.select(); 423 } else if (control.setSelectionRange) { // FF 424 offset = offset || 0; 425 //If display is none firefox will throw the following error 426 //Error: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMHTMLTextAreaElement.setSelectionRange] 427 //checking offsetHeight to check whether it is rendered or not 428 if (control.offsetHeight) { 429 control.setSelectionRange(offset, offset); 430 } 431 } 432 } else { 433 this._moveCaretToTopHtml(true, offset); 434 } 435 }; 436 437 ZmHtmlEditor.prototype._moveCaretToTopHtml = 438 function(tryOnTimer, offset) { 439 var editor = this.getEditor(); 440 var body = editor && editor.getDoc().body; 441 var success = false; 442 if (AjxEnv.isIE) { 443 if (body) { 444 var range = body.createTextRange(); 445 if (offset) { 446 range.move('character', offset); 447 } else { 448 range.collapse(true); 449 } 450 success = true; 451 } 452 } else { 453 var selection = editor && editor.selection ? editor.selection.getSel() : ""; 454 if (selection) { 455 if (offset) { // if we get an offset, use it as character count into text node 456 var target = body.firstChild; 457 while (target) { 458 if (offset === 0) { 459 selection.collapse(target, offset); 460 break; 461 } 462 if (target.nodeName === "#text") { 463 var textLength = target.length; 464 if (offset > textLength) { 465 offset = offset - textLength; 466 } else { 467 selection.collapse(target, offset); 468 break; 469 } 470 } else if (target.nodeName === "BR") {//text.length is also including \n count. so if there is br reduce offset by 1 471 offset = offset - 1; 472 } 473 target = target.nextSibling; 474 } 475 } 476 else { 477 selection.collapse(body, 0); 478 } 479 success = true; 480 } 481 } 482 483 if (success) { 484 editor.focus(); 485 } else if (tryOnTimer) { 486 if (editor) { 487 var action = new AjxTimedAction(this, this._moveCaretToTopHtml); 488 AjxTimedAction.scheduleAction(action, ZmHtmlEditor._INITDELAY + 1); 489 } else { 490 var cb = ZmHtmlEditor.prototype._moveCaretToTopHtml; 491 this._initCallbacks.push(cb.bind(this, tryOnTimer, offset)); 492 } 493 } 494 }; 495 496 ZmHtmlEditor.prototype.hasFocus = 497 function() { 498 return Boolean(this._hasFocus[this._mode]); 499 }; 500 501 /*ZmSignature editor contains getIframeDoc method dont want to break the existing code*/ 502 ZmHtmlEditor.prototype._getIframeDoc = ZmHtmlEditor.prototype.getIframeDoc = 503 function() { 504 var editor = this.getEditor(); 505 return editor ? editor.getDoc() : null; 506 }; 507 508 ZmHtmlEditor.prototype._getIframeWin = 509 function() { 510 var editor = this.getEditor(); 511 return editor ? editor.getWin() : null; 512 }; 513 514 ZmHtmlEditor.prototype.clear = 515 function() { 516 var editor = this.getEditor(); 517 if (editor && this._editorInitialized) { 518 editor.undoManager && editor.undoManager.clear(); 519 this.clearDirty(); 520 } 521 var textField = this.getContentField(); 522 if (!textField) { 523 return; 524 } 525 526 //If HTML editor is not initialized and the current mode is HTML, then HTML editor is currently getting initialized. Text area should not be replaced at this time, as this will make the TinyMCE JavaScript reference empty for the text area. 527 if (!this.isHtmlModeInited() && this.getMode() === Dwt.HTML) { 528 return; 529 } 530 var textEl = textField.cloneNode(false); 531 textField.parentNode.replaceChild(textEl, textField);//To clear undo/redo queue of textarea 532 //cloning and replacing node will remove event handlers and hence adding it once again 533 Dwt.setHandler(textEl, DwtEvent.ONFOCUS, this._onTextareaFocus.bind(this, true, true)); 534 Dwt.setHandler(textEl, DwtEvent.ONBLUR, this.setFocusStatus.bind(this, false, true)); 535 Dwt.setHandler(textEl, DwtEvent.ONKEYDOWN, this._handleTextareaKeyEvent.bind(this)); 536 if (editor) { 537 // TinyMCE internally stored textarea element reference as targetElm which is lost after the above operation. Once targetElm is undefined TinyMCE will try to get the element using it's id. 538 editor.targetElm = null; 539 } 540 }; 541 542 ZmHtmlEditor.prototype.initTinyMCEEditor = function(params) { 543 544 var htmlEl = this.getHtmlElement(); 545 //textarea on which html editor is constructed 546 var id = this._bodyTextAreaId; 547 var textEl = document.createElement("textarea"); 548 textEl.setAttribute("id", id); 549 textEl.setAttribute("name", id); 550 textEl.setAttribute("aria-label", ZmMsg.composeBody); 551 if( appCtxt.get(ZmSetting.COMPOSE_INIT_DIRECTION) === ZmSetting.RTL ){ 552 textEl.setAttribute("dir", ZmSetting.RTL); 553 } 554 textEl.className = "ZmHtmlEditorTextArea"; 555 if ( params.content !== null ) { 556 textEl.value = params.content; 557 } 558 if (this._mode === Dwt.HTML) { 559 //If the mode is HTML set the text area display as none. After editor is rendered with the content, TinyMCE editor's show method will be called for displaying the editor on the post render event. 560 Dwt.setVisible(textEl, false); 561 } 562 htmlEl.appendChild(textEl); 563 this._textAreaId = id; 564 565 Dwt.setHandler(textEl, DwtEvent.ONFOCUS, this._onTextareaFocus.bind(this, true, true)); 566 Dwt.setHandler(textEl, DwtEvent.ONBLUR, this.setFocusStatus.bind(this, false, true)); 567 Dwt.setHandler(textEl, DwtEvent.ONKEYDOWN, this._handleTextareaKeyEvent.bind(this)); 568 569 if (!window.tinyMCE) { 570 window.tinyMCEPreInit = {}; 571 window.tinyMCEPreInit.suffix = ''; 572 window.tinyMCEPreInit.base = appContextPath + ZmHtmlEditor.TINY_MCE_PATH; // SET PATH TO TINYMCE HERE 573 // Tell TinyMCE that the page has already been loaded 574 window.tinyMCE_GZ = {}; 575 window.tinyMCE_GZ.loaded = true; 576 577 var callback = this.initEditorManager.bind(this, id, params.autoFocus); 578 AjxDispatcher.require(["TinyMCE"], true, callback); 579 } else { 580 this.initEditorManager(id, params.autoFocus); 581 } 582 }; 583 584 ZmHtmlEditor.prototype.addOnContentInitializedListener = 585 function(callback) { 586 this._onContentInitializeCallbacks.push(callback); 587 }; 588 589 ZmHtmlEditor.prototype.clearOnContentInitializedListeners = 590 function() { 591 this._onContentInitializeCallback = null; 592 }; 593 594 ZmHtmlEditor.prototype._handleEditorKeyEvent = function(ev) { 595 596 var ed = this.getEditor(), 597 retVal = true; 598 599 if (DwtKeyboardMgr.isPossibleInputShortcut(ev) || (ev.keyCode === DwtKeyEvent.KEY_TAB && (ev.shiftKey || !appCtxt.get(ZmSetting.TAB_IN_EDITOR)))) { 600 // pass to keyboard mgr for kb nav 601 retVal = DwtKeyboardMgr.__keyDownHdlr(ev); 602 } 603 else if (DwtKeyEvent.IS_RETURN[ev.keyCode]) { // enter key 604 var parent, 605 selection, 606 startContainer, 607 editorDom, 608 uniqueId, 609 blockquote, 610 nextSibling, 611 divElement, 612 splitElement; 613 614 if (ev.shiftKey) { 615 return; 616 } 617 618 selection = ed.selection; 619 parent = startContainer = selection.getRng(true).startContainer; 620 if (!startContainer) { 621 return; 622 } 623 624 editorDom = ed.dom; 625 //Gets all parent block elements 626 blockquote = editorDom.getParents(startContainer, "blockquote", ed.getBody()); 627 if (!blockquote) { 628 return; 629 } 630 631 blockquote = blockquote.pop();//Gets the last blockquote element 632 if (!blockquote || !blockquote.style.borderLeft) {//Checking blockquote left border for verifying it is reply blockquote 633 return; 634 } 635 636 uniqueId = editorDom.uniqueId(); 637 ed.undoManager.add(); 638 try { 639 selection.setContent("<div id='" + uniqueId + "'><br></div>"); 640 } 641 catch (e) { 642 return; 643 } 644 645 divElement = ed.getDoc().getElementById(uniqueId); 646 if (divElement) { 647 divElement.removeAttribute("id"); 648 } 649 else { 650 return; 651 } 652 653 nextSibling = divElement.nextSibling; 654 if (nextSibling && nextSibling.nodeName === "BR") { 655 nextSibling.parentNode.removeChild(nextSibling); 656 } 657 658 try { 659 splitElement = editorDom.split(blockquote, divElement); 660 if (splitElement) { 661 selection.select(splitElement); 662 selection.collapse(true); 663 ev.preventDefault(); 664 } 665 } 666 catch (e) { 667 } 668 } 669 else if (ZmHtmlEditor.isEditorTab(ev)) { 670 ed.execCommand('mceInsertContent', false, ' '); 671 DwtUiEvent.setBehaviour(ev, true, false); 672 return false; 673 } 674 675 676 if (window.DwtIdleTimer) { 677 DwtIdleTimer.resetIdle(); 678 } 679 680 if (window.onkeydown) { 681 window.onkeydown.call(this); 682 } 683 684 return retVal; 685 }; 686 687 // Text mode key event handler 688 ZmHtmlEditor.prototype._handleTextareaKeyEvent = function(ev) { 689 690 if (ZmHtmlEditor.isEditorTab(ev)) { 691 Dwt.insertText(this.getContentField(), '\t'); 692 DwtUiEvent.setBehaviour(ev, true, false); 693 return false; 694 } 695 return true; 696 }; 697 698 //Notifies mousedown event in tinymce editor to ZCS 699 ZmHtmlEditor.prototype._handleEditorMouseDownEvent = 700 function(ev) { 701 DwtOutsideMouseEventMgr.forwardEvent(ev); 702 }; 703 704 ZmHtmlEditor.prototype.onLoadContent = 705 function(ev) { 706 if (this._onContentInitializeCallbacks) { 707 AjxDebug.println(AjxDebug.REPLY, "ZmHtmlEditor::onLoadContent - run callbacks"); 708 AjxUtil.foreach(this._onContentInitializeCallbacks, 709 function(fn) { fn.run() }); 710 } 711 }; 712 713 ZmHtmlEditor.prototype.setFocusStatus = 714 function(hasFocus, isTextModeFocus) { 715 var mode = isTextModeFocus ? Dwt.TEXT : Dwt.HTML; 716 this._hasFocus[mode] = hasFocus; 717 718 Dwt.condClass(this.getHtmlElement(), hasFocus, DwtControl.FOCUSED); 719 720 if (!isTextModeFocus) { 721 Dwt.condClass(this.getEditor().getBody(), hasFocus, 722 'mce-active-editor', 'mce-inactive-editor'); 723 } 724 }; 725 726 ZmHtmlEditor.prototype._onTextareaFocus = function() { 727 728 this.setFocusStatus(true, true); 729 appCtxt.getKeyboardMgr().updateFocus(this.getContentField()); 730 }; 731 732 ZmHtmlEditor.prototype.initEditorManager = 733 function(id, autoFocus) { 734 735 var obj = this; 736 737 if (!window.tinyMCE) {//some problem in loading TinyMCE files 738 return; 739 } 740 741 var urlParts = AjxStringUtil.parseURL(location.href); 742 743 //important: tinymce doesn't handle url parsing well when loaded from REST URL - override baseURL/baseURI to fix this 744 tinymce.baseURL = appContextPath + ZmHtmlEditor.TINY_MCE_PATH; 745 746 if (tinymce.EditorManager) { 747 tinymce.EditorManager.baseURI = new tinymce.util.URI(urlParts.protocol + "://" + urlParts.authority + tinymce.baseURL); 748 } 749 750 if (tinymce.dom) { 751 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); 752 } 753 754 if (tinymce.dom && tinymce.dom.Event) { 755 tinymce.dom.Event.domLoaded = true; 756 } 757 758 var toolbarbuttons = [ 759 'fontselect fontsizeselect formatselect |', 760 'bold italic underline strikethrough removeformat |', 761 'forecolor backcolor |', 762 'outdent indent bullist numlist blockquote |', 763 'alignleft aligncenter alignright alignjustify |', 764 this._attachmentCallback ? 'zimage' : 'image', 765 'link zemoticons charmap hr table |', 766 appCtxt.get(ZmSetting.SHOW_COMPOSE_DIRECTION_BUTTONS) ? 'ltr rtl |' : '', 767 'undo redo |', 768 'pastetext code' 769 ]; 770 771 // NB: contextmenu plugin deliberately omitted; it's confusing 772 var plugins = [ 773 "zemoticons", 774 "table", "directionality", "textcolor", "lists", "advlist", 775 "link", "hr", "charmap", "code", "image" 776 ]; 777 778 if (this._attachmentCallback) { 779 tinymce.PluginManager.add('zimage', function(editor) { 780 editor.addButton('zimage', { 781 icon: 'image', 782 tooltip: ZmMsg.insertImage, 783 onclick: obj._attachmentCallback, 784 stateSelector: 'img:not([data-mce-object])' 785 }); 786 }); 787 788 plugins.push('zimage'); 789 } 790 791 var fonts = []; 792 var KEYS = [ "fontFamilyIntl", "fontFamilyBase" ]; 793 var i, j, key, value, name; 794 for (j = 0; j < KEYS.length; j++) { 795 for (i = 1; value = AjxMsg[KEYS[j]+i+".css"]; i++) { 796 if (value.match(/^#+$/)) break; 797 value = value.replace(/,\s/g,","); 798 name = AjxMsg[KEYS[j]+i+".display"]; 799 fonts.push(name+"="+value); 800 } 801 } 802 803 if (!autoFocus) { 804 // if !true, Set to false in case undefined 805 autoFocus = false; 806 } 807 var tinyMCEInitObj = { 808 // General options 809 mode : (this._mode == Dwt.HTML)? "exact" : "none", 810 theme: 'modern', 811 auto_focus: autoFocus, 812 elements: id, 813 plugins : plugins.join(' '), 814 toolbar: toolbarbuttons.join(' '), 815 toolbar_items_size: 'small', 816 statusbar: false, 817 menubar: false, 818 ie7_compat: false, 819 object_resizing : true, 820 font_formats : fonts.join(";"), 821 fontsize_formats : AjxMsg.fontSizes || '', 822 convert_urls : false, 823 verify_html : false, 824 browser_spellcheck : true, 825 content_css : appContextPath + '/css/tinymce-content.css?v=' + cacheKillerVersion, 826 dialog_type : "modal", 827 forced_root_block : "div", 828 width: "100%", 829 height: "auto", 830 visual: false, 831 language: tinyMCE.getlanguage(appCtxt.get(ZmSetting.LOCALE_NAME)), 832 directionality : appCtxt.get(ZmSetting.COMPOSE_INIT_DIRECTION), 833 paste_retain_style_properties : "all", 834 paste_data_images: false, 835 paste_remove_styles_if_webkit : false, 836 table_default_attributes: { cellpadding: '3px', border: '1px' }, 837 table_default_styles: { width: '90%', tableLayout: 'fixed' }, 838 setup : function(ed) { 839 ed.on('LoadContent', obj.onLoadContent.bind(obj)); 840 ed.on('PostRender', obj.onPostRender.bind(obj)); 841 ed.on('init', obj.onInit.bind(obj)); 842 ed.on('keydown', obj._handleEditorKeyEvent.bind(obj)); 843 ed.on('MouseDown', obj._handleEditorMouseDownEvent.bind(obj)); 844 ed.on('paste', obj.onPaste.bind(obj)); 845 ed.on('PastePostProcess', obj.pastePostProcess.bind(obj)); 846 ed.on('BeforeExecCommand', obj.onBeforeExecCommand.bind(obj)); 847 848 ed.on('contextmenu', obj._handleEditorEvent.bind(obj)); 849 ed.on('mouseup', obj._handleEditorEvent.bind(obj)); 850 } 851 }; 852 853 tinyMCE.init(tinyMCEInitObj); 854 this._editor = this.getEditor(); 855 }; 856 857 ZmHtmlEditor.prototype.onPaste = function(ev) { 858 if (!this._pasteCallback) 859 return; 860 861 var items = ((ev.clipboardData && 862 (ev.clipboardData.items || ev.clipboardData.files)) || 863 (window.clipboardData && clipboardData.files)), 864 item = items && items[0], 865 file, name, type, 866 view; 867 868 if (item && item.getAsFile) { 869 file = item.getAsFile(); 870 name = file && file.fileName; 871 type = file && file.type; 872 } else if (item && item.type) { 873 file = item; 874 name = file.name; 875 type = file.type; 876 } 877 878 if (file) { 879 ev.stopPropagation(); 880 ev.preventDefault(); 881 var headers = { 882 "Cache-Control": "no-cache", 883 "X-Requested-With": "XMLHttpRequest", 884 "Content-Type": type, 885 //For paste from clipboard filename is undefined 886 "Content-Disposition": 'attachment; filename="' + (name ? AjxUtil.convertToEntities(name) : ev.timeStamp || new Date().getTime()) + '"' 887 }; 888 var url = (appCtxt.get(ZmSetting.CSFE_ATTACHMENT_UPLOAD_URI) + 889 "?fmt=extended,raw"); 890 891 var fn = AjxRpc.invoke.bind(AjxRpc, file, url, headers, 892 this._handlePasteUpload.bind(this), 893 AjxRpcRequest.HTTP_POST); 894 895 // IE11 appears to disallow AJAX requests within the event handler 896 if (AjxEnv.isTrident) { 897 setTimeout(fn, 0); 898 } else { 899 fn(); 900 } 901 } else { 902 var clipboardContent = this.getClipboardContent(ev); 903 if (this.hasContentType(clipboardContent, 'text/html')) { 904 var content = clipboardContent['text/html']; 905 if (content) { 906 this.pasteHtml(content); 907 ev.stopPropagation(); 908 ev.preventDefault(); 909 } 910 } 911 } 912 }; 913 914 ZmHtmlEditor.prototype.getDataTransferItems = function(dataTransfer) { 915 var data = {}; 916 917 if (dataTransfer) { 918 // Use old WebKit/IE API 919 if (dataTransfer.getData) { 920 var legacyText = dataTransfer.getData('Text'); 921 if (legacyText && legacyText.length > 0) { 922 data['text/plain'] = legacyText; 923 } 924 } 925 926 if (dataTransfer.types) { 927 for (var i = 0; i < dataTransfer.types.length; i++) { 928 var contentType = dataTransfer.types[i]; 929 data[contentType] = dataTransfer.getData(contentType); 930 } 931 } 932 } 933 934 return data; 935 }; 936 937 938 ZmHtmlEditor.prototype.hasContentType = function(clipboardContent, mimeType) { 939 return mimeType in clipboardContent && clipboardContent[mimeType].length > 0; 940 }; 941 942 ZmHtmlEditor.prototype.getClipboardContent = function(clipboardEvent) { 943 return this.getDataTransferItems(clipboardEvent.clipboardData || this.getEditor().getDoc().dataTransfer); 944 }; 945 946 947 ZmHtmlEditor.prototype.pasteHtml = function(html) { 948 var ed = this.getEditor(); 949 var args, dom = ed.dom; 950 951 var document = ed.getDoc(); 952 var numOriginalStyleSheets = document.styleSheets ? document.styleSheets.length : 0; 953 var styleSheets = document.styleSheets; 954 955 // We need to attach the element to the DOM so Sizzle selectors work on the contents 956 var tempBody = dom.add(ed.getBody(), 'div', {style: 'display:none'}, html); 957 args = ed.fire('PastePostProcess', {node: tempBody}); 958 html = args.node.innerHTML; 959 960 var styleNodes = []; 961 if (!args.isDefaultPrevented()) { 962 var re; 963 for (var i = numOriginalStyleSheets; i < styleSheets.length; i++) { 964 // Access and update the stylesheet class names, to insure no collisions 965 var stylesheet = styleSheets[i]; 966 var updates = this._getPastedClassUpdates(stylesheet); 967 var styleHtml = stylesheet.ownerNode.innerHTML; 968 for (var selectorText in updates) { 969 // Replace the non-unique Excel class names with unique new ones in style html and pasted content html. 970 var newSelectorText = updates[selectorText]; 971 re = new RegExp(selectorText.substring(1), 'g'); 972 html = html.replace(re, newSelectorText.substring(1)); 973 styleHtml = styleHtml.replace(selectorText, newSelectorText); 974 } 975 // Excel .5pt line doesn't display in Chrome - use a 1pt line. Somewhat fragile (Assuming width is the 976 // first attribute for border, following the ':'), but need to do so that we only replace a standalone .5pt 977 re = new RegExp(":.5pt", 'g'); 978 styleHtml = styleHtml.replace(re, ":1pt"); 979 // Microsoft special, just use 'black' 980 re = new RegExp("windowtext", 'g'); 981 styleHtml = styleHtml.replace(re, "black"); 982 983 // Create a new style node and record it; it will be added below to the body with the new content 984 var styleNode = document.createElement('style'); 985 styleNode.type = "text/css"; 986 var scoped = document.createAttribute("scoped"); 987 styleNode.setAttributeNode(scoped); 988 styleNode.innerHTML = styleHtml; 989 styleNodes.push(styleNode); 990 } 991 } 992 993 dom.remove(tempBody); 994 995 if (!args.isDefaultPrevented()) { 996 var body = document.body; 997 for (var i = 0; i < styleNodes.length; i++) { 998 // Insert the styles into the body. Modern browsers support this (even though its not strictly valid), and 999 // the 'scoped' attribute added above means that future browsers should treat it as valid. 1000 body.insertBefore(styleNodes[i], body.childNodes[0]); 1001 } 1002 ed.insertContent(html, {merge: ed.settings.paste_merge_formats !== false}); 1003 } 1004 }; 1005 1006 ZmHtmlEditor.prototype._getPastedClassUpdates = function(styleSheet) { 1007 var cssRules = styleSheet.cssRules; 1008 var updates = {}; 1009 if (cssRules) { 1010 for (var i = 0; i < cssRules.length; i++) { 1011 var selectorText = cssRules[i].selectorText; 1012 // Excel class definitions (for now) start with ".xl", but this tries to be a little less specific (and fragile). 1013 // Convert the Excel class names (which may be duplicated with each paste) to unique class names, so that 1014 // later paste formatting doesn't step on previous formatting. 1015 if (selectorText && selectorText.indexOf(".") == 0) { 1016 // Create a new unique class name that will be used instead 1017 var newSelectorText = ".zimbra" + (++this._classCount).toString(); 1018 updates[selectorText] = newSelectorText; 1019 } 1020 } 1021 } 1022 // Return a map of { oldClassName : newClassName } 1023 return updates; 1024 } 1025 1026 ZmHtmlEditor.prototype._handlePasteUpload = function(r) { 1027 if (r && r.success) { 1028 var resp = eval("["+r.text+"]"); 1029 if(resp.length === 3) { 1030 resp[2].clipboardPaste = true; 1031 } 1032 this._pasteCallback(resp); 1033 } 1034 }; 1035 1036 1037 ZmHtmlEditor.prototype.onPostRender = function(ev) { 1038 var ed = this.getEditor(); 1039 1040 ed.dom.setStyles(ed.getBody(), {"font-family" : appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_FAMILY), 1041 "font-size" : appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_SIZE), 1042 "color" : appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_COLOR) 1043 }); 1044 //Shows the editor and hides any textarea/div that the editor is supposed to replace. 1045 ed.show(); 1046 this._resetSize(); 1047 }; 1048 1049 ZmHtmlEditor.prototype.onInit = function(ev) { 1050 1051 var ed = this.getEditor(); 1052 var obj = this, 1053 tinymceEvent = tinymce.dom.Event, 1054 doc = ed.getDoc(), 1055 win = ed.getWin(), 1056 view = obj.parent; 1057 1058 obj.setFocusStatus(false); 1059 1060 ed.on('focus', function(e) { 1061 DBG.println(AjxDebug.FOCUS, "EDITOR got focus"); 1062 appCtxt.getKeyboardMgr().updateFocus(obj._getIframeDoc().body); 1063 obj.setFocusStatus(true); 1064 }); 1065 ed.on('blur', function(e) { 1066 obj.setFocusStatus(false); 1067 }); 1068 // Sets up the a range for the current ins point or selection. This is IE only because the iFrame can 1069 // easily lose focus (e.g. by clicking on a button in the toolbar) and we need to be able to get back 1070 // to the correct insertion point/selection. 1071 // Here we are registering this dedicated event to store the bookmark which will fire when focus moves outside the editor 1072 if(AjxEnv.isIE){ 1073 tinymceEvent.bind(doc, 'beforedeactivate', function(e) { 1074 if(ed.windowManager){ 1075 ed.windowManager.bookmark = ed.selection.getBookmark(1); 1076 } 1077 }); 1078 } 1079 1080 // must be assigned on init, to ensure that our handlers are called after 1081 // in TinyMCE's in 'FormatControls.js'. 1082 ed.on('nodeChange', obj.onNodeChange.bind(obj)); 1083 1084 ed.on('open', ZmHtmlEditor.onPopupOpen); 1085 if (view && view.toString() === "ZmComposeView" && ZmDragAndDrop.isSupported()) { 1086 var dnd = view._dnd; 1087 tinymceEvent.bind(doc, 'dragenter', this._onDragEnter.bind(this)); 1088 tinymceEvent.bind(doc, 'dragleave', this._onDragLeave.bind(this)); 1089 tinymceEvent.bind(doc, 'dragover', this._onDragOver.bind(this, dnd)); 1090 tinymceEvent.bind(doc, 'drop', this._onDrop.bind(this, dnd)); 1091 } 1092 1093 this._overrideTinyMCEMethods(); 1094 1095 obj._editorInitialized = true; 1096 1097 // Access the content stored in the textArea (if any) 1098 var contentField = this.getContentField(); 1099 var content = contentField.value; 1100 contentField.value = ""; 1101 // Use our setContent to set up the content using the 'raw' format, which preserves styling 1102 this.setContent(content); 1103 1104 this._resetSize(); 1105 this._setupTabGroup(); 1106 1107 var iframe = Dwt.getElement(this._iFrameId); 1108 if (iframe) { 1109 Dwt.addClass(iframe, 'ZmHtmlEditorIFrame'); 1110 iframe.setAttribute('title', ZmMsg.htmlEditorTitle); 1111 var body = this._getIframeDoc().body; 1112 if (body) { 1113 body.setAttribute('aria-label', ZmMsg.composeBody); 1114 } 1115 } 1116 1117 AjxUtil.foreach(this._initCallbacks, function(fn) { fn.run() }); 1118 }; 1119 1120 ZmHtmlEditor.prototype._onFocus = function() { 1121 var editor = this.getEditor(); 1122 1123 if (this._mode === Dwt.HTML && editor) { 1124 editor.fire('focus', {focusedEditor: editor}); 1125 } 1126 }; 1127 1128 ZmHtmlEditor.prototype._onBlur = function() { 1129 var editor = this.getEditor(); 1130 1131 if (this._mode === Dwt.HTML && editor) { 1132 editor.fire('blur', {focusedEditor: null}); 1133 } 1134 }; 1135 1136 1137 ZmHtmlEditor.prototype.__getEditorControl = function(type, tooltip) { 1138 // This method provides a naive emulation of the control manager offered in 1139 // TinyMCE 3.x. We assume that there's only one control of a given type 1140 // with a given tooltip in the entire TinyMCE control hierarchy. Hopefully, 1141 // this heuristic won't prove too fragile. 1142 var ed = this.getEditor(); 1143 1144 function finditem(item) { 1145 // the tooltip in settings appears constant and unlocalized 1146 if (item.type === type && item.settings.tooltip === tooltip) 1147 return item; 1148 1149 if (typeof item.items === 'function') { 1150 var items = item.items(); 1151 1152 for (var i = 0; i < items.length; i++) { 1153 var r = finditem(items[i]); 1154 if (r) 1155 return r; 1156 } 1157 } 1158 1159 if (typeof item.menu === 'object') { 1160 return finditem(item.menu); 1161 } 1162 }; 1163 1164 return ed ? finditem(ed.theme.panel) : null; 1165 }; 1166 1167 ZmHtmlEditor.prototype.onNodeChange = function(event) { 1168 // Firefox fires NodeChange events whether the editor is visible or not 1169 if (this._mode !== Dwt.HTML) { 1170 return; 1171 } 1172 1173 // update the font size box -- TinyMCE only checks for it on SPANs 1174 var fontsizebtn = this.__getEditorControl('listbox', 'Font Sizes'); 1175 var found = false; 1176 1177 var normalize = function(v) { 1178 return Math.round(DwtCssStyle.asPixelCount(v)); 1179 }; 1180 1181 for (var i = 0; !found && i < event.parents.length; i++) { 1182 var element = event.parents[i]; 1183 if (element.nodeType === Node.ELEMENT_NODE) { 1184 var fontsize = normalize(DwtCssStyle.getProperty(element, 'font-size')); 1185 if (fontsize !== -1) { 1186 for (var j = 0; !found && j < fontsizebtn._values.length; j++) { 1187 var value = fontsizebtn._values[j].value; 1188 1189 if (normalize(value) === fontsize) { 1190 fontsizebtn.value(value); 1191 found = true; 1192 } 1193 } 1194 } 1195 } 1196 } 1197 1198 // update the font family box -- TinyMCE only checks for it on SPANs 1199 var fontfamilybtn = this.__getEditorControl('listbox', 'Font Family'); 1200 var found = false; 1201 1202 var normalize = function(v) { 1203 return v.replace(/,\s+/g, ',').replace(/[\'\"]/g, ''); 1204 }; 1205 1206 for (var i = 0; !found && i < event.parents.length; i++) { 1207 var element = event.parents[i]; 1208 if (element.nodeType === Node.ELEMENT_NODE) { 1209 var fontfamily = normalize(DwtCssStyle.getProperty(element, 'font-family')); 1210 for (var j = 0; !found && j < fontfamilybtn._values.length; j++) { 1211 var value = fontfamilybtn._values[j].value; 1212 1213 if (normalize(value) === fontfamily) { 1214 fontfamilybtn.value(value); 1215 found = true; 1216 } 1217 } 1218 } 1219 } 1220 }; 1221 1222 1223 /* 1224 ** TinyMCE will fire onBeforeExecCommand before executing all commands 1225 */ 1226 ZmHtmlEditor.prototype.onBeforeExecCommand = function(ev) { 1227 if (ev.command === "mceImage") { 1228 this.onBeforeInsertImage(ev); 1229 } 1230 else if (ev.command === "mceRepaint") { //img src modified 1231 this.onBeforeRepaint(ev); 1232 } 1233 }; 1234 1235 ZmHtmlEditor.prototype.onBeforeInsertImage = function(ev) { 1236 var element = ev.target.selection.getNode(); 1237 if (element && element.nodeName === "IMG") { 1238 element.setAttribute("data-mce-src", element.src); 1239 element.setAttribute("data-mce-zsrc", element.src);//To find out whether src is modified or not set a dummy attribute 1240 } 1241 }; 1242 1243 ZmHtmlEditor.prototype.onBeforeRepaint = function(ev) { 1244 var element = ev.target.selection.getNode(); 1245 if (element && element.nodeName === "IMG") { 1246 if (element.src !== element.getAttribute("data-mce-zsrc")) { 1247 element.removeAttribute("dfsrc"); 1248 } 1249 element.removeAttribute("data-mce-zsrc"); 1250 } 1251 }; 1252 1253 ZmHtmlEditor.prototype._onDragEnter = function() { 1254 Dwt.addClass(Dwt.getElement(this._iFrameId), "DropTarget"); 1255 }; 1256 1257 ZmHtmlEditor.prototype._onDragLeave = function() { 1258 Dwt.delClass(Dwt.getElement(this._iFrameId), "DropTarget"); 1259 }; 1260 1261 ZmHtmlEditor.prototype._onDragOver = function(dnd, ev) { 1262 dnd._onDragOver(ev); 1263 }; 1264 1265 ZmHtmlEditor.prototype._onDrop = function(dnd, ev) { 1266 dnd._onDrop(ev, true); 1267 Dwt.delClass(Dwt.getElement(this._iFrameId), "DropTarget"); 1268 }; 1269 1270 ZmHtmlEditor.prototype.setMode = function (mode, convert, convertor) { 1271 1272 this.discardMisspelledWords(); 1273 if (mode === this._mode || (mode !== Dwt.HTML && mode !== Dwt.TEXT)) { 1274 return; 1275 } 1276 this._mode = mode; 1277 var textarea = this.getContentField(); 1278 if (mode === Dwt.HTML) { 1279 if (convert) { 1280 textarea.value = AjxStringUtil.convertToHtml(textarea.value, true); 1281 } 1282 if (this._editorInitialized) { 1283 // tinymce will automatically toggle the editor and set the corresponding content. 1284 tinyMCE.execCommand('mceToggleEditor', false, this._bodyTextAreaId); 1285 } 1286 else { 1287 //switching from plain text to html using tinymces mceToggleEditor method is always 1288 // using the last editor creation setting. Due to this current ZmHtmlEditor object 1289 // always point to last ZmHtmlEditor object. Hence initializing the tinymce editor 1290 // again for the first time when mode is switched from plain text to html. 1291 this.initEditorManager(this._bodyTextAreaId); 1292 } 1293 } else { 1294 if (convert) { 1295 var content; 1296 if (this._editorInitialized) { 1297 content = this._convertHtml2Text(convertor); 1298 } 1299 else { 1300 content = AjxStringUtil.convertHtml2Text(textarea.value); 1301 } 1302 } 1303 if (this._editorInitialized) { 1304 //tinymce will automatically toggles the editor and sets the corresponding content. 1305 tinyMCE.execCommand('mceToggleEditor', false, this._bodyTextAreaId); 1306 } 1307 if (convert) { 1308 //tinymce will set html content directly in textarea. Resetting the content after removing the html tags. 1309 this.setContent(content); 1310 } 1311 1312 Dwt.setVisible(textarea, true); 1313 } 1314 1315 textarea = this.getContentField(); 1316 textarea.setAttribute('aria-hidden', !Dwt.getVisible(textarea)); 1317 1318 this._setupTabGroup(); 1319 this._resetSize(); 1320 }; 1321 1322 ZmHtmlEditor.prototype.getContentField = 1323 function() { 1324 return document.getElementById(this._bodyTextAreaId); 1325 }; 1326 1327 ZmHtmlEditor.prototype.insertImage = 1328 function(src, dontExecCommand, width, height, dfsrc) { 1329 // We can have a situation where: 1330 // Paste plugin does a createPasteBin, creating a marker element that it uses 1331 // We upload a pasted image. 1332 // The upload completes, and we do a SaveDraft. It calls insertImage. 1333 // A timeout function from the plugin executes before or after insertImage, and calls removePasteBin. 1334 // 1335 // InsertImage executes. If the pasteBin has not been removed when we try to insert the image, it interferes with 1336 // tinyMCE insertion. No image is inserted in the editor body, and we end up with an attachment 1337 // bubble instead. 1338 var pasteBinClone; 1339 var ed = this.getEditor(); 1340 1341 // *** Begin code copied from Paste Plugin Clipboard.js, removePasteBin 1342 while ((pasteBinClone = ed.dom.get('mcepastebin'))) { 1343 ed.dom.remove(pasteBinClone); 1344 ed.dom.unbind(pasteBinClone); 1345 } 1346 // *** End copied code from removePasteBin 1347 1348 var html = []; 1349 var idx= 0 ; 1350 1351 html[idx++] = "<img"; 1352 html[idx++] = " src='"; 1353 html[idx++] = src; 1354 html[idx++] = "'"; 1355 1356 if ( dfsrc != null) { 1357 html[idx++] = " dfsrc='"; 1358 html[idx++] = dfsrc; 1359 html[idx++] = "'"; 1360 } 1361 if (width != null) { 1362 html[idx++] = " width='" + width + "'"; 1363 } 1364 if (height != null) { 1365 html[idx++] = " height='" + height + "'"; 1366 } 1367 html[idx++] = ">"; 1368 1369 1370 ed.focus(); 1371 1372 //tinymce modifies the source when using mceInsertContent 1373 //ed.execCommand('mceInsertContent', false, html.join(""), {skip_undo : 1}); 1374 ed.execCommand('mceInsertRawHTML', false, html.join(""), {skip_undo : 1}); 1375 }; 1376 1377 ZmHtmlEditor.prototype.replaceImage = 1378 function(id, src){ 1379 var doc = this.getEditor().getDoc(); 1380 if(doc){ 1381 var img = doc.getElementById(id); 1382 if( img && img.getAttribute("data-zim-uri") === id ){ 1383 img.src = src; 1384 img.removeAttribute("id"); 1385 img.removeAttribute("data-mce-src"); 1386 img.removeAttribute("data-zim-uri"); 1387 } 1388 } 1389 }; 1390 1391 /* 1392 This function will replace all the img elements matching src 1393 */ 1394 ZmHtmlEditor.prototype.replaceImageSrc = 1395 function(src, newsrc){ 1396 var doc = this.getEditor().getDoc(); 1397 if(doc){ 1398 var images = doc.getElementsByTagName('img'); 1399 if (images && images.length > 0) { 1400 AjxUtil.foreach(images,function(img) { 1401 try { 1402 var imgsrc = img && img.src; 1403 } catch(e) { 1404 //IE8 throws invalid pointer exception for src attribute when src is a data uri 1405 return; 1406 } 1407 if (imgsrc && imgsrc == src) { 1408 img.src = newsrc; 1409 img.removeAttribute("id"); 1410 img.removeAttribute("data-mce-src"); 1411 img.removeAttribute("data-zim-uri"); 1412 } 1413 }); 1414 } 1415 } 1416 }; 1417 1418 ZmHtmlEditor.prototype.addCSSForDefaultFontSize = 1419 function(editor) { 1420 var selectorText = "body,td,pre"; 1421 var ruleText = [ 1422 "font-family:", appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_FAMILY),";", 1423 "font-size:", appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_SIZE),";", 1424 "color:", appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_COLOR),";" 1425 ].join(""); 1426 var doc = editor ? editor.getDoc() : null; 1427 if (doc) { 1428 this.insertDefaultCSS(doc, selectorText, ruleText); 1429 } 1430 }; 1431 1432 ZmHtmlEditor.prototype.insertDefaultCSS = 1433 function(doc, selectorText, ruleText) { 1434 var sheet, styleElement; 1435 if (doc.createStyleSheet) { 1436 sheet = doc.createStyleSheet(); 1437 } else { 1438 styleElement = doc.createElement("style"); 1439 doc.getElementsByTagName("head")[0].appendChild(styleElement); 1440 sheet = styleElement.styleSheet ? styleElement.styleSheet : styleElement.sheet; 1441 } 1442 1443 if (!sheet && styleElement) { 1444 //remove braces 1445 ruleText = ruleText.replace(/^\{?([^\}])/, "$1"); 1446 styleElement.innerHTML = selectorText + ruleText; 1447 } else if (sheet.addRule) { 1448 //remove braces 1449 ruleText = ruleText.replace(/^\{?([^\}])/, "$1"); 1450 DBG.println("ruleText:" + ruleText + ",selector:" + selectorText); 1451 sheet.addRule(selectorText, ruleText); 1452 } else if (sheet.insertRule) { 1453 //need braces 1454 if (!/^\{[^\}]*\}$/.test(ruleText)) ruleText = "{" + ruleText + "}"; 1455 sheet.insertRule(selectorText + " " + ruleText, sheet.cssRules.length); 1456 } 1457 }; 1458 1459 ZmHtmlEditor.prototype.resetSpellCheck = 1460 function() { 1461 //todo: remove this when spellcheck is disabled 1462 this.discardMisspelledWords(); 1463 this._spellCheckHideModeDiv(); 1464 }; 1465 1466 /**SpellCheck modules**/ 1467 1468 ZmHtmlEditor.prototype.checkMisspelledWords = 1469 function(callback, onExitCallback, errCallback){ 1470 var text = this.getTextVersion(); 1471 if (/\S/.test(text)) { 1472 AjxDispatcher.require("Extras"); 1473 this._spellChecker = new ZmSpellChecker(this); 1474 this._spellCheck = null; 1475 this._spellCheckSuggestionListenerObj = new AjxListener(this, this._spellCheckSuggestionListener); 1476 if (!this.onExitSpellChecker) { 1477 this.onExitSpellChecker = onExitCallback; 1478 } 1479 var params = { 1480 text: text, 1481 ignore: AjxUtil.keys(this._ignoreWords).join() 1482 }; 1483 this._spellChecker.check(params, callback, errCallback); 1484 return true; 1485 } 1486 1487 return false; 1488 }; 1489 1490 ZmHtmlEditor.prototype.spellCheck = 1491 function(callback, keepModeDiv) { 1492 var text = this.getTextVersion(null, keepModeDiv); 1493 1494 if (/\S/.test(text)) { 1495 AjxDispatcher.require("Extras"); 1496 this._spellChecker = new ZmSpellChecker(this); 1497 this._spellCheck = null; 1498 this._spellCheckSuggestionListenerObj = new AjxListener(this, this._spellCheckSuggestionListener); 1499 if (!this.onExitSpellChecker) { 1500 this.onExitSpellChecker = callback; 1501 } 1502 var params = { 1503 text: text, 1504 ignore: AjxUtil.keys(this._ignoreWords).join() 1505 }; 1506 this._spellChecker.check(params, new AjxCallback(this, this._spellCheckCallback)); 1507 return true; 1508 } 1509 1510 return false; 1511 }; 1512 1513 ZmHtmlEditor.prototype._spellCheckCallback = 1514 function(words) { 1515 // Remove the below comment for hard coded spell check response for development 1516 //words = {"misspelled":[{"word":"onee","suggestions":"one,nee,knee,once,ones,one's"},{"word":"twoo","suggestions":"two,too,woo,twos,two's"},{"word":"fourrr","suggestions":"Fourier,furor,furry,firer,fuhrer,fore,furrier,four,furrow,fora,fury,fours,ferry,foray,flurry,four's"}],"available":true}; 1517 var wordsFound = false; 1518 1519 if (words && words.available) { 1520 var misspelled = words.misspelled; 1521 if (misspelled == null || misspelled.length == 0) { 1522 appCtxt.setStatusMsg(ZmMsg.noMisspellingsFound, ZmStatusView.LEVEL_INFO); 1523 this._spellCheckHideModeDiv(); 1524 } else { 1525 var msg = AjxMessageFormat.format(ZmMsg.misspellingsResult, misspelled.length); 1526 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING); 1527 1528 this.highlightMisspelledWords(misspelled); 1529 wordsFound = true; 1530 } 1531 } else { 1532 appCtxt.setStatusMsg(ZmMsg.spellCheckUnavailable, ZmStatusView.LEVEL_CRITICAL); 1533 } 1534 1535 if (AjxEnv.isGeckoBased && this._mode == Dwt.HTML) { 1536 setTimeout(AjxCallback.simpleClosure(this.focus, this), 10); 1537 } 1538 1539 if (this.onExitSpellChecker) { 1540 this.onExitSpellChecker.run(wordsFound); 1541 } 1542 }; 1543 1544 ZmHtmlEditor.prototype._spellCheckSuggestionListener = 1545 function(ev) { 1546 var self = this; 1547 var item = ev.item; 1548 var orig = item.getData("orig"); 1549 if (!orig) { return; } 1550 1551 var val = item.getData(ZmHtmlEditor.VALUE); 1552 var plainText = this._mode == Dwt.TEXT; 1553 var fixall = item.getData("fixall"); 1554 var doc = plainText ? document : this._getIframeDoc(); 1555 var span = doc.getElementById(item.getData("spanId")); 1556 var action = item.getData(ZmOperation.MENUITEM_ID); 1557 switch (action) { 1558 case "ignore": 1559 val = orig; 1560 this._ignoreWords[val] = true; 1561 // if (fixall) { 1562 // TODO: visually "correct" all of them 1563 // } 1564 break; 1565 case "add": 1566 val = orig; 1567 // add word to user's personal dictionary 1568 var soapDoc = AjxSoapDoc.create("ModifyPrefsRequest", "urn:zimbraAccount"); 1569 var prefEl = soapDoc.set("pref", val); 1570 prefEl.setAttribute("name", "+zimbraPrefSpellIgnoreWord"); 1571 var params = { 1572 soapDoc: soapDoc, 1573 asyncMode: true, 1574 callback: new AjxCallback(appCtxt, appCtxt.setStatusMsg, [ZmMsg.wordAddedToDictionary]) 1575 }; 1576 appCtxt.getAppController().sendRequest(params); 1577 this._ignoreWords[val] = true; 1578 break; 1579 default: break; 1580 } 1581 1582 if (plainText && val == null) { 1583 this._editWord(fixall, span); 1584 } 1585 else { 1586 var spanEls = fixall ? this._spellCheck.wordIds[orig] : span; 1587 this._editWordFix(spanEls, val); 1588 } 1589 1590 this._handleSpellCheckerEvents(null); 1591 }; 1592 1593 ZmHtmlEditor.prototype._getEditorDocument = function() { 1594 var plainText = this._mode == Dwt.TEXT; 1595 return plainText ? document : this._getIframeDoc(); 1596 }; 1597 1598 ZmHtmlEditor.prototype._editWord = function(fixall, spanEl) { 1599 // edit clicked 1600 var doc = this._getEditorDocument(); 1601 var input = doc.createElement("input"); 1602 input.type = "text"; 1603 input.value = AjxUtil.getInnerText(spanEl); 1604 input.className = "SpellCheckInputField"; 1605 input.style.left = spanEl.offsetLeft - 2 + "px"; 1606 input.style.top = spanEl.offsetTop - 2 + "px"; 1607 input.style.width = spanEl.offsetWidth + 4 + "px"; 1608 var div = doc.getElementById(this._spellCheckDivId); 1609 var scrollTop = div.scrollTop; 1610 div.appendChild(input); 1611 div.scrollTop = scrollTop; // this gets resetted when we add an input field (at least Gecko) 1612 input.setAttribute("autocomplete", "off"); 1613 input.focus(); 1614 if (!AjxEnv.isGeckoBased) 1615 input.select(); 1616 else 1617 input.setSelectionRange(0, input.value.length); 1618 var inputListener = AjxCallback.simpleClosure(this._editWordHandler, this, fixall, spanEl); 1619 input.onblur = inputListener; 1620 input.onkeydown = inputListener; 1621 }; 1622 1623 ZmHtmlEditor.prototype._editWordHandler = function(fixall, spanEl, ev) { 1624 // the event gets lost after 20 milliseconds so we need 1625 // to save the following :( 1626 setTimeout(AjxCallback.simpleClosure(this._editWordHandler2, this, fixall, spanEl, ev), 20); 1627 }; 1628 ZmHtmlEditor.prototype._editWordHandler2 = function(fixall, spanEl, ev) { 1629 ev = DwtUiEvent.getEvent(ev); 1630 var evType = ev.type; 1631 var evKeyCode = ev.keyCode; 1632 var evCtrlKey = ev.ctrlKey; 1633 var input = DwtUiEvent.getTarget(ev); 1634 var keyEvent = /key/.test(evType); 1635 var removeInput = true; 1636 if (/blur/.test(evType) || (keyEvent && DwtKeyEvent.IS_RETURN[evKeyCode])) { 1637 if (evCtrlKey) 1638 fixall =! fixall; 1639 var orig = AjxUtil.getInnerText(spanEl); 1640 var spanEls = fixall ? this._spellCheck.wordIds[orig] : spanEl; 1641 this._editWordFix(spanEls, input.value); 1642 } else if (keyEvent && evKeyCode === DwtKeyEvent.KEY_ESCAPE) { 1643 this._editWordFix(spanEl, AjxUtil.getInnerText(spanEl)); 1644 } else { 1645 removeInput = false; 1646 } 1647 if (removeInput) { 1648 input.onblur = null; 1649 input.onkeydown = null; 1650 if (input.parentNode) { 1651 input.parentNode.removeChild(input); 1652 } 1653 } 1654 this._handleSpellCheckerEvents(null); 1655 }; 1656 1657 ZmHtmlEditor.prototype._editWordFix = function(spanEls, value) { 1658 spanEls = spanEls instanceof Array ? spanEls : [ spanEls ]; 1659 var doc = this._getEditorDocument(); 1660 for (var i = spanEls.length - 1; i >= 0; i--) { 1661 var spanEl = spanEls[i]; 1662 if (typeof spanEl == "string") { 1663 spanEl = doc.getElementById(spanEl); 1664 } 1665 if (spanEl) { 1666 spanEl.innerHTML = value; 1667 } 1668 } 1669 }; 1670 1671 ZmHtmlEditor.prototype._getParentElement = 1672 function() { 1673 var ed = this.getEditor(); 1674 if (ed.selection) { 1675 return ed.selection.getNode(); 1676 } else { 1677 var doc = this._getIframeDoc(); 1678 return doc ? doc.body : null; 1679 } 1680 }; 1681 1682 ZmHtmlEditor.prototype._handleSpellCheckerEvents = 1683 function(ev) { 1684 var plainText = this._mode == Dwt.TEXT; 1685 var p = plainText ? (ev ? DwtUiEvent.getTarget(ev) : null) : this._getParentElement(), 1686 span, ids, i, suggestions, 1687 self = this, 1688 sc = this._spellCheck, 1689 doc = plainText ? document : this._getIframeDoc(), 1690 modified = false, 1691 word = ""; 1692 if (ev && /^span$/i.test(p.tagName) && /ZM-SPELLCHECK/.test(p.className)) { 1693 // stuff. 1694 word = p.getAttribute("word"); 1695 // FIXME: not sure this is OK. 1696 window.status = "Suggestions: " + sc.suggestions[word].join(", "); 1697 modified = word != AjxUtil.getInnerText(p); 1698 } 1699 1700 // <FIXME: there's plenty of room for optimization here> 1701 ids = sc.spanIds; 1702 for (i in ids) { 1703 span = doc.getElementById(i); 1704 if (span) { 1705 if (ids[i] != AjxUtil.getInnerText(span) || this._ignoreWords[ids[i]]) 1706 span.className = "ZM-SPELLCHECK-FIXED"; 1707 else if (ids[i] == word) 1708 span.className = "ZM-SPELLCHECK-MISSPELLED2"; 1709 else 1710 span.className = "ZM-SPELLCHECK-MISSPELLED"; 1711 } 1712 } 1713 // </FIXME> 1714 1715 // Dismiss the menu if it is present AND: 1716 // - we have no event, OR 1717 // - it's a mouse(down|up) event, OR 1718 // - it's a KEY event AND there's no word under the caret, OR the word was modified. 1719 // I know, it's ugly. 1720 if (sc.menu && 1721 (!ev || ( /click|mousedown|mouseup|contextmenu/.test(ev.type) 1722 || ( /key/.test(ev.type) 1723 && (!word || modified) ) 1724 ))) 1725 { 1726 sc.menu.dispose(); 1727 sc.menu = null; 1728 window.status = ""; 1729 } 1730 // but that's even uglier: 1731 if (ev && word && (suggestions = sc.suggestions[word]) && 1732 (/mouseup|contextmenu/i.test(ev.type) || 1733 (plainText && /(click|mousedown|contextmenu)/i.test(ev.type))) && 1734 (word == AjxUtil.getInnerText(p) && !this._ignoreWords[word])) 1735 { 1736 sc.menu = this._spellCheckCreateMenu(this.parent, 0, suggestions, word, p.id, modified); 1737 var pos, ms = sc.menu.getSize(), ws = this.shell.getSize(); 1738 if (!plainText) { 1739 // bug fix #5857 - use Dwt.toWindow instead of Dwt.getLocation so we can turn off dontIncScrollTop 1740 pos = Dwt.toWindow(document.getElementById(this._iFrameId), 0, 0, null, true); 1741 var pos2 = Dwt.toWindow(p, 0, 0, null, true); 1742 pos.x += pos2.x 1743 - (doc.documentElement.scrollLeft || doc.body.scrollLeft); 1744 pos.y += pos2.y 1745 - (doc.documentElement.scrollTop || doc.body.scrollTop); 1746 } else { 1747 // bug fix #5857 1748 pos = Dwt.toWindow(p, 0, 0, null, true); 1749 var div = document.getElementById(this._spellCheckDivId); 1750 pos.x -= div.scrollLeft; 1751 pos.y -= div.scrollTop; 1752 } 1753 pos.y += p.offsetHeight; 1754 // let's make sure we look nice, shall we. 1755 if (pos.y + ms.y > ws.y) 1756 pos.y -= ms.y + p.offsetHeight; 1757 sc.menu.popup(0, pos.x, pos.y); 1758 ev._stopPropagation = true; 1759 ev._returnValue = false; 1760 return false; 1761 } 1762 }; 1763 1764 ZmHtmlEditor.prototype._spellCheckCreateMenu = function(parent, fixall, suggestions, word, spanId, modified) { 1765 1766 var menu = new ZmPopupMenu(parent); 1767 // menu.dontStealFocus(); 1768 1769 if (modified) { 1770 var txt = "<b>" + word + "</b>"; 1771 this._spellCheckCreateMenuItem(menu, "orig", {text:txt}, fixall, word, word, spanId); 1772 } 1773 1774 if (suggestions.length > 0) { 1775 for (var i = 0; i < suggestions.length; ++i) { 1776 this._spellCheckCreateMenuItem( 1777 menu, "sug-"+i, {text:suggestions[i], className: ""}, 1778 fixall, suggestions[i], word, spanId 1779 ); 1780 } 1781 if (!(parent instanceof DwtMenuItem) && this._spellCheck.wordIds[word].length > 1) { 1782 if (!this._replaceAllFormatter) { 1783 this._replaceAllFormatter = new AjxMessageFormat(ZmMsg.replaceAllMenu); 1784 } 1785 var txt = "<i>"+this._replaceAllFormatter.format(this._spellCheck.wordIds[word].length)+"</i>"; 1786 var item = menu.createMenuItem("fixall", {text:txt}); 1787 var submenu = this._spellCheckCreateMenu(item, 1, suggestions, word, spanId, modified); 1788 item.setMenu(submenu); 1789 } 1790 } 1791 else { 1792 var item = this._spellCheckCreateMenuItem(menu, "noop", {text:ZmMsg.noSuggestions}, fixall, "", word, spanId); 1793 item.setEnabled(false); 1794 this._spellCheckCreateMenuItem(menu, "clear", {text:"<i>"+ZmMsg.clearText+"</i>" }, fixall, "", word, spanId); 1795 } 1796 1797 var plainText = this._mode == Dwt.TEXT; 1798 if (!fixall || plainText) { 1799 menu.createSeparator(); 1800 } 1801 1802 if (plainText) { 1803 // in plain text mode we want to be able to edit misspelled words 1804 var txt = fixall ? ZmMsg.editAll : ZmMsg.edit; 1805 this._spellCheckCreateMenuItem(menu, "edit", {text:txt}, fixall, null, word, spanId); 1806 } 1807 1808 if (!fixall) { 1809 this._spellCheckCreateMenuItem(menu, "ignore", {text:ZmMsg.ignoreWord}, 0, null, word, spanId); 1810 // this._spellCheckCreateMenuItem(menu, "ignore", {text:ZmMsg.ignoreWordAll}, 1, null, word, spanId); 1811 } 1812 1813 if (!fixall && appCtxt.get(ZmSetting.SPELL_CHECK_ADD_WORD_ENABLED)) { 1814 this._spellCheckCreateMenuItem(menu, "add", {text:ZmMsg.addWord}, fixall, null, word, spanId); 1815 } 1816 1817 return menu; 1818 }; 1819 1820 ZmHtmlEditor.prototype._spellCheckCreateMenuItem = 1821 function(menu, id, params, fixall, value, word, spanId, listener) { 1822 if (params.className == null) { 1823 params.className = "ZMenuItem ZmSpellMenuItem"; 1824 } 1825 var item = menu.createMenuItem(id, params); 1826 item.setData("fixall", fixall); 1827 item.setData("value", value); 1828 item.setData("orig", word); 1829 item.setData("spanId", spanId); 1830 item.addSelectionListener(listener || this._spellCheckSuggestionListenerObj); 1831 return item; 1832 }; 1833 1834 ZmHtmlEditor.prototype.discardMisspelledWords = 1835 function(keepModeDiv) { 1836 if (!this._spellCheck) { return; } 1837 1838 var size = this.getSize(); 1839 if (this._mode == Dwt.HTML) { 1840 var doc = this._getIframeDoc(); 1841 doc.body.style.display = "none"; 1842 1843 var p = null; 1844 var spanIds = this._spellCheck.spanIds; 1845 for (var i in spanIds) { 1846 var span = doc.getElementById(i); 1847 if (!span) continue; 1848 1849 p = span.parentNode; 1850 while (span.firstChild) { 1851 p.insertBefore(span.firstChild, span); 1852 } 1853 p.removeChild(span); 1854 } 1855 1856 if (!AjxEnv.isIE) { 1857 doc.body.normalize(); // IE crashes here. 1858 } else { 1859 doc.body.innerHTML = doc.body.innerHTML; // WTF. 1860 } 1861 1862 // remove the spell check styles 1863 p = doc.getElementById("ZM-SPELLCHECK-STYLE"); 1864 if (p) { 1865 p.parentNode.removeChild(p); 1866 } 1867 1868 doc.body.style.display = ""; 1869 this._unregisterEditorEventHandler(doc, "contextmenu"); 1870 size.y = size.y - (keepModeDiv ? 0 : 2); 1871 } else if (this._spellCheckDivId != null) { 1872 var div = document.getElementById(this._spellCheckDivId); 1873 var scrollTop = div.scrollTop; 1874 var textArea = document.getElementById(this._textAreaId); 1875 // bug: 41760 - HACK. Convert the nbsps back to spaces since Gecko seems 1876 // to return control characters for HTML entities. 1877 if (AjxEnv.isGeckoBased) { 1878 div.innerHTML = AjxStringUtil.htmlDecode(div.innerHTML, true); 1879 } 1880 textArea.value = AjxUtil.getInnerText(div); 1881 1882 // avoid mem. leaks, hopefully 1883 div.onclick = null; 1884 div.oncontextmenu = null; 1885 div.onmousedown = null; 1886 div.parentNode.removeChild(div); 1887 textArea.style.display = ""; 1888 textArea.scrollTop = scrollTop; 1889 size.y = size.y + (keepModeDiv ? 2 : 0); 1890 } 1891 1892 this._spellCheckDivId = this._spellCheck = null; 1893 window.status = ""; 1894 1895 if (!keepModeDiv) { 1896 this._spellCheckHideModeDiv(); 1897 } 1898 1899 if (this.onExitSpellChecker) { 1900 this.onExitSpellChecker.run(); 1901 } 1902 this._resetSize(); 1903 }; 1904 1905 ZmHtmlEditor.prototype._spellCheckShowModeDiv = 1906 function() { 1907 var size = this.getSize(); 1908 1909 if (!this._spellCheckModeDivId) { 1910 var div = document.createElement("div"); 1911 div.className = "SpellCheckModeDiv"; 1912 div.id = this._spellCheckModeDivId = Dwt.getNextId(); 1913 var html = new Array(); 1914 var i = 0; 1915 html[i++] = "<table border=0 cellpadding=0 cellspacing=0><tr><td style='width:25'>"; 1916 html[i++] = AjxImg.getImageHtml("SpellCheck"); 1917 html[i++] = "</td><td style='white-space:nowrap'><span class='SpellCheckLink'>"; 1918 html[i++] = ZmMsg.resumeEditing; 1919 html[i++] = "</span> | <span class='SpellCheckLink'>"; 1920 html[i++] = ZmMsg.checkAgain; 1921 html[i++] = "</span></td></tr></table>"; 1922 div.innerHTML = html.join(""); 1923 1924 //var editable = document.getElementById((this._spellCheckDivId || this.getBodyFieldId())); 1925 //editable.parentNode.insertBefore(div, editable); 1926 var container = this.getHtmlElement(); 1927 container.insertBefore(div, container.firstChild); 1928 1929 var el = div.getElementsByTagName("span"); 1930 Dwt.associateElementWithObject(el[0], this); 1931 Dwt.setHandler(el[0], "onclick", ZmHtmlEditor._spellCheckResumeEditing); 1932 Dwt.associateElementWithObject(el[1], this); 1933 Dwt.setHandler(el[1], "onclick", ZmHtmlEditor._spellCheckAgain); 1934 } 1935 else { 1936 document.getElementById(this._spellCheckModeDivId).style.display = ""; 1937 } 1938 this._resetSize(); 1939 }; 1940 1941 ZmHtmlEditor._spellCheckResumeEditing = 1942 function() { 1943 var editor = Dwt.getObjectFromElement(this); 1944 editor.discardMisspelledWords(); 1945 editor.focus(); 1946 }; 1947 1948 ZmHtmlEditor._spellCheckAgain = 1949 function() { 1950 Dwt.getObjectFromElement(this).spellCheck(null, true); 1951 }; 1952 1953 1954 ZmHtmlEditor.prototype._spellCheckHideModeDiv = 1955 function() { 1956 var size = this.getSize(); 1957 if (this._spellCheckModeDivId) { 1958 document.getElementById(this._spellCheckModeDivId).style.display = "none"; 1959 } 1960 this._resetSize(); 1961 }; 1962 1963 ZmHtmlEditor.prototype.highlightMisspelledWords = 1964 function(words, keepModeDiv) { 1965 this.discardMisspelledWords(keepModeDiv); 1966 1967 var word, style, doc, body, self = this, 1968 spanIds = {}, 1969 wordIds = {}, 1970 regexp = [ "([^A-Za-z0-9']|^)(" ], 1971 suggestions = {}; 1972 1973 // preparations: initialize some variables that we then save in 1974 // this._spellCheck (the current spell checker context). 1975 for (var i = 0; i < words.length; ++i) { 1976 word = words[i].word; 1977 if (!suggestions[word]) { 1978 i && regexp.push("|"); 1979 regexp.push(word); 1980 var a = words[i].suggestions.split(/\s*,\s*/); 1981 if (!a[a.length-1]) 1982 a.pop(); 1983 suggestions[word] = a; 1984 if (suggestions[word].length > 5) 1985 suggestions[word].length = 5; 1986 } 1987 } 1988 regexp.push(")([^A-Za-z0-9']|$)"); 1989 regexp = new RegExp(regexp.join(""), "gm"); 1990 1991 function hiliteWords(text, textWhiteSpace) { 1992 text = textWhiteSpace 1993 ? AjxStringUtil.convertToHtml(text) 1994 : AjxStringUtil.htmlEncode(text); 1995 1996 var m; 1997 1998 regexp.lastIndex = 0; 1999 while (m = regexp.exec(text)) { 2000 var str = m[0]; 2001 var prefix = m[1]; 2002 var word = m[2]; 2003 var suffix = m[3]; 2004 2005 var id = Dwt.getNextId(); 2006 spanIds[id] = word; 2007 if (!wordIds[word]) 2008 wordIds[word] = []; 2009 wordIds[word].push(id); 2010 2011 var repl = [ 2012 prefix, 2013 '<span word="', 2014 word, '" id="', id, '" class="ZM-SPELLCHECK-MISSPELLED">', 2015 word, '</span>', 2016 suffix 2017 ].join(""); 2018 text = [ 2019 text.substr(0, m.index), 2020 repl, 2021 text.substr(m.index + str.length) 2022 ].join(""); 2023 2024 // All this crap necessary because the suffix 2025 // must be taken into account at the next 2026 // match and JS regexps don't have look-ahead 2027 // constructs (except \b, which sucks). Oh well. 2028 regexp.lastIndex = m.index + repl.length - suffix.length; 2029 } 2030 return text; 2031 }; 2032 2033 var doc; 2034 2035 // having the data, this function will parse the DOM and replace 2036 // occurrences of the misspelled words with <span 2037 // class="ZM-SPELLCHECK-MISSPELLED">word</span> 2038 rec = function(node) { 2039 switch (node.nodeType) { 2040 case 1: /* ELEMENT */ 2041 for (var i = node.firstChild; i; i = rec(i)) {} 2042 node = node.nextSibling; 2043 break; 2044 case 3: /* TEXT */ 2045 if (!/[^\s\xA0]/.test(node.data)) { 2046 node = node.nextSibling; 2047 break; 2048 } 2049 // for correct handling of whitespace we should 2050 // not mess ourselves with leading/trailing 2051 // whitespace, thus we save it in 2 text nodes. 2052 var a = null, b = null; 2053 2054 var result = /^[\s\xA0]+/.exec(node.data); 2055 if (result) { 2056 // a will contain the leading space 2057 a = node; 2058 node = node.splitText(result[0].length); 2059 } 2060 result = /[\s\xA0]+$/.exec(node.data); 2061 if (result) { 2062 // and b will contain the trailing space 2063 b = node.splitText(node.data.length - result[0].length); 2064 } 2065 2066 var text = hiliteWords(node.data, false); 2067 text = text.replace(/^ +/, " ").replace(/ +$/, " "); 2068 var div = doc.createElement("div"); 2069 div.innerHTML = text; 2070 2071 // restore whitespace now 2072 if (a) { 2073 div.insertBefore(a, div.firstChild); 2074 } 2075 if (b) { 2076 div.appendChild(b); 2077 } 2078 2079 var p = node.parentNode; 2080 while (div.firstChild) { 2081 p.insertBefore(div.firstChild, node); 2082 } 2083 div = node.nextSibling; 2084 p.removeChild(node); 2085 node = div; 2086 break; 2087 default : 2088 node = node.nextSibling; 2089 } 2090 return node; 2091 }; 2092 2093 if (this._mode == Dwt.HTML) { 2094 // HTML mode; See the "else" branch for the TEXT mode--code differs 2095 // quite a lot. We should probably implement separate functions as 2096 // this already becomes long. 2097 2098 doc = this._getIframeDoc(); 2099 body = doc.body; 2100 2101 // load the spell check styles, if not already there. 2102 this._loadExternalStyle("/css/spellcheck.css"); 2103 2104 body.style.display = "none"; // seems to have a good impact on speed, 2105 // since we may modify a lot of the DOM 2106 if (!AjxEnv.isIE) { 2107 body.normalize(); 2108 } else { 2109 body.innerHTML = body.innerHTML; 2110 } 2111 rec(body); 2112 if (!AjxEnv.isIE) { 2113 body.normalize(); 2114 } else { 2115 body.innerHTML = body.innerHTML; 2116 } 2117 body.style.display = ""; // redisplay the body 2118 } 2119 else { // TEXT mode 2120 var textArea = document.getElementById(this._textAreaId); 2121 var scrollTop = textArea.scrollTop; 2122 var size = Dwt.getSize(textArea); 2123 textArea.style.display = "none"; 2124 var div = document.createElement("div"); 2125 div.className = "TextSpellChecker"; 2126 this._spellCheckDivId = div.id = Dwt.getNextId(); 2127 div.style.overflow = "auto"; 2128 if (!AjxEnv.isIE) { 2129 // FIXME: we substract borders/padding here. this sucks. 2130 size.x -= 4; 2131 size.y -= 6; 2132 } 2133 div.style.height = size.y + "px"; 2134 2135 div.innerHTML = AjxStringUtil.convertToHtml(this.getContent()); 2136 doc = document; 2137 rec(div); 2138 2139 textArea.parentNode.insertBefore(div, textArea); 2140 div.scrollTop = scrollTop; 2141 div.oncontextmenu = div.onclick 2142 = function(ev) { self._handleSpellCheckerEvents(ev || window.event); }; 2143 } 2144 2145 this._spellCheckShowModeDiv(); 2146 2147 // save the spell checker context 2148 this._spellCheck = { 2149 suggestions: suggestions, 2150 spanIds: spanIds, 2151 wordIds: wordIds 2152 }; 2153 }; 2154 2155 /** 2156 * Returns true if editor content is spell checked 2157 */ 2158 ZmHtmlEditor.prototype.isSpellCheckMode = function() { 2159 return Boolean( this._spellCheck ); 2160 }; 2161 2162 ZmHtmlEditor.prototype._loadExternalStyle = 2163 function(path) { 2164 var doc = this._getIframeDoc(); 2165 // check if already loaded 2166 var style = doc.getElementById(path); 2167 if (!style) { 2168 style = doc.createElement("link"); 2169 style.id = path; 2170 style.rel = "stylesheet"; 2171 style.type = "text/css"; 2172 var style_url = appContextPath + path + "?v=" + cacheKillerVersion; 2173 if (AjxEnv.isGeckoBased || AjxEnv.isSafari) { 2174 style_url = document.baseURI.replace( 2175 /^(https?:\x2f\x2f[^\x2f]+).*$/, "$1") + style_url; 2176 } 2177 style.href = style_url; 2178 var head = doc.getElementsByTagName("head")[0]; 2179 if (!head) { 2180 head = doc.createElement("head"); 2181 var docEl = doc.documentElement; 2182 if (docEl) { 2183 docEl.insertBefore(head, docEl.firstChild); 2184 } 2185 } 2186 head.appendChild(style); 2187 } 2188 }; 2189 2190 ZmHtmlEditor.prototype._registerEditorEventHandler = function(iFrameDoc, name) { 2191 2192 if (iFrameDoc.attachEvent) { 2193 iFrameDoc.attachEvent("on" + name, this.__eventClosure); 2194 } 2195 else if (iFrameDoc.addEventListener) { 2196 iFrameDoc.addEventListener(name, this.__eventClosure, true); 2197 } 2198 }; 2199 2200 ZmHtmlEditor.prototype._unregisterEditorEventHandler = function(iFrameDoc, name) { 2201 2202 if (iFrameDoc.detachEvent) { 2203 iFrameDoc.detachEvent("on" + name, this.__eventClosure); 2204 } 2205 else if (iFrameDoc.removeEventListener) { 2206 iFrameDoc.removeEventListener(name, this.__eventClosure, true); 2207 } 2208 }; 2209 2210 ZmHtmlEditor.prototype.__eventClosure = 2211 function(ev) { 2212 this._handleEditorEvent(AjxEnv.isIE ? this._getIframeWin().event : ev); 2213 return tinymce.dom.Event.cancel(ev); 2214 }; 2215 2216 2217 ZmHtmlEditor.prototype._handleEditorEvent = 2218 function(ev) { 2219 var ed = this.getEditor(); 2220 var retVal = true; 2221 2222 var self = this; 2223 2224 var target = ev.srcElement || ev.target; //in FF we get ev.target and not ev.srcElement. 2225 if (this._spellCheck && target && target.id in this._spellCheck.spanIds) { 2226 var dw; 2227 // This probably sucks. 2228 if (/mouse|context|click|select/i.test(ev.type)) { 2229 dw = new DwtMouseEvent(true); 2230 } else { 2231 dw = new DwtUiEvent(true); 2232 } 2233 dw.setFromDhtmlEvent(ev); 2234 this._TIMER_spell = setTimeout(function() { 2235 self._handleSpellCheckerEvents(dw); 2236 this._TIMER_spell = null; 2237 }, 100); 2238 ev.stopImmediatePropagation(); 2239 ev.stopPropagation(); 2240 ev.preventDefault(); 2241 return tinymce.dom.Event.cancel(ev); 2242 } 2243 2244 return retVal; 2245 }; 2246 2247 ZmHtmlEditor.prototype._getSelection = 2248 function() { 2249 if (AjxEnv.isIE) { 2250 return this._getIframeDoc().selection; 2251 } else { 2252 return this._getIframeWin().getSelection(); 2253 } 2254 }; 2255 2256 /* 2257 * Returns toolbar row of tinymce 2258 * 2259 * @param {Number} Toolbar Row Number 1,2 2260 * @param {object} tinymce editor 2261 * @return {Toolbar HTML Element} 2262 */ 2263 ZmHtmlEditor.prototype.getToolbar = 2264 function(number, editor) { 2265 var controlManager, 2266 toolbar; 2267 2268 editor = editor || this.getEditor(); 2269 if (editor && number) { 2270 controlManager = editor.controlManager; 2271 if (controlManager) { 2272 toolbar = controlManager.get("toolbar"+number); 2273 if (toolbar && toolbar.id) { 2274 return document.getElementById(toolbar.id); 2275 } 2276 } 2277 } 2278 }; 2279 2280 /* 2281 * Returns toolbar button of tinymce 2282 * 2283 * @param {String} button name 2284 * @param {object} tinymce editor 2285 * @return {Toolbar Button HTML Element} 2286 */ 2287 ZmHtmlEditor.prototype.getToolbarButton = 2288 function(buttonName, editor) { 2289 var controlManager, 2290 toolbarButton; 2291 2292 if (editor && buttonName) { 2293 controlManager = editor.controlManager; 2294 if (controlManager) { 2295 toolbarButton = controlManager.get(buttonName); 2296 if (toolbarButton && toolbarButton.id) { 2297 return document.getElementById(toolbarButton.id); 2298 } 2299 } 2300 } 2301 }; 2302 2303 /* 2304 * Inserting image for signature 2305 */ 2306 ZmHtmlEditor.prototype.insertImageDoc = 2307 function(file) { 2308 var src = file.rest; 2309 if (!src) { return; } 2310 var path = appCtxt.get(ZmSetting.REST_URL) + ZmFolder.SEP; 2311 var dfsrc = file.docpath; 2312 if (dfsrc && dfsrc.indexOf("doc:") == 0) { 2313 var url = [path, dfsrc.substring(4)].join(''); 2314 src = AjxStringUtil.fixCrossDomainReference(url, false, true); 2315 } 2316 this.insertImage(src, null, null, null, dfsrc); 2317 }; 2318 2319 /* 2320 * Insert image callback 2321 */ 2322 ZmHtmlEditor.prototype._imageUploaded = function(folder, fileNames, files) { 2323 2324 for (var i = 0; i < files.length; i++) { 2325 var file = files[i]; 2326 var path = appCtxt.get(ZmSetting.REST_URL) + ZmFolder.SEP; 2327 var docPath = folder.getRestUrl() + ZmFolder.SEP + file.name; 2328 file.docpath = ["doc:", docPath.substr(docPath.indexOf(path) + path.length)].join(""); 2329 file.rest = folder.getRestUrl() + ZmFolder.SEP + AjxStringUtil.urlComponentEncode(file.name); 2330 2331 this.insertImageDoc(file); 2332 } 2333 2334 //note - it's always one file so far even though the code above support a more than one item array. 2335 //toast so the user understands uploading an image result in it being in the briefcase. 2336 appCtxt.setStatusMsg(ZmMsg.imageUploadedToBriefcase); 2337 2338 }; 2339 2340 /** 2341 * This will be fired before every popup open 2342 * 2343 * @param {windowManager} tinymce window manager for popups 2344 * @param {popupWindow} contains tinymce popup info or popup DOM Window 2345 * 2346 */ 2347 ZmHtmlEditor.onPopupOpen = function(windowManager, popupWindow) { 2348 if (!popupWindow) { 2349 return; 2350 } 2351 if (popupWindow.resizable) { 2352 popupWindow.resizable = 0; 2353 } 2354 2355 var popupIframe = popupWindow.frameElement, 2356 popupIframeLoad; 2357 2358 if (popupIframe && popupIframe.src && popupIframe.src.match("/table.htm")) {//Table dialog 2359 popupIframeLoad = function(popupWindow, popupIframe) { 2360 var doc,align,width; 2361 if (popupWindow.action === "insert") {//Insert Table Action 2362 doc = popupWindow.document; 2363 if (doc) { 2364 align = doc.getElementById("align"); 2365 width = doc.getElementById("width"); 2366 align && (align.value = "center"); 2367 width && (width.value = "90%"); 2368 } 2369 } 2370 if (this._popupIframeLoad) { 2371 popupIframe.detachEvent("onload", this._popupIframeLoad); 2372 delete this._popupIframeLoad; 2373 } 2374 else { 2375 popupIframe.onload = null; 2376 } 2377 }; 2378 2379 if (popupIframe.attachEvent) { 2380 this._popupIframeLoad = popupIframeLoad.bind(this, popupWindow, popupIframe); 2381 popupIframe.attachEvent("onload", this._popupIframeLoad); 2382 } 2383 else { 2384 popupIframe.onload = popupIframeLoad.bind(this, popupWindow, popupIframe); 2385 } 2386 } 2387 }; 2388 2389 /** 2390 * Returns true if editor content is modified 2391 */ 2392 ZmHtmlEditor.prototype.isDirty = function(){ 2393 if( this._mode === Dwt.HTML ){ 2394 var editor = this.getEditor(); 2395 if (editor) { 2396 return editor.isDirty(); 2397 } 2398 } 2399 return false; 2400 }; 2401 2402 /** 2403 * Mark the editor content as unmodified; e.g. as freshly saved. 2404 */ 2405 ZmHtmlEditor.prototype.clearDirty = function(){ 2406 var ed = this.getEditor(); 2407 if (ed) { 2408 this.getEditor().isNotDirty = true; 2409 } 2410 }; 2411 2412 /** 2413 * Listen for change in fontfamily, fontsize, fontcolor, direction and showing compose direction buttons preference and update the corresponding one. 2414 */ 2415 ZmHtmlEditor.prototype._settingChangeListener = function(ev) { 2416 if (ev.type != ZmEvent.S_SETTING) { return; } 2417 2418 var id = ev.source.id, 2419 editor, 2420 body, 2421 textArea, 2422 direction, 2423 showDirectionButtons, 2424 ltrButton; 2425 2426 if (id === ZmSetting.COMPOSE_INIT_DIRECTION) { 2427 textArea = this.getContentField(); 2428 direction = appCtxt.get(ZmSetting.COMPOSE_INIT_DIRECTION); 2429 if (direction === ZmSetting.RTL) { 2430 textArea.setAttribute("dir", ZmSetting.RTL); 2431 } 2432 else{ 2433 textArea.removeAttribute("dir"); 2434 } 2435 } 2436 2437 editor = this.getEditor(); 2438 body = editor ? editor.getBody() : null; 2439 if(!body) 2440 return; 2441 2442 if (id === ZmSetting.COMPOSE_INIT_FONT_FAMILY) { 2443 body.style.fontFamily = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_FAMILY); 2444 } 2445 else if (id === ZmSetting.COMPOSE_INIT_FONT_SIZE) { 2446 body.style.fontSize = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_SIZE); 2447 } 2448 else if (id === ZmSetting.COMPOSE_INIT_FONT_COLOR) { 2449 body.style.color = appCtxt.get(ZmSetting.COMPOSE_INIT_FONT_COLOR); 2450 } 2451 else if (id === ZmSetting.SHOW_COMPOSE_DIRECTION_BUTTONS) { 2452 showDirectionButtons = appCtxt.get(ZmSetting.SHOW_COMPOSE_DIRECTION_BUTTONS); 2453 ltrButton = this.getToolbarButton("ltr", editor).parentNode; 2454 if (ltrButton) { 2455 Dwt.setVisible(ltrButton, showDirectionButtons); 2456 Dwt.setVisible(ltrButton.previousSibling, showDirectionButtons); 2457 } 2458 Dwt.setVisible(this.getToolbarButton("rtl", editor).parentNode, showDirectionButtons); 2459 } 2460 else if (id === ZmSetting.COMPOSE_INIT_DIRECTION) { 2461 if (direction === ZmSetting.RTL) { 2462 body.dir = ZmSetting.RTL; 2463 } 2464 else{ 2465 body.removeAttribute("dir"); 2466 } 2467 } 2468 editor.nodeChanged && editor.nodeChanged();//update the toolbar state 2469 }; 2470 2471 /** 2472 * This will be fired after every tinymce menu open. Listen for outside events happening in ZCS 2473 * 2474 * @param {menu} tinymce menu object 2475 */ 2476 ZmHtmlEditor.onShowMenu = 2477 function(menu) { 2478 if (menu && menu._visible) { 2479 var omemParams = { 2480 id: "ZmHtmlEditor" + menu._id, 2481 elementId: menu._id, 2482 outsideListener: menu.hide.bind(menu) 2483 }; 2484 appCtxt.getOutsideMouseEventMgr().startListening(omemParams); 2485 } 2486 }; 2487 2488 /** 2489 * This will be fired after every tinymce menu hide. Removing the outside event listener registered in onShowMenu 2490 * 2491 * @param {menu} tinymce menu object 2492 */ 2493 ZmHtmlEditor.onHideMenu = 2494 function(menu) { 2495 if (menu && !menu._visible) { 2496 var omemParams = { 2497 id: "ZmHtmlEditor" + menu._id, 2498 elementId: menu._id 2499 }; 2500 appCtxt.getOutsideMouseEventMgr().stopListening(omemParams); 2501 } 2502 }; 2503 2504 /* 2505 * TinyMCE paste Callback function to execute after the contents has been converted into a DOM structure. 2506 */ 2507 ZmHtmlEditor.prototype.pastePostProcess = 2508 function(ev) { 2509 if (!ev || !ev.node || !ev.target || ev.node.children.length === 0) { 2510 return; 2511 } 2512 2513 var editor = ev.target, tables = editor.dom.select("TABLE", ev.node); 2514 2515 // Add a border to all tables in the pasted content 2516 for (var i = 0; i < tables.length; i++) { 2517 var table = tables[i]; 2518 // set the table border as 1 if it is 0 or unset 2519 if (table && (table.border === "0" || table.border === "")) { 2520 table.border = 1; 2521 } 2522 } 2523 2524 // does any child have a 'float' style? 2525 var hasFloats = editor.dom.select('*', ev.node).some(function(node) { 2526 return node.style['float']; 2527 }); 2528 2529 // If the pasted content contains a table then append a DIV so 2530 // that focus can be set outside the table, and to prevent any floats from 2531 // overlapping other elements 2532 if (hasFloats || tables.length > 0) { 2533 var div = editor.getDoc().createElement("DIV"); 2534 div.style.clear = 'both'; 2535 ev.node.appendChild(div); 2536 } 2537 2538 // Find all paragraphs in the pasted content and set the margin to 0 2539 var paragraphs = editor.dom.select("p", ev.node); 2540 2541 for (var i = 0; i < paragraphs.length; i++) { 2542 editor.dom.setStyle(paragraphs[i], "margin", "0"); 2543 } 2544 }; 2545 2546 ZmHtmlEditor.prototype._getTabGroup = function() { 2547 if (!this.__tabGroup) { 2548 this.__tabGroup = new DwtTabGroup(this.toString()); 2549 } 2550 return this.__tabGroup; 2551 }; 2552 2553 ZmHtmlEditor.prototype.getTabGroupMember = function() { 2554 var tabGroup = this._getTabGroup(); 2555 this._setupTabGroup(tabGroup); 2556 2557 return tabGroup; 2558 }; 2559 2560 /** 2561 * Set up the editor tab group. This is done by having a separate tab group for each compose mode: one for HTML, one 2562 * for TEXT. The current one will be attached to the main tab group. We rebuild the tab group each time to avoid all kinds of issues 2563 * 2564 * @private 2565 */ 2566 ZmHtmlEditor.prototype._setupTabGroup = function(mainTabGroup) { 2567 2568 var mode = this.getMode(); 2569 mainTabGroup = mainTabGroup || this._getTabGroup(); 2570 2571 mainTabGroup.removeAllMembers(); 2572 var modeTabGroup = new DwtTabGroup(this.toString() + '-' + mode); 2573 if (mode === Dwt.HTML) { 2574 // tab group for HTML has first toolbar button and IFRAME 2575 var firstbutton = this.__getEditorControl('listbox', 'Font Family'); 2576 if (firstbutton) { 2577 modeTabGroup.addMember(firstbutton.getEl()); 2578 } 2579 var iframe = this._getIframeDoc(); 2580 if (iframe) { //iframe not avail first time this is called. But it's fixed subsequently 2581 modeTabGroup.addMember(iframe.body); 2582 } 2583 } 2584 else { 2585 // tab group for TEXT has the TEXTAREA 2586 modeTabGroup.addMember(this.getContentField()); 2587 } 2588 mainTabGroup.addMember(modeTabGroup); 2589 }; 2590 2591 /** 2592 Overriding TinyMCE's default show and hide methods of floatpanel and panelbutton. Notifying ZmHtmlEditor about the menu's show and hide events (useful for hiding the menu when mousdedown event happens outside the editor) 2593 **/ 2594 ZmHtmlEditor.prototype._overrideTinyMCEMethods = function() { 2595 var tinymceUI = tinymce.ui; 2596 if (!tinymceUI) { 2597 return; 2598 } 2599 2600 var floatPanelPrototype = tinymceUI.FloatPanel && tinymceUI.FloatPanel.prototype; 2601 if (floatPanelPrototype) { 2602 2603 var tinyMCEShow = floatPanelPrototype.show; 2604 floatPanelPrototype.show = function() { 2605 tinyMCEShow.apply(this, arguments); 2606 ZmHtmlEditor.onShowMenu(this); 2607 }; 2608 2609 var tinyMCEHide = floatPanelPrototype.hide; 2610 floatPanelPrototype.hide = function() { 2611 tinyMCEHide.apply(this, arguments); 2612 ZmHtmlEditor.onHideMenu(this); 2613 }; 2614 } 2615 2616 var panelButtonPrototype = tinymceUI.PanelButton && tinymceUI.PanelButton.prototype; 2617 if (panelButtonPrototype) { 2618 var tinyMCEShowPanel = panelButtonPrototype.showPanel; 2619 panelButtonPrototype.showPanel = function() { 2620 var isPanelExist = this.panel; 2621 tinyMCEShowPanel.apply(this, arguments); 2622 //when isPanelExist is true, floatPanelPrototype.show method will be called which will call ZmHtmlEditor.onShowMenu method. 2623 if (!isPanelExist) { 2624 ZmHtmlEditor.onShowMenu(this.panel); 2625 } 2626 } 2627 } 2628 }; 2629 2630 // Returns true if the user is inserting a Tab into the editor (rather than moving focus) 2631 ZmHtmlEditor.isEditorTab = function(ev) { 2632 2633 return appCtxt.get(ZmSetting.TAB_IN_EDITOR) && ev && ev.keyCode === DwtKeyEvent.KEY_TAB && !ev.shiftKey && !DwtKeyMapMgr.hasModifier(ev); 2634 }; 2635