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