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      * Range intersection.. the hard stuff...
881      *  '-1' = before
882      *  '0' = hits..
883      *  '1' = after.
884      *         [ -- selected range --- ]
885      *   [fail]                        [fail]
886      *
887      *    basically..
888      *      if end is before start or  hits it. fail.
889      *      if start is after end or hits it fail.
890      *
891      *   if either hits (but other is outside. - then it's not 
892      *   
893      *    
894      **/
895     
896     
897     // @see http://www.thismuchiknow.co.uk/?p=64.
898     rangeIntersectsNode : function(range, node)
899     {
900         var nodeRange = node.ownerDocument.createRange();
901         try {
902             nodeRange.selectNode(node);
903         } catch (e) {
904             nodeRange.selectNodeContents(node);
905         }
906     
907         var rangeStartRange = range.cloneRange();
908         rangeStartRange.collapse(true);
909     
910         var rangeEndRange = range.cloneRange();
911         rangeEndRange.collapse(false);
912     
913         var nodeStartRange = nodeRange.cloneRange();
914         nodeStartRange.collapse(true);
915     
916         var nodeEndRange = nodeRange.cloneRange();
917         nodeEndRange.collapse(false);
918     
919         return rangeStartRange.compareBoundaryPoints(
920                  Range.START_TO_START, nodeEndRange) == -1 &&
921                rangeEndRange.compareBoundaryPoints(
922                  Range.START_TO_START, nodeStartRange) == 1;
923         
924          
925     },
926     rangeCompareNode : function(range, node)
927     {
928         var nodeRange = node.ownerDocument.createRange();
929         try {
930             nodeRange.selectNode(node);
931         } catch (e) {
932             nodeRange.selectNodeContents(node);
933         }
934         var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
935         var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
936
937         if (nodeIsBefore && !nodeIsAfter)
938             return 0;
939         if (!nodeIsBefore && nodeIsAfter)
940             return 1;
941         if (nodeIsBefore && nodeIsAfter)
942             return 2;
943
944         return 3;
945     },
946
947     // private? - in a new class?
948     cleanUpPaste :  function()
949     {
950         // cleans up the whole document..
951       //  console.log('cleanuppaste');
952         this.cleanUpChildren(this.doc.body);
953         
954         
955     },
956     cleanUpChildren : function (n)
957     {
958         if (!n.childNodes.length) {
959             return;
960         }
961         for (var i = n.childNodes.length-1; i > -1 ; i--) {
962            this.cleanUpChild(n.childNodes[i]);
963         }
964     },
965     
966     
967         
968     
969     cleanUpChild : function (node)
970     {
971         //console.log(node);
972         if (node.nodeName == "#text") {
973             // clean up silly Windows -- stuff?
974             return; 
975         }
976         if (node.nodeName == "#comment") {
977             node.parentNode.removeChild(node);
978             // clean up silly Windows -- stuff?
979             return; 
980         }
981         
982         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
983             // remove node.
984             node.parentNode.removeChild(node);
985             return;
986             
987         }
988         if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
989             this.cleanUpChildren(node);
990             // inserts everything just before this node...
991             while (node.childNodes.length) {
992                 var cn = node.childNodes[0];
993                 node.removeChild(cn);
994                 node.parentNode.insertBefore(cn, node);
995             }
996             node.parentNode.removeChild(node);
997             return;
998         }
999         
1000         if (!node.attributes || !node.attributes.length) {
1001             this.cleanUpChildren(node);
1002             return;
1003         }
1004         
1005         function cleanAttr(n,v)
1006         {
1007             
1008             if (v.match(/^\./) || v.match(/^\//)) {
1009                 return;
1010             }
1011             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1012                 return;
1013             }
1014             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1015             node.removeAttribute(n);
1016             
1017         }
1018         
1019         function cleanStyle(n,v)
1020         {
1021             if (v.match(/expression/)) { //XSS?? should we even bother..
1022                 node.removeAttribute(n);
1023                 return;
1024             }
1025             
1026             
1027             var parts = v.split(/;/);
1028             Roo.each(parts, function(p) {
1029                 p = p.replace(/\s+/g,'');
1030                 if (!p.length) {
1031                     return true;
1032                 }
1033                 var l = p.split(':').shift().replace(/\s+/g,'');
1034                 
1035                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1036                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1037                     node.removeAttribute(n);
1038                     return false;
1039                 }
1040                 return true;
1041             });
1042             
1043             
1044         }
1045         
1046         
1047         for (var i = node.attributes.length-1; i > -1 ; i--) {
1048             var a = node.attributes[i];
1049             //console.log(a);
1050             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1051                 node.removeAttribute(a.name);
1052                 return;
1053             }
1054             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1055                 cleanAttr(a.name,a.value); // fixme..
1056                 return;
1057             }
1058             if (a.name == 'style') {
1059                 cleanStyle(a.name,a.value);
1060             }
1061             /// clean up MS crap..
1062             if (a.name == 'class') {
1063                 if (a.value.match(/^Mso/)) {
1064                     node.className = '';
1065                 }
1066             }
1067             
1068             // style cleanup!?
1069             // class cleanup?
1070             
1071         }
1072         
1073         
1074         this.cleanUpChildren(node);
1075         
1076         
1077     }
1078     
1079     
1080     // hide stuff that is not compatible
1081     /**
1082      * @event blur
1083      * @hide
1084      */
1085     /**
1086      * @event change
1087      * @hide
1088      */
1089     /**
1090      * @event focus
1091      * @hide
1092      */
1093     /**
1094      * @event specialkey
1095      * @hide
1096      */
1097     /**
1098      * @cfg {String} fieldClass @hide
1099      */
1100     /**
1101      * @cfg {String} focusClass @hide
1102      */
1103     /**
1104      * @cfg {String} autoCreate @hide
1105      */
1106     /**
1107      * @cfg {String} inputType @hide
1108      */
1109     /**
1110      * @cfg {String} invalidClass @hide
1111      */
1112     /**
1113      * @cfg {String} invalidText @hide
1114      */
1115     /**
1116      * @cfg {String} msgFx @hide
1117      */
1118     /**
1119      * @cfg {String} validateOnBlur @hide
1120      */
1121 });
1122
1123 Roo.form.HtmlEditor.white = [
1124         'area', 'br', 'img', 'input', 'hr', 'wbr',
1125         
1126        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1127        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1128        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1129        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1130        'table',   'ul',         'xmp', 
1131        
1132        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1133       'thead',   'tr', 
1134      
1135       'dir', 'menu', 'ol', 'ul', 'dl',
1136        
1137       'embed',  'object'
1138 ];
1139
1140
1141 Roo.form.HtmlEditor.black = [
1142     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1143         'applet', // 
1144         'base',   'basefont', 'bgsound', 'blink',  'body', 
1145         'frame',  'frameset', 'head',    'html',   'ilayer', 
1146         'iframe', 'layer',  'link',     'meta',    'object',   
1147         'script', 'style' ,'title',  'xml' // clean later..
1148 ];
1149 Roo.form.HtmlEditor.clean = [
1150     'script', 'style', 'title', 'xml'
1151 ];
1152 Roo.form.HtmlEditor.remove = [
1153     'font'
1154 ];
1155 // attributes..
1156
1157 Roo.form.HtmlEditor.ablack = [
1158     'on'
1159 ];
1160     
1161 Roo.form.HtmlEditor.aclean = [ 
1162     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1163 ];
1164
1165 // protocols..
1166 Roo.form.HtmlEditor.pwhite= [
1167         'http',  'https',  'mailto'
1168 ];
1169
1170 Roo.form.HtmlEditor.cwhite= [
1171         'text-align',
1172         'font-size'
1173 ];
1174