Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / HtmlEditor.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be
17  * automatically hidden when needed. These are noted in the config options where appropriate.
18  *
19  * The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not
20  * enabled by default unless the global {@link Ext.tip.QuickTipManager} singleton is
21  * {@link Ext.tip.QuickTipManager#init initialized}.
22  *
23  * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an
24  * Editor within any element that has display set to 'none' can cause problems in Safari and Firefox due to their
25  * default iframe reloading bugs.
26  *
27  * # Example usage
28  *
29  * Simple example rendered with default options:
30  *
31  *     @example
32  *     Ext.tip.QuickTipManager.init();  // enable tooltips
33  *     Ext.create('Ext.form.HtmlEditor', {
34  *         width: 580,
35  *         height: 250,
36  *         renderTo: Ext.getBody()
37  *     });
38  *
39  * Passed via xtype into a container and with custom options:
40  *
41  *     @example
42  *     Ext.tip.QuickTipManager.init();  // enable tooltips
43  *     new Ext.panel.Panel({
44  *         title: 'HTML Editor',
45  *         renderTo: Ext.getBody(),
46  *         width: 550,
47  *         height: 250,
48  *         frame: true,
49  *         layout: 'fit',
50  *         items: {
51  *             xtype: 'htmleditor',
52  *             enableColors: false,
53  *             enableAlignments: false
54  *         }
55  *     });
56  */
57 Ext.define('Ext.form.field.HtmlEditor', {
58     extend:'Ext.Component',
59     mixins: {
60         labelable: 'Ext.form.Labelable',
61         field: 'Ext.form.field.Field'
62     },
63     alias: 'widget.htmleditor',
64     alternateClassName: 'Ext.form.HtmlEditor',
65     requires: [
66         'Ext.tip.QuickTipManager',
67         'Ext.picker.Color',
68         'Ext.toolbar.Item',
69         'Ext.toolbar.Toolbar',
70         'Ext.util.Format',
71         'Ext.layout.component.field.HtmlEditor'
72     ],
73
74     fieldSubTpl: [
75         '<div id="{cmpId}-toolbarWrap" class="{toolbarWrapCls}"></div>',
76         '<textarea id="{cmpId}-textareaEl" name="{name}" tabIndex="-1" class="{textareaCls}" ',
77             'style="{size}" autocomplete="off"></textarea>',
78         '<iframe id="{cmpId}-iframeEl" name="{iframeName}" frameBorder="0" style="overflow:auto;{size}" src="{iframeSrc}"></iframe>',
79         {
80             compiled: true,
81             disableFormats: true
82         }
83     ],
84
85     /**
86      * @cfg {Boolean} enableFormat
87      * Enable the bold, italic and underline buttons
88      */
89     enableFormat : true,
90     /**
91      * @cfg {Boolean} enableFontSize
92      * Enable the increase/decrease font size buttons
93      */
94     enableFontSize : true,
95     /**
96      * @cfg {Boolean} enableColors
97      * Enable the fore/highlight color buttons
98      */
99     enableColors : true,
100     /**
101      * @cfg {Boolean} enableAlignments
102      * Enable the left, center, right alignment buttons
103      */
104     enableAlignments : true,
105     /**
106      * @cfg {Boolean} enableLists
107      * Enable the bullet and numbered list buttons. Not available in Safari.
108      */
109     enableLists : true,
110     /**
111      * @cfg {Boolean} enableSourceEdit
112      * Enable the switch to source edit button. Not available in Safari.
113      */
114     enableSourceEdit : true,
115     /**
116      * @cfg {Boolean} enableLinks
117      * Enable the create link button. Not available in Safari.
118      */
119     enableLinks : true,
120     /**
121      * @cfg {Boolean} enableFont
122      * Enable font selection. Not available in Safari.
123      */
124     enableFont : true,
125     /**
126      * @cfg {String} createLinkText
127      * The default text for the create link prompt
128      */
129     createLinkText : 'Please enter the URL for the link:',
130     /**
131      * @cfg {String} [defaultLinkValue='http://']
132      * The default value for the create link prompt
133      */
134     defaultLinkValue : 'http:/'+'/',
135     /**
136      * @cfg {String[]} fontFamilies
137      * An array of available font families
138      */
139     fontFamilies : [
140         'Arial',
141         'Courier New',
142         'Tahoma',
143         'Times New Roman',
144         'Verdana'
145     ],
146     defaultFont: 'tahoma',
147     /**
148      * @cfg {String} defaultValue
149      * A default value to be put into the editor to resolve focus issues (defaults to (Non-breaking space) in Opera
150      * and IE6, ​(Zero-width space) in all other browsers).
151      */
152     defaultValue: (Ext.isOpera || Ext.isIE6) ? '&#160;' : '&#8203;',
153
154     fieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap',
155
156     componentLayout: 'htmleditor',
157
158     // private properties
159     initialized : false,
160     activated : false,
161     sourceEditMode : false,
162     iframePad:3,
163     hideMode:'offsets',
164
165     maskOnDisable: true,
166
167     // private
168     initComponent : function(){
169         var me = this;
170
171         me.addEvents(
172             /**
173              * @event initialize
174              * Fires when the editor is fully initialized (including the iframe)
175              * @param {Ext.form.field.HtmlEditor} this
176              */
177             'initialize',
178             /**
179              * @event activate
180              * Fires when the editor is first receives the focus. Any insertion must wait until after this event.
181              * @param {Ext.form.field.HtmlEditor} this
182              */
183             'activate',
184              /**
185              * @event beforesync
186              * Fires before the textarea is updated with content from the editor iframe. Return false to cancel the
187              * sync.
188              * @param {Ext.form.field.HtmlEditor} this
189              * @param {String} html
190              */
191             'beforesync',
192              /**
193              * @event beforepush
194              * Fires before the iframe editor is updated with content from the textarea. Return false to cancel the
195              * push.
196              * @param {Ext.form.field.HtmlEditor} this
197              * @param {String} html
198              */
199             'beforepush',
200              /**
201              * @event sync
202              * Fires when the textarea is updated with content from the editor iframe.
203              * @param {Ext.form.field.HtmlEditor} this
204              * @param {String} html
205              */
206             'sync',
207              /**
208              * @event push
209              * Fires when the iframe editor is updated with content from the textarea.
210              * @param {Ext.form.field.HtmlEditor} this
211              * @param {String} html
212              */
213             'push',
214              /**
215              * @event editmodechange
216              * Fires when the editor switches edit modes
217              * @param {Ext.form.field.HtmlEditor} this
218              * @param {Boolean} sourceEdit True if source edit, false if standard editing.
219              */
220             'editmodechange'
221         );
222
223         me.callParent(arguments);
224
225         // Init mixins
226         me.initLabelable();
227         me.initField();
228     },
229
230     /**
231      * Called when the editor creates its toolbar. Override this method if you need to
232      * add custom toolbar buttons.
233      * @param {Ext.form.field.HtmlEditor} editor
234      * @protected
235      */
236     createToolbar : function(editor){
237         var me = this,
238             items = [],
239             tipsEnabled = Ext.tip.QuickTipManager && Ext.tip.QuickTipManager.isEnabled(),
240             baseCSSPrefix = Ext.baseCSSPrefix,
241             fontSelectItem, toolbar, undef;
242
243         function btn(id, toggle, handler){
244             return {
245                 itemId : id,
246                 cls : baseCSSPrefix + 'btn-icon',
247                 iconCls: baseCSSPrefix + 'edit-'+id,
248                 enableToggle:toggle !== false,
249                 scope: editor,
250                 handler:handler||editor.relayBtnCmd,
251                 clickEvent:'mousedown',
252                 tooltip: tipsEnabled ? editor.buttonTips[id] || undef : undef,
253                 overflowText: editor.buttonTips[id].title || undef,
254                 tabIndex:-1
255             };
256         }
257
258
259         if (me.enableFont && !Ext.isSafari2) {
260             fontSelectItem = Ext.widget('component', {
261                 renderTpl: [
262                     '<select id="{id}-selectEl" class="{cls}">',
263                         '<tpl for="fonts">',
264                             '<option value="{[values.toLowerCase()]}" style="font-family:{.}"<tpl if="values.toLowerCase()==parent.defaultFont"> selected</tpl>>{.}</option>',
265                         '</tpl>',
266                     '</select>'
267                 ],
268                 renderData: {
269                     cls: baseCSSPrefix + 'font-select',
270                     fonts: me.fontFamilies,
271                     defaultFont: me.defaultFont
272                 },
273                 childEls: ['selectEl'],
274                 onDisable: function() {
275                     var selectEl = this.selectEl;
276                     if (selectEl) {
277                         selectEl.dom.disabled = true;
278                     }
279                     Ext.Component.superclass.onDisable.apply(this, arguments);
280                 },
281                 onEnable: function() {
282                     var selectEl = this.selectEl;
283                     if (selectEl) {
284                         selectEl.dom.disabled = false;
285                     }
286                     Ext.Component.superclass.onEnable.apply(this, arguments);
287                 }
288             });
289
290             items.push(
291                 fontSelectItem,
292                 '-'
293             );
294         }
295
296         if (me.enableFormat) {
297             items.push(
298                 btn('bold'),
299                 btn('italic'),
300                 btn('underline')
301             );
302         }
303
304         if (me.enableFontSize) {
305             items.push(
306                 '-',
307                 btn('increasefontsize', false, me.adjustFont),
308                 btn('decreasefontsize', false, me.adjustFont)
309             );
310         }
311
312         if (me.enableColors) {
313             items.push(
314                 '-', {
315                     itemId: 'forecolor',
316                     cls: baseCSSPrefix + 'btn-icon',
317                     iconCls: baseCSSPrefix + 'edit-forecolor',
318                     overflowText: editor.buttonTips.forecolor.title,
319                     tooltip: tipsEnabled ? editor.buttonTips.forecolor || undef : undef,
320                     tabIndex:-1,
321                     menu : Ext.widget('menu', {
322                         plain: true,
323                         items: [{
324                             xtype: 'colorpicker',
325                             allowReselect: true,
326                             focus: Ext.emptyFn,
327                             value: '000000',
328                             plain: true,
329                             clickEvent: 'mousedown',
330                             handler: function(cp, color) {
331                                 me.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
332                                 me.deferFocus();
333                                 this.up('menu').hide();
334                             }
335                         }]
336                     })
337                 }, {
338                     itemId: 'backcolor',
339                     cls: baseCSSPrefix + 'btn-icon',
340                     iconCls: baseCSSPrefix + 'edit-backcolor',
341                     overflowText: editor.buttonTips.backcolor.title,
342                     tooltip: tipsEnabled ? editor.buttonTips.backcolor || undef : undef,
343                     tabIndex:-1,
344                     menu : Ext.widget('menu', {
345                         plain: true,
346                         items: [{
347                             xtype: 'colorpicker',
348                             focus: Ext.emptyFn,
349                             value: 'FFFFFF',
350                             plain: true,
351                             allowReselect: true,
352                             clickEvent: 'mousedown',
353                             handler: function(cp, color) {
354                                 if (Ext.isGecko) {
355                                     me.execCmd('useCSS', false);
356                                     me.execCmd('hilitecolor', color);
357                                     me.execCmd('useCSS', true);
358                                     me.deferFocus();
359                                 } else {
360                                     me.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
361                                     me.deferFocus();
362                                 }
363                                 this.up('menu').hide();
364                             }
365                         }]
366                     })
367                 }
368             );
369         }
370
371         if (me.enableAlignments) {
372             items.push(
373                 '-',
374                 btn('justifyleft'),
375                 btn('justifycenter'),
376                 btn('justifyright')
377             );
378         }
379
380         if (!Ext.isSafari2) {
381             if (me.enableLinks) {
382                 items.push(
383                     '-',
384                     btn('createlink', false, me.createLink)
385                 );
386             }
387
388             if (me.enableLists) {
389                 items.push(
390                     '-',
391                     btn('insertorderedlist'),
392                     btn('insertunorderedlist')
393                 );
394             }
395             if (me.enableSourceEdit) {
396                 items.push(
397                     '-',
398                     btn('sourceedit', true, function(btn){
399                         me.toggleSourceEdit(!me.sourceEditMode);
400                     })
401                 );
402             }
403         }
404
405         // build the toolbar
406         toolbar = Ext.widget('toolbar', {
407             renderTo: me.toolbarWrap,
408             enableOverflow: true,
409             items: items
410         });
411
412         if (fontSelectItem) {
413             me.fontSelect = fontSelectItem.selectEl;
414
415             me.mon(me.fontSelect, 'change', function(){
416                 me.relayCmd('fontname', me.fontSelect.dom.value);
417                 me.deferFocus();
418             });
419         }
420
421         // stop form submits
422         me.mon(toolbar.el, 'click', function(e){
423             e.preventDefault();
424         });
425
426         me.toolbar = toolbar;
427     },
428
429     onDisable: function() {
430         this.bodyEl.mask();
431         this.callParent(arguments);
432     },
433
434     onEnable: function() {
435         this.bodyEl.unmask();
436         this.callParent(arguments);
437     },
438
439     /**
440      * Sets the read only state of this field.
441      * @param {Boolean} readOnly Whether the field should be read only.
442      */
443     setReadOnly: function(readOnly) {
444         var me = this,
445             textareaEl = me.textareaEl,
446             iframeEl = me.iframeEl,
447             body;
448
449         me.readOnly = readOnly;
450
451         if (textareaEl) {
452             textareaEl.dom.readOnly = readOnly;
453         }
454
455         if (me.initialized) {
456             body = me.getEditorBody();
457             if (Ext.isIE) {
458                 // Hide the iframe while setting contentEditable so it doesn't grab focus
459                 iframeEl.setDisplayed(false);
460                 body.contentEditable = !readOnly;
461                 iframeEl.setDisplayed(true);
462             } else {
463                 me.setDesignMode(!readOnly);
464             }
465             if (body) {
466                 body.style.cursor = readOnly ? 'default' : 'text';
467             }
468             me.disableItems(readOnly);
469         }
470     },
471
472     /**
473      * Called when the editor initializes the iframe with HTML contents. Override this method if you
474      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
475      *
476      * **Note:** IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility.
477      * Also note that forcing IE7 mode works when the page is loaded normally, but if you are using IE's Web
478      * Developer Tools to manually set the document mode, that will take precedence and override what this
479      * code sets by default. This can be confusing when developing, but is not a user-facing issue.
480      * @protected
481      */
482     getDocMarkup: function() {
483         var me = this,
484             h = me.iframeEl.getHeight() - me.iframePad * 2;
485         return Ext.String.format('<html><head><style type="text/css">body{border:0;margin:0;padding:{0}px;height:{1}px;box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;cursor:text}</style></head><body></body></html>', me.iframePad, h);
486     },
487
488     // private
489     getEditorBody: function() {
490         var doc = this.getDoc();
491         return doc.body || doc.documentElement;
492     },
493
494     // private
495     getDoc: function() {
496         return (!Ext.isIE && this.iframeEl.dom.contentDocument) || this.getWin().document;
497     },
498
499     // private
500     getWin: function() {
501         return Ext.isIE ? this.iframeEl.dom.contentWindow : window.frames[this.iframeEl.dom.name];
502     },
503
504     // private
505     onRender: function() {
506         var me = this;
507
508         me.onLabelableRender();
509
510         me.addChildEls('toolbarWrap', 'iframeEl', 'textareaEl');
511
512         me.callParent(arguments);
513
514         me.textareaEl.dom.value = me.value || '';
515
516         // Start polling for when the iframe document is ready to be manipulated
517         me.monitorTask = Ext.TaskManager.start({
518             run: me.checkDesignMode,
519             scope: me,
520             interval:100
521         });
522
523         me.createToolbar(me);
524         me.disableItems(true);
525     },
526
527     initRenderTpl: function() {
528         var me = this;
529         if (!me.hasOwnProperty('renderTpl')) {
530             me.renderTpl = me.getTpl('labelableRenderTpl');
531         }
532         return me.callParent();
533     },
534
535     initRenderData: function() {
536         return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
537     },
538
539     getSubTplData: function() {
540         var cssPrefix = Ext.baseCSSPrefix;
541         return {
542             cmpId: this.id,
543             id: this.getInputId(),
544             toolbarWrapCls: cssPrefix + 'html-editor-tb',
545             textareaCls: cssPrefix + 'hidden',
546             iframeName: Ext.id(),
547             iframeSrc: Ext.SSL_SECURE_URL,
548             size: 'height:100px;'
549         };
550     },
551
552     getSubTplMarkup: function() {
553         var data = this.getSubTplData();
554         return this.getTpl('fieldSubTpl').apply(data);
555     },
556
557     getBodyNaturalWidth: function() {
558         return 565;
559     },
560
561     initFrameDoc: function() {
562         var me = this,
563             doc, task;
564
565         Ext.TaskManager.stop(me.monitorTask);
566
567         doc = me.getDoc();
568         me.win = me.getWin();
569
570         doc.open();
571         doc.write(me.getDocMarkup());
572         doc.close();
573
574         task = { // must defer to wait for browser to be ready
575             run: function() {
576                 var doc = me.getDoc();
577                 if (doc.body || doc.readyState === 'complete') {
578                     Ext.TaskManager.stop(task);
579                     me.setDesignMode(true);
580                     Ext.defer(me.initEditor, 10, me);
581                 }
582             },
583             interval : 10,
584             duration:10000,
585             scope: me
586         };
587         Ext.TaskManager.start(task);
588     },
589
590     checkDesignMode: function() {
591         var me = this,
592             doc = me.getDoc();
593         if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) {
594             me.initFrameDoc();
595         }
596     },
597
598     /**
599      * @private
600      * Sets current design mode. To enable, mode can be true or 'on', off otherwise
601      */
602     setDesignMode: function(mode) {
603         var me = this,
604             doc = me.getDoc();
605         if (doc) {
606             if (me.readOnly) {
607                 mode = false;
608             }
609             doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off';
610         }
611     },
612
613     // private
614     getDesignMode: function() {
615         var doc = this.getDoc();
616         return !doc ? '' : String(doc.designMode).toLowerCase();
617     },
618
619     disableItems: function(disabled) {
620         this.getToolbar().items.each(function(item){
621             if(item.getItemId() !== 'sourceedit'){
622                 item.setDisabled(disabled);
623             }
624         });
625     },
626
627     /**
628      * Toggles the editor between standard and source edit mode.
629      * @param {Boolean} sourceEditMode (optional) True for source edit, false for standard
630      */
631     toggleSourceEdit: function(sourceEditMode) {
632         var me = this,
633             iframe = me.iframeEl,
634             textarea = me.textareaEl,
635             hiddenCls = Ext.baseCSSPrefix + 'hidden',
636             btn = me.getToolbar().getComponent('sourceedit');
637
638         if (!Ext.isBoolean(sourceEditMode)) {
639             sourceEditMode = !me.sourceEditMode;
640         }
641         me.sourceEditMode = sourceEditMode;
642
643         if (btn.pressed !== sourceEditMode) {
644             btn.toggle(sourceEditMode);
645         }
646         if (sourceEditMode) {
647             me.disableItems(true);
648             me.syncValue();
649             iframe.addCls(hiddenCls);
650             textarea.removeCls(hiddenCls);
651             textarea.dom.removeAttribute('tabIndex');
652             textarea.focus();
653         }
654         else {
655             if (me.initialized) {
656                 me.disableItems(me.readOnly);
657             }
658             me.pushValue();
659             iframe.removeCls(hiddenCls);
660             textarea.addCls(hiddenCls);
661             textarea.dom.setAttribute('tabIndex', -1);
662             me.deferFocus();
663         }
664         me.fireEvent('editmodechange', me, sourceEditMode);
665         me.doComponentLayout();
666     },
667
668     // private used internally
669     createLink : function() {
670         var url = prompt(this.createLinkText, this.defaultLinkValue);
671         if (url && url !== 'http:/'+'/') {
672             this.relayCmd('createlink', url);
673         }
674     },
675
676     clearInvalid: Ext.emptyFn,
677
678     // docs inherit from Field
679     setValue: function(value) {
680         var me = this,
681             textarea = me.textareaEl;
682         me.mixins.field.setValue.call(me, value);
683         if (value === null || value === undefined) {
684             value = '';
685         }
686         if (textarea) {
687             textarea.dom.value = value;
688         }
689         me.pushValue();
690         return this;
691     },
692
693     /**
694      * If you need/want custom HTML cleanup, this is the method you should override.
695      * @param {String} html The HTML to be cleaned
696      * @return {String} The cleaned HTML
697      * @protected
698      */
699     cleanHtml: function(html) {
700         html = String(html);
701         if (Ext.isWebKit) { // strip safari nonsense
702             html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
703         }
704
705         /*
706          * Neat little hack. Strips out all the non-digit characters from the default
707          * value and compares it to the character code of the first character in the string
708          * because it can cause encoding issues when posted to the server.
709          */
710         if (html.charCodeAt(0) === this.defaultValue.replace(/\D/g, '')) {
711             html = html.substring(1);
712         }
713         return html;
714     },
715
716     /**
717      * Syncs the contents of the editor iframe with the textarea.
718      * @protected
719      */
720     syncValue : function(){
721         var me = this,
722             body, html, bodyStyle, match;
723         if (me.initialized) {
724             body = me.getEditorBody();
725             html = body.innerHTML;
726             if (Ext.isWebKit) {
727                 bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element!
728                 match = bodyStyle.match(/text-align:(.*?);/i);
729                 if (match && match[1]) {
730                     html = '<div style="' + match[0] + '">' + html + '</div>';
731                 }
732             }
733             html = me.cleanHtml(html);
734             if (me.fireEvent('beforesync', me, html) !== false) {
735                 me.textareaEl.dom.value = html;
736                 me.fireEvent('sync', me, html);
737             }
738         }
739     },
740
741     //docs inherit from Field
742     getValue : function() {
743         var me = this,
744             value;
745         if (!me.sourceEditMode) {
746             me.syncValue();
747         }
748         value = me.rendered ? me.textareaEl.dom.value : me.value;
749         me.value = value;
750         return value;
751     },
752
753     /**
754      * Pushes the value of the textarea into the iframe editor.
755      * @protected
756      */
757     pushValue: function() {
758         var me = this,
759             v;
760         if(me.initialized){
761             v = me.textareaEl.dom.value || '';
762             if (!me.activated && v.length < 1) {
763                 v = me.defaultValue;
764             }
765             if (me.fireEvent('beforepush', me, v) !== false) {
766                 me.getEditorBody().innerHTML = v;
767                 if (Ext.isGecko) {
768                     // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8
769                     me.setDesignMode(false);  //toggle off first
770                     me.setDesignMode(true);
771                 }
772                 me.fireEvent('push', me, v);
773             }
774         }
775     },
776
777     // private
778     deferFocus : function(){
779          this.focus(false, true);
780     },
781
782     getFocusEl: function() {
783         var me = this,
784             win = me.win;
785         return win && !me.sourceEditMode ? win : me.textareaEl;
786     },
787
788     // private
789     initEditor : function(){
790         //Destroying the component during/before initEditor can cause issues.
791         try {
792             var me = this,
793                 dbody = me.getEditorBody(),
794                 ss = me.textareaEl.getStyles('font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color'),
795                 doc,
796                 fn;
797
798             ss['background-attachment'] = 'fixed'; // w3c
799             dbody.bgProperties = 'fixed'; // ie
800
801             Ext.DomHelper.applyStyles(dbody, ss);
802
803             doc = me.getDoc();
804
805             if (doc) {
806                 try {
807                     Ext.EventManager.removeAll(doc);
808                 } catch(e) {}
809             }
810
811             /*
812              * We need to use createDelegate here, because when using buffer, the delayed task is added
813              * as a property to the function. When the listener is removed, the task is deleted from the function.
814              * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors
815              * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function.
816              */
817             fn = Ext.Function.bind(me.onEditorEvent, me);
818             Ext.EventManager.on(doc, {
819                 mousedown: fn,
820                 dblclick: fn,
821                 click: fn,
822                 keyup: fn,
823                 buffer:100
824             });
825
826             // These events need to be relayed from the inner document (where they stop
827             // bubbling) up to the outer document. This has to be done at the DOM level so
828             // the event reaches listeners on elements like the document body. The effected
829             // mechanisms that depend on this bubbling behavior are listed to the right
830             // of the event.
831             fn = me.onRelayedEvent;
832             Ext.EventManager.on(doc, {
833                 mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
834                 mousemove: fn, // window resize drag detection
835                 mouseup: fn,   // window resize termination
836                 click: fn,     // not sure, but just to be safe
837                 dblclick: fn,  // not sure again
838                 scope: me
839             });
840
841             if (Ext.isGecko) {
842                 Ext.EventManager.on(doc, 'keypress', me.applyCommand, me);
843             }
844             if (me.fixKeys) {
845                 Ext.EventManager.on(doc, 'keydown', me.fixKeys, me);
846             }
847
848             // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
849             Ext.EventManager.on(window, 'unload', me.beforeDestroy, me);
850             doc.editorInitialized = true;
851
852             me.initialized = true;
853             me.pushValue();
854             me.setReadOnly(me.readOnly);
855             me.fireEvent('initialize', me);
856         } catch(ex) {
857             // ignore (why?)
858         }
859     },
860
861     // private
862     beforeDestroy : function(){
863         var me = this,
864             monitorTask = me.monitorTask,
865             doc, prop;
866
867         if (monitorTask) {
868             Ext.TaskManager.stop(monitorTask);
869         }
870         if (me.rendered) {
871             try {
872                 doc = me.getDoc();
873                 if (doc) {
874                     Ext.EventManager.removeAll(doc);
875                     for (prop in doc) {
876                         if (doc.hasOwnProperty(prop)) {
877                             delete doc[prop];
878                         }
879                     }
880                 }
881             } catch(e) {
882                 // ignore (why?)
883             }
884             Ext.destroyMembers(me, 'tb', 'toolbarWrap', 'iframeEl', 'textareaEl');
885         }
886         me.callParent();
887     },
888
889     // private
890     onRelayedEvent: function (event) {
891         // relay event from the iframe's document to the document that owns the iframe...
892
893         var iframeEl = this.iframeEl,
894             iframeXY = iframeEl.getXY(),
895             eventXY = event.getXY();
896
897         // the event from the inner document has XY relative to that document's origin,
898         // so adjust it to use the origin of the iframe in the outer document:
899         event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
900
901         event.injectEvent(iframeEl); // blame the iframe for the event...
902
903         event.xy = eventXY; // restore the original XY (just for safety)
904     },
905
906     // private
907     onFirstFocus : function(){
908         var me = this,
909             selection, range;
910         me.activated = true;
911         me.disableItems(me.readOnly);
912         if (Ext.isGecko) { // prevent silly gecko errors
913             me.win.focus();
914             selection = me.win.getSelection();
915             if (!selection.focusNode || selection.focusNode.nodeType !== 3) {
916                 range = selection.getRangeAt(0);
917                 range.selectNodeContents(me.getEditorBody());
918                 range.collapse(true);
919                 me.deferFocus();
920             }
921             try {
922                 me.execCmd('useCSS', true);
923                 me.execCmd('styleWithCSS', false);
924             } catch(e) {
925                 // ignore (why?)
926             }
927         }
928         me.fireEvent('activate', me);
929     },
930
931     // private
932     adjustFont: function(btn) {
933         var adjust = btn.getItemId() === 'increasefontsize' ? 1 : -1,
934             size = this.getDoc().queryCommandValue('FontSize') || '2',
935             isPxSize = Ext.isString(size) && size.indexOf('px') !== -1,
936             isSafari;
937         size = parseInt(size, 10);
938         if (isPxSize) {
939             // Safari 3 values
940             // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px
941             if (size <= 10) {
942                 size = 1 + adjust;
943             }
944             else if (size <= 13) {
945                 size = 2 + adjust;
946             }
947             else if (size <= 16) {
948                 size = 3 + adjust;
949             }
950             else if (size <= 18) {
951                 size = 4 + adjust;
952             }
953             else if (size <= 24) {
954                 size = 5 + adjust;
955             }
956             else {
957                 size = 6 + adjust;
958             }
959             size = Ext.Number.constrain(size, 1, 6);
960         } else {
961             isSafari = Ext.isSafari;
962             if (isSafari) { // safari
963                 adjust *= 2;
964             }
965             size = Math.max(1, size + adjust) + (isSafari ? 'px' : 0);
966         }
967         this.execCmd('FontSize', size);
968     },
969
970     // private
971     onEditorEvent: function(e) {
972         this.updateToolbar();
973     },
974
975     /**
976      * Triggers a toolbar update by reading the markup state of the current selection in the editor.
977      * @protected
978      */
979     updateToolbar: function() {
980         var me = this,
981             btns, doc, name, fontSelect;
982
983         if (me.readOnly) {
984             return;
985         }
986
987         if (!me.activated) {
988             me.onFirstFocus();
989             return;
990         }
991
992         btns = me.getToolbar().items.map;
993         doc = me.getDoc();
994
995         if (me.enableFont && !Ext.isSafari2) {
996             name = (doc.queryCommandValue('FontName') || me.defaultFont).toLowerCase();
997             fontSelect = me.fontSelect.dom;
998             if (name !== fontSelect.value) {
999                 fontSelect.value = name;
1000             }
1001         }
1002
1003         function updateButtons() {
1004             Ext.Array.forEach(Ext.Array.toArray(arguments), function(name) {
1005                 btns[name].toggle(doc.queryCommandState(name));
1006             });
1007         }
1008         if(me.enableFormat){
1009             updateButtons('bold', 'italic', 'underline');
1010         }
1011         if(me.enableAlignments){
1012             updateButtons('justifyleft', 'justifycenter', 'justifyright');
1013         }
1014         if(!Ext.isSafari2 && me.enableLists){
1015             updateButtons('insertorderedlist', 'insertunorderedlist');
1016         }
1017
1018         Ext.menu.Manager.hideAll();
1019
1020         me.syncValue();
1021     },
1022
1023     // private
1024     relayBtnCmd: function(btn) {
1025         this.relayCmd(btn.getItemId());
1026     },
1027
1028     /**
1029      * Executes a Midas editor command on the editor document and performs necessary focus and toolbar updates.
1030      * **This should only be called after the editor is initialized.**
1031      * @param {String} cmd The Midas command
1032      * @param {String/Boolean} [value=null] The value to pass to the command
1033      */
1034     relayCmd: function(cmd, value) {
1035         Ext.defer(function() {
1036             var me = this;
1037             me.focus();
1038             me.execCmd(cmd, value);
1039             me.updateToolbar();
1040         }, 10, this);
1041     },
1042
1043     /**
1044      * Executes a Midas editor command directly on the editor document. For visual commands, you should use
1045      * {@link #relayCmd} instead. **This should only be called after the editor is initialized.**
1046      * @param {String} cmd The Midas command
1047      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
1048      */
1049     execCmd : function(cmd, value){
1050         var me = this,
1051             doc = me.getDoc(),
1052             undef;
1053         doc.execCommand(cmd, false, value === undef ? null : value);
1054         me.syncValue();
1055     },
1056
1057     // private
1058     applyCommand : function(e){
1059         if (e.ctrlKey) {
1060             var me = this,
1061                 c = e.getCharCode(), cmd;
1062             if (c > 0) {
1063                 c = String.fromCharCode(c);
1064                 switch (c) {
1065                     case 'b':
1066                         cmd = 'bold';
1067                     break;
1068                     case 'i':
1069                         cmd = 'italic';
1070                     break;
1071                     case 'u':
1072                         cmd = 'underline';
1073                     break;
1074                 }
1075                 if (cmd) {
1076                     me.win.focus();
1077                     me.execCmd(cmd);
1078                     me.deferFocus();
1079                     e.preventDefault();
1080                 }
1081             }
1082         }
1083     },
1084
1085     /**
1086      * Inserts the passed text at the current cursor position.
1087      * Note: the editor must be initialized and activated to insert text.
1088      * @param {String} text
1089      */
1090     insertAtCursor : function(text){
1091         var me = this,
1092             range;
1093
1094         if (me.activated) {
1095             me.win.focus();
1096             if (Ext.isIE) {
1097                 range = me.getDoc().selection.createRange();
1098                 if (range) {
1099                     range.pasteHTML(text);
1100                     me.syncValue();
1101                     me.deferFocus();
1102                 }
1103             }else{
1104                 me.execCmd('InsertHTML', text);
1105                 me.deferFocus();
1106             }
1107         }
1108     },
1109
1110     // private
1111     fixKeys: function() { // load time branching for fastest keydown performance
1112         if (Ext.isIE) {
1113             return function(e){
1114                 var me = this,
1115                     k = e.getKey(),
1116                     doc = me.getDoc(),
1117                     range, target;
1118                 if (k === e.TAB) {
1119                     e.stopEvent();
1120                     range = doc.selection.createRange();
1121                     if(range){
1122                         range.collapse(true);
1123                         range.pasteHTML('&nbsp;&nbsp;&nbsp;&nbsp;');
1124                         me.deferFocus();
1125                     }
1126                 }
1127                 else if (k === e.ENTER) {
1128                     range = doc.selection.createRange();
1129                     if (range) {
1130                         target = range.parentElement();
1131                         if(!target || target.tagName.toLowerCase() !== 'li'){
1132                             e.stopEvent();
1133                             range.pasteHTML('<br />');
1134                             range.collapse(false);
1135                             range.select();
1136                         }
1137                     }
1138                 }
1139             };
1140         }
1141
1142         if (Ext.isOpera) {
1143             return function(e){
1144                 var me = this;
1145                 if (e.getKey() === e.TAB) {
1146                     e.stopEvent();
1147                     me.win.focus();
1148                     me.execCmd('InsertHTML','&nbsp;&nbsp;&nbsp;&nbsp;');
1149                     me.deferFocus();
1150                 }
1151             };
1152         }
1153
1154         if (Ext.isWebKit) {
1155             return function(e){
1156                 var me = this,
1157                     k = e.getKey();
1158                 if (k === e.TAB) {
1159                     e.stopEvent();
1160                     me.execCmd('InsertText','\t');
1161                     me.deferFocus();
1162                 }
1163                 else if (k === e.ENTER) {
1164                     e.stopEvent();
1165                     me.execCmd('InsertHtml','<br /><br />');
1166                     me.deferFocus();
1167                 }
1168             };
1169         }
1170
1171         return null; // not needed, so null
1172     }(),
1173
1174     /**
1175      * Returns the editor's toolbar. **This is only available after the editor has been rendered.**
1176      * @return {Ext.toolbar.Toolbar}
1177      */
1178     getToolbar : function(){
1179         return this.toolbar;
1180     },
1181
1182     /**
1183      * @property {Object} buttonTips
1184      * Object collection of toolbar tooltips for the buttons in the editor. The key is the command id associated with
1185      * that button and the value is a valid QuickTips object. For example:
1186      *
1187      *     {
1188      *         bold : {
1189      *             title: 'Bold (Ctrl+B)',
1190      *             text: 'Make the selected text bold.',
1191      *             cls: 'x-html-editor-tip'
1192      *         },
1193      *         italic : {
1194      *             title: 'Italic (Ctrl+I)',
1195      *             text: 'Make the selected text italic.',
1196      *             cls: 'x-html-editor-tip'
1197      *         },
1198      *         ...
1199      */
1200     buttonTips : {
1201         bold : {
1202             title: 'Bold (Ctrl+B)',
1203             text: 'Make the selected text bold.',
1204             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1205         },
1206         italic : {
1207             title: 'Italic (Ctrl+I)',
1208             text: 'Make the selected text italic.',
1209             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1210         },
1211         underline : {
1212             title: 'Underline (Ctrl+U)',
1213             text: 'Underline the selected text.',
1214             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1215         },
1216         increasefontsize : {
1217             title: 'Grow Text',
1218             text: 'Increase the font size.',
1219             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1220         },
1221         decreasefontsize : {
1222             title: 'Shrink Text',
1223             text: 'Decrease the font size.',
1224             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1225         },
1226         backcolor : {
1227             title: 'Text Highlight Color',
1228             text: 'Change the background color of the selected text.',
1229             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1230         },
1231         forecolor : {
1232             title: 'Font Color',
1233             text: 'Change the color of the selected text.',
1234             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1235         },
1236         justifyleft : {
1237             title: 'Align Text Left',
1238             text: 'Align text to the left.',
1239             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1240         },
1241         justifycenter : {
1242             title: 'Center Text',
1243             text: 'Center text in the editor.',
1244             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1245         },
1246         justifyright : {
1247             title: 'Align Text Right',
1248             text: 'Align text to the right.',
1249             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1250         },
1251         insertunorderedlist : {
1252             title: 'Bullet List',
1253             text: 'Start a bulleted list.',
1254             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1255         },
1256         insertorderedlist : {
1257             title: 'Numbered List',
1258             text: 'Start a numbered list.',
1259             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1260         },
1261         createlink : {
1262             title: 'Hyperlink',
1263             text: 'Make the selected text a hyperlink.',
1264             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1265         },
1266         sourceedit : {
1267             title: 'Source Edit',
1268             text: 'Switch to source editing mode.',
1269             cls: Ext.baseCSSPrefix + 'html-editor-tip'
1270         }
1271     }
1272
1273     // hide stuff that is not compatible
1274     /**
1275      * @event blur
1276      * @hide
1277      */
1278     /**
1279      * @event change
1280      * @hide
1281      */
1282     /**
1283      * @event focus
1284      * @hide
1285      */
1286     /**
1287      * @event specialkey
1288      * @hide
1289      */
1290     /**
1291      * @cfg {String} fieldCls @hide
1292      */
1293     /**
1294      * @cfg {String} focusCls @hide
1295      */
1296     /**
1297      * @cfg {String} autoCreate @hide
1298      */
1299     /**
1300      * @cfg {String} inputType @hide
1301      */
1302     /**
1303      * @cfg {String} invalidCls @hide
1304      */
1305     /**
1306      * @cfg {String} invalidText @hide
1307      */
1308     /**
1309      * @cfg {String} msgFx @hide
1310      */
1311     /**
1312      * @cfg {Boolean} allowDomMove @hide
1313      */
1314     /**
1315      * @cfg {String} applyTo @hide
1316      */
1317     /**
1318      * @cfg {String} readOnly  @hide
1319      */
1320     /**
1321      * @cfg {String} tabIndex  @hide
1322      */
1323     /**
1324      * @method validate
1325      * @hide
1326      */
1327 });
1328