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