Upgrade to ExtJS 3.2.0 - Released 03/30/2010
[extjs.git] / src / widgets / form / HtmlEditor.js
1 /*!
2  * Ext JS Library 3.2.0
3  * Copyright(c) 2006-2010 Ext JS, Inc.
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();
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){
223             var fontSelectItem = new Ext.Toolbar.Item({
224                autoEl: {
225                     tag:'select',
226                     cls:'x-font-select',
227                     html: this.createFontOptions()
228                }
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         }
341
342         // build the toolbar
343         var tb = new Ext.Toolbar({
344             renderTo: this.wrap.dom.firstChild,
345             items: items
346         });
347
348         if (fontSelectItem) {
349             this.fontSelect = fontSelectItem.el;
350
351             this.mon(this.fontSelect, 'change', function(){
352                 var font = this.fontSelect.dom.value;
353                 this.relayCmd('fontname', font);
354                 this.deferFocus();
355             }, this);
356         }
357
358         // stop form submits
359         this.mon(tb.el, 'click', function(e){
360             e.preventDefault();
361         });
362
363         this.tb = tb;
364         this.tb.doLayout();
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){
381             this.setDesignMode(!readOnly);
382             var bd = this.getEditorBody();
383             if(bd){
384                 bd.style.cursor = this.readOnly ? 'default' : 'text';
385             }
386             this.disableItems(readOnly);
387         }
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      *
395      * Note: IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility
396      */
397     getDocMarkup : function(){
398         var h = Ext.fly(this.iframe).getHeight() - this.iframePad * 2;
399         return String.format('<html><head><style type="text/css">body{border: 0; margin: 0; padding: {0}px; height: {1}px; cursor: text}</style></head><body></body></html>', this.iframePad, h);
400     },
401
402     // private
403     getEditorBody : function(){
404         var doc = this.getDoc();
405         return doc.body || doc.documentElement;
406     },
407
408     // private
409     getDoc : function(){
410         return Ext.isIE ? this.getWin().document : (this.iframe.contentDocument || this.getWin().document);
411     },
412
413     // private
414     getWin : function(){
415         return Ext.isIE ? this.iframe.contentWindow : window.frames[this.iframe.name];
416     },
417
418     // private
419     onRender : function(ct, position){
420         Ext.form.HtmlEditor.superclass.onRender.call(this, ct, position);
421         this.el.dom.style.border = '0 none';
422         this.el.dom.setAttribute('tabIndex', -1);
423         this.el.addClass('x-hidden');
424         if(Ext.isIE){ // fix IE 1px bogus margin
425             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;');
426         }
427         this.wrap = this.el.wrap({
428             cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
429         });
430
431         this.createToolbar(this);
432
433         this.disableItems(true);
434
435         this.tb.doLayout();
436
437         this.createIFrame();
438
439         if(!this.width){
440             var sz = this.el.getSize();
441             this.setSize(sz.width, this.height || sz.height);
442         }
443         this.resizeEl = this.positionEl = this.wrap;
444     },
445
446     createIFrame: function(){
447         var iframe = document.createElement('iframe');
448         iframe.name = Ext.id();
449         iframe.frameBorder = '0';
450         iframe.style.overflow = 'auto';
451
452         this.wrap.dom.appendChild(iframe);
453         this.iframe = iframe;
454
455         this.monitorTask = Ext.TaskMgr.start({
456             run: this.checkDesignMode,
457             scope: this,
458             interval:100
459         });
460     },
461
462     initFrame : function(){
463         Ext.TaskMgr.stop(this.monitorTask);
464         var doc = this.getDoc();
465         this.win = this.getWin();
466
467         doc.open();
468         doc.write(this.getDocMarkup());
469         doc.close();
470
471         var task = { // must defer to wait for browser to be ready
472             run : function(){
473                 var doc = this.getDoc();
474                 if(doc.body || doc.readyState == 'complete'){
475                     Ext.TaskMgr.stop(task);
476                     this.setDesignMode(true);
477                     this.initEditor.defer(10, this);
478                 }
479             },
480             interval : 10,
481             duration:10000,
482             scope: this
483         };
484         Ext.TaskMgr.start(task);
485     },
486
487
488     checkDesignMode : function(){
489         if(this.wrap && this.wrap.dom.offsetWidth){
490             var doc = this.getDoc();
491             if(!doc){
492                 return;
493             }
494             if(!doc.editorInitialized || this.getDesignMode() != 'on'){
495                 this.initFrame();
496             }
497         }
498     },
499
500     /* private
501      * set current design mode. To enable, mode can be true or 'on', off otherwise
502      */
503     setDesignMode : function(mode){
504         var doc ;
505         if(doc = this.getDoc()){
506             if(this.readOnly){
507                 mode = false;
508             }
509             doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off';
510         }
511
512     },
513
514     // private
515     getDesignMode : function(){
516         var doc = this.getDoc();
517         if(!doc){ return ''; }
518         return String(doc.designMode).toLowerCase();
519
520     },
521
522     disableItems: function(disabled){
523         if(this.fontSelect){
524             this.fontSelect.dom.disabled = disabled;
525         }
526         this.tb.items.each(function(item){
527             if(item.getItemId() != 'sourceedit'){
528                 item.setDisabled(disabled);
529             }
530         });
531     },
532
533     // private
534     onResize : function(w, h){
535         Ext.form.HtmlEditor.superclass.onResize.apply(this, arguments);
536         if(this.el && this.iframe){
537             if(Ext.isNumber(w)){
538                 var aw = w - this.wrap.getFrameWidth('lr');
539                 this.el.setWidth(aw);
540                 this.tb.setWidth(aw);
541                 this.iframe.style.width = Math.max(aw, 0) + 'px';
542             }
543             if(Ext.isNumber(h)){
544                 var ah = h - this.wrap.getFrameWidth('tb') - this.tb.el.getHeight();
545                 this.el.setHeight(ah);
546                 this.iframe.style.height = Math.max(ah, 0) + 'px';
547                 var bd = this.getEditorBody();
548                 if(bd){
549                     bd.style.height = Math.max((ah - (this.iframePad*2)), 0) + 'px';
550                 }
551             }
552         }
553     },
554
555     /**
556      * Toggles the editor between standard and source edit mode.
557      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
558      */
559     toggleSourceEdit : function(sourceEditMode){
560         var iframeHeight, elHeight;
561         if(sourceEditMode === undefined){
562             sourceEditMode = !this.sourceEditMode;
563         }
564         this.sourceEditMode = sourceEditMode === true;
565         var btn = this.tb.getComponent('sourceedit');
566
567         if(btn.pressed !== this.sourceEditMode){
568             btn.toggle(this.sourceEditMode);
569             if(!btn.xtbHidden){
570                 return;
571             }
572         }
573         if(this.sourceEditMode){
574             // grab the height of the containing panel before we hide the iframe
575             ls = this.getSize();
576
577             iframeHeight = Ext.get(this.iframe).getHeight();
578
579             this.disableItems(true);
580             this.syncValue();
581             this.iframe.className = 'x-hidden';
582             this.el.removeClass('x-hidden');
583             this.el.dom.removeAttribute('tabIndex');
584             this.el.focus();
585             this.el.dom.style.height = iframeHeight + 'px';
586         }else{
587             elHeight = parseInt(this.el.dom.style.height, 10);
588             if(this.initialized){
589                 this.disableItems(this.readOnly);
590             }
591             this.pushValue();
592             this.iframe.className = '';
593             this.el.addClass('x-hidden');
594             this.el.dom.setAttribute('tabIndex', -1);
595             this.deferFocus();
596
597             this.setSize(ls);
598             this.iframe.style.height = elHeight + 'px';
599         }
600         this.fireEvent('editmodechange', this, this.sourceEditMode);
601     },
602
603     // private used internally
604     createLink : function(){
605         var url = prompt(this.createLinkText, this.defaultLinkValue);
606         if(url && url != 'http:/'+'/'){
607             this.relayCmd('createlink', url);
608         }
609     },
610
611     // private
612     initEvents : function(){
613         this.originalValue = this.getValue();
614     },
615
616     /**
617      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
618      * @method
619      */
620     markInvalid : Ext.emptyFn,
621
622     /**
623      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
624      * @method
625      */
626     clearInvalid : Ext.emptyFn,
627
628     // docs inherit from Field
629     setValue : function(v){
630         Ext.form.HtmlEditor.superclass.setValue.call(this, v);
631         this.pushValue();
632         return this;
633     },
634
635     /**
636      * Protected method that will not generally be called directly. If you need/want
637      * custom HTML cleanup, this is the method you should override.
638      * @param {String} html The HTML to be cleaned
639      * @return {String} The cleaned HTML
640      */
641     cleanHtml: function(html) {
642         html = String(html);
643         if(Ext.isWebKit){ // strip safari nonsense
644             html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
645         }
646
647         /*
648          * Neat little hack. Strips out all the non-digit characters from the default
649          * value and compares it to the character code of the first character in the string
650          * because it can cause encoding issues when posted to the server.
651          */
652         if(html.charCodeAt(0) == this.defaultValue.replace(/\D/g, '')){
653             html = html.substring(1);
654         }
655         return html;
656     },
657
658     /**
659      * Protected method that will not generally be called directly. Syncs the contents
660      * of the editor iframe with the textarea.
661      */
662     syncValue : function(){
663         if(this.initialized){
664             var bd = this.getEditorBody();
665             var html = bd.innerHTML;
666             if(Ext.isWebKit){
667                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
668                 var m = bs.match(/text-align:(.*?);/i);
669                 if(m && m[1]){
670                     html = '<div style="'+m[0]+'">' + html + '</div>';
671                 }
672             }
673             html = this.cleanHtml(html);
674             if(this.fireEvent('beforesync', this, html) !== false){
675                 this.el.dom.value = html;
676                 this.fireEvent('sync', this, html);
677             }
678         }
679     },
680
681     //docs inherit from Field
682     getValue : function() {
683         this[this.sourceEditMode ? 'pushValue' : 'syncValue']();
684         return Ext.form.HtmlEditor.superclass.getValue.call(this);
685     },
686
687     /**
688      * Protected method that will not generally be called directly. Pushes the value of the textarea
689      * into the iframe editor.
690      */
691     pushValue : function(){
692         if(this.initialized){
693             var v = this.el.dom.value;
694             if(!this.activated && v.length < 1){
695                 v = this.defaultValue;
696             }
697             if(this.fireEvent('beforepush', this, v) !== false){
698                 this.getEditorBody().innerHTML = v;
699                 if(Ext.isGecko){
700                     // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8
701                     this.setDesignMode(false);  //toggle off first
702
703                 }
704                 this.setDesignMode(true);
705                 this.fireEvent('push', this, v);
706             }
707
708         }
709     },
710
711     // private
712     deferFocus : function(){
713         this.focus.defer(10, this);
714     },
715
716     // docs inherit from Field
717     focus : function(){
718         if(this.win && !this.sourceEditMode){
719             this.win.focus();
720         }else{
721             this.el.focus();
722         }
723     },
724
725     // private
726     initEditor : function(){
727         //Destroying the component during/before initEditor can cause issues.
728         try{
729             var dbody = this.getEditorBody(),
730                 ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color'),
731                 doc,
732                 fn;
733
734             ss['background-attachment'] = 'fixed'; // w3c
735             dbody.bgProperties = 'fixed'; // ie
736
737             Ext.DomHelper.applyStyles(dbody, ss);
738
739             doc = this.getDoc();
740
741             if(doc){
742                 try{
743                     Ext.EventManager.removeAll(doc);
744                 }catch(e){}
745             }
746
747             /*
748              * We need to use createDelegate here, because when using buffer, the delayed task is added
749              * as a property to the function. When the listener is removed, the task is deleted from the function.
750              * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors
751              * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function.
752              */
753             fn = this.onEditorEvent.createDelegate(this);
754             Ext.EventManager.on(doc, {
755                 mousedown: fn,
756                 dblclick: fn,
757                 click: fn,
758                 keyup: fn,
759                 buffer:100
760             });
761
762             if(Ext.isGecko){
763                 Ext.EventManager.on(doc, 'keypress', this.applyCommand, this);
764             }
765             if(Ext.isIE || Ext.isWebKit || Ext.isOpera){
766                 Ext.EventManager.on(doc, 'keydown', this.fixKeys, this);
767             }
768             doc.editorInitialized = true;
769             this.initialized = true;
770             this.pushValue();
771             this.setReadOnly(this.readOnly);
772             this.fireEvent('initialize', this);
773         }catch(e){}
774     },
775
776     // private
777     onDestroy : function(){
778         if(this.monitorTask){
779             Ext.TaskMgr.stop(this.monitorTask);
780         }
781         if(this.rendered){
782             Ext.destroy(this.tb);
783             var doc = this.getDoc();
784             if(doc){
785                 try{
786                     Ext.EventManager.removeAll(doc);
787                     for (var prop in doc){
788                         delete doc[prop];
789                     }
790                 }catch(e){}
791             }
792             if(this.wrap){
793                 this.wrap.dom.innerHTML = '';
794                 this.wrap.remove();
795             }
796         }
797
798         if(this.el){
799             this.el.removeAllListeners();
800             this.el.remove();
801         }
802         this.purgeListeners();
803     },
804
805     // private
806     onFirstFocus : function(){
807         this.activated = true;
808         this.disableItems(this.readOnly);
809         if(Ext.isGecko){ // prevent silly gecko errors
810             this.win.focus();
811             var s = this.win.getSelection();
812             if(!s.focusNode || s.focusNode.nodeType != 3){
813                 var r = s.getRangeAt(0);
814                 r.selectNodeContents(this.getEditorBody());
815                 r.collapse(true);
816                 this.deferFocus();
817             }
818             try{
819                 this.execCmd('useCSS', true);
820                 this.execCmd('styleWithCSS', false);
821             }catch(e){}
822         }
823         this.fireEvent('activate', this);
824     },
825
826     // private
827     adjustFont: function(btn){
828         var adjust = btn.getItemId() == 'increasefontsize' ? 1 : -1,
829             doc = this.getDoc(),
830             v = parseInt(doc.queryCommandValue('FontSize') || 2, 10);
831         if((Ext.isSafari && !Ext.isSafari2) || Ext.isChrome || Ext.isAir){
832             // Safari 3 values
833             // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px
834             if(v <= 10){
835                 v = 1 + adjust;
836             }else if(v <= 13){
837                 v = 2 + adjust;
838             }else if(v <= 16){
839                 v = 3 + adjust;
840             }else if(v <= 18){
841                 v = 4 + adjust;
842             }else if(v <= 24){
843                 v = 5 + adjust;
844             }else {
845                 v = 6 + adjust;
846             }
847             v = v.constrain(1, 6);
848         }else{
849             if(Ext.isSafari){ // safari
850                 adjust *= 2;
851             }
852             v = Math.max(1, v+adjust) + (Ext.isSafari ? 'px' : 0);
853         }
854         this.execCmd('FontSize', v);
855     },
856
857     // private
858     onEditorEvent : function(e){
859         this.updateToolbar();
860     },
861
862
863     /**
864      * Protected method that will not generally be called directly. It triggers
865      * a toolbar update by reading the markup state of the current selection in the editor.
866      */
867     updateToolbar: function(){
868
869         if(this.readOnly){
870             return;
871         }
872
873         if(!this.activated){
874             this.onFirstFocus();
875             return;
876         }
877
878         var btns = this.tb.items.map,
879             doc = this.getDoc();
880
881         if(this.enableFont && !Ext.isSafari2){
882             var name = (doc.queryCommandValue('FontName')||this.defaultFont).toLowerCase();
883             if(name != this.fontSelect.dom.value){
884                 this.fontSelect.dom.value = name;
885             }
886         }
887         if(this.enableFormat){
888             btns.bold.toggle(doc.queryCommandState('bold'));
889             btns.italic.toggle(doc.queryCommandState('italic'));
890             btns.underline.toggle(doc.queryCommandState('underline'));
891         }
892         if(this.enableAlignments){
893             btns.justifyleft.toggle(doc.queryCommandState('justifyleft'));
894             btns.justifycenter.toggle(doc.queryCommandState('justifycenter'));
895             btns.justifyright.toggle(doc.queryCommandState('justifyright'));
896         }
897         if(!Ext.isSafari2 && this.enableLists){
898             btns.insertorderedlist.toggle(doc.queryCommandState('insertorderedlist'));
899             btns.insertunorderedlist.toggle(doc.queryCommandState('insertunorderedlist'));
900         }
901
902         Ext.menu.MenuMgr.hideAll();
903
904         this.syncValue();
905     },
906
907     // private
908     relayBtnCmd : function(btn){
909         this.relayCmd(btn.getItemId());
910     },
911
912     /**
913      * Executes a Midas editor command on the editor document and performs necessary focus and
914      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
915      * @param {String} cmd The Midas command
916      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
917      */
918     relayCmd : function(cmd, value){
919         (function(){
920             this.focus();
921             this.execCmd(cmd, value);
922             this.updateToolbar();
923         }).defer(10, this);
924     },
925
926     /**
927      * Executes a Midas editor command directly on the editor document.
928      * For visual commands, you should use {@link #relayCmd} instead.
929      * <b>This should only be called after the editor is initialized.</b>
930      * @param {String} cmd The Midas command
931      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
932      */
933     execCmd : function(cmd, value){
934         var doc = this.getDoc();
935         doc.execCommand(cmd, false, value === undefined ? null : value);
936         this.syncValue();
937     },
938
939     // private
940     applyCommand : function(e){
941         if(e.ctrlKey){
942             var c = e.getCharCode(), cmd;
943             if(c > 0){
944                 c = String.fromCharCode(c);
945                 switch(c){
946                     case 'b':
947                         cmd = 'bold';
948                     break;
949                     case 'i':
950                         cmd = 'italic';
951                     break;
952                     case 'u':
953                         cmd = 'underline';
954                     break;
955                 }
956                 if(cmd){
957                     this.win.focus();
958                     this.execCmd(cmd);
959                     this.deferFocus();
960                     e.preventDefault();
961                 }
962             }
963         }
964     },
965
966     /**
967      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
968      * to insert text.
969      * @param {String} text
970      */
971     insertAtCursor : function(text){
972         if(!this.activated){
973             return;
974         }
975         if(Ext.isIE){
976             this.win.focus();
977             var doc = this.getDoc(),
978                 r = doc.selection.createRange();
979             if(r){
980                 r.pasteHTML(text);
981                 this.syncValue();
982                 this.deferFocus();
983             }
984         }else{
985             this.win.focus();
986             this.execCmd('InsertHTML', text);
987             this.deferFocus();
988         }
989     },
990
991     // private
992     fixKeys : function(){ // load time branching for fastest keydown performance
993         if(Ext.isIE){
994             return function(e){
995                 var k = e.getKey(),
996                     doc = this.getDoc(),
997                         r;
998                 if(k == e.TAB){
999                     e.stopEvent();
1000                     r = doc.selection.createRange();
1001                     if(r){
1002                         r.collapse(true);
1003                         r.pasteHTML('&nbsp;&nbsp;&nbsp;&nbsp;');
1004                         this.deferFocus();
1005                     }
1006                 }else if(k == e.ENTER){
1007                     r = doc.selection.createRange();
1008                     if(r){
1009                         var target = r.parentElement();
1010                         if(!target || target.tagName.toLowerCase() != 'li'){
1011                             e.stopEvent();
1012                             r.pasteHTML('<br />');
1013                             r.collapse(false);
1014                             r.select();
1015                         }
1016                     }
1017                 }
1018             };
1019         }else if(Ext.isOpera){
1020             return function(e){
1021                 var k = e.getKey();
1022                 if(k == e.TAB){
1023                     e.stopEvent();
1024                     this.win.focus();
1025                     this.execCmd('InsertHTML','&nbsp;&nbsp;&nbsp;&nbsp;');
1026                     this.deferFocus();
1027                 }
1028             };
1029         }else if(Ext.isWebKit){
1030             return function(e){
1031                 var k = e.getKey();
1032                 if(k == e.TAB){
1033                     e.stopEvent();
1034                     this.execCmd('InsertText','\t');
1035                     this.deferFocus();
1036                 }else if(k == e.ENTER){
1037                     e.stopEvent();
1038                     this.execCmd('InsertHtml','<br /><br />');
1039                     this.deferFocus();
1040                 }
1041              };
1042         }
1043     }(),
1044
1045     /**
1046      * Returns the editor's toolbar. <b>This is only available after the editor has been rendered.</b>
1047      * @return {Ext.Toolbar}
1048      */
1049     getToolbar : function(){
1050         return this.tb;
1051     },
1052
1053     /**
1054      * Object collection of toolbar tooltips for the buttons in the editor. The key
1055      * is the command id associated with that button and the value is a valid QuickTips object.
1056      * For example:
1057 <pre><code>
1058 {
1059     bold : {
1060         title: 'Bold (Ctrl+B)',
1061         text: 'Make the selected text bold.',
1062         cls: 'x-html-editor-tip'
1063     },
1064     italic : {
1065         title: 'Italic (Ctrl+I)',
1066         text: 'Make the selected text italic.',
1067         cls: 'x-html-editor-tip'
1068     },
1069     ...
1070 </code></pre>
1071     * @type Object
1072      */
1073     buttonTips : {
1074         bold : {
1075             title: 'Bold (Ctrl+B)',
1076             text: 'Make the selected text bold.',
1077             cls: 'x-html-editor-tip'
1078         },
1079         italic : {
1080             title: 'Italic (Ctrl+I)',
1081             text: 'Make the selected text italic.',
1082             cls: 'x-html-editor-tip'
1083         },
1084         underline : {
1085             title: 'Underline (Ctrl+U)',
1086             text: 'Underline the selected text.',
1087             cls: 'x-html-editor-tip'
1088         },
1089         increasefontsize : {
1090             title: 'Grow Text',
1091             text: 'Increase the font size.',
1092             cls: 'x-html-editor-tip'
1093         },
1094         decreasefontsize : {
1095             title: 'Shrink Text',
1096             text: 'Decrease the font size.',
1097             cls: 'x-html-editor-tip'
1098         },
1099         backcolor : {
1100             title: 'Text Highlight Color',
1101             text: 'Change the background color of the selected text.',
1102             cls: 'x-html-editor-tip'
1103         },
1104         forecolor : {
1105             title: 'Font Color',
1106             text: 'Change the color of the selected text.',
1107             cls: 'x-html-editor-tip'
1108         },
1109         justifyleft : {
1110             title: 'Align Text Left',
1111             text: 'Align text to the left.',
1112             cls: 'x-html-editor-tip'
1113         },
1114         justifycenter : {
1115             title: 'Center Text',
1116             text: 'Center text in the editor.',
1117             cls: 'x-html-editor-tip'
1118         },
1119         justifyright : {
1120             title: 'Align Text Right',
1121             text: 'Align text to the right.',
1122             cls: 'x-html-editor-tip'
1123         },
1124         insertunorderedlist : {
1125             title: 'Bullet List',
1126             text: 'Start a bulleted list.',
1127             cls: 'x-html-editor-tip'
1128         },
1129         insertorderedlist : {
1130             title: 'Numbered List',
1131             text: 'Start a numbered list.',
1132             cls: 'x-html-editor-tip'
1133         },
1134         createlink : {
1135             title: 'Hyperlink',
1136             text: 'Make the selected text a hyperlink.',
1137             cls: 'x-html-editor-tip'
1138         },
1139         sourceedit : {
1140             title: 'Source Edit',
1141             text: 'Switch to source editing mode.',
1142             cls: 'x-html-editor-tip'
1143         }
1144     }
1145
1146     // hide stuff that is not compatible
1147     /**
1148      * @event blur
1149      * @hide
1150      */
1151     /**
1152      * @event change
1153      * @hide
1154      */
1155     /**
1156      * @event focus
1157      * @hide
1158      */
1159     /**
1160      * @event specialkey
1161      * @hide
1162      */
1163     /**
1164      * @cfg {String} fieldClass @hide
1165      */
1166     /**
1167      * @cfg {String} focusClass @hide
1168      */
1169     /**
1170      * @cfg {String} autoCreate @hide
1171      */
1172     /**
1173      * @cfg {String} inputType @hide
1174      */
1175     /**
1176      * @cfg {String} invalidClass @hide
1177      */
1178     /**
1179      * @cfg {String} invalidText @hide
1180      */
1181     /**
1182      * @cfg {String} msgFx @hide
1183      */
1184     /**
1185      * @cfg {String} validateOnBlur @hide
1186      */
1187     /**
1188      * @cfg {Boolean} allowDomMove  @hide
1189      */
1190     /**
1191      * @cfg {String} applyTo @hide
1192      */
1193     /**
1194      * @cfg {String} autoHeight  @hide
1195      */
1196     /**
1197      * @cfg {String} autoWidth  @hide
1198      */
1199     /**
1200      * @cfg {String} cls  @hide
1201      */
1202     /**
1203      * @cfg {String} disabled  @hide
1204      */
1205     /**
1206      * @cfg {String} disabledClass  @hide
1207      */
1208     /**
1209      * @cfg {String} msgTarget  @hide
1210      */
1211     /**
1212      * @cfg {String} readOnly  @hide
1213      */
1214     /**
1215      * @cfg {String} style  @hide
1216      */
1217     /**
1218      * @cfg {String} validationDelay  @hide
1219      */
1220     /**
1221      * @cfg {String} validationEvent  @hide
1222      */
1223     /**
1224      * @cfg {String} tabIndex  @hide
1225      */
1226     /**
1227      * @property disabled
1228      * @hide
1229      */
1230     /**
1231      * @method applyToMarkup
1232      * @hide
1233      */
1234     /**
1235      * @method disable
1236      * @hide
1237      */
1238     /**
1239      * @method enable
1240      * @hide
1241      */
1242     /**
1243      * @method validate
1244      * @hide
1245      */
1246     /**
1247      * @event valid
1248      * @hide
1249      */
1250     /**
1251      * @method setDisabled
1252      * @hide
1253      */
1254     /**
1255      * @cfg keys
1256      * @hide
1257      */
1258 });
1259 Ext.reg('htmleditor', Ext.form.HtmlEditor);