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