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