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