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  * WARNING - THIS CURRENTlY ONLY WORKS ON FIREFOX - USE FCKeditor for a cross platform version
27  * 
28  * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
29  * supported by this editor.</b><br/><br/>
30  * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
31  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
32  */
33 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
34       /**
35      * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
36      */
37     toolbars : false,
38     /**
39      * @cfg {String} createLinkText The default text for the create link prompt
40      */
41     createLinkText : 'Please enter the URL for the link:',
42     /**
43      * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
44      */
45     defaultLinkValue : 'http:/'+'/',
46    
47      /**
48      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
49      *                        Roo.resizable.
50      */
51     resizable : false,
52      /**
53      * @cfg {Number} height (in pixels)
54      */   
55     height: 300,
56    
57     // id of frame..
58     frameId: false,
59     
60     // private properties
61     validationEvent : false,
62     deferHeight: true,
63     initialized : false,
64     activated : false,
65     sourceEditMode : false,
66     onFocus : Roo.emptyFn,
67     iframePad:3,
68     hideMode:'offsets',
69     
70     defaultAutoCreate : {
71         tag: "textarea",
72         style:"width:500px;height:300px;",
73         autocomplete: "off"
74     },
75
76     // private
77     initComponent : function(){
78         this.addEvents({
79             /**
80              * @event initialize
81              * Fires when the editor is fully initialized (including the iframe)
82              * @param {HtmlEditor} this
83              */
84             initialize: true,
85             /**
86              * @event activate
87              * Fires when the editor is first receives the focus. Any insertion must wait
88              * until after this event.
89              * @param {HtmlEditor} this
90              */
91             activate: true,
92              /**
93              * @event beforesync
94              * Fires before the textarea is updated with content from the editor iframe. Return false
95              * to cancel the sync.
96              * @param {HtmlEditor} this
97              * @param {String} html
98              */
99             beforesync: true,
100              /**
101              * @event beforepush
102              * Fires before the iframe editor is updated with content from the textarea. Return false
103              * to cancel the push.
104              * @param {HtmlEditor} this
105              * @param {String} html
106              */
107             beforepush: true,
108              /**
109              * @event sync
110              * Fires when the textarea is updated with content from the editor iframe.
111              * @param {HtmlEditor} this
112              * @param {String} html
113              */
114             sync: true,
115              /**
116              * @event push
117              * Fires when the iframe editor is updated with content from the textarea.
118              * @param {HtmlEditor} this
119              * @param {String} html
120              */
121             push: true,
122              /**
123              * @event editmodechange
124              * Fires when the editor switches edit modes
125              * @param {HtmlEditor} this
126              * @param {Boolean} sourceEdit True if source edit, false if standard editing.
127              */
128             editmodechange: true,
129             /**
130              * @event editorevent
131              * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
132              * @param {HtmlEditor} this
133              */
134             editorevent: true
135         });
136         this.defaultAutoCreate =  {
137             tag: "textarea",
138             style:'width: ' + this.width + 'px;height: ' + this.height + 'px;",
139             autocomplete: "off"
140         },
141     },
142
143     /**
144      * Protected method that will not generally be called directly. It
145      * is called when the editor creates its toolbar. Override this method if you need to
146      * add custom toolbar buttons.
147      * @param {HtmlEditor} editor
148      */
149     createToolbar : function(editor){
150         if (!editor.toolbars || !editor.toolbars.length) {
151             editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
152         }
153         
154         for (var i =0 ; i < editor.toolbars.length;i++) {
155             editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
156             editor.toolbars[i].init(editor);
157         }
158          
159         
160     },
161
162     /**
163      * Protected method that will not generally be called directly. It
164      * is called when the editor initializes the iframe with HTML contents. Override this method if you
165      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
166      */
167     getDocMarkup : function(){
168         return '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style></head><body></body></html>';
169     },
170
171     // private
172     onRender : function(ct, position){
173         Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
174         this.el.dom.style.border = '0 none';
175         this.el.dom.setAttribute('tabIndex', -1);
176         this.el.addClass('x-hidden');
177         if(Roo.isIE){ // fix IE 1px bogus margin
178             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
179         }
180         this.wrap = this.el.wrap({
181             cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
182         });
183         
184         if (this.resizable) {
185             this.resizeEl = new Roo.Resizable(this.wrap, {
186                 wrap: true,
187                 dynamic : true,
188                 minHeight : this.height,
189                 handles : this.resizable
190             });
191             
192         }
193
194         this.frameId = Roo.id();
195         this.createToolbar(this);
196         
197         
198         
199         
200       
201         
202         var iframe = this.wrap.createChild({
203             tag: 'iframe',
204             id: this.frameId,
205             name: this.frameId,
206             frameBorder : 'no',
207             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
208         });
209         
210        // console.log(iframe);
211         //this.wrap.dom.appendChild(iframe);
212
213         this.iframe = iframe.dom;
214
215          this.assignDocWin();
216         
217         this.doc.designMode = 'on';
218        
219         this.doc.open();
220         this.doc.write(this.getDocMarkup());
221         this.doc.close();
222
223         
224         var task = { // must defer to wait for browser to be ready
225             run : function(){
226                 //console.log("run task?" + this.doc.readyState);
227                 this.assignDocWin();
228                 if(this.doc.body || this.doc.readyState == 'complete'){
229                     try {
230                         this.doc.designMode="on";
231                     } catch (e) {
232                         return;
233                     }
234                     Roo.TaskMgr.stop(task);
235                     this.initEditor.defer(10, this);
236                 }
237             },
238             interval : 10,
239             duration:10000,
240             scope: this
241         };
242         Roo.TaskMgr.start(task);
243
244         if(!this.width){
245             this.setSize(this.el.getSize());
246         }
247     },
248
249     // private
250     onResize : function(w, h){
251         Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
252         if(this.el && this.iframe){
253             if(typeof w == 'number'){
254                 var aw = w - this.wrap.getFrameWidth('lr');
255                 this.el.setWidth(this.adjustWidth('textarea', aw));
256                 this.iframe.style.width = aw + 'px';
257             }
258             if(typeof h == 'number'){
259                 var tbh = 0;
260                 for (var i =0; i < this.toolbars.length;i++) {
261                     // fixme - ask toolbars for heights?
262                     tbh += this.toolbars[i].tb.el.getHeight();
263                 }
264                 
265                 
266                 
267                 
268                 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
269                 this.el.setHeight(this.adjustWidth('textarea', ah));
270                 this.iframe.style.height = ah + 'px';
271                 if(this.doc){
272                     (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
273                 }
274             }
275         }
276     },
277
278     /**
279      * Toggles the editor between standard and source edit mode.
280      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
281      */
282     toggleSourceEdit : function(sourceEditMode){
283         
284         this.sourceEditMode = sourceEditMode === true;
285         
286         if(this.sourceEditMode){
287           
288             this.syncValue();
289             this.iframe.className = 'x-hidden';
290             this.el.removeClass('x-hidden');
291             this.el.dom.removeAttribute('tabIndex');
292             this.el.focus();
293         }else{
294              
295             this.pushValue();
296             this.iframe.className = '';
297             this.el.addClass('x-hidden');
298             this.el.dom.setAttribute('tabIndex', -1);
299             this.deferFocus();
300         }
301         this.setSize(this.wrap.getSize());
302         this.fireEvent('editmodechange', this, this.sourceEditMode);
303     },
304
305     // private used internally
306     createLink : function(){
307         var url = prompt(this.createLinkText, this.defaultLinkValue);
308         if(url && url != 'http:/'+'/'){
309             this.relayCmd('createlink', url);
310         }
311     },
312
313     // private (for BoxComponent)
314     adjustSize : Roo.BoxComponent.prototype.adjustSize,
315
316     // private (for BoxComponent)
317     getResizeEl : function(){
318         return this.wrap;
319     },
320
321     // private (for BoxComponent)
322     getPositionEl : function(){
323         return this.wrap;
324     },
325
326     // private
327     initEvents : function(){
328         this.originalValue = this.getValue();
329     },
330
331     /**
332      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
333      * @method
334      */
335     markInvalid : Roo.emptyFn,
336     /**
337      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
338      * @method
339      */
340     clearInvalid : Roo.emptyFn,
341
342     setValue : function(v){
343         Roo.form.HtmlEditor.superclass.setValue.call(this, v);
344         this.pushValue();
345     },
346
347     /**
348      * Protected method that will not generally be called directly. If you need/want
349      * custom HTML cleanup, this is the method you should override.
350      * @param {String} html The HTML to be cleaned
351      * return {String} The cleaned HTML
352      */
353     cleanHtml : function(html){
354         html = String(html);
355         if(html.length > 5){
356             if(Roo.isSafari){ // strip safari nonsense
357                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
358             }
359         }
360         if(html == '&nbsp;'){
361             html = '';
362         }
363         return html;
364     },
365
366     /**
367      * Protected method that will not generally be called directly. Syncs the contents
368      * of the editor iframe with the textarea.
369      */
370     syncValue : function(){
371         if(this.initialized){
372             var bd = (this.doc.body || this.doc.documentElement);
373             this.cleanUpPaste();
374             var html = bd.innerHTML;
375             if(Roo.isSafari){
376                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
377                 var m = bs.match(/text-align:(.*?);/i);
378                 if(m && m[1]){
379                     html = '<div style="'+m[0]+'">' + html + '</div>';
380                 }
381             }
382             html = this.cleanHtml(html);
383             if(this.fireEvent('beforesync', this, html) !== false){
384                 this.el.dom.value = html;
385                 this.fireEvent('sync', this, html);
386             }
387         }
388     },
389
390     /**
391      * Protected method that will not generally be called directly. Pushes the value of the textarea
392      * into the iframe editor.
393      */
394     pushValue : function(){
395         if(this.initialized){
396             var v = this.el.dom.value;
397             if(v.length < 1){
398                 v = '&#160;';
399             }
400             
401             if(this.fireEvent('beforepush', this, v) !== false){
402                 var d = (this.doc.body || this.doc.documentElement);
403                 d.innerHTML = v;
404                 this.cleanUpPaste();
405                 this.el.dom.value = d.innerHTML;
406                 this.fireEvent('push', this, v);
407             }
408         }
409     },
410
411     // private
412     deferFocus : function(){
413         this.focus.defer(10, this);
414     },
415
416     // doc'ed in Field
417     focus : function(){
418         if(this.win && !this.sourceEditMode){
419             this.win.focus();
420         }else{
421             this.el.focus();
422         }
423     },
424     
425     assignDocWin: function()
426     {
427         var iframe = this.iframe;
428         
429          if(Roo.isIE){
430             this.doc = iframe.contentWindow.document;
431             this.win = iframe.contentWindow;
432         } else {
433             if (!Roo.get(this.frameId)) {
434                 return;
435             }
436             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
437             this.win = Roo.get(this.frameId).dom.contentWindow;
438         }
439     },
440     
441     // private
442     initEditor : function(){
443         //console.log("INIT EDITOR");
444         this.assignDocWin();
445         
446         
447         
448         this.doc.designMode="on";
449         this.doc.open();
450         this.doc.write(this.getDocMarkup());
451         this.doc.close();
452         
453         var dbody = (this.doc.body || this.doc.documentElement);
454         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
455         // this copies styles from the containing element into thsi one..
456         // not sure why we need all of this..
457         var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
458         ss['background-attachment'] = 'fixed'; // w3c
459         dbody.bgProperties = 'fixed'; // ie
460         Roo.DomHelper.applyStyles(dbody, ss);
461         Roo.EventManager.on(this.doc, {
462             'mousedown': this.onEditorEvent,
463             'dblclick': this.onEditorEvent,
464             'click': this.onEditorEvent,
465             'keyup': this.onEditorEvent,
466             buffer:100,
467             scope: this
468         });
469         if(Roo.isGecko){
470             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
471         }
472         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
473             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
474         }
475         this.initialized = true;
476
477         this.fireEvent('initialize', this);
478         this.pushValue();
479     },
480
481     // private
482     onDestroy : function(){
483         
484         
485         
486         if(this.rendered){
487             
488             for (var i =0; i < this.toolbars.length;i++) {
489                 // fixme - ask toolbars for heights?
490                 this.toolbars[i].onDestroy();
491             }
492             
493             this.wrap.dom.innerHTML = '';
494             this.wrap.remove();
495         }
496     },
497
498     // private
499     onFirstFocus : function(){
500         
501         this.assignDocWin();
502         
503         
504         this.activated = true;
505         for (var i =0; i < this.toolbars.length;i++) {
506             this.toolbars[i].onFirstFocus();
507         }
508        
509         if(Roo.isGecko){ // prevent silly gecko errors
510             this.win.focus();
511             var s = this.win.getSelection();
512             if(!s.focusNode || s.focusNode.nodeType != 3){
513                 var r = s.getRangeAt(0);
514                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
515                 r.collapse(true);
516                 this.deferFocus();
517             }
518             try{
519                 this.execCmd('useCSS', true);
520                 this.execCmd('styleWithCSS', false);
521             }catch(e){}
522         }
523         this.fireEvent('activate', this);
524     },
525
526     // private
527     adjustFont: function(btn){
528         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
529         //if(Roo.isSafari){ // safari
530         //    adjust *= 2;
531        // }
532         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
533         if(Roo.isSafari){ // safari
534             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
535             v =  (v < 10) ? 10 : v;
536             v =  (v > 48) ? 48 : v;
537             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
538             
539         }
540         
541         
542         v = Math.max(1, v+adjust);
543         
544         this.execCmd('FontSize', v  );
545     },
546
547     onEditorEvent : function(e){
548         this.fireEvent('editorevent', this, e);
549       //  this.updateToolbar();
550         this.syncValue();
551     },
552
553     insertTag : function(tg)
554     {
555         // could be a bit smarter... -> wrap the current selected tRoo..
556         
557         this.execCmd("formatblock",   tg);
558         
559     },
560     
561     insertText : function(txt)
562     {
563         
564         
565         range = this.createRange();
566         range.deleteContents();
567                //alert(Sender.getAttribute('label'));
568                
569         range.insertNode(this.doc.createTextNode(txt));
570     } ,
571     
572     // private
573     relayBtnCmd : function(btn){
574         this.relayCmd(btn.cmd);
575     },
576
577     /**
578      * Executes a Midas editor command on the editor document and performs necessary focus and
579      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
580      * @param {String} cmd The Midas command
581      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
582      */
583     relayCmd : function(cmd, value){
584         this.win.focus();
585         this.execCmd(cmd, value);
586         this.fireEvent('editorevent', this);
587         //this.updateToolbar();
588         this.deferFocus();
589     },
590
591     /**
592      * Executes a Midas editor command directly on the editor document.
593      * For visual commands, you should use {@link #relayCmd} instead.
594      * <b>This should only be called after the editor is initialized.</b>
595      * @param {String} cmd The Midas command
596      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
597      */
598     execCmd : function(cmd, value){
599         this.doc.execCommand(cmd, false, value === undefined ? null : value);
600         this.syncValue();
601     },
602
603    
604     /**
605      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
606      * to insert tRoo.
607      * @param {String} text
608      */
609     insertAtCursor : function(text){
610         if(!this.activated){
611             return;
612         }
613         if(Roo.isIE){
614             this.win.focus();
615             var r = this.doc.selection.createRange();
616             if(r){
617                 r.collapse(true);
618                 r.pasteHTML(text);
619                 this.syncValue();
620                 this.deferFocus();
621             }
622         }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
623             this.win.focus();
624             this.execCmd('InsertHTML', text);
625             this.deferFocus();
626         }
627     },
628  // private
629     mozKeyPress : function(e){
630         if(e.ctrlKey){
631             var c = e.getCharCode(), cmd;
632           
633             if(c > 0){
634                 c = String.fromCharCode(c).toLowerCase();
635                 switch(c){
636                     case 'b':
637                         cmd = 'bold';
638                     break;
639                     case 'i':
640                         cmd = 'italic';
641                     break;
642                     case 'u':
643                         cmd = 'underline';
644                     case 'v':
645                         this.cleanUpPaste.defer(100, this);
646                         return;
647                     break;
648                 }
649                 if(cmd){
650                     this.win.focus();
651                     this.execCmd(cmd);
652                     this.deferFocus();
653                     e.preventDefault();
654                 }
655                 
656             }
657         }
658     },
659
660     // private
661     fixKeys : function(){ // load time branching for fastest keydown performance
662         if(Roo.isIE){
663             return function(e){
664                 var k = e.getKey(), r;
665                 if(k == e.TAB){
666                     e.stopEvent();
667                     r = this.doc.selection.createRange();
668                     if(r){
669                         r.collapse(true);
670                         r.pasteHTML('&#160;&#160;&#160;&#160;');
671                         this.deferFocus();
672                     }
673                     return;
674                 }
675                 
676                 if(k == e.ENTER){
677                     r = this.doc.selection.createRange();
678                     if(r){
679                         var target = r.parentElement();
680                         if(!target || target.tagName.toLowerCase() != 'li'){
681                             e.stopEvent();
682                             r.pasteHTML('<br />');
683                             r.collapse(false);
684                             r.select();
685                         }
686                     }
687                 }
688                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
689                     this.cleanUpPaste.defer(100, this);
690                     return;
691                 }
692                 
693                 
694             };
695         }else if(Roo.isOpera){
696             return function(e){
697                 var k = e.getKey();
698                 if(k == e.TAB){
699                     e.stopEvent();
700                     this.win.focus();
701                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
702                     this.deferFocus();
703                 }
704                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
705                     this.cleanUpPaste.defer(100, this);
706                     return;
707                 }
708                 
709             };
710         }else if(Roo.isSafari){
711             return function(e){
712                 var k = e.getKey();
713                 
714                 if(k == e.TAB){
715                     e.stopEvent();
716                     this.execCmd('InsertText','\t');
717                     this.deferFocus();
718                     return;
719                 }
720                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
721                     this.cleanUpPaste.defer(100, this);
722                     return;
723                 }
724                 
725              };
726         }
727     }(),
728     
729     getAllAncestors: function()
730     {
731         var p = this.getSelectedNode();
732         var a = [];
733         if (!p) {
734             a.push(p); // push blank onto stack..
735             p = this.getParentElement();
736         }
737         
738         
739         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
740             a.push(p);
741             p = p.parentNode;
742         }
743         a.push(this.doc.body);
744         return a;
745     },
746     lastSel : false,
747     lastSelNode : false,
748     
749     
750     getSelection : function() 
751     {
752         this.assignDocWin();
753         return Roo.isIE ? this.doc.selection : this.win.getSelection();
754     },
755     
756     getSelectedNode: function() 
757     {
758         // this may only work on Gecko!!!
759         
760         // should we cache this!!!!
761         
762         
763         
764          
765         var range = this.createRange(this.getSelection());
766         
767         if (Roo.isIE) {
768             var parent = range.parentElement();
769             while (true) {
770                 var testRange = range.duplicate();
771                 testRange.moveToElementText(parent);
772                 if (testRange.inRange(range)) {
773                     break;
774                 }
775                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
776                     break;
777                 }
778                 parent = parent.parentElement;
779             }
780             return parent;
781         }
782         
783         
784         var ar = range.endContainer.childNodes;
785         if (!ar.length) {
786             ar = range.commonAncestorContainer.childNodes;
787             //alert(ar.length);
788         }
789         var nodes = [];
790         var other_nodes = [];
791         var has_other_nodes = false;
792         for (var i=0;i<ar.length;i++) {
793             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
794                 continue;
795             }
796             // fullly contained node.
797             
798             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
799                 nodes.push(ar[i]);
800                 continue;
801             }
802             
803             // probably selected..
804             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
805                 other_nodes.push(ar[i]);
806                 continue;
807             }
808             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
809                 continue;
810             }
811             
812             
813             has_other_nodes = true;
814         }
815         if (!nodes.length && other_nodes.length) {
816             nodes= other_nodes;
817         }
818         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
819             return false;
820         }
821         
822         return nodes[0];
823     },
824     createRange: function(sel)
825     {
826         // this has strange effects when using with 
827         // top toolbar - not sure if it's a great idea.
828         //this.editor.contentWindow.focus();
829         if (typeof sel != "undefined") {
830             try {
831                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
832             } catch(e) {
833                 return this.doc.createRange();
834             }
835         } else {
836             return this.doc.createRange();
837         }
838     },
839     getParentElement: function()
840     {
841         
842         this.assignDocWin();
843         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
844         
845         var range = this.createRange(sel);
846          
847         try {
848             var p = range.commonAncestorContainer;
849             while (p.nodeType == 3) { // text node
850                 p = p.parentNode;
851             }
852             return p;
853         } catch (e) {
854             return null;
855         }
856     
857     },
858     
859     
860     
861     // BC Hacks - cause I cant work out what i was trying to do..
862     rangeIntersectsNode : function(range, node)
863     {
864         var nodeRange = node.ownerDocument.createRange();
865         try {
866             nodeRange.selectNode(node);
867         }
868         catch (e) {
869             nodeRange.selectNodeContents(node);
870         }
871
872         return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
873                  range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
874     },
875     rangeCompareNode : function(range, node) {
876         var nodeRange = node.ownerDocument.createRange();
877         try {
878             nodeRange.selectNode(node);
879         } catch (e) {
880             nodeRange.selectNodeContents(node);
881         }
882         var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
883         var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
884
885         if (nodeIsBefore && !nodeIsAfter)
886             return 0;
887         if (!nodeIsBefore && nodeIsAfter)
888             return 1;
889         if (nodeIsBefore && nodeIsAfter)
890             return 2;
891
892         return 3;
893     },
894
895     // private? - in a new class?
896     cleanUpPaste :  function()
897     {
898         // cleans up the whole document..
899       //  console.log('cleanuppaste');
900         this.cleanUpChildren(this.doc.body);
901         
902         
903     },
904     cleanUpChildren : function (n)
905     {
906         if (!n.childNodes.length) {
907             return;
908         }
909         for (var i = n.childNodes.length-1; i > -1 ; i--) {
910            this.cleanUpChild(n.childNodes[i]);
911         }
912     },
913     
914     
915         
916     
917     cleanUpChild : function (node)
918     {
919         //console.log(node);
920         if (node.nodeName == "#text") {
921             // clean up silly Windows -- stuff?
922             return; 
923         }
924         if (node.nodeName == "#comment") {
925             node.parentNode.removeChild(node);
926             // clean up silly Windows -- stuff?
927             return; 
928         }
929         
930         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
931             // remove node.
932             node.parentNode.removeChild(node);
933             return;
934             
935         }
936         if (!node.attributes || !node.attributes.length) {
937             this.cleanUpChildren(node);
938             return;
939         }
940         
941         function cleanAttr(n,v)
942         {
943             
944             if (v.match(/^\./) || v.match(/^\//)) {
945                 return;
946             }
947             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
948                 return;
949             }
950             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
951             node.removeAttribute(n);
952             
953         }
954         
955         function cleanStyle(n,v)
956         {
957             if (v.match(/expression/)) { //XSS?? should we even bother..
958                 node.removeAttribute(n);
959                 return;
960             }
961             
962             
963             var parts = v.split(/;/);
964             Roo.each(parts, function(p) {
965                 p = p.replace(/\s+/g,'');
966                 if (!p.length) {
967                     return;
968                 }
969                 var l = p.split(':').shift().replace(/\s+/g,'');
970                 
971                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
972                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
973                     node.removeAttribute(n);
974                     return false;
975                 }
976             });
977             
978             
979         }
980         
981         
982         for (var i = node.attributes.length-1; i > -1 ; i--) {
983             var a = node.attributes[i];
984             //console.log(a);
985             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
986                 node.removeAttribute(a.name);
987                 return;
988             }
989             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
990                 cleanAttr(a.name,a.value); // fixme..
991                 return;
992             }
993             if (a.name == 'style') {
994                 cleanStyle(a.name,a.value);
995             }
996             /// clean up MS crap..
997             if (a.name == 'class') {
998                 if (a.value.match(/^Mso/)) {
999                     node.className = '';
1000                 }
1001             }
1002             
1003             // style cleanup!?
1004             // class cleanup?
1005             
1006         }
1007         
1008         
1009         this.cleanUpChildren(node);
1010         
1011         
1012     }
1013     
1014     
1015     // hide stuff that is not compatible
1016     /**
1017      * @event blur
1018      * @hide
1019      */
1020     /**
1021      * @event change
1022      * @hide
1023      */
1024     /**
1025      * @event focus
1026      * @hide
1027      */
1028     /**
1029      * @event specialkey
1030      * @hide
1031      */
1032     /**
1033      * @cfg {String} fieldClass @hide
1034      */
1035     /**
1036      * @cfg {String} focusClass @hide
1037      */
1038     /**
1039      * @cfg {String} autoCreate @hide
1040      */
1041     /**
1042      * @cfg {String} inputType @hide
1043      */
1044     /**
1045      * @cfg {String} invalidClass @hide
1046      */
1047     /**
1048      * @cfg {String} invalidText @hide
1049      */
1050     /**
1051      * @cfg {String} msgFx @hide
1052      */
1053     /**
1054      * @cfg {String} validateOnBlur @hide
1055      */
1056 });
1057
1058 Roo.form.HtmlEditor.white = [
1059         'area', 'br', 'img', 'input', 'hr', 'wbr',
1060         
1061        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1062        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1063        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1064        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1065        'table',   'ul',         'xmp', 
1066        
1067        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1068       'thead',   'tr', 
1069      
1070       'dir', 'menu', 'ol', 'ul', 'dl',
1071        
1072       'embed',  'object'
1073 ];
1074
1075
1076 Roo.form.HtmlEditor.black = [
1077     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1078         'applet', // 
1079         'base',   'basefont', 'bgsound', 'blink',  'body', 
1080         'frame',  'frameset', 'head',    'html',   'ilayer', 
1081         'iframe', 'layer',  'link',     'meta',    'object',   
1082         'script', 'style' ,'title',  'xml' // clean later..
1083 ];
1084 Roo.form.HtmlEditor.clean = [
1085     'script', 'style', 'title', 'xml'
1086 ];
1087
1088 // attributes..
1089
1090 Roo.form.HtmlEditor.ablack = [
1091     'on'
1092 ];
1093     
1094 Roo.form.HtmlEditor.aclean = [ 
1095     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1096 ];
1097
1098 // protocols..
1099 Roo.form.HtmlEditor.pwhite= [
1100         'http',  'https',  'mailto'
1101 ];
1102
1103 Roo.form.HtmlEditor.cwhite= [
1104         'text-align',
1105         'font-size'
1106 ];
1107