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