roojs-ui.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           
351             this.syncValue();
352             this.iframe.className = 'x-hidden';
353             this.el.removeClass('x-hidden');
354             this.el.dom.removeAttribute('tabIndex');
355             this.el.focus();
356         }else{
357              
358             this.pushValue();
359             this.iframe.className = '';
360             this.el.addClass('x-hidden');
361             this.el.dom.setAttribute('tabIndex', -1);
362             this.deferFocus();
363         }
364         this.setSize(this.wrap.getSize());
365         this.fireEvent('editmodechange', this, this.sourceEditMode);
366     },
367
368     // private used internally
369     createLink : function(){
370         var url = prompt(this.createLinkText, this.defaultLinkValue);
371         if(url && url != 'http:/'+'/'){
372             this.relayCmd('createlink', url);
373         }
374     },
375
376     // private (for BoxComponent)
377     adjustSize : Roo.BoxComponent.prototype.adjustSize,
378
379     // private (for BoxComponent)
380     getResizeEl : function(){
381         return this.wrap;
382     },
383
384     // private (for BoxComponent)
385     getPositionEl : function(){
386         return this.wrap;
387     },
388
389     // private
390     initEvents : function(){
391         this.originalValue = this.getValue();
392     },
393
394     /**
395      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
396      * @method
397      */
398     markInvalid : Roo.emptyFn,
399     /**
400      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
401      * @method
402      */
403     clearInvalid : Roo.emptyFn,
404
405     setValue : function(v){
406         Roo.form.HtmlEditor.superclass.setValue.call(this, v);
407         this.pushValue();
408     },
409
410     /**
411      * Protected method that will not generally be called directly. If you need/want
412      * custom HTML cleanup, this is the method you should override.
413      * @param {String} html The HTML to be cleaned
414      * return {String} The cleaned HTML
415      */
416     cleanHtml : function(html){
417         html = String(html);
418         if(html.length > 5){
419             if(Roo.isSafari){ // strip safari nonsense
420                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
421             }
422         }
423         if(html == '&nbsp;'){
424             html = '';
425         }
426         return html;
427     },
428
429     /**
430      * Protected method that will not generally be called directly. Syncs the contents
431      * of the editor iframe with the textarea.
432      */
433     syncValue : function(){
434         if(this.initialized){
435             var bd = (this.doc.body || this.doc.documentElement);
436             //this.cleanUpPaste(); -- this is done else where and causes havoc..
437             var html = bd.innerHTML;
438             if(Roo.isSafari){
439                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
440                 var m = bs.match(/text-align:(.*?);/i);
441                 if(m && m[1]){
442                     html = '<div style="'+m[0]+'">' + html + '</div>';
443                 }
444             }
445             html = this.cleanHtml(html);
446             // fix up the special chars..
447             html = html.replace(/([\x80-\uffff])/g, function (a, b) {
448                 return "&#"+b.charCodeAt()+";" 
449             });
450             if(this.fireEvent('beforesync', this, html) !== false){
451                 this.el.dom.value = html;
452                 this.fireEvent('sync', this, html);
453             }
454         }
455     },
456
457     /**
458      * Protected method that will not generally be called directly. Pushes the value of the textarea
459      * into the iframe editor.
460      */
461     pushValue : function(){
462         if(this.initialized){
463             var v = this.el.dom.value;
464             if(v.length < 1){
465                 v = '&#160;';
466             }
467             
468             if(this.fireEvent('beforepush', this, v) !== false){
469                 var d = (this.doc.body || this.doc.documentElement);
470                 d.innerHTML = v;
471                 this.cleanUpPaste();
472                 this.el.dom.value = d.innerHTML;
473                 this.fireEvent('push', this, v);
474             }
475         }
476     },
477
478     // private
479     deferFocus : function(){
480         this.focus.defer(10, this);
481     },
482
483     // doc'ed in Field
484     focus : function(){
485         if(this.win && !this.sourceEditMode){
486             this.win.focus();
487         }else{
488             this.el.focus();
489         }
490     },
491     
492     assignDocWin: function()
493     {
494         var iframe = this.iframe;
495         
496          if(Roo.isIE){
497             this.doc = iframe.contentWindow.document;
498             this.win = iframe.contentWindow;
499         } else {
500             if (!Roo.get(this.frameId)) {
501                 return;
502             }
503             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
504             this.win = Roo.get(this.frameId).dom.contentWindow;
505         }
506     },
507     
508     // private
509     initEditor : function(){
510         //console.log("INIT EDITOR");
511         this.assignDocWin();
512         
513         
514         
515         this.doc.designMode="on";
516         this.doc.open();
517         this.doc.write(this.getDocMarkup());
518         this.doc.close();
519         
520         var dbody = (this.doc.body || this.doc.documentElement);
521         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
522         // this copies styles from the containing element into thsi one..
523         // not sure why we need all of this..
524         var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
525         ss['background-attachment'] = 'fixed'; // w3c
526         dbody.bgProperties = 'fixed'; // ie
527         Roo.DomHelper.applyStyles(dbody, ss);
528         Roo.EventManager.on(this.doc, {
529             //'mousedown': this.onEditorEvent,
530             'mouseup': this.onEditorEvent,
531             'dblclick': this.onEditorEvent,
532             'click': this.onEditorEvent,
533             'keyup': this.onEditorEvent,
534             buffer:100,
535             scope: this
536         });
537         if(Roo.isGecko){
538             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
539         }
540         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
541             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
542         }
543         this.initialized = true;
544
545         this.fireEvent('initialize', this);
546         this.pushValue();
547     },
548
549     // private
550     onDestroy : function(){
551         
552         
553         
554         if(this.rendered){
555             
556             for (var i =0; i < this.toolbars.length;i++) {
557                 // fixme - ask toolbars for heights?
558                 this.toolbars[i].onDestroy();
559             }
560             
561             this.wrap.dom.innerHTML = '';
562             this.wrap.remove();
563         }
564     },
565
566     // private
567     onFirstFocus : function(){
568         
569         this.assignDocWin();
570         
571         
572         this.activated = true;
573         for (var i =0; i < this.toolbars.length;i++) {
574             this.toolbars[i].onFirstFocus();
575         }
576        
577         if(Roo.isGecko){ // prevent silly gecko errors
578             this.win.focus();
579             var s = this.win.getSelection();
580             if(!s.focusNode || s.focusNode.nodeType != 3){
581                 var r = s.getRangeAt(0);
582                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
583                 r.collapse(true);
584                 this.deferFocus();
585             }
586             try{
587                 this.execCmd('useCSS', true);
588                 this.execCmd('styleWithCSS', false);
589             }catch(e){}
590         }
591         this.fireEvent('activate', this);
592     },
593
594     // private
595     adjustFont: function(btn){
596         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
597         //if(Roo.isSafari){ // safari
598         //    adjust *= 2;
599        // }
600         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
601         if(Roo.isSafari){ // safari
602             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
603             v =  (v < 10) ? 10 : v;
604             v =  (v > 48) ? 48 : v;
605             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
606             
607         }
608         
609         
610         v = Math.max(1, v+adjust);
611         
612         this.execCmd('FontSize', v  );
613     },
614
615     onEditorEvent : function(e){
616         this.fireEvent('editorevent', this, e);
617       //  this.updateToolbar();
618         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
619     },
620
621     insertTag : function(tg)
622     {
623         // could be a bit smarter... -> wrap the current selected tRoo..
624         
625         this.execCmd("formatblock",   tg);
626         
627     },
628     
629     insertText : function(txt)
630     {
631         
632         
633         range = this.createRange();
634         range.deleteContents();
635                //alert(Sender.getAttribute('label'));
636                
637         range.insertNode(this.doc.createTextNode(txt));
638     } ,
639     
640     // private
641     relayBtnCmd : function(btn){
642         this.relayCmd(btn.cmd);
643     },
644
645     /**
646      * Executes a Midas editor command on the editor document and performs necessary focus and
647      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
648      * @param {String} cmd The Midas command
649      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
650      */
651     relayCmd : function(cmd, value){
652         this.win.focus();
653         this.execCmd(cmd, value);
654         this.fireEvent('editorevent', this);
655         //this.updateToolbar();
656         this.deferFocus();
657     },
658
659     /**
660      * Executes a Midas editor command directly on the editor document.
661      * For visual commands, you should use {@link #relayCmd} instead.
662      * <b>This should only be called after the editor is initialized.</b>
663      * @param {String} cmd The Midas command
664      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
665      */
666     execCmd : function(cmd, value){
667         this.doc.execCommand(cmd, false, value === undefined ? null : value);
668         this.syncValue();
669     },
670  
671  
672    
673     /**
674      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
675      * to insert tRoo.
676      * @param {String} text | dom node.. 
677      */
678     insertAtCursor : function(text)
679     {
680         
681         
682         
683         if(!this.activated){
684             return;
685         }
686         /*
687         if(Roo.isIE){
688             this.win.focus();
689             var r = this.doc.selection.createRange();
690             if(r){
691                 r.collapse(true);
692                 r.pasteHTML(text);
693                 this.syncValue();
694                 this.deferFocus();
695             
696             }
697             return;
698         }
699         */
700         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
701             this.win.focus();
702             
703             
704             // from jquery ui (MIT licenced)
705             var range, node;
706             var win = this.win;
707             
708             if (win.getSelection && win.getSelection().getRangeAt) {
709                 range = win.getSelection().getRangeAt(0);
710                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
711                 range.insertNode(node);
712             } else if (win.document.selection && win.document.selection.createRange) {
713                 // no firefox support
714                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
715                 win.document.selection.createRange().pasteHTML(txt);
716             } else {
717                 // no firefox support
718                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
719                 this.execCmd('InsertHTML', txt);
720             } 
721             
722             this.syncValue();
723             
724             this.deferFocus();
725         }
726     },
727  // private
728     mozKeyPress : function(e){
729         if(e.ctrlKey){
730             var c = e.getCharCode(), cmd;
731           
732             if(c > 0){
733                 c = String.fromCharCode(c).toLowerCase();
734                 switch(c){
735                     case 'b':
736                         cmd = 'bold';
737                         break;
738                     case 'i':
739                         cmd = 'italic';
740                         break;
741                     
742                     case 'u':
743                         cmd = 'underline';
744                         break;
745                     
746                     case 'v':
747                         this.cleanUpPaste.defer(100, this);
748                         return;
749                         
750                 }
751                 if(cmd){
752                     this.win.focus();
753                     this.execCmd(cmd);
754                     this.deferFocus();
755                     e.preventDefault();
756                 }
757                 
758             }
759         }
760     },
761
762     // private
763     fixKeys : function(){ // load time branching for fastest keydown performance
764         if(Roo.isIE){
765             return function(e){
766                 var k = e.getKey(), r;
767                 if(k == e.TAB){
768                     e.stopEvent();
769                     r = this.doc.selection.createRange();
770                     if(r){
771                         r.collapse(true);
772                         r.pasteHTML('&#160;&#160;&#160;&#160;');
773                         this.deferFocus();
774                     }
775                     return;
776                 }
777                 
778                 if(k == e.ENTER){
779                     r = this.doc.selection.createRange();
780                     if(r){
781                         var target = r.parentElement();
782                         if(!target || target.tagName.toLowerCase() != 'li'){
783                             e.stopEvent();
784                             r.pasteHTML('<br />');
785                             r.collapse(false);
786                             r.select();
787                         }
788                     }
789                 }
790                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
791                     this.cleanUpPaste.defer(100, this);
792                     return;
793                 }
794                 
795                 
796             };
797         }else if(Roo.isOpera){
798             return function(e){
799                 var k = e.getKey();
800                 if(k == e.TAB){
801                     e.stopEvent();
802                     this.win.focus();
803                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
804                     this.deferFocus();
805                 }
806                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
807                     this.cleanUpPaste.defer(100, this);
808                     return;
809                 }
810                 
811             };
812         }else if(Roo.isSafari){
813             return function(e){
814                 var k = e.getKey();
815                 
816                 if(k == e.TAB){
817                     e.stopEvent();
818                     this.execCmd('InsertText','\t');
819                     this.deferFocus();
820                     return;
821                 }
822                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
823                     this.cleanUpPaste.defer(100, this);
824                     return;
825                 }
826                 
827              };
828         }
829     }(),
830     
831     getAllAncestors: function()
832     {
833         var p = this.getSelectedNode();
834         var a = [];
835         if (!p) {
836             a.push(p); // push blank onto stack..
837             p = this.getParentElement();
838         }
839         
840         
841         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
842             a.push(p);
843             p = p.parentNode;
844         }
845         a.push(this.doc.body);
846         return a;
847     },
848     lastSel : false,
849     lastSelNode : false,
850     
851     
852     getSelection : function() 
853     {
854         this.assignDocWin();
855         return Roo.isIE ? this.doc.selection : this.win.getSelection();
856     },
857     
858     getSelectedNode: function() 
859     {
860         // this may only work on Gecko!!!
861         
862         // should we cache this!!!!
863         
864         
865         
866          
867         var range = this.createRange(this.getSelection()).cloneRange();
868         
869         if (Roo.isIE) {
870             var parent = range.parentElement();
871             while (true) {
872                 var testRange = range.duplicate();
873                 testRange.moveToElementText(parent);
874                 if (testRange.inRange(range)) {
875                     break;
876                 }
877                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
878                     break;
879                 }
880                 parent = parent.parentElement;
881             }
882             return parent;
883         }
884         
885         // is ancestor a text element.
886         var ac =  range.commonAncestorContainer;
887         if (ac.nodeType == 3) {
888             ac = ac.parentNode;
889         }
890         
891         var ar = ac.childNodes;
892          
893         var nodes = [];
894         var other_nodes = [];
895         var has_other_nodes = false;
896         for (var i=0;i<ar.length;i++) {
897             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
898                 continue;
899             }
900             // fullly contained node.
901             
902             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
903                 nodes.push(ar[i]);
904                 continue;
905             }
906             
907             // probably selected..
908             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
909                 other_nodes.push(ar[i]);
910                 continue;
911             }
912             // outer..
913             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
914                 continue;
915             }
916             
917             
918             has_other_nodes = true;
919         }
920         if (!nodes.length && other_nodes.length) {
921             nodes= other_nodes;
922         }
923         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
924             return false;
925         }
926         
927         return nodes[0];
928     },
929     createRange: function(sel)
930     {
931         // this has strange effects when using with 
932         // top toolbar - not sure if it's a great idea.
933         //this.editor.contentWindow.focus();
934         if (typeof sel != "undefined") {
935             try {
936                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
937             } catch(e) {
938                 return this.doc.createRange();
939             }
940         } else {
941             return this.doc.createRange();
942         }
943     },
944     getParentElement: function()
945     {
946         
947         this.assignDocWin();
948         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
949         
950         var range = this.createRange(sel);
951          
952         try {
953             var p = range.commonAncestorContainer;
954             while (p.nodeType == 3) { // text node
955                 p = p.parentNode;
956             }
957             return p;
958         } catch (e) {
959             return null;
960         }
961     
962     },
963     /***
964      *
965      * Range intersection.. the hard stuff...
966      *  '-1' = before
967      *  '0' = hits..
968      *  '1' = after.
969      *         [ -- selected range --- ]
970      *   [fail]                        [fail]
971      *
972      *    basically..
973      *      if end is before start or  hits it. fail.
974      *      if start is after end or hits it fail.
975      *
976      *   if either hits (but other is outside. - then it's not 
977      *   
978      *    
979      **/
980     
981     
982     // @see http://www.thismuchiknow.co.uk/?p=64.
983     rangeIntersectsNode : function(range, node)
984     {
985         var nodeRange = node.ownerDocument.createRange();
986         try {
987             nodeRange.selectNode(node);
988         } catch (e) {
989             nodeRange.selectNodeContents(node);
990         }
991     
992         var rangeStartRange = range.cloneRange();
993         rangeStartRange.collapse(true);
994     
995         var rangeEndRange = range.cloneRange();
996         rangeEndRange.collapse(false);
997     
998         var nodeStartRange = nodeRange.cloneRange();
999         nodeStartRange.collapse(true);
1000     
1001         var nodeEndRange = nodeRange.cloneRange();
1002         nodeEndRange.collapse(false);
1003     
1004         return rangeStartRange.compareBoundaryPoints(
1005                  Range.START_TO_START, nodeEndRange) == -1 &&
1006                rangeEndRange.compareBoundaryPoints(
1007                  Range.START_TO_START, nodeStartRange) == 1;
1008         
1009          
1010     },
1011     rangeCompareNode : function(range, node)
1012     {
1013         var nodeRange = node.ownerDocument.createRange();
1014         try {
1015             nodeRange.selectNode(node);
1016         } catch (e) {
1017             nodeRange.selectNodeContents(node);
1018         }
1019         
1020         
1021         range.collapse(true);
1022     
1023         nodeRange.collapse(true);
1024      
1025         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1026         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1027          
1028         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1029         
1030         var nodeIsBefore   =  ss == 1;
1031         var nodeIsAfter    = ee == -1;
1032         
1033         if (nodeIsBefore && nodeIsAfter)
1034             return 0; // outer
1035         if (!nodeIsBefore && nodeIsAfter)
1036             return 1; //right trailed.
1037         
1038         if (nodeIsBefore && !nodeIsAfter)
1039             return 2;  // left trailed.
1040         // fully contined.
1041         return 3;
1042     },
1043
1044     // private? - in a new class?
1045     cleanUpPaste :  function()
1046     {
1047         // cleans up the whole document..
1048          Roo.log('cleanuppaste');
1049         this.cleanUpChildren(this.doc.body);
1050         var clean = this.cleanWordChars(this.doc.body.innerHTML);
1051         if (clean != this.doc.body.innerHTML) {
1052             this.doc.body.innerHTML = clean;
1053         }
1054         
1055     },
1056     
1057     cleanWordChars : function(input) {
1058         var he = Roo.form.HtmlEditor;
1059     
1060         var output = input;
1061         Roo.each(he.swapCodes, function(sw) { 
1062         
1063             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1064             output = output.replace(swapper, sw[1]);
1065         });
1066         return output;
1067     },
1068     
1069     
1070     cleanUpChildren : function (n)
1071     {
1072         if (!n.childNodes.length) {
1073             return;
1074         }
1075         for (var i = n.childNodes.length-1; i > -1 ; i--) {
1076            this.cleanUpChild(n.childNodes[i]);
1077         }
1078     },
1079     
1080     
1081         
1082     
1083     cleanUpChild : function (node)
1084     {
1085         //console.log(node);
1086         if (node.nodeName == "#text") {
1087             // clean up silly Windows -- stuff?
1088             return; 
1089         }
1090         if (node.nodeName == "#comment") {
1091             node.parentNode.removeChild(node);
1092             // clean up silly Windows -- stuff?
1093             return; 
1094         }
1095         
1096         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1097             // remove node.
1098             node.parentNode.removeChild(node);
1099             return;
1100             
1101         }
1102         
1103         var remove_keep_children= Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1;
1104         
1105         // remove <a name=....> as rendering on yahoo mailer is bored with this.
1106         
1107         if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1108             remove_keep_children = true;
1109         }
1110         
1111         if (remove_keep_children) {
1112             this.cleanUpChildren(node);
1113             // inserts everything just before this node...
1114             while (node.childNodes.length) {
1115                 var cn = node.childNodes[0];
1116                 node.removeChild(cn);
1117                 node.parentNode.insertBefore(cn, node);
1118             }
1119             node.parentNode.removeChild(node);
1120             return;
1121         }
1122         
1123         if (!node.attributes || !node.attributes.length) {
1124             this.cleanUpChildren(node);
1125             return;
1126         }
1127         
1128         function cleanAttr(n,v)
1129         {
1130             
1131             if (v.match(/^\./) || v.match(/^\//)) {
1132                 return;
1133             }
1134             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1135                 return;
1136             }
1137             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1138             node.removeAttribute(n);
1139             
1140         }
1141         
1142         function cleanStyle(n,v)
1143         {
1144             if (v.match(/expression/)) { //XSS?? should we even bother..
1145                 node.removeAttribute(n);
1146                 return;
1147             }
1148             
1149             
1150             var parts = v.split(/;/);
1151             Roo.each(parts, function(p) {
1152                 p = p.replace(/\s+/g,'');
1153                 if (!p.length) {
1154                     return true;
1155                 }
1156                 var l = p.split(':').shift().replace(/\s+/g,'');
1157                 
1158                 // only allow 'c whitelisted system attributes'
1159                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1160                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1161                     node.removeAttribute(n);
1162                     return false;
1163                 }
1164                 return true;
1165             });
1166             
1167             
1168         }
1169         
1170         
1171         for (var i = node.attributes.length-1; i > -1 ; i--) {
1172             var a = node.attributes[i];
1173             //console.log(a);
1174             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1175                 node.removeAttribute(a.name);
1176                 return;
1177             }
1178             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1179                 cleanAttr(a.name,a.value); // fixme..
1180                 return;
1181             }
1182             if (a.name == 'style') {
1183                 cleanStyle(a.name,a.value);
1184             }
1185             /// clean up MS crap..
1186             // tecnically this should be a list of valid class'es..
1187             
1188             
1189             if (a.name == 'class') {
1190                 if (a.value.match(/^Mso/)) {
1191                     node.className = '';
1192                 }
1193                 
1194                 if (a.value.match(/body/)) {
1195                     node.className = '';
1196                 }
1197             }
1198             
1199             // style cleanup!?
1200             // class cleanup?
1201             
1202         }
1203         
1204         
1205         this.cleanUpChildren(node);
1206         
1207         
1208     }
1209     
1210     
1211     // hide stuff that is not compatible
1212     /**
1213      * @event blur
1214      * @hide
1215      */
1216     /**
1217      * @event change
1218      * @hide
1219      */
1220     /**
1221      * @event focus
1222      * @hide
1223      */
1224     /**
1225      * @event specialkey
1226      * @hide
1227      */
1228     /**
1229      * @cfg {String} fieldClass @hide
1230      */
1231     /**
1232      * @cfg {String} focusClass @hide
1233      */
1234     /**
1235      * @cfg {String} autoCreate @hide
1236      */
1237     /**
1238      * @cfg {String} inputType @hide
1239      */
1240     /**
1241      * @cfg {String} invalidClass @hide
1242      */
1243     /**
1244      * @cfg {String} invalidText @hide
1245      */
1246     /**
1247      * @cfg {String} msgFx @hide
1248      */
1249     /**
1250      * @cfg {String} validateOnBlur @hide
1251      */
1252 });
1253
1254 Roo.form.HtmlEditor.white = [
1255         'area', 'br', 'img', 'input', 'hr', 'wbr',
1256         
1257        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1258        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1259        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1260        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1261        'table',   'ul',         'xmp', 
1262        
1263        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1264       'thead',   'tr', 
1265      
1266       'dir', 'menu', 'ol', 'ul', 'dl',
1267        
1268       'embed',  'object'
1269 ];
1270
1271
1272 Roo.form.HtmlEditor.black = [
1273     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1274         'applet', // 
1275         'base',   'basefont', 'bgsound', 'blink',  'body', 
1276         'frame',  'frameset', 'head',    'html',   'ilayer', 
1277         'iframe', 'layer',  'link',     'meta',    'object',   
1278         'script', 'style' ,'title',  'xml' // clean later..
1279 ];
1280 Roo.form.HtmlEditor.clean = [
1281     'script', 'style', 'title', 'xml'
1282 ];
1283 Roo.form.HtmlEditor.remove = [
1284     'font'
1285 ];
1286 // attributes..
1287
1288 Roo.form.HtmlEditor.ablack = [
1289     'on'
1290 ];
1291     
1292 Roo.form.HtmlEditor.aclean = [ 
1293     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1294 ];
1295
1296 // protocols..
1297 Roo.form.HtmlEditor.pwhite= [
1298         'http',  'https',  'mailto'
1299 ];
1300
1301 // white listed style attributes.
1302 Roo.form.HtmlEditor.cwhite= [
1303         'text-align',
1304         'font-size'
1305 ];
1306
1307
1308 Roo.form.HtmlEditor.swapCodes   =[ 
1309     [    8211, "--" ], 
1310     [    8212, "--" ], 
1311     [    8216,  "'" ],  
1312     [    8217, "'" ],  
1313     [    8220, '"' ],  
1314     [    8221, '"' ],  
1315     [    8226, "*" ],  
1316     [    8230, "..." ]
1317 ]; 
1318
1319