Roo.form.HtmlEditor add @config resizable = 's' or 'r'
[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 (!node.attributes || !node.attributes.length) {
957             this.cleanUpChildren(node);
958             return;
959         }
960         
961         function cleanAttr(n,v)
962         {
963             
964             if (v.match(/^\./) || v.match(/^\//)) {
965                 return;
966             }
967             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
968                 return;
969             }
970             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
971             node.removeAttribute(n);
972             
973         }
974         
975         function cleanStyle(n,v)
976         {
977             if (v.match(/expression/)) { //XSS?? should we even bother..
978                 node.removeAttribute(n);
979                 return;
980             }
981             
982             
983             var parts = v.split(/;/);
984             Roo.each(parts, function(p) {
985                 p = p.replace(/\s+/g,'');
986                 if (!p.length) {
987                     return;
988                 }
989                 var l = p.split(':').shift().replace(/\s+/g,'');
990                 
991                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
992                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
993                     node.removeAttribute(n);
994                     return false;
995                 }
996             });
997             
998             
999         }
1000         
1001         
1002         for (var i = node.attributes.length-1; i > -1 ; i--) {
1003             var a = node.attributes[i];
1004             //console.log(a);
1005             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1006                 node.removeAttribute(a.name);
1007                 return;
1008             }
1009             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1010                 cleanAttr(a.name,a.value); // fixme..
1011                 return;
1012             }
1013             if (a.name == 'style') {
1014                 cleanStyle(a.name,a.value);
1015             }
1016             /// clean up MS crap..
1017             if (a.name == 'class') {
1018                 if (a.value.match(/^Mso/)) {
1019                     node.className = '';
1020                 }
1021             }
1022             
1023             // style cleanup!?
1024             // class cleanup?
1025             
1026         }
1027         
1028         
1029         this.cleanUpChildren(node);
1030         
1031         
1032     }
1033     
1034     
1035     // hide stuff that is not compatible
1036     /**
1037      * @event blur
1038      * @hide
1039      */
1040     /**
1041      * @event change
1042      * @hide
1043      */
1044     /**
1045      * @event focus
1046      * @hide
1047      */
1048     /**
1049      * @event specialkey
1050      * @hide
1051      */
1052     /**
1053      * @cfg {String} fieldClass @hide
1054      */
1055     /**
1056      * @cfg {String} focusClass @hide
1057      */
1058     /**
1059      * @cfg {String} autoCreate @hide
1060      */
1061     /**
1062      * @cfg {String} inputType @hide
1063      */
1064     /**
1065      * @cfg {String} invalidClass @hide
1066      */
1067     /**
1068      * @cfg {String} invalidText @hide
1069      */
1070     /**
1071      * @cfg {String} msgFx @hide
1072      */
1073     /**
1074      * @cfg {String} validateOnBlur @hide
1075      */
1076 });
1077
1078 Roo.form.HtmlEditor.white = [
1079         'area', 'br', 'img', 'input', 'hr', 'wbr',
1080         
1081        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1082        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1083        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1084        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1085        'table',   'ul',         'xmp', 
1086        
1087        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1088       'thead',   'tr', 
1089      
1090       'dir', 'menu', 'ol', 'ul', 'dl',
1091        
1092       'embed',  'object'
1093 ];
1094
1095
1096 Roo.form.HtmlEditor.black = [
1097     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1098         'applet', // 
1099         'base',   'basefont', 'bgsound', 'blink',  'body', 
1100         'frame',  'frameset', 'head',    'html',   'ilayer', 
1101         'iframe', 'layer',  'link',     'meta',    'object',   
1102         'script', 'style' ,'title',  'xml' // clean later..
1103 ];
1104 Roo.form.HtmlEditor.clean = [
1105     'script', 'style', 'title', 'xml'
1106 ];
1107
1108 // attributes..
1109
1110 Roo.form.HtmlEditor.ablack = [
1111     'on'
1112 ];
1113     
1114 Roo.form.HtmlEditor.aclean = [ 
1115     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1116 ];
1117
1118 // protocols..
1119 Roo.form.HtmlEditor.pwhite= [
1120         'http',  'https',  'mailto'
1121 ];
1122
1123 Roo.form.HtmlEditor.cwhite= [
1124         'text-align',
1125         'font-size'
1126 ];
1127