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