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