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