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