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