Roo/form/HtmlEditor.js
[roojs1] / Roo / form / HtmlEditor.js
1 //<script type="text/javascript">
2
3 /*
4  * Ext JS Library 1.1.1
5  * Copyright(c) 2006-2007, Ext JS, LLC.
6  * licensing@extjs.com
7  * 
8  * http://www.extjs.com/license
9  */
10  
11  /*
12   * 
13   * Known bugs:
14   * Default CSS appears to render it as fixed text by default (should really be Sans-Serif)
15   * - IE ? - no idea how much works there.
16   * 
17   * 
18   * 
19   */
20  
21
22 /**
23  * @class Ext.form.HtmlEditor
24  * @extends Ext.form.Field
25  * Provides a lightweight HTML Editor component.
26  *
27  * This has been tested on Fireforx / Chrome.. IE may not be so great..
28  * 
29  * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
30  * supported by this editor.</b><br/><br/>
31  * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
32  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
33  */
34 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
35       /**
36      * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
37      */
38     toolbars : false,
39     /**
40      * @cfg {String} createLinkText The default text for the create link prompt
41      */
42     createLinkText : 'Please enter the URL for the link:',
43     /**
44      * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
45      */
46     defaultLinkValue : 'http:/'+'/',
47    
48      /**
49      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
50      *                        Roo.resizable.
51      */
52     resizable : false,
53      /**
54      * @cfg {Number} height (in pixels)
55      */   
56     height: 300,
57    /**
58      * @cfg {Number} width (in pixels)
59      */   
60     width: 500,
61     
62     /**
63      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
64      * 
65      */
66     stylesheets: false,
67     
68     // id of frame..
69     frameId: false,
70     
71     // private properties
72     validationEvent : false,
73     deferHeight: true,
74     initialized : false,
75     activated : false,
76     sourceEditMode : false,
77     onFocus : Roo.emptyFn,
78     iframePad:3,
79     hideMode:'offsets',
80     
81     defaultAutoCreate : { // modified by initCompnoent..
82         tag: "textarea",
83         style:"width:500px;height:300px;",
84         autocomplete: "off"
85     },
86
87     // private
88     initComponent : function(){
89         this.addEvents({
90             /**
91              * @event initialize
92              * Fires when the editor is fully initialized (including the iframe)
93              * @param {HtmlEditor} this
94              */
95             initialize: true,
96             /**
97              * @event activate
98              * Fires when the editor is first receives the focus. Any insertion must wait
99              * until after this event.
100              * @param {HtmlEditor} this
101              */
102             activate: true,
103              /**
104              * @event beforesync
105              * Fires before the textarea is updated with content from the editor iframe. Return false
106              * to cancel the sync.
107              * @param {HtmlEditor} this
108              * @param {String} html
109              */
110             beforesync: true,
111              /**
112              * @event beforepush
113              * Fires before the iframe editor is updated with content from the textarea. Return false
114              * to cancel the push.
115              * @param {HtmlEditor} this
116              * @param {String} html
117              */
118             beforepush: true,
119              /**
120              * @event sync
121              * Fires when the textarea is updated with content from the editor iframe.
122              * @param {HtmlEditor} this
123              * @param {String} html
124              */
125             sync: true,
126              /**
127              * @event push
128              * Fires when the iframe editor is updated with content from the textarea.
129              * @param {HtmlEditor} this
130              * @param {String} html
131              */
132             push: true,
133              /**
134              * @event editmodechange
135              * Fires when the editor switches edit modes
136              * @param {HtmlEditor} this
137              * @param {Boolean} sourceEdit True if source edit, false if standard editing.
138              */
139             editmodechange: true,
140             /**
141              * @event editorevent
142              * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
143              * @param {HtmlEditor} this
144              */
145             editorevent: true
146         });
147         this.defaultAutoCreate =  {
148             tag: "textarea",
149             style:'width: ' + this.width + 'px;height: ' + this.height + 'px;',
150             autocomplete: "off"
151         };
152     },
153
154     /**
155      * Protected method that will not generally be called directly. It
156      * is called when the editor creates its toolbar. Override this method if you need to
157      * add custom toolbar buttons.
158      * @param {HtmlEditor} editor
159      */
160     createToolbar : function(editor){
161         if (!editor.toolbars || !editor.toolbars.length) {
162             editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
163         }
164         
165         for (var i =0 ; i < editor.toolbars.length;i++) {
166             editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
167             editor.toolbars[i].init(editor);
168         }
169          
170         
171     },
172
173     /**
174      * Protected method that will not generally be called directly. It
175      * is called when the editor initializes the iframe with HTML contents. Override this method if you
176      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
177      */
178     getDocMarkup : function(){
179         // body styles..
180         var st = '';
181         if (this.stylesheets === false) {
182             
183             Roo.get(document.head).select('style').each(function(node) {
184                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
185             });
186             
187             Roo.get(document.head).select('link').each(function(node) { 
188                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
189             });
190             
191         } else if (!this.stylesheets.length) {
192                 // simple..
193                 st = '<style type="text/css">' +
194                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
195                    '</style>';
196         } else {
197             Roo.each(this.stylesheets, function(s) {
198                 st += '<link rel="stylesheet" type="text/css" href="' + s +'" />'
199             });
200             
201         }
202         
203         st +=  '<style type="text/css">' +
204             'IMG { cursor: pointer } ' +
205         '</style>';
206
207         
208         return '<html><head>' + st  +
209             //<style type="text/css">' +
210             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
211             //'</style>' +
212             ' </head><body class="roo-htmleditor-body"></body></html>';
213     },
214
215     // private
216     onRender : function(ct, position)
217     {
218         var _t = this;
219         Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
220         this.el.dom.style.border = '0 none';
221         this.el.dom.setAttribute('tabIndex', -1);
222         this.el.addClass('x-hidden');
223         if(Roo.isIE){ // fix IE 1px bogus margin
224             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
225         }
226         this.wrap = this.el.wrap({
227             cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
228         });
229         
230         if (this.resizable) {
231             this.resizeEl = new Roo.Resizable(this.wrap, {
232                 pinned : true,
233                 wrap: true,
234                 dynamic : true,
235                 minHeight : this.height,
236                 height: this.height,
237                 handles : this.resizable,
238                 width: this.width,
239                 listeners : {
240                     resize : function(r, w, h) {
241                         _t.onResize(w,h); // -something
242                     }
243                 }
244             });
245             
246         }
247
248         this.frameId = Roo.id();
249         
250         this.createToolbar(this);
251         
252       
253         
254         var iframe = this.wrap.createChild({
255             tag: 'iframe',
256             id: this.frameId,
257             name: this.frameId,
258             frameBorder : 'no',
259             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
260         }, this.el
261         );
262         
263        // console.log(iframe);
264         //this.wrap.dom.appendChild(iframe);
265
266         this.iframe = iframe.dom;
267
268          this.assignDocWin();
269         
270         this.doc.designMode = 'on';
271        
272         this.doc.open();
273         this.doc.write(this.getDocMarkup());
274         this.doc.close();
275
276         
277         var task = { // must defer to wait for browser to be ready
278             run : function(){
279                 //console.log("run task?" + this.doc.readyState);
280                 this.assignDocWin();
281                 if(this.doc.body || this.doc.readyState == 'complete'){
282                     try {
283                         this.doc.designMode="on";
284                     } catch (e) {
285                         return;
286                     }
287                     Roo.TaskMgr.stop(task);
288                     this.initEditor.defer(10, this);
289                 }
290             },
291             interval : 10,
292             duration:10000,
293             scope: this
294         };
295         Roo.TaskMgr.start(task);
296
297         if(!this.width){
298             this.setSize(this.wrap.getSize());
299         }
300         if (this.resizeEl) {
301             this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
302             // should trigger onReize..
303         }
304     },
305
306     // private
307     onResize : function(w, h)
308     {
309         //Roo.log('resize: ' +w + ',' + h );
310         Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
311         if(this.el && this.iframe){
312             if(typeof w == 'number'){
313                 var aw = w - this.wrap.getFrameWidth('lr');
314                 this.el.setWidth(this.adjustWidth('textarea', aw));
315                 this.iframe.style.width = aw + 'px';
316             }
317             if(typeof h == 'number'){
318                 var tbh = 0;
319                 for (var i =0; i < this.toolbars.length;i++) {
320                     // fixme - ask toolbars for heights?
321                     tbh += this.toolbars[i].tb.el.getHeight();
322                     if (this.toolbars[i].footer) {
323                         tbh += this.toolbars[i].footer.el.getHeight();
324                     }
325                 }
326                 
327                 
328                 
329                 
330                 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
331                 ah -= 5; // knock a few pixes off for look..
332                 this.el.setHeight(this.adjustWidth('textarea', ah));
333                 this.iframe.style.height = ah + 'px';
334                 if(this.doc){
335                     (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
336                 }
337             }
338         }
339     },
340
341     /**
342      * Toggles the editor between standard and source edit mode.
343      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
344      */
345     toggleSourceEdit : function(sourceEditMode){
346         
347         this.sourceEditMode = sourceEditMode === true;
348         
349         if(this.sourceEditMode){
350 //            Roo.log('in');
351 //            Roo.log(this.syncValue());
352             this.syncValue();
353             this.iframe.className = 'x-hidden';
354             this.el.removeClass('x-hidden');
355             this.el.dom.removeAttribute('tabIndex');
356             this.el.focus();
357         }else{
358 //            Roo.log('out')
359 //            Roo.log(this.pushValue()); 
360             this.pushValue();
361             this.iframe.className = '';
362             this.el.addClass('x-hidden');
363             this.el.dom.setAttribute('tabIndex', -1);
364             this.deferFocus();
365         }
366         this.setSize(this.wrap.getSize());
367         this.fireEvent('editmodechange', this, this.sourceEditMode);
368     },
369
370     // private used internally
371     createLink : function(){
372         var url = prompt(this.createLinkText, this.defaultLinkValue);
373         if(url && url != 'http:/'+'/'){
374             this.relayCmd('createlink', url);
375         }
376     },
377
378     // private (for BoxComponent)
379     adjustSize : Roo.BoxComponent.prototype.adjustSize,
380
381     // private (for BoxComponent)
382     getResizeEl : function(){
383         return this.wrap;
384     },
385
386     // private (for BoxComponent)
387     getPositionEl : function(){
388         return this.wrap;
389     },
390
391     // private
392     initEvents : function(){
393         this.originalValue = this.getValue();
394     },
395
396     /**
397      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
398      * @method
399      */
400     markInvalid : Roo.emptyFn,
401     /**
402      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
403      * @method
404      */
405     clearInvalid : Roo.emptyFn,
406
407     setValue : function(v){
408         Roo.form.HtmlEditor.superclass.setValue.call(this, v);
409         this.pushValue();
410     },
411
412     /**
413      * Protected method that will not generally be called directly. If you need/want
414      * custom HTML cleanup, this is the method you should override.
415      * @param {String} html The HTML to be cleaned
416      * return {String} The cleaned HTML
417      */
418     cleanHtml : function(html){
419         html = String(html);
420         if(html.length > 5){
421             if(Roo.isSafari){ // strip safari nonsense
422                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
423             }
424         }
425         if(html == '&nbsp;'){
426             html = '';
427         }
428         return html;
429     },
430
431     /**
432      * Protected method that will not generally be called directly. Syncs the contents
433      * of the editor iframe with the textarea.
434      */
435     syncValue : function(){
436         if(this.initialized){
437             var bd = (this.doc.body || this.doc.documentElement);
438             //this.cleanUpPaste(); -- this is done else where and causes havoc..
439             var html = bd.innerHTML;
440             if(Roo.isSafari){
441                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
442                 var m = bs.match(/text-align:(.*?);/i);
443                 if(m && m[1]){
444                     html = '<div style="'+m[0]+'">' + html + '</div>';
445                 }
446             }
447             html = this.cleanHtml(html);
448             // fix up the special chars.. normaly like back quotes in word...
449             // however we do not want to do this with chinese..
450             html = html.replace(/([\x80-\uffff])/g, function (a, b) {
451                 var cc = b.charCodeAt();
452                 if (
453                     (cc >= 0x4E00 && cc < 0xA000 ) ||
454                     (cc >= 0x3400 && cc < 0x4E00 ) ||
455                     (cc >= 0xf900 && cc < 0xfb00 )
456                 ) {
457                         return b;
458                 }
459                 return "&#"+cc+";" 
460             });
461             if(this.fireEvent('beforesync', this, html) !== false){
462                 this.el.dom.value = html;
463                 this.fireEvent('sync', this, html);
464             }
465         }
466     },
467
468     /**
469      * Protected method that will not generally be called directly. Pushes the value of the textarea
470      * into the iframe editor.
471      */
472     pushValue : function(){
473         if(this.initialized){
474             var v = this.el.dom.value;
475             
476             if(v.length < 1){
477                 v = '&#160;';
478             }
479             
480             if(this.fireEvent('beforepush', this, v) !== false){
481                 var d = (this.doc.body || this.doc.documentElement);
482                 d.innerHTML = v;
483                 this.cleanUpPaste();
484                 this.el.dom.value = d.innerHTML;
485                 this.fireEvent('push', this, v);
486             }
487         }
488     },
489
490     // private
491     deferFocus : function(){
492         this.focus.defer(10, this);
493     },
494
495     // doc'ed in Field
496     focus : function(){
497         if(this.win && !this.sourceEditMode){
498             this.win.focus();
499         }else{
500             this.el.focus();
501         }
502     },
503     
504     assignDocWin: function()
505     {
506         var iframe = this.iframe;
507         
508          if(Roo.isIE){
509             this.doc = iframe.contentWindow.document;
510             this.win = iframe.contentWindow;
511         } else {
512             if (!Roo.get(this.frameId)) {
513                 return;
514             }
515             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
516             this.win = Roo.get(this.frameId).dom.contentWindow;
517         }
518     },
519     
520     // private
521     initEditor : function(){
522         //console.log("INIT EDITOR");
523         this.assignDocWin();
524         
525         
526         
527         this.doc.designMode="on";
528         this.doc.open();
529         this.doc.write(this.getDocMarkup());
530         this.doc.close();
531         
532         var dbody = (this.doc.body || this.doc.documentElement);
533         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
534         // this copies styles from the containing element into thsi one..
535         // not sure why we need all of this..
536         var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
537         ss['background-attachment'] = 'fixed'; // w3c
538         dbody.bgProperties = 'fixed'; // ie
539         Roo.DomHelper.applyStyles(dbody, ss);
540         Roo.EventManager.on(this.doc, {
541             //'mousedown': this.onEditorEvent,
542             'mouseup': this.onEditorEvent,
543             'dblclick': this.onEditorEvent,
544             'click': this.onEditorEvent,
545             'keyup': this.onEditorEvent,
546             buffer:100,
547             scope: this
548         });
549         if(Roo.isGecko){
550             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
551         }
552         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
553             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
554         }
555         this.initialized = true;
556
557         this.fireEvent('initialize', this);
558         this.pushValue();
559     },
560
561     // private
562     onDestroy : function(){
563         
564         
565         
566         if(this.rendered){
567             
568             for (var i =0; i < this.toolbars.length;i++) {
569                 // fixme - ask toolbars for heights?
570                 this.toolbars[i].onDestroy();
571             }
572             
573             this.wrap.dom.innerHTML = '';
574             this.wrap.remove();
575         }
576     },
577
578     // private
579     onFirstFocus : function(){
580         
581         this.assignDocWin();
582         
583         
584         this.activated = true;
585         for (var i =0; i < this.toolbars.length;i++) {
586             this.toolbars[i].onFirstFocus();
587         }
588        
589         if(Roo.isGecko){ // prevent silly gecko errors
590             this.win.focus();
591             var s = this.win.getSelection();
592             if(!s.focusNode || s.focusNode.nodeType != 3){
593                 var r = s.getRangeAt(0);
594                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
595                 r.collapse(true);
596                 this.deferFocus();
597             }
598             try{
599                 this.execCmd('useCSS', true);
600                 this.execCmd('styleWithCSS', false);
601             }catch(e){}
602         }
603         this.fireEvent('activate', this);
604     },
605
606     // private
607     adjustFont: function(btn){
608         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
609         //if(Roo.isSafari){ // safari
610         //    adjust *= 2;
611        // }
612         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
613         if(Roo.isSafari){ // safari
614             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
615             v =  (v < 10) ? 10 : v;
616             v =  (v > 48) ? 48 : v;
617             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
618             
619         }
620         
621         
622         v = Math.max(1, v+adjust);
623         
624         this.execCmd('FontSize', v  );
625     },
626
627     onEditorEvent : function(e){
628         this.fireEvent('editorevent', this, e);
629       //  this.updateToolbar();
630         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
631     },
632
633     insertTag : function(tg)
634     {
635         // could be a bit smarter... -> wrap the current selected tRoo..
636         
637         this.execCmd("formatblock",   tg);
638         
639     },
640     
641     insertText : function(txt)
642     {
643         
644         
645         range = this.createRange();
646         range.deleteContents();
647                //alert(Sender.getAttribute('label'));
648                
649         range.insertNode(this.doc.createTextNode(txt));
650     } ,
651     
652     // private
653     relayBtnCmd : function(btn){
654         this.relayCmd(btn.cmd);
655     },
656
657     /**
658      * Executes a Midas editor command on the editor document and performs necessary focus and
659      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
660      * @param {String} cmd The Midas command
661      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
662      */
663     relayCmd : function(cmd, value){
664         this.win.focus();
665         this.execCmd(cmd, value);
666         this.fireEvent('editorevent', this);
667         //this.updateToolbar();
668         this.deferFocus();
669     },
670
671     /**
672      * Executes a Midas editor command directly on the editor document.
673      * For visual commands, you should use {@link #relayCmd} instead.
674      * <b>This should only be called after the editor is initialized.</b>
675      * @param {String} cmd The Midas command
676      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
677      */
678     execCmd : function(cmd, value){
679         this.doc.execCommand(cmd, false, value === undefined ? null : value);
680         this.syncValue();
681     },
682  
683  
684    
685     /**
686      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
687      * to insert tRoo.
688      * @param {String} text | dom node.. 
689      */
690     insertAtCursor : function(text)
691     {
692         
693         
694         
695         if(!this.activated){
696             return;
697         }
698         /*
699         if(Roo.isIE){
700             this.win.focus();
701             var r = this.doc.selection.createRange();
702             if(r){
703                 r.collapse(true);
704                 r.pasteHTML(text);
705                 this.syncValue();
706                 this.deferFocus();
707             
708             }
709             return;
710         }
711         */
712         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
713             this.win.focus();
714             
715             
716             // from jquery ui (MIT licenced)
717             var range, node;
718             var win = this.win;
719             
720             if (win.getSelection && win.getSelection().getRangeAt) {
721                 range = win.getSelection().getRangeAt(0);
722                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
723                 range.insertNode(node);
724             } else if (win.document.selection && win.document.selection.createRange) {
725                 // no firefox support
726                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
727                 win.document.selection.createRange().pasteHTML(txt);
728             } else {
729                 // no firefox support
730                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
731                 this.execCmd('InsertHTML', txt);
732             } 
733             
734             this.syncValue();
735             
736             this.deferFocus();
737         }
738     },
739  // private
740     mozKeyPress : function(e){
741         if(e.ctrlKey){
742             var c = e.getCharCode(), cmd;
743           
744             if(c > 0){
745                 c = String.fromCharCode(c).toLowerCase();
746                 switch(c){
747                     case 'b':
748                         cmd = 'bold';
749                         break;
750                     case 'i':
751                         cmd = 'italic';
752                         break;
753                     
754                     case 'u':
755                         cmd = 'underline';
756                         break;
757                     
758                     case 'v':
759                         this.cleanUpPaste.defer(100, this);
760                         return;
761                         
762                 }
763                 if(cmd){
764                     this.win.focus();
765                     this.execCmd(cmd);
766                     this.deferFocus();
767                     e.preventDefault();
768                 }
769                 
770             }
771         }
772     },
773
774     // private
775     fixKeys : function(){ // load time branching for fastest keydown performance
776         if(Roo.isIE){
777             return function(e){
778                 var k = e.getKey(), r;
779                 if(k == e.TAB){
780                     e.stopEvent();
781                     r = this.doc.selection.createRange();
782                     if(r){
783                         r.collapse(true);
784                         r.pasteHTML('&#160;&#160;&#160;&#160;');
785                         this.deferFocus();
786                     }
787                     return;
788                 }
789                 
790                 if(k == e.ENTER){
791                     r = this.doc.selection.createRange();
792                     if(r){
793                         var target = r.parentElement();
794                         if(!target || target.tagName.toLowerCase() != 'li'){
795                             e.stopEvent();
796                             r.pasteHTML('<br />');
797                             r.collapse(false);
798                             r.select();
799                         }
800                     }
801                 }
802                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
803                     this.cleanUpPaste.defer(100, this);
804                     return;
805                 }
806                 
807                 
808             };
809         }else if(Roo.isOpera){
810             return function(e){
811                 var k = e.getKey();
812                 if(k == e.TAB){
813                     e.stopEvent();
814                     this.win.focus();
815                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
816                     this.deferFocus();
817                 }
818                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
819                     this.cleanUpPaste.defer(100, this);
820                     return;
821                 }
822                 
823             };
824         }else if(Roo.isSafari){
825             return function(e){
826                 var k = e.getKey();
827                 
828                 if(k == e.TAB){
829                     e.stopEvent();
830                     this.execCmd('InsertText','\t');
831                     this.deferFocus();
832                     return;
833                 }
834                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
835                     this.cleanUpPaste.defer(100, this);
836                     return;
837                 }
838                 
839              };
840         }
841     }(),
842     
843     getAllAncestors: function()
844     {
845         var p = this.getSelectedNode();
846         var a = [];
847         if (!p) {
848             a.push(p); // push blank onto stack..
849             p = this.getParentElement();
850         }
851         
852         
853         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
854             a.push(p);
855             p = p.parentNode;
856         }
857         a.push(this.doc.body);
858         return a;
859     },
860     lastSel : false,
861     lastSelNode : false,
862     
863     
864     getSelection : function() 
865     {
866         this.assignDocWin();
867         return Roo.isIE ? this.doc.selection : this.win.getSelection();
868     },
869     
870     getSelectedNode: function() 
871     {
872         // this may only work on Gecko!!!
873         
874         // should we cache this!!!!
875         
876         
877         
878          
879         var range = this.createRange(this.getSelection()).cloneRange();
880         
881         if (Roo.isIE) {
882             var parent = range.parentElement();
883             while (true) {
884                 var testRange = range.duplicate();
885                 testRange.moveToElementText(parent);
886                 if (testRange.inRange(range)) {
887                     break;
888                 }
889                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
890                     break;
891                 }
892                 parent = parent.parentElement;
893             }
894             return parent;
895         }
896         
897         // is ancestor a text element.
898         var ac =  range.commonAncestorContainer;
899         if (ac.nodeType == 3) {
900             ac = ac.parentNode;
901         }
902         
903         var ar = ac.childNodes;
904          
905         var nodes = [];
906         var other_nodes = [];
907         var has_other_nodes = false;
908         for (var i=0;i<ar.length;i++) {
909             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
910                 continue;
911             }
912             // fullly contained node.
913             
914             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
915                 nodes.push(ar[i]);
916                 continue;
917             }
918             
919             // probably selected..
920             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
921                 other_nodes.push(ar[i]);
922                 continue;
923             }
924             // outer..
925             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
926                 continue;
927             }
928             
929             
930             has_other_nodes = true;
931         }
932         if (!nodes.length && other_nodes.length) {
933             nodes= other_nodes;
934         }
935         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
936             return false;
937         }
938         
939         return nodes[0];
940     },
941     createRange: function(sel)
942     {
943         // this has strange effects when using with 
944         // top toolbar - not sure if it's a great idea.
945         //this.editor.contentWindow.focus();
946         if (typeof sel != "undefined") {
947             try {
948                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
949             } catch(e) {
950                 return this.doc.createRange();
951             }
952         } else {
953             return this.doc.createRange();
954         }
955     },
956     getParentElement: function()
957     {
958         
959         this.assignDocWin();
960         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
961         
962         var range = this.createRange(sel);
963          
964         try {
965             var p = range.commonAncestorContainer;
966             while (p.nodeType == 3) { // text node
967                 p = p.parentNode;
968             }
969             return p;
970         } catch (e) {
971             return null;
972         }
973     
974     },
975     /***
976      *
977      * Range intersection.. the hard stuff...
978      *  '-1' = before
979      *  '0' = hits..
980      *  '1' = after.
981      *         [ -- selected range --- ]
982      *   [fail]                        [fail]
983      *
984      *    basically..
985      *      if end is before start or  hits it. fail.
986      *      if start is after end or hits it fail.
987      *
988      *   if either hits (but other is outside. - then it's not 
989      *   
990      *    
991      **/
992     
993     
994     // @see http://www.thismuchiknow.co.uk/?p=64.
995     rangeIntersectsNode : function(range, node)
996     {
997         var nodeRange = node.ownerDocument.createRange();
998         try {
999             nodeRange.selectNode(node);
1000         } catch (e) {
1001             nodeRange.selectNodeContents(node);
1002         }
1003     
1004         var rangeStartRange = range.cloneRange();
1005         rangeStartRange.collapse(true);
1006     
1007         var rangeEndRange = range.cloneRange();
1008         rangeEndRange.collapse(false);
1009     
1010         var nodeStartRange = nodeRange.cloneRange();
1011         nodeStartRange.collapse(true);
1012     
1013         var nodeEndRange = nodeRange.cloneRange();
1014         nodeEndRange.collapse(false);
1015     
1016         return rangeStartRange.compareBoundaryPoints(
1017                  Range.START_TO_START, nodeEndRange) == -1 &&
1018                rangeEndRange.compareBoundaryPoints(
1019                  Range.START_TO_START, nodeStartRange) == 1;
1020         
1021          
1022     },
1023     rangeCompareNode : function(range, node)
1024     {
1025         var nodeRange = node.ownerDocument.createRange();
1026         try {
1027             nodeRange.selectNode(node);
1028         } catch (e) {
1029             nodeRange.selectNodeContents(node);
1030         }
1031         
1032         
1033         range.collapse(true);
1034     
1035         nodeRange.collapse(true);
1036      
1037         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1038         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1039          
1040         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1041         
1042         var nodeIsBefore   =  ss == 1;
1043         var nodeIsAfter    = ee == -1;
1044         
1045         if (nodeIsBefore && nodeIsAfter)
1046             return 0; // outer
1047         if (!nodeIsBefore && nodeIsAfter)
1048             return 1; //right trailed.
1049         
1050         if (nodeIsBefore && !nodeIsAfter)
1051             return 2;  // left trailed.
1052         // fully contined.
1053         return 3;
1054     },
1055
1056     // private? - in a new class?
1057     cleanUpPaste :  function()
1058     {
1059         // cleans up the whole document..
1060          Roo.log('cleanuppaste');
1061         this.cleanUpChildren(this.doc.body);
1062         var clean = this.cleanWordChars(this.doc.body.innerHTML);
1063         if (clean != this.doc.body.innerHTML) {
1064             this.doc.body.innerHTML = clean;
1065         }
1066         
1067     },
1068     
1069     cleanWordChars : function(input) {// change the chars to hex code
1070         var he = Roo.form.HtmlEditor;
1071         
1072         var output = input;
1073         Roo.each(he.swapCodes, function(sw) { 
1074             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1075             
1076             output = output.replace(swapper, sw[1]);
1077         });
1078         
1079         return output;
1080     },
1081     
1082     
1083     cleanUpChildren : function (n)
1084     {
1085         if (!n.childNodes.length) {
1086             return;
1087         }
1088         for (var i = n.childNodes.length-1; i > -1 ; i--) {
1089            this.cleanUpChild(n.childNodes[i]);
1090         }
1091     },
1092     
1093     
1094         
1095     
1096     cleanUpChild : function (node)
1097     {
1098         var ed = this;
1099         //console.log(node);
1100         if (node.nodeName == "#text") {
1101             // clean up silly Windows -- stuff?
1102             return; 
1103         }
1104         if (node.nodeName == "#comment") {
1105             node.parentNode.removeChild(node);
1106             // clean up silly Windows -- stuff?
1107             return; 
1108         }
1109         
1110         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1111             // remove node.
1112             node.parentNode.removeChild(node);
1113             return;
1114             
1115         }
1116         
1117         var remove_keep_children= Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1;
1118         
1119         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1120         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1121         
1122         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1123         //    remove_keep_children = true;
1124         //}
1125         
1126         if (remove_keep_children) {
1127             this.cleanUpChildren(node);
1128             // inserts everything just before this node...
1129             while (node.childNodes.length) {
1130                 var cn = node.childNodes[0];
1131                 node.removeChild(cn);
1132                 node.parentNode.insertBefore(cn, node);
1133             }
1134             node.parentNode.removeChild(node);
1135             return;
1136         }
1137         
1138         if (!node.attributes || !node.attributes.length) {
1139             this.cleanUpChildren(node);
1140             return;
1141         }
1142         
1143         function cleanAttr(n,v)
1144         {
1145             
1146             if (v.match(/^\./) || v.match(/^\//)) {
1147                 return;
1148             }
1149             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1150                 return;
1151             }
1152             if (v.match(/^#/)) {
1153                 return;
1154             }
1155 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1156             node.removeAttribute(n);
1157             
1158         }
1159         
1160         function cleanStyle(n,v)
1161         {
1162             if (v.match(/expression/)) { //XSS?? should we even bother..
1163                 node.removeAttribute(n);
1164                 return;
1165             }
1166             var cwhite = typeof(ed.cwhite) == 'undefined' ? Roo.form.HtmlEditor.cwhite : ed.cwhite;
1167             
1168             var parts = v.split(/;/);
1169             var clean = [];
1170             
1171             Roo.each(parts, function(p) {
1172                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1173                 if (!p.length) {
1174                     return true;
1175                 }
1176                 var l = p.split(':').shift().replace(/\s+/g,'');
1177                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1178                 
1179                 // only allow 'c whitelisted system attributes'
1180                 if ( cwhite.indexOf(l) < 0) {
1181 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1182                     //node.removeAttribute(n);
1183                     return true;
1184                 }
1185                 
1186 //                if(l == 'font-size'){
1187 ////                    Roo.log('(REMOVE FONT SIZE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1188 //                    return true;
1189 //                }
1190                 
1191                 clean.push(p);
1192                 return true;
1193             });
1194             if (clean.length) { 
1195                 node.setAttribute(n, clean.join(';'));
1196             } else {
1197                 node.removeAttribute(n);
1198             }
1199             
1200         }
1201         
1202         
1203         for (var i = node.attributes.length-1; i > -1 ; i--) {
1204             var a = node.attributes[i];
1205             //console.log(a);
1206             
1207             if (a.name.toLowerCase().substr(0,2)=='on')  {
1208                 node.removeAttribute(a.name);
1209                 continue;
1210             }
1211             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1212                 node.removeAttribute(a.name);
1213                 continue;
1214             }
1215             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1216                 cleanAttr(a.name,a.value); // fixme..
1217                 continue;
1218             }
1219             if (a.name == 'style') {
1220                 cleanStyle(a.name,a.value);
1221                 continue;
1222             }
1223             /// clean up MS crap..
1224             // tecnically this should be a list of valid class'es..
1225             
1226             
1227             if (a.name == 'class') {
1228                 if (a.value.match(/^Mso/)) {
1229                     node.className = '';
1230                 }
1231                 
1232                 if (a.value.match(/body/)) {
1233                     node.className = '';
1234                 }
1235                 continue;
1236             }
1237             
1238             // style cleanup!?
1239             // class cleanup?
1240             
1241         }
1242         
1243         
1244         this.cleanUpChildren(node);
1245         
1246         
1247     }
1248     
1249     
1250     // hide stuff that is not compatible
1251     /**
1252      * @event blur
1253      * @hide
1254      */
1255     /**
1256      * @event change
1257      * @hide
1258      */
1259     /**
1260      * @event focus
1261      * @hide
1262      */
1263     /**
1264      * @event specialkey
1265      * @hide
1266      */
1267     /**
1268      * @cfg {String} fieldClass @hide
1269      */
1270     /**
1271      * @cfg {String} focusClass @hide
1272      */
1273     /**
1274      * @cfg {String} autoCreate @hide
1275      */
1276     /**
1277      * @cfg {String} inputType @hide
1278      */
1279     /**
1280      * @cfg {String} invalidClass @hide
1281      */
1282     /**
1283      * @cfg {String} invalidText @hide
1284      */
1285     /**
1286      * @cfg {String} msgFx @hide
1287      */
1288     /**
1289      * @cfg {String} validateOnBlur @hide
1290      */
1291 });
1292
1293 Roo.form.HtmlEditor.white = [
1294         'area', 'br', 'img', 'input', 'hr', 'wbr',
1295         
1296        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1297        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1298        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1299        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1300        'table',   'ul',         'xmp', 
1301        
1302        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1303       'thead',   'tr', 
1304      
1305       'dir', 'menu', 'ol', 'ul', 'dl',
1306        
1307       'embed',  'object'
1308 ];
1309
1310
1311 Roo.form.HtmlEditor.black = [
1312     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1313         'applet', // 
1314         'base',   'basefont', 'bgsound', 'blink',  'body', 
1315         'frame',  'frameset', 'head',    'html',   'ilayer', 
1316         'iframe', 'layer',  'link',     'meta',    'object',   
1317         'script', 'style' ,'title',  'xml' // clean later..
1318 ];
1319 Roo.form.HtmlEditor.clean = [
1320     'script', 'style', 'title', 'xml'
1321 ];
1322 Roo.form.HtmlEditor.remove = [
1323     'font'
1324 ];
1325 // attributes..
1326
1327 Roo.form.HtmlEditor.ablack = [
1328     'on'
1329 ];
1330     
1331 Roo.form.HtmlEditor.aclean = [ 
1332     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1333 ];
1334
1335 // protocols..
1336 Roo.form.HtmlEditor.pwhite= [
1337         'http',  'https',  'mailto'
1338 ];
1339
1340 // white listed style attributes.
1341 Roo.form.HtmlEditor.cwhite= [
1342         'text-align',
1343 //        'font-size'//??
1344 ];
1345
1346 // black listed style attributes.
1347 Roo.form.HtmlEditor.cblack= [
1348         'text-align',
1349         'font-size'//??
1350 ];
1351
1352
1353 Roo.form.HtmlEditor.swapCodes   =[ 
1354     [    8211, "--" ], 
1355     [    8212, "--" ], 
1356     [    8216,  "'" ],  
1357     [    8217, "'" ],  
1358     [    8220, '"' ],  
1359     [    8221, '"' ],  
1360     [    8226, "*" ],  
1361     [    8230, "..." ]
1362 ]; 
1363
1364