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