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