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