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