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 -= 10; // 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             'dblclick': this.onEditorEvent,
486             'click': this.onEditorEvent,
487             'keyup': this.onEditorEvent,
488             buffer:100,
489             scope: this
490         });
491         if(Roo.isGecko){
492             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
493         }
494         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
495             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
496         }
497         this.initialized = true;
498
499         this.fireEvent('initialize', this);
500         this.pushValue();
501     },
502
503     // private
504     onDestroy : function(){
505         
506         
507         
508         if(this.rendered){
509             
510             for (var i =0; i < this.toolbars.length;i++) {
511                 // fixme - ask toolbars for heights?
512                 this.toolbars[i].onDestroy();
513             }
514             
515             this.wrap.dom.innerHTML = '';
516             this.wrap.remove();
517         }
518     },
519
520     // private
521     onFirstFocus : function(){
522         
523         this.assignDocWin();
524         
525         
526         this.activated = true;
527         for (var i =0; i < this.toolbars.length;i++) {
528             this.toolbars[i].onFirstFocus();
529         }
530        
531         if(Roo.isGecko){ // prevent silly gecko errors
532             this.win.focus();
533             var s = this.win.getSelection();
534             if(!s.focusNode || s.focusNode.nodeType != 3){
535                 var r = s.getRangeAt(0);
536                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
537                 r.collapse(true);
538                 this.deferFocus();
539             }
540             try{
541                 this.execCmd('useCSS', true);
542                 this.execCmd('styleWithCSS', false);
543             }catch(e){}
544         }
545         this.fireEvent('activate', this);
546     },
547
548     // private
549     adjustFont: function(btn){
550         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
551         //if(Roo.isSafari){ // safari
552         //    adjust *= 2;
553        // }
554         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
555         if(Roo.isSafari){ // safari
556             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
557             v =  (v < 10) ? 10 : v;
558             v =  (v > 48) ? 48 : v;
559             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
560             
561         }
562         
563         
564         v = Math.max(1, v+adjust);
565         
566         this.execCmd('FontSize', v  );
567     },
568
569     onEditorEvent : function(e){
570         this.fireEvent('editorevent', this, e);
571       //  this.updateToolbar();
572         this.syncValue();
573     },
574
575     insertTag : function(tg)
576     {
577         // could be a bit smarter... -> wrap the current selected tRoo..
578         
579         this.execCmd("formatblock",   tg);
580         
581     },
582     
583     insertText : function(txt)
584     {
585         
586         
587         range = this.createRange();
588         range.deleteContents();
589                //alert(Sender.getAttribute('label'));
590                
591         range.insertNode(this.doc.createTextNode(txt));
592     } ,
593     
594     // private
595     relayBtnCmd : function(btn){
596         this.relayCmd(btn.cmd);
597     },
598
599     /**
600      * Executes a Midas editor command on the editor document and performs necessary focus and
601      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
602      * @param {String} cmd The Midas command
603      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
604      */
605     relayCmd : function(cmd, value){
606         this.win.focus();
607         this.execCmd(cmd, value);
608         this.fireEvent('editorevent', this);
609         //this.updateToolbar();
610         this.deferFocus();
611     },
612
613     /**
614      * Executes a Midas editor command directly on the editor document.
615      * For visual commands, you should use {@link #relayCmd} instead.
616      * <b>This should only be called after the editor is initialized.</b>
617      * @param {String} cmd The Midas command
618      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
619      */
620     execCmd : function(cmd, value){
621         this.doc.execCommand(cmd, false, value === undefined ? null : value);
622         this.syncValue();
623     },
624
625    
626     /**
627      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
628      * to insert tRoo.
629      * @param {String} text
630      */
631     insertAtCursor : function(text){
632         if(!this.activated){
633             return;
634         }
635         if(Roo.isIE){
636             this.win.focus();
637             var r = this.doc.selection.createRange();
638             if(r){
639                 r.collapse(true);
640                 r.pasteHTML(text);
641                 this.syncValue();
642                 this.deferFocus();
643             }
644         }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
645             this.win.focus();
646             this.execCmd('InsertHTML', text);
647             this.deferFocus();
648         }
649     },
650  // private
651     mozKeyPress : function(e){
652         if(e.ctrlKey){
653             var c = e.getCharCode(), cmd;
654           
655             if(c > 0){
656                 c = String.fromCharCode(c).toLowerCase();
657                 switch(c){
658                     case 'b':
659                         cmd = 'bold';
660                     break;
661                     case 'i':
662                         cmd = 'italic';
663                     break;
664                     case 'u':
665                         cmd = 'underline';
666                     case 'v':
667                         this.cleanUpPaste.defer(100, this);
668                         return;
669                     break;
670                 }
671                 if(cmd){
672                     this.win.focus();
673                     this.execCmd(cmd);
674                     this.deferFocus();
675                     e.preventDefault();
676                 }
677                 
678             }
679         }
680     },
681
682     // private
683     fixKeys : function(){ // load time branching for fastest keydown performance
684         if(Roo.isIE){
685             return function(e){
686                 var k = e.getKey(), r;
687                 if(k == e.TAB){
688                     e.stopEvent();
689                     r = this.doc.selection.createRange();
690                     if(r){
691                         r.collapse(true);
692                         r.pasteHTML('&#160;&#160;&#160;&#160;');
693                         this.deferFocus();
694                     }
695                     return;
696                 }
697                 
698                 if(k == e.ENTER){
699                     r = this.doc.selection.createRange();
700                     if(r){
701                         var target = r.parentElement();
702                         if(!target || target.tagName.toLowerCase() != 'li'){
703                             e.stopEvent();
704                             r.pasteHTML('<br />');
705                             r.collapse(false);
706                             r.select();
707                         }
708                     }
709                 }
710                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
711                     this.cleanUpPaste.defer(100, this);
712                     return;
713                 }
714                 
715                 
716             };
717         }else if(Roo.isOpera){
718             return function(e){
719                 var k = e.getKey();
720                 if(k == e.TAB){
721                     e.stopEvent();
722                     this.win.focus();
723                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
724                     this.deferFocus();
725                 }
726                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
727                     this.cleanUpPaste.defer(100, this);
728                     return;
729                 }
730                 
731             };
732         }else if(Roo.isSafari){
733             return function(e){
734                 var k = e.getKey();
735                 
736                 if(k == e.TAB){
737                     e.stopEvent();
738                     this.execCmd('InsertText','\t');
739                     this.deferFocus();
740                     return;
741                 }
742                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
743                     this.cleanUpPaste.defer(100, this);
744                     return;
745                 }
746                 
747              };
748         }
749     }(),
750     
751     getAllAncestors: function()
752     {
753         var p = this.getSelectedNode();
754         var a = [];
755         if (!p) {
756             a.push(p); // push blank onto stack..
757             p = this.getParentElement();
758         }
759         
760         
761         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
762             a.push(p);
763             p = p.parentNode;
764         }
765         a.push(this.doc.body);
766         return a;
767     },
768     lastSel : false,
769     lastSelNode : false,
770     
771     
772     getSelection : function() 
773     {
774         this.assignDocWin();
775         return Roo.isIE ? this.doc.selection : this.win.getSelection();
776     },
777     
778     getSelectedNode: function() 
779     {
780         // this may only work on Gecko!!!
781         
782         // should we cache this!!!!
783         
784         
785         
786          
787         var range = this.createRange(this.getSelection());
788         
789         if (Roo.isIE) {
790             var parent = range.parentElement();
791             while (true) {
792                 var testRange = range.duplicate();
793                 testRange.moveToElementText(parent);
794                 if (testRange.inRange(range)) {
795                     break;
796                 }
797                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
798                     break;
799                 }
800                 parent = parent.parentElement;
801             }
802             return parent;
803         }
804         
805         
806         var ar = range.endContainer.childNodes;
807         if (!ar.length) {
808             ar = range.commonAncestorContainer.childNodes;
809             //alert(ar.length);
810         }
811         var nodes = [];
812         var other_nodes = [];
813         var has_other_nodes = false;
814         for (var i=0;i<ar.length;i++) {
815             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
816                 continue;
817             }
818             // fullly contained node.
819             
820             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
821                 nodes.push(ar[i]);
822                 continue;
823             }
824             
825             // probably selected..
826             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
827                 other_nodes.push(ar[i]);
828                 continue;
829             }
830             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
831                 continue;
832             }
833             
834             
835             has_other_nodes = true;
836         }
837         if (!nodes.length && other_nodes.length) {
838             nodes= other_nodes;
839         }
840         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
841             return false;
842         }
843         
844         return nodes[0];
845     },
846     createRange: function(sel)
847     {
848         // this has strange effects when using with 
849         // top toolbar - not sure if it's a great idea.
850         //this.editor.contentWindow.focus();
851         if (typeof sel != "undefined") {
852             try {
853                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
854             } catch(e) {
855                 return this.doc.createRange();
856             }
857         } else {
858             return this.doc.createRange();
859         }
860     },
861     getParentElement: function()
862     {
863         
864         this.assignDocWin();
865         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
866         
867         var range = this.createRange(sel);
868          
869         try {
870             var p = range.commonAncestorContainer;
871             while (p.nodeType == 3) { // text node
872                 p = p.parentNode;
873             }
874             return p;
875         } catch (e) {
876             return null;
877         }
878     
879     },
880     
881     
882     
883     // BC Hacks - cause I cant work out what i was trying to do..
884     rangeIntersectsNode : function(range, node)
885     {
886         var nodeRange = node.ownerDocument.createRange();
887         try {
888             nodeRange.selectNode(node);
889         }
890         catch (e) {
891             nodeRange.selectNodeContents(node);
892         }
893
894         return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
895                  range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
896     },
897     rangeCompareNode : function(range, node) {
898         var nodeRange = node.ownerDocument.createRange();
899         try {
900             nodeRange.selectNode(node);
901         } catch (e) {
902             nodeRange.selectNodeContents(node);
903         }
904         var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
905         var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
906
907         if (nodeIsBefore && !nodeIsAfter)
908             return 0;
909         if (!nodeIsBefore && nodeIsAfter)
910             return 1;
911         if (nodeIsBefore && nodeIsAfter)
912             return 2;
913
914         return 3;
915     },
916
917     // private? - in a new class?
918     cleanUpPaste :  function()
919     {
920         // cleans up the whole document..
921       //  console.log('cleanuppaste');
922         this.cleanUpChildren(this.doc.body);
923         
924         
925     },
926     cleanUpChildren : function (n)
927     {
928         if (!n.childNodes.length) {
929             return;
930         }
931         for (var i = n.childNodes.length-1; i > -1 ; i--) {
932            this.cleanUpChild(n.childNodes[i]);
933         }
934     },
935     
936     
937         
938     
939     cleanUpChild : function (node)
940     {
941         //console.log(node);
942         if (node.nodeName == "#text") {
943             // clean up silly Windows -- stuff?
944             return; 
945         }
946         if (node.nodeName == "#comment") {
947             node.parentNode.removeChild(node);
948             // clean up silly Windows -- stuff?
949             return; 
950         }
951         
952         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
953             // remove node.
954             node.parentNode.removeChild(node);
955             return;
956             
957         }
958         if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
959             this.cleanUpChildren(node);
960             // inserts everything just before this node...
961             while (node.childNodes.length) {
962                 var cn = node.childNodes[0];
963                 node.removeChild(cn);
964                 node.parentNode.insertBefore(cn, node);
965             }
966             node.parentNode.removeChild(node);
967             return;
968         }
969         
970         if (!node.attributes || !node.attributes.length) {
971             this.cleanUpChildren(node);
972             return;
973         }
974         
975         function cleanAttr(n,v)
976         {
977             
978             if (v.match(/^\./) || v.match(/^\//)) {
979                 return;
980             }
981             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
982                 return;
983             }
984             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
985             node.removeAttribute(n);
986             
987         }
988         
989         function cleanStyle(n,v)
990         {
991             if (v.match(/expression/)) { //XSS?? should we even bother..
992                 node.removeAttribute(n);
993                 return;
994             }
995             
996             
997             var parts = v.split(/;/);
998             Roo.each(parts, function(p) {
999                 p = p.replace(/\s+/g,'');
1000                 if (!p.length) {
1001                     return true;
1002                 }
1003                 var l = p.split(':').shift().replace(/\s+/g,'');
1004                 
1005                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1006                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1007                     node.removeAttribute(n);
1008                     return false;
1009                 }
1010                 return true;
1011             });
1012             
1013             
1014         }
1015         
1016         
1017         for (var i = node.attributes.length-1; i > -1 ; i--) {
1018             var a = node.attributes[i];
1019             //console.log(a);
1020             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1021                 node.removeAttribute(a.name);
1022                 return;
1023             }
1024             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1025                 cleanAttr(a.name,a.value); // fixme..
1026                 return;
1027             }
1028             if (a.name == 'style') {
1029                 cleanStyle(a.name,a.value);
1030             }
1031             /// clean up MS crap..
1032             if (a.name == 'class') {
1033                 if (a.value.match(/^Mso/)) {
1034                     node.className = '';
1035                 }
1036             }
1037             
1038             // style cleanup!?
1039             // class cleanup?
1040             
1041         }
1042         
1043         
1044         this.cleanUpChildren(node);
1045         
1046         
1047     }
1048     
1049     
1050     // hide stuff that is not compatible
1051     /**
1052      * @event blur
1053      * @hide
1054      */
1055     /**
1056      * @event change
1057      * @hide
1058      */
1059     /**
1060      * @event focus
1061      * @hide
1062      */
1063     /**
1064      * @event specialkey
1065      * @hide
1066      */
1067     /**
1068      * @cfg {String} fieldClass @hide
1069      */
1070     /**
1071      * @cfg {String} focusClass @hide
1072      */
1073     /**
1074      * @cfg {String} autoCreate @hide
1075      */
1076     /**
1077      * @cfg {String} inputType @hide
1078      */
1079     /**
1080      * @cfg {String} invalidClass @hide
1081      */
1082     /**
1083      * @cfg {String} invalidText @hide
1084      */
1085     /**
1086      * @cfg {String} msgFx @hide
1087      */
1088     /**
1089      * @cfg {String} validateOnBlur @hide
1090      */
1091 });
1092
1093 Roo.form.HtmlEditor.white = [
1094         'area', 'br', 'img', 'input', 'hr', 'wbr',
1095         
1096        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1097        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1098        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1099        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1100        'table',   'ul',         'xmp', 
1101        
1102        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1103       'thead',   'tr', 
1104      
1105       'dir', 'menu', 'ol', 'ul', 'dl',
1106        
1107       'embed',  'object'
1108 ];
1109
1110
1111 Roo.form.HtmlEditor.black = [
1112     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1113         'applet', // 
1114         'base',   'basefont', 'bgsound', 'blink',  'body', 
1115         'frame',  'frameset', 'head',    'html',   'ilayer', 
1116         'iframe', 'layer',  'link',     'meta',    'object',   
1117         'script', 'style' ,'title',  'xml' // clean later..
1118 ];
1119 Roo.form.HtmlEditor.clean = [
1120     'script', 'style', 'title', 'xml'
1121 ];
1122 Roo.form.HtmlEditor.remove = [
1123     'font'
1124 ];
1125 // attributes..
1126
1127 Roo.form.HtmlEditor.ablack = [
1128     'on'
1129 ];
1130     
1131 Roo.form.HtmlEditor.aclean = [ 
1132     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1133 ];
1134
1135 // protocols..
1136 Roo.form.HtmlEditor.pwhite= [
1137         'http',  'https',  'mailto'
1138 ];
1139
1140 Roo.form.HtmlEditor.cwhite= [
1141         'text-align',
1142         'font-size'
1143 ];
1144