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