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