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