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