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