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