fix html editor sync - allow attributes for youtube embed
[roojs1] / Roo / HtmlEditorCore.js
1 //<script type="text/javascript">
2
3 /*
4  * Based  Ext JS Library 1.1.1
5  * Copyright(c) 2006-2007, Ext JS, LLC.
6  * LGPL
7  *
8  */
9  
10 /**
11  * @class Roo.HtmlEditorCore
12  * @extends Roo.Component
13  * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
14  *
15  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
16  */
17
18 Roo.HtmlEditorCore = function(config){
19     
20     
21     Roo.HtmlEditorCore.superclass.constructor.call(this, config);
22     
23     
24     this.addEvents({
25         /**
26          * @event initialize
27          * Fires when the editor is fully initialized (including the iframe)
28          * @param {Roo.HtmlEditorCore} this
29          */
30         initialize: true,
31         /**
32          * @event activate
33          * Fires when the editor is first receives the focus. Any insertion must wait
34          * until after this event.
35          * @param {Roo.HtmlEditorCore} this
36          */
37         activate: true,
38          /**
39          * @event beforesync
40          * Fires before the textarea is updated with content from the editor iframe. Return false
41          * to cancel the sync.
42          * @param {Roo.HtmlEditorCore} this
43          * @param {String} html
44          */
45         beforesync: true,
46          /**
47          * @event beforepush
48          * Fires before the iframe editor is updated with content from the textarea. Return false
49          * to cancel the push.
50          * @param {Roo.HtmlEditorCore} this
51          * @param {String} html
52          */
53         beforepush: true,
54          /**
55          * @event sync
56          * Fires when the textarea is updated with content from the editor iframe.
57          * @param {Roo.HtmlEditorCore} this
58          * @param {String} html
59          */
60         sync: true,
61          /**
62          * @event push
63          * Fires when the iframe editor is updated with content from the textarea.
64          * @param {Roo.HtmlEditorCore} this
65          * @param {String} html
66          */
67         push: true,
68         
69         /**
70          * @event editorevent
71          * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72          * @param {Roo.HtmlEditorCore} this
73          */
74         editorevent: true 
75          
76         
77     });
78     
79     // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80     
81     // defaults : white / black...
82     this.applyBlacklists();
83     
84     
85     
86 };
87
88
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component,  {
90
91
92      /**
93      * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field 
94      */
95     
96     owner : false,
97     
98      /**
99      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
100      *                        Roo.resizable.
101      */
102     resizable : false,
103      /**
104      * @cfg {Number} height (in pixels)
105      */   
106     height: 300,
107    /**
108      * @cfg {Number} width (in pixels)
109      */   
110     width: 500,
111      /**
112      * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113      *         if you are doing an email editor, this probably needs disabling, it's designed
114      */
115     autoClean: true,
116     
117     /**
118      * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
119      */
120     enableBlocks : true,
121     /**
122      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
123      * 
124      */
125     stylesheets: false,
126      /**
127      * @cfg {String} language default en - language of text (usefull for rtl languages)
128      * 
129      */
130     language: 'en',
131     
132     /**
133      * @cfg {boolean} allowComments - default false - allow comments in HTML source
134      *          - by default they are stripped - if you are editing email you may need this.
135      */
136     allowComments: false,
137     // id of frame..
138     frameId: false,
139     
140     // private properties
141     validationEvent : false,
142     deferHeight: true,
143     initialized : false,
144     activated : false,
145     sourceEditMode : false,
146     onFocus : Roo.emptyFn,
147     iframePad:3,
148     hideMode:'offsets',
149     
150     clearUp: true,
151     
152     // blacklist + whitelisted elements..
153     black: false,
154     white: false,
155      
156     bodyCls : '',
157
158     
159     undoManager : false,
160     /**
161      * Protected method that will not generally be called directly. It
162      * is called when the editor initializes the iframe with HTML contents. Override this method if you
163      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
164      */
165     getDocMarkup : function(){
166         // body styles..
167         var st = '';
168         
169         // inherit styels from page...?? 
170         if (this.stylesheets === false) {
171             
172             Roo.get(document.head).select('style').each(function(node) {
173                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
174             });
175             
176             Roo.get(document.head).select('link').each(function(node) { 
177                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
178             });
179             
180         } else if (!this.stylesheets.length) {
181                 // simple..
182                 st = '<style type="text/css">' +
183                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
184                    '</style>';
185         } else {
186             for (var i in this.stylesheets) {
187                 if (typeof(this.stylesheets[i]) != 'string') {
188                     continue;
189                 }
190                 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
191             }
192             
193         }
194         
195         st +=  '<style type="text/css">' +
196             'IMG { cursor: pointer } ' +
197         '</style>';
198         
199         st += '<meta name="google" content="notranslate">';
200         
201         var cls = 'notranslate roo-htmleditor-body';
202         
203         if(this.bodyCls.length){
204             cls += ' ' + this.bodyCls;
205         }
206         
207         return '<html  class="notranslate" translate="no"><head>' + st  +
208             //<style type="text/css">' +
209             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
210             //'</style>' +
211             ' </head><body contenteditable="true" data-enable-grammerly="true" class="' +  cls + '"></body></html>';
212     },
213
214     // private
215     onRender : function(ct, position)
216     {
217         var _t = this;
218         //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
219         this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
220         
221         
222         this.el.dom.style.border = '0 none';
223         this.el.dom.setAttribute('tabIndex', -1);
224         this.el.addClass('x-hidden hide');
225         
226         
227         
228         if(Roo.isIE){ // fix IE 1px bogus margin
229             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
230         }
231        
232         
233         this.frameId = Roo.id();
234         
235          
236         
237         var iframe = this.owner.wrap.createChild({
238             tag: 'iframe',
239             cls: 'form-control', // bootstrap..
240             id: this.frameId,
241             name: this.frameId,
242             frameBorder : 'no',
243             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
244         }, this.el
245         );
246         
247         
248         this.iframe = iframe.dom;
249
250         this.assignDocWin();
251         
252         this.doc.designMode = 'on';
253        
254         this.doc.open();
255         this.doc.write(this.getDocMarkup());
256         this.doc.close();
257
258         
259         var task = { // must defer to wait for browser to be ready
260             run : function(){
261                 //console.log("run task?" + this.doc.readyState);
262                 this.assignDocWin();
263                 if(this.doc.body || this.doc.readyState == 'complete'){
264                     try {
265                         this.doc.designMode="on";
266                         
267                     } catch (e) {
268                         return;
269                     }
270                     Roo.TaskMgr.stop(task);
271                     this.initEditor.defer(10, this);
272                 }
273             },
274             interval : 10,
275             duration: 10000,
276             scope: this
277         };
278         Roo.TaskMgr.start(task);
279
280     },
281
282     // private
283     onResize : function(w, h)
284     {
285          Roo.log('resize: ' +w + ',' + h );
286         //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
287         if(!this.iframe){
288             return;
289         }
290         if(typeof w == 'number'){
291             
292             this.iframe.style.width = w + 'px';
293         }
294         if(typeof h == 'number'){
295             
296             this.iframe.style.height = h + 'px';
297             if(this.doc){
298                 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
299             }
300         }
301         
302     },
303
304     /**
305      * Toggles the editor between standard and source edit mode.
306      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
307      */
308     toggleSourceEdit : function(sourceEditMode){
309         
310         this.sourceEditMode = sourceEditMode === true;
311         
312         if(this.sourceEditMode){
313  
314             Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']);     //FIXME - what's the BS styles for these
315             
316         }else{
317             Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
318             //this.iframe.className = '';
319             this.deferFocus();
320         }
321         //this.setSize(this.owner.wrap.getSize());
322         //this.fireEvent('editmodechange', this, this.sourceEditMode);
323     },
324
325     
326   
327
328     /**
329      * Protected method that will not generally be called directly. If you need/want
330      * custom HTML cleanup, this is the method you should override.
331      * @param {String} html The HTML to be cleaned
332      * return {String} The cleaned HTML
333      */
334     cleanHtml : function(html)
335     {
336         html = String(html);
337         if(html.length > 5){
338             if(Roo.isSafari){ // strip safari nonsense
339                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
340             }
341         }
342         if(html == '&nbsp;'){
343             html = '';
344         }
345         return html;
346     },
347
348     /**
349      * HTML Editor -> Textarea
350      * Protected method that will not generally be called directly. Syncs the contents
351      * of the editor iframe with the textarea.
352      */
353     syncValue : function()
354     {
355         //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
356         if(this.initialized){
357             
358             if (this.undoManager) {
359                 this.undoManager.addEvent();
360             }
361
362             
363             var bd = (this.doc.body || this.doc.documentElement);
364            
365             
366             var sel = this.win.getSelection();
367             
368             var div = document.createElement('div');
369             div.innerHTML = bd.innerHTML;
370             var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
371             if (gtx.length > 0) {
372                 var rm = gtx.item(0).parentNode;
373                 rm.parentNode.removeChild(rm);
374             }
375             
376            
377             if (this.enableBlocks) {
378                 new Roo.htmleditor.FilterBlock({ node : div });
379             }
380             
381             var html = div.innerHTML;
382             
383             //?? tidy?
384             if (this.autoClean) {
385                 
386                 new Roo.htmleditor.FilterAttributes({
387                     node : div,
388                     attrib_white : [
389                             'href',
390                             'src',
391                             'name',
392                             'align',
393                             'colspan',
394                             'rowspan',
395                             'data-display',
396                             'data-width',
397                             'start' ,
398                             'style',
399                             // youtube embed.
400                             'class',
401                             'allowfullscreen',
402                             'frameborder',
403                             'width',
404                             'height',
405                             'alt'
406                             ],
407                     attrib_clean : ['href', 'src' ] 
408                 });
409                 
410                 var tidy = new Roo.htmleditor.TidySerializer({
411                     inner:  true
412                 });
413                 html  = tidy.serialize(div);
414                 
415             }
416             
417             
418             if(Roo.isSafari){
419                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
420                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
421                 if(m && m[1]){
422                     html = '<div style="'+m[0]+'">' + html + '</div>';
423                 }
424             }
425             html = this.cleanHtml(html);
426             // fix up the special chars.. normaly like back quotes in word...
427             // however we do not want to do this with chinese..
428             html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
429                 
430                 var cc = match.charCodeAt();
431
432                 // Get the character value, handling surrogate pairs
433                 if (match.length == 2) {
434                     // It's a surrogate pair, calculate the Unicode code point
435                     var high = match.charCodeAt(0) - 0xD800;
436                     var low  = match.charCodeAt(1) - 0xDC00;
437                     cc = (high * 0x400) + low + 0x10000;
438                 }  else if (
439                     (cc >= 0x4E00 && cc < 0xA000 ) ||
440                     (cc >= 0x3400 && cc < 0x4E00 ) ||
441                     (cc >= 0xf900 && cc < 0xfb00 )
442                 ) {
443                         return match;
444                 }  
445          
446                 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
447                 return "&#" + cc + ";";
448                 
449                 
450             });
451             
452             
453              
454             if(this.owner.fireEvent('beforesync', this, html) !== false){
455                 this.el.dom.value = html;
456                 this.owner.fireEvent('sync', this, html);
457             }
458         }
459     },
460
461     /**
462      * TEXTAREA -> EDITABLE
463      * Protected method that will not generally be called directly. Pushes the value of the textarea
464      * into the iframe editor.
465      */
466     pushValue : function()
467     {
468         //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
469         if(this.initialized){
470             var v = this.el.dom.value.trim();
471             
472             
473             if(this.owner.fireEvent('beforepush', this, v) !== false){
474                 var d = (this.doc.body || this.doc.documentElement);
475                 d.innerHTML = v;
476                  
477                 this.el.dom.value = d.innerHTML;
478                 this.owner.fireEvent('push', this, v);
479             }
480             if (this.autoClean) {
481                 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
482                 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
483             }
484             if (this.enableBlocks) {
485                 Roo.htmleditor.Block.initAll(this.doc.body);
486             }
487             
488             this.updateLanguage();
489             
490             var lc = this.doc.body.lastChild;
491             if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
492                 // add an extra line at the end.
493                 this.doc.body.appendChild(this.doc.createElement('br'));
494             }
495             
496             
497         }
498     },
499
500     // private
501     deferFocus : function(){
502         this.focus.defer(10, this);
503     },
504
505     // doc'ed in Field
506     focus : function(){
507         if(this.win && !this.sourceEditMode){
508             this.win.focus();
509         }else{
510             this.el.focus();
511         }
512     },
513     
514     assignDocWin: function()
515     {
516         var iframe = this.iframe;
517         
518          if(Roo.isIE){
519             this.doc = iframe.contentWindow.document;
520             this.win = iframe.contentWindow;
521         } else {
522 //            if (!Roo.get(this.frameId)) {
523 //                return;
524 //            }
525 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
526 //            this.win = Roo.get(this.frameId).dom.contentWindow;
527             
528             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
529                 return;
530             }
531             
532             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
533             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
534         }
535     },
536     
537     // private
538     initEditor : function(){
539         //console.log("INIT EDITOR");
540         this.assignDocWin();
541         
542         
543         
544         this.doc.designMode="on";
545         this.doc.open();
546         this.doc.write(this.getDocMarkup());
547         this.doc.close();
548         
549         var dbody = (this.doc.body || this.doc.documentElement);
550         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
551         // this copies styles from the containing element into thsi one..
552         // not sure why we need all of this..
553         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
554         
555         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
556         //ss['background-attachment'] = 'fixed'; // w3c
557         dbody.bgProperties = 'fixed'; // ie
558         dbody.setAttribute("translate", "no");
559         
560         //Roo.DomHelper.applyStyles(dbody, ss);
561         Roo.EventManager.on(this.doc, {
562              
563             'mouseup': this.onEditorEvent,
564             'dblclick': this.onEditorEvent,
565             'click': this.onEditorEvent,
566             'keyup': this.onEditorEvent,
567             
568             buffer:100,
569             scope: this
570         });
571         Roo.EventManager.on(this.doc, {
572             'paste': this.onPasteEvent,
573             scope : this
574         });
575         if(Roo.isGecko){
576             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
577         }
578         //??? needed???
579         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
580             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
581         }
582         this.initialized = true;
583
584         
585         // initialize special key events - enter
586         new Roo.htmleditor.KeyEnter({core : this});
587         
588          
589         
590         this.owner.fireEvent('initialize', this);
591         this.pushValue();
592     },
593     // this is to prevent a href clicks resulting in a redirect?
594    
595     onPasteEvent : function(e,v)
596     {
597         // I think we better assume paste is going to be a dirty load of rubish from word..
598         
599         // even pasting into a 'email version' of this widget will have to clean up that mess.
600         var cd = (e.browserEvent.clipboardData || window.clipboardData);
601         
602         // check what type of paste - if it's an image, then handle it differently.
603         if (cd.files && cd.files.length > 0) {
604             // pasting images?
605             var urlAPI = (window.createObjectURL && window) || 
606                 (window.URL && URL.revokeObjectURL && URL) || 
607                 (window.webkitURL && webkitURL);
608     
609             var url = urlAPI.createObjectURL( cd.files[0]);
610             this.insertAtCursor('<img src=" + url + ">');
611             return false;
612         }
613         if (cd.types.indexOf('text/html') < 0 ) {
614             return false;
615         }
616         var images = [];
617         var html = cd.getData('text/html'); // clipboard event
618         if (cd.types.indexOf('text/rtf') > -1) {
619             var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
620             images = parser.doc ? parser.doc.getElementsByType('pict') : [];
621         }
622         //Roo.log(images);
623         //Roo.log(imgs);
624         // fixme..
625         images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
626                        .map(function(g) { return g.toDataURL(); })
627                        .filter(function(g) { return g != 'about:blank'; });
628         
629         //Roo.log(html);
630         html = this.cleanWordChars(html);
631         
632         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
633         
634         
635         var sn = this.getParentElement();
636         // check if d contains a table, and prevent nesting??
637         //Roo.log(d.getElementsByTagName('table'));
638         //Roo.log(sn);
639         //Roo.log(sn.closest('table'));
640         if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
641             e.preventDefault();
642             this.insertAtCursor("You can not nest tables");
643             //Roo.log("prevent?"); // fixme - 
644             return false;
645         }
646         
647         
648         
649         if (images.length > 0) {
650             // replace all v:imagedata - with img.
651             var ar = Array.from(d.getElementsByTagName('v:imagedata'));
652             Roo.each(ar, function(node) {
653                 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
654                 node.parentNode.removeChild(node);
655             });
656             
657             
658             Roo.each(d.getElementsByTagName('img'), function(img, i) {
659                 img.setAttribute('src', images[i]);
660             });
661         }
662         if (this.autoClean) {
663             new Roo.htmleditor.FilterWord({ node : d });
664             
665             new Roo.htmleditor.FilterStyleToTag({ node : d });
666             new Roo.htmleditor.FilterAttributes({
667                 node : d,
668                 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
669                 attrib_clean : ['href', 'src' ] 
670             });
671             new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
672             // should be fonts..
673             new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
674             new Roo.htmleditor.FilterParagraph({ node : d });
675             new Roo.htmleditor.FilterSpan({ node : d });
676             new Roo.htmleditor.FilterLongBr({ node : d });
677             new Roo.htmleditor.FilterComment({ node : d });
678             
679             
680         }
681         if (this.enableBlocks) {
682                 
683             Array.from(d.getElementsByTagName('img')).forEach(function(img) {
684                 if (img.closest('figure')) { // assume!! that it's aready
685                     return;
686                 }
687                 var fig  = new Roo.htmleditor.BlockFigure({
688                     image_src  : img.src
689                 });
690                 fig.updateElement(img); // replace it..
691                 
692             });
693         }
694         
695         
696         this.insertAtCursor(d.innerHTML.replace(/&nbsp;/g,' '));
697         if (this.enableBlocks) {
698             Roo.htmleditor.Block.initAll(this.doc.body);
699         }
700          
701         
702         e.preventDefault();
703         return false;
704         // default behaveiour should be our local cleanup paste? (optional?)
705         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
706         //this.owner.fireEvent('paste', e, v);
707     },
708     // private
709     onDestroy : function(){
710         
711         
712         
713         if(this.rendered){
714             
715             //for (var i =0; i < this.toolbars.length;i++) {
716             //    // fixme - ask toolbars for heights?
717             //    this.toolbars[i].onDestroy();
718            // }
719             
720             //this.wrap.dom.innerHTML = '';
721             //this.wrap.remove();
722         }
723     },
724
725     // private
726     onFirstFocus : function(){
727         
728         this.assignDocWin();
729         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
730         
731         this.activated = true;
732          
733     
734         if(Roo.isGecko){ // prevent silly gecko errors
735             this.win.focus();
736             var s = this.win.getSelection();
737             if(!s.focusNode || s.focusNode.nodeType != 3){
738                 var r = s.getRangeAt(0);
739                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
740                 r.collapse(true);
741                 this.deferFocus();
742             }
743             try{
744                 this.execCmd('useCSS', true);
745                 this.execCmd('styleWithCSS', false);
746             }catch(e){}
747         }
748         this.owner.fireEvent('activate', this);
749     },
750
751     // private
752     adjustFont: function(btn){
753         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
754         //if(Roo.isSafari){ // safari
755         //    adjust *= 2;
756        // }
757         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
758         if(Roo.isSafari){ // safari
759             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
760             v =  (v < 10) ? 10 : v;
761             v =  (v > 48) ? 48 : v;
762             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
763             
764         }
765         
766         
767         v = Math.max(1, v+adjust);
768         
769         this.execCmd('FontSize', v  );
770     },
771
772     onEditorEvent : function(e)
773     {
774          
775         
776         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
777             return; // we do not handle this.. (undo manager does..)
778         }
779         // in theory this detects if the last element is not a br, then we try and do that.
780         // its so clicking in space at bottom triggers adding a br and moving the cursor.
781         if (e &&
782             e.target.nodeName == 'BODY' &&
783             e.type == "mouseup" &&
784             this.doc.body.lastChild
785            ) {
786             var lc = this.doc.body.lastChild;
787             // gtx-trans is google translate plugin adding crap.
788             while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
789                 lc = lc.previousSibling;
790             }
791             if (lc.nodeType == 1 && lc.nodeName != 'BR') {
792             // if last element is <BR> - then dont do anything.
793             
794                 var ns = this.doc.createElement('br');
795                 this.doc.body.appendChild(ns);
796                 range = this.doc.createRange();
797                 range.setStartAfter(ns);
798                 range.collapse(true);
799                 var sel = this.win.getSelection();
800                 sel.removeAllRanges();
801                 sel.addRange(range);
802             }
803         }
804         
805         
806         
807         this.fireEditorEvent(e);
808       //  this.updateToolbar();
809         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
810     },
811     
812     fireEditorEvent: function(e)
813     {
814         this.owner.fireEvent('editorevent', this, e);
815     },
816
817     insertTag : function(tg)
818     {
819         // could be a bit smarter... -> wrap the current selected tRoo..
820         if (tg.toLowerCase() == 'span' ||
821             tg.toLowerCase() == 'code' ||
822             tg.toLowerCase() == 'sup' ||
823             tg.toLowerCase() == 'sub' 
824             ) {
825             
826             range = this.createRange(this.getSelection());
827             var wrappingNode = this.doc.createElement(tg.toLowerCase());
828             wrappingNode.appendChild(range.extractContents());
829             range.insertNode(wrappingNode);
830
831             return;
832             
833             
834             
835         }
836         this.execCmd("formatblock",   tg);
837         this.undoManager.addEvent(); 
838     },
839     
840     insertText : function(txt)
841     {
842         
843         
844         var range = this.createRange();
845         range.deleteContents();
846                //alert(Sender.getAttribute('label'));
847                
848         range.insertNode(this.doc.createTextNode(txt));
849         this.undoManager.addEvent();
850     } ,
851     
852      
853
854     /**
855      * Executes a Midas editor command on the editor document and performs necessary focus and
856      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
857      * @param {String} cmd The Midas command
858      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
859      */
860     relayCmd : function(cmd, value)
861     {
862         
863         switch (cmd) {
864             case 'justifyleft':
865             case 'justifyright':
866             case 'justifycenter':
867                 // if we are in a cell, then we will adjust the
868                 var n = this.getParentElement();
869                 var td = n.closest('td');
870                 if (td) {
871                     var bl = Roo.htmleditor.Block.factory(td);
872                     bl.textAlign = cmd.replace('justify','');
873                     bl.updateElement();
874                     this.owner.fireEvent('editorevent', this);
875                     return;
876                 }
877                 this.execCmd('styleWithCSS', true); // 
878                 break;
879             case 'bold':
880             case 'italic':
881                 // if there is no selection, then we insert, and set the curson inside it..
882                 this.execCmd('styleWithCSS', false); 
883                 break;
884                 
885         
886             default:
887                 break;
888         }
889         
890         
891         this.win.focus();
892         this.execCmd(cmd, value);
893         this.owner.fireEvent('editorevent', this);
894         //this.updateToolbar();
895         this.owner.deferFocus();
896     },
897
898     /**
899      * Executes a Midas editor command directly on the editor document.
900      * For visual commands, you should use {@link #relayCmd} instead.
901      * <b>This should only be called after the editor is initialized.</b>
902      * @param {String} cmd The Midas command
903      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
904      */
905     execCmd : function(cmd, value){
906         this.doc.execCommand(cmd, false, value === undefined ? null : value);
907         this.syncValue();
908     },
909  
910  
911    
912     /**
913      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
914      * to insert tRoo.
915      * @param {String} text | dom node.. 
916      */
917     insertAtCursor : function(text)
918     {
919         
920         if(!this.activated){
921             return;
922         }
923          
924         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
925             this.win.focus();
926             
927             
928             // from jquery ui (MIT licenced)
929             var range, node;
930             var win = this.win;
931             
932             if (win.getSelection && win.getSelection().getRangeAt) {
933                 
934                 // delete the existing?
935                 
936                 this.createRange(this.getSelection()).deleteContents();
937                 range = win.getSelection().getRangeAt(0);
938                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
939                 range.insertNode(node);
940                 range = range.cloneRange();
941                 range.collapse(false);
942                  
943                 win.getSelection().removeAllRanges();
944                 win.getSelection().addRange(range);
945                 
946                 
947                 
948             } else if (win.document.selection && win.document.selection.createRange) {
949                 // no firefox support
950                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
951                 win.document.selection.createRange().pasteHTML(txt);
952             
953             } else {
954                 // no firefox support
955                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
956                 this.execCmd('InsertHTML', txt);
957             } 
958             this.syncValue();
959             
960             this.deferFocus();
961         }
962     },
963  // private
964     mozKeyPress : function(e){
965         if(e.ctrlKey){
966             var c = e.getCharCode(), cmd;
967           
968             if(c > 0){
969                 c = String.fromCharCode(c).toLowerCase();
970                 switch(c){
971                     case 'b':
972                         cmd = 'bold';
973                         break;
974                     case 'i':
975                         cmd = 'italic';
976                         break;
977                     
978                     case 'u':
979                         cmd = 'underline';
980                         break;
981                     
982                     //case 'v':
983                       //  this.cleanUpPaste.defer(100, this);
984                       //  return;
985                         
986                 }
987                 if(cmd){
988                     
989                     this.relayCmd(cmd);
990                     //this.win.focus();
991                     //this.execCmd(cmd);
992                     //this.deferFocus();
993                     e.preventDefault();
994                 }
995                 
996             }
997         }
998     },
999
1000     // private
1001     fixKeys : function(){ // load time branching for fastest keydown performance
1002         
1003         
1004         if(Roo.isIE){
1005             return function(e){
1006                 var k = e.getKey(), r;
1007                 if(k == e.TAB){
1008                     e.stopEvent();
1009                     r = this.doc.selection.createRange();
1010                     if(r){
1011                         r.collapse(true);
1012                         r.pasteHTML('&#160;&#160;&#160;&#160;');
1013                         this.deferFocus();
1014                     }
1015                     return;
1016                 }
1017                 /// this is handled by Roo.htmleditor.KeyEnter
1018                  /*
1019                 if(k == e.ENTER){
1020                     r = this.doc.selection.createRange();
1021                     if(r){
1022                         var target = r.parentElement();
1023                         if(!target || target.tagName.toLowerCase() != 'li'){
1024                             e.stopEvent();
1025                             r.pasteHTML('<br/>');
1026                             r.collapse(false);
1027                             r.select();
1028                         }
1029                     }
1030                 }
1031                 */
1032                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1033                 //    this.cleanUpPaste.defer(100, this);
1034                 //    return;
1035                 //}
1036                 
1037                 
1038             };
1039         }else if(Roo.isOpera){
1040             return function(e){
1041                 var k = e.getKey();
1042                 if(k == e.TAB){
1043                     e.stopEvent();
1044                     this.win.focus();
1045                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
1046                     this.deferFocus();
1047                 }
1048                
1049                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1050                 //    this.cleanUpPaste.defer(100, this);
1051                  //   return;
1052                 //}
1053                 
1054             };
1055         }else if(Roo.isSafari){
1056             return function(e){
1057                 var k = e.getKey();
1058                 
1059                 if(k == e.TAB){
1060                     e.stopEvent();
1061                     this.execCmd('InsertText','\t');
1062                     this.deferFocus();
1063                     return;
1064                 }
1065                  this.mozKeyPress(e);
1066                 
1067                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1068                  //   this.cleanUpPaste.defer(100, this);
1069                  //   return;
1070                // }
1071                 
1072              };
1073         }
1074     }(),
1075     
1076     getAllAncestors: function()
1077     {
1078         var p = this.getSelectedNode();
1079         var a = [];
1080         if (!p) {
1081             a.push(p); // push blank onto stack..
1082             p = this.getParentElement();
1083         }
1084         
1085         
1086         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1087             a.push(p);
1088             p = p.parentNode;
1089         }
1090         a.push(this.doc.body);
1091         return a;
1092     },
1093     lastSel : false,
1094     lastSelNode : false,
1095     
1096     
1097     getSelection : function() 
1098     {
1099         this.assignDocWin();
1100         return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1101     },
1102     /**
1103      * Select a dom node
1104      * @param {DomElement} node the node to select
1105      */
1106     selectNode : function(node, collapse)
1107     {
1108         var nodeRange = node.ownerDocument.createRange();
1109         try {
1110             nodeRange.selectNode(node);
1111         } catch (e) {
1112             nodeRange.selectNodeContents(node);
1113         }
1114         if (collapse === true) {
1115             nodeRange.collapse(true);
1116         }
1117         //
1118         var s = this.win.getSelection();
1119         s.removeAllRanges();
1120         s.addRange(nodeRange);
1121     },
1122     
1123     getSelectedNode: function() 
1124     {
1125         // this may only work on Gecko!!!
1126         
1127         // should we cache this!!!!
1128         
1129          
1130          
1131         var range = this.createRange(this.getSelection()).cloneRange();
1132         
1133         if (Roo.isIE) {
1134             var parent = range.parentElement();
1135             while (true) {
1136                 var testRange = range.duplicate();
1137                 testRange.moveToElementText(parent);
1138                 if (testRange.inRange(range)) {
1139                     break;
1140                 }
1141                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1142                     break;
1143                 }
1144                 parent = parent.parentElement;
1145             }
1146             return parent;
1147         }
1148         
1149         // is ancestor a text element.
1150         var ac =  range.commonAncestorContainer;
1151         if (ac.nodeType == 3) {
1152             ac = ac.parentNode;
1153         }
1154         
1155         var ar = ac.childNodes;
1156          
1157         var nodes = [];
1158         var other_nodes = [];
1159         var has_other_nodes = false;
1160         for (var i=0;i<ar.length;i++) {
1161             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1162                 continue;
1163             }
1164             // fullly contained node.
1165             
1166             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1167                 nodes.push(ar[i]);
1168                 continue;
1169             }
1170             
1171             // probably selected..
1172             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1173                 other_nodes.push(ar[i]);
1174                 continue;
1175             }
1176             // outer..
1177             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1178                 continue;
1179             }
1180             
1181             
1182             has_other_nodes = true;
1183         }
1184         if (!nodes.length && other_nodes.length) {
1185             nodes= other_nodes;
1186         }
1187         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1188             return false;
1189         }
1190         
1191         return nodes[0];
1192     },
1193     
1194     
1195     createRange: function(sel)
1196     {
1197         // this has strange effects when using with 
1198         // top toolbar - not sure if it's a great idea.
1199         //this.editor.contentWindow.focus();
1200         if (typeof sel != "undefined") {
1201             try {
1202                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1203             } catch(e) {
1204                 return this.doc.createRange();
1205             }
1206         } else {
1207             return this.doc.createRange();
1208         }
1209     },
1210     getParentElement: function()
1211     {
1212         
1213         this.assignDocWin();
1214         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1215         
1216         var range = this.createRange(sel);
1217          
1218         try {
1219             var p = range.commonAncestorContainer;
1220             while (p.nodeType == 3) { // text node
1221                 p = p.parentNode;
1222             }
1223             return p;
1224         } catch (e) {
1225             return null;
1226         }
1227     
1228     },
1229     /***
1230      *
1231      * Range intersection.. the hard stuff...
1232      *  '-1' = before
1233      *  '0' = hits..
1234      *  '1' = after.
1235      *         [ -- selected range --- ]
1236      *   [fail]                        [fail]
1237      *
1238      *    basically..
1239      *      if end is before start or  hits it. fail.
1240      *      if start is after end or hits it fail.
1241      *
1242      *   if either hits (but other is outside. - then it's not 
1243      *   
1244      *    
1245      **/
1246     
1247     
1248     // @see http://www.thismuchiknow.co.uk/?p=64.
1249     rangeIntersectsNode : function(range, node)
1250     {
1251         var nodeRange = node.ownerDocument.createRange();
1252         try {
1253             nodeRange.selectNode(node);
1254         } catch (e) {
1255             nodeRange.selectNodeContents(node);
1256         }
1257     
1258         var rangeStartRange = range.cloneRange();
1259         rangeStartRange.collapse(true);
1260     
1261         var rangeEndRange = range.cloneRange();
1262         rangeEndRange.collapse(false);
1263     
1264         var nodeStartRange = nodeRange.cloneRange();
1265         nodeStartRange.collapse(true);
1266     
1267         var nodeEndRange = nodeRange.cloneRange();
1268         nodeEndRange.collapse(false);
1269     
1270         return rangeStartRange.compareBoundaryPoints(
1271                  Range.START_TO_START, nodeEndRange) == -1 &&
1272                rangeEndRange.compareBoundaryPoints(
1273                  Range.START_TO_START, nodeStartRange) == 1;
1274         
1275          
1276     },
1277     rangeCompareNode : function(range, node)
1278     {
1279         var nodeRange = node.ownerDocument.createRange();
1280         try {
1281             nodeRange.selectNode(node);
1282         } catch (e) {
1283             nodeRange.selectNodeContents(node);
1284         }
1285         
1286         
1287         range.collapse(true);
1288     
1289         nodeRange.collapse(true);
1290      
1291         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1292         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1293          
1294         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1295         
1296         var nodeIsBefore   =  ss == 1;
1297         var nodeIsAfter    = ee == -1;
1298         
1299         if (nodeIsBefore && nodeIsAfter) {
1300             return 0; // outer
1301         }
1302         if (!nodeIsBefore && nodeIsAfter) {
1303             return 1; //right trailed.
1304         }
1305         
1306         if (nodeIsBefore && !nodeIsAfter) {
1307             return 2;  // left trailed.
1308         }
1309         // fully contined.
1310         return 3;
1311     },
1312  
1313     cleanWordChars : function(input) {// change the chars to hex code
1314         
1315        var swapCodes  = [ 
1316             [    8211, "&#8211;" ], 
1317             [    8212, "&#8212;" ], 
1318             [    8216,  "'" ],  
1319             [    8217, "'" ],  
1320             [    8220, '"' ],  
1321             [    8221, '"' ],  
1322             [    8226, "*" ],  
1323             [    8230, "..." ]
1324         ]; 
1325         var output = input;
1326         Roo.each(swapCodes, function(sw) { 
1327             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1328             
1329             output = output.replace(swapper, sw[1]);
1330         });
1331         
1332         return output;
1333     },
1334     
1335      
1336     
1337         
1338     
1339     cleanUpChild : function (node)
1340     {
1341         
1342         new Roo.htmleditor.FilterComment({node : node});
1343         new Roo.htmleditor.FilterAttributes({
1344                 node : node,
1345                 attrib_black : this.ablack,
1346                 attrib_clean : this.aclean,
1347                 style_white : this.cwhite,
1348                 style_black : this.cblack
1349         });
1350         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1351         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1352          
1353         
1354     },
1355     
1356     /**
1357      * Clean up MS wordisms...
1358      * @deprecated - use filter directly
1359      */
1360     cleanWord : function(node)
1361     {
1362         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1363         new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1364         
1365     },
1366    
1367     
1368     /**
1369
1370      * @deprecated - use filters
1371      */
1372     cleanTableWidths : function(node)
1373     {
1374         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1375         
1376  
1377     },
1378     
1379      
1380         
1381     applyBlacklists : function()
1382     {
1383         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1384         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1385         
1386         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1387         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1388         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1389         
1390         this.white = [];
1391         this.black = [];
1392         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1393             if (b.indexOf(tag) > -1) {
1394                 return;
1395             }
1396             this.white.push(tag);
1397             
1398         }, this);
1399         
1400         Roo.each(w, function(tag) {
1401             if (b.indexOf(tag) > -1) {
1402                 return;
1403             }
1404             if (this.white.indexOf(tag) > -1) {
1405                 return;
1406             }
1407             this.white.push(tag);
1408             
1409         }, this);
1410         
1411         
1412         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1413             if (w.indexOf(tag) > -1) {
1414                 return;
1415             }
1416             this.black.push(tag);
1417             
1418         }, this);
1419         
1420         Roo.each(b, function(tag) {
1421             if (w.indexOf(tag) > -1) {
1422                 return;
1423             }
1424             if (this.black.indexOf(tag) > -1) {
1425                 return;
1426             }
1427             this.black.push(tag);
1428             
1429         }, this);
1430         
1431         
1432         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1433         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1434         
1435         this.cwhite = [];
1436         this.cblack = [];
1437         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1438             if (b.indexOf(tag) > -1) {
1439                 return;
1440             }
1441             this.cwhite.push(tag);
1442             
1443         }, this);
1444         
1445         Roo.each(w, function(tag) {
1446             if (b.indexOf(tag) > -1) {
1447                 return;
1448             }
1449             if (this.cwhite.indexOf(tag) > -1) {
1450                 return;
1451             }
1452             this.cwhite.push(tag);
1453             
1454         }, this);
1455         
1456         
1457         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1458             if (w.indexOf(tag) > -1) {
1459                 return;
1460             }
1461             this.cblack.push(tag);
1462             
1463         }, this);
1464         
1465         Roo.each(b, function(tag) {
1466             if (w.indexOf(tag) > -1) {
1467                 return;
1468             }
1469             if (this.cblack.indexOf(tag) > -1) {
1470                 return;
1471             }
1472             this.cblack.push(tag);
1473             
1474         }, this);
1475     },
1476     
1477     setStylesheets : function(stylesheets)
1478     {
1479         if(typeof(stylesheets) == 'string'){
1480             Roo.get(this.iframe.contentDocument.head).createChild({
1481                 tag : 'link',
1482                 rel : 'stylesheet',
1483                 type : 'text/css',
1484                 href : stylesheets
1485             });
1486             
1487             return;
1488         }
1489         var _this = this;
1490      
1491         Roo.each(stylesheets, function(s) {
1492             if(!s.length){
1493                 return;
1494             }
1495             
1496             Roo.get(_this.iframe.contentDocument.head).createChild({
1497                 tag : 'link',
1498                 rel : 'stylesheet',
1499                 type : 'text/css',
1500                 href : s
1501             });
1502         });
1503
1504         
1505     },
1506     
1507     
1508     updateLanguage : function()
1509     {
1510         if (!this.iframe || !this.iframe.contentDocument) {
1511             return;
1512         }
1513         Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1514     },
1515     
1516     
1517     removeStylesheets : function()
1518     {
1519         var _this = this;
1520         
1521         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1522             s.remove();
1523         });
1524     },
1525     
1526     setStyle : function(style)
1527     {
1528         Roo.get(this.iframe.contentDocument.head).createChild({
1529             tag : 'style',
1530             type : 'text/css',
1531             html : style
1532         });
1533
1534         return;
1535     }
1536     
1537     // hide stuff that is not compatible
1538     /**
1539      * @event blur
1540      * @hide
1541      */
1542     /**
1543      * @event change
1544      * @hide
1545      */
1546     /**
1547      * @event focus
1548      * @hide
1549      */
1550     /**
1551      * @event specialkey
1552      * @hide
1553      */
1554     /**
1555      * @cfg {String} fieldClass @hide
1556      */
1557     /**
1558      * @cfg {String} focusClass @hide
1559      */
1560     /**
1561      * @cfg {String} autoCreate @hide
1562      */
1563     /**
1564      * @cfg {String} inputType @hide
1565      */
1566     /**
1567      * @cfg {String} invalidClass @hide
1568      */
1569     /**
1570      * @cfg {String} invalidText @hide
1571      */
1572     /**
1573      * @cfg {String} msgFx @hide
1574      */
1575     /**
1576      * @cfg {String} validateOnBlur @hide
1577      */
1578 });
1579
1580 Roo.HtmlEditorCore.white = [
1581         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1582         
1583        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1584        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1585        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1586        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1587        'TABLE',   'UL',         'XMP', 
1588        
1589        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1590       'THEAD',   'TR', 
1591      
1592       'DIR', 'MENU', 'OL', 'UL', 'DL',
1593        
1594       'EMBED',  'OBJECT'
1595 ];
1596
1597
1598 Roo.HtmlEditorCore.black = [
1599     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1600         'APPLET', // 
1601         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1602         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1603         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1604         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1605         //'FONT' // CLEAN LATER..
1606         'COLGROUP', 'COL'   // messy tables.
1607         
1608         
1609 ];
1610 Roo.HtmlEditorCore.clean = [ // ?? needed???
1611      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1612 ];
1613 Roo.HtmlEditorCore.tag_remove = [
1614     'FONT', 'TBODY'  
1615 ];
1616 // attributes..
1617
1618 Roo.HtmlEditorCore.ablack = [
1619     'on'
1620 ];
1621     
1622 Roo.HtmlEditorCore.aclean = [ 
1623     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1624 ];
1625
1626 // protocols..
1627 Roo.HtmlEditorCore.pwhite= [
1628         'http',  'https',  'mailto'
1629 ];
1630
1631 // white listed style attributes.
1632 Roo.HtmlEditorCore.cwhite= [
1633       //  'text-align', /// default is to allow most things..
1634       
1635          
1636 //        'font-size'//??
1637 ];
1638
1639 // black listed style attributes.
1640 Roo.HtmlEditorCore.cblack= [
1641       //  'font-size' -- this can be set by the project 
1642 ];
1643
1644
1645
1646
1647