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