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