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