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.stylesheet.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();
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         }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
681             this.win.focus();
682             this.execCmd('InsertHTML', text);
683             this.deferFocus();
684         }
685     },
686  // private
687     mozKeyPress : function(e){
688         if(e.ctrlKey){
689             var c = e.getCharCode(), cmd;
690           
691             if(c > 0){
692                 c = String.fromCharCode(c).toLowerCase();
693                 switch(c){
694                     case 'b':
695                         cmd = 'bold';
696                     break;
697                     case 'i':
698                         cmd = 'italic';
699                     break;
700                     case 'u':
701                         cmd = 'underline';
702                     case 'v':
703                         this.cleanUpPaste.defer(100, this);
704                         return;
705                     break;
706                 }
707                 if(cmd){
708                     this.win.focus();
709                     this.execCmd(cmd);
710                     this.deferFocus();
711                     e.preventDefault();
712                 }
713                 
714             }
715         }
716     },
717
718     // private
719     fixKeys : function(){ // load time branching for fastest keydown performance
720         if(Roo.isIE){
721             return function(e){
722                 var k = e.getKey(), r;
723                 if(k == e.TAB){
724                     e.stopEvent();
725                     r = this.doc.selection.createRange();
726                     if(r){
727                         r.collapse(true);
728                         r.pasteHTML('&#160;&#160;&#160;&#160;');
729                         this.deferFocus();
730                     }
731                     return;
732                 }
733                 
734                 if(k == e.ENTER){
735                     r = this.doc.selection.createRange();
736                     if(r){
737                         var target = r.parentElement();
738                         if(!target || target.tagName.toLowerCase() != 'li'){
739                             e.stopEvent();
740                             r.pasteHTML('<br />');
741                             r.collapse(false);
742                             r.select();
743                         }
744                     }
745                 }
746                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
747                     this.cleanUpPaste.defer(100, this);
748                     return;
749                 }
750                 
751                 
752             };
753         }else if(Roo.isOpera){
754             return function(e){
755                 var k = e.getKey();
756                 if(k == e.TAB){
757                     e.stopEvent();
758                     this.win.focus();
759                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
760                     this.deferFocus();
761                 }
762                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
763                     this.cleanUpPaste.defer(100, this);
764                     return;
765                 }
766                 
767             };
768         }else if(Roo.isSafari){
769             return function(e){
770                 var k = e.getKey();
771                 
772                 if(k == e.TAB){
773                     e.stopEvent();
774                     this.execCmd('InsertText','\t');
775                     this.deferFocus();
776                     return;
777                 }
778                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
779                     this.cleanUpPaste.defer(100, this);
780                     return;
781                 }
782                 
783              };
784         }
785     }(),
786     
787     getAllAncestors: function()
788     {
789         var p = this.getSelectedNode();
790         var a = [];
791         if (!p) {
792             a.push(p); // push blank onto stack..
793             p = this.getParentElement();
794         }
795         
796         
797         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
798             a.push(p);
799             p = p.parentNode;
800         }
801         a.push(this.doc.body);
802         return a;
803     },
804     lastSel : false,
805     lastSelNode : false,
806     
807     
808     getSelection : function() 
809     {
810         this.assignDocWin();
811         return Roo.isIE ? this.doc.selection : this.win.getSelection();
812     },
813     
814     getSelectedNode: function() 
815     {
816         // this may only work on Gecko!!!
817         
818         // should we cache this!!!!
819         
820         
821         
822          
823         var range = this.createRange(this.getSelection());
824         
825         if (Roo.isIE) {
826             var parent = range.parentElement();
827             while (true) {
828                 var testRange = range.duplicate();
829                 testRange.moveToElementText(parent);
830                 if (testRange.inRange(range)) {
831                     break;
832                 }
833                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
834                     break;
835                 }
836                 parent = parent.parentElement;
837             }
838             return parent;
839         }
840         
841         // is ancestor a text element.
842         var ac =  range.commonAncestorContainer;
843         if (ac.nodeType == 3) {
844             ac = ac.parentNode;
845         }
846         
847         var ar = ac.childNodes;
848          
849         var nodes = [];
850         var other_nodes = [];
851         var has_other_nodes = false;
852         for (var i=0;i<ar.length;i++) {
853             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
854                 continue;
855             }
856             // fullly contained node.
857             
858             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
859                 nodes.push(ar[i]);
860                 continue;
861             }
862             
863             // probably selected..
864             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
865                 other_nodes.push(ar[i]);
866                 continue;
867             }
868             // outer..
869             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
870                 continue;
871             }
872             
873             
874             has_other_nodes = true;
875         }
876         if (!nodes.length && other_nodes.length) {
877             nodes= other_nodes;
878         }
879         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
880             return false;
881         }
882         
883         return nodes[0];
884     },
885     createRange: function(sel)
886     {
887         // this has strange effects when using with 
888         // top toolbar - not sure if it's a great idea.
889         //this.editor.contentWindow.focus();
890         if (typeof sel != "undefined") {
891             try {
892                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
893             } catch(e) {
894                 return this.doc.createRange();
895             }
896         } else {
897             return this.doc.createRange();
898         }
899     },
900     getParentElement: function()
901     {
902         
903         this.assignDocWin();
904         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
905         
906         var range = this.createRange(sel);
907          
908         try {
909             var p = range.commonAncestorContainer;
910             while (p.nodeType == 3) { // text node
911                 p = p.parentNode;
912             }
913             return p;
914         } catch (e) {
915             return null;
916         }
917     
918     },
919     /***
920      *
921      * Range intersection.. the hard stuff...
922      *  '-1' = before
923      *  '0' = hits..
924      *  '1' = after.
925      *         [ -- selected range --- ]
926      *   [fail]                        [fail]
927      *
928      *    basically..
929      *      if end is before start or  hits it. fail.
930      *      if start is after end or hits it fail.
931      *
932      *   if either hits (but other is outside. - then it's not 
933      *   
934      *    
935      **/
936     
937     
938     // @see http://www.thismuchiknow.co.uk/?p=64.
939     rangeIntersectsNode : function(range, node)
940     {
941         var nodeRange = node.ownerDocument.createRange();
942         try {
943             nodeRange.selectNode(node);
944         } catch (e) {
945             nodeRange.selectNodeContents(node);
946         }
947     
948         var rangeStartRange = range.cloneRange();
949         rangeStartRange.collapse(true);
950     
951         var rangeEndRange = range.cloneRange();
952         rangeEndRange.collapse(false);
953     
954         var nodeStartRange = nodeRange.cloneRange();
955         nodeStartRange.collapse(true);
956     
957         var nodeEndRange = nodeRange.cloneRange();
958         nodeEndRange.collapse(false);
959     
960         return rangeStartRange.compareBoundaryPoints(
961                  Range.START_TO_START, nodeEndRange) == -1 &&
962                rangeEndRange.compareBoundaryPoints(
963                  Range.START_TO_START, nodeStartRange) == 1;
964         
965          
966     },
967     rangeCompareNode : function(range, node)
968     {
969         var nodeRange = node.ownerDocument.createRange();
970         try {
971             nodeRange.selectNode(node);
972         } catch (e) {
973             nodeRange.selectNodeContents(node);
974         }
975         
976         
977         range.collapse(true);
978     
979         nodeRange.collapse(true);
980      
981         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
982         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
983          
984         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
985         
986         var nodeIsBefore   =  ss == 1;
987         var nodeIsAfter    = ee == -1;
988         
989         if (nodeIsBefore && nodeIsAfter)
990             return 0; // outer
991         if (!nodeIsBefore && nodeIsAfter)
992             return 1; //right trailed.
993         
994         if (nodeIsBefore && !nodeIsAfter)
995             return 2;  // left trailed.
996         // fully contined.
997         return 3;
998     },
999
1000     // private? - in a new class?
1001     cleanUpPaste :  function()
1002     {
1003         // cleans up the whole document..
1004       //  console.log('cleanuppaste');
1005         this.cleanUpChildren(this.doc.body);
1006         
1007         
1008     },
1009     cleanUpChildren : function (n)
1010     {
1011         if (!n.childNodes.length) {
1012             return;
1013         }
1014         for (var i = n.childNodes.length-1; i > -1 ; i--) {
1015            this.cleanUpChild(n.childNodes[i]);
1016         }
1017     },
1018     
1019     
1020         
1021     
1022     cleanUpChild : function (node)
1023     {
1024         //console.log(node);
1025         if (node.nodeName == "#text") {
1026             // clean up silly Windows -- stuff?
1027             return; 
1028         }
1029         if (node.nodeName == "#comment") {
1030             node.parentNode.removeChild(node);
1031             // clean up silly Windows -- stuff?
1032             return; 
1033         }
1034         
1035         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1036             // remove node.
1037             node.parentNode.removeChild(node);
1038             return;
1039             
1040         }
1041         if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
1042             this.cleanUpChildren(node);
1043             // inserts everything just before this node...
1044             while (node.childNodes.length) {
1045                 var cn = node.childNodes[0];
1046                 node.removeChild(cn);
1047                 node.parentNode.insertBefore(cn, node);
1048             }
1049             node.parentNode.removeChild(node);
1050             return;
1051         }
1052         
1053         if (!node.attributes || !node.attributes.length) {
1054             this.cleanUpChildren(node);
1055             return;
1056         }
1057         
1058         function cleanAttr(n,v)
1059         {
1060             
1061             if (v.match(/^\./) || v.match(/^\//)) {
1062                 return;
1063             }
1064             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1065                 return;
1066             }
1067             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1068             node.removeAttribute(n);
1069             
1070         }
1071         
1072         function cleanStyle(n,v)
1073         {
1074             if (v.match(/expression/)) { //XSS?? should we even bother..
1075                 node.removeAttribute(n);
1076                 return;
1077             }
1078             
1079             
1080             var parts = v.split(/;/);
1081             Roo.each(parts, function(p) {
1082                 p = p.replace(/\s+/g,'');
1083                 if (!p.length) {
1084                     return true;
1085                 }
1086                 var l = p.split(':').shift().replace(/\s+/g,'');
1087                 
1088                 // only allow 'c whitelisted system attributes'
1089                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1090                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1091                     node.removeAttribute(n);
1092                     return false;
1093                 }
1094                 return true;
1095             });
1096             
1097             
1098         }
1099         
1100         
1101         for (var i = node.attributes.length-1; i > -1 ; i--) {
1102             var a = node.attributes[i];
1103             //console.log(a);
1104             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1105                 node.removeAttribute(a.name);
1106                 return;
1107             }
1108             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1109                 cleanAttr(a.name,a.value); // fixme..
1110                 return;
1111             }
1112             if (a.name == 'style') {
1113                 cleanStyle(a.name,a.value);
1114             }
1115             /// clean up MS crap..
1116             if (a.name == 'class') {
1117                 if (a.value.match(/^Mso/)) {
1118                     node.className = '';
1119                 }
1120             }
1121             
1122             // style cleanup!?
1123             // class cleanup?
1124             
1125         }
1126         
1127         
1128         this.cleanUpChildren(node);
1129         
1130         
1131     }
1132     
1133     
1134     // hide stuff that is not compatible
1135     /**
1136      * @event blur
1137      * @hide
1138      */
1139     /**
1140      * @event change
1141      * @hide
1142      */
1143     /**
1144      * @event focus
1145      * @hide
1146      */
1147     /**
1148      * @event specialkey
1149      * @hide
1150      */
1151     /**
1152      * @cfg {String} fieldClass @hide
1153      */
1154     /**
1155      * @cfg {String} focusClass @hide
1156      */
1157     /**
1158      * @cfg {String} autoCreate @hide
1159      */
1160     /**
1161      * @cfg {String} inputType @hide
1162      */
1163     /**
1164      * @cfg {String} invalidClass @hide
1165      */
1166     /**
1167      * @cfg {String} invalidText @hide
1168      */
1169     /**
1170      * @cfg {String} msgFx @hide
1171      */
1172     /**
1173      * @cfg {String} validateOnBlur @hide
1174      */
1175 });
1176
1177 Roo.form.HtmlEditor.white = [
1178         'area', 'br', 'img', 'input', 'hr', 'wbr',
1179         
1180        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1181        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1182        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1183        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1184        'table',   'ul',         'xmp', 
1185        
1186        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1187       'thead',   'tr', 
1188      
1189       'dir', 'menu', 'ol', 'ul', 'dl',
1190        
1191       'embed',  'object'
1192 ];
1193
1194
1195 Roo.form.HtmlEditor.black = [
1196     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1197         'applet', // 
1198         'base',   'basefont', 'bgsound', 'blink',  'body', 
1199         'frame',  'frameset', 'head',    'html',   'ilayer', 
1200         'iframe', 'layer',  'link',     'meta',    'object',   
1201         'script', 'style' ,'title',  'xml' // clean later..
1202 ];
1203 Roo.form.HtmlEditor.clean = [
1204     'script', 'style', 'title', 'xml'
1205 ];
1206 Roo.form.HtmlEditor.remove = [
1207     'font'
1208 ];
1209 // attributes..
1210
1211 Roo.form.HtmlEditor.ablack = [
1212     'on'
1213 ];
1214     
1215 Roo.form.HtmlEditor.aclean = [ 
1216     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1217 ];
1218
1219 // protocols..
1220 Roo.form.HtmlEditor.pwhite= [
1221         'http',  'https',  'mailto'
1222 ];
1223
1224 // white listed style attributes.
1225 Roo.form.HtmlEditor.cwhite= [
1226         'text-align',
1227         'font-size'
1228 ];
1229