Roo/HtmlEditorCore.js
[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             //?? tidy?
381             var tidy = new Roo.htmleditor.TidySerializer({
382                 inner:  true
383             });
384             var html  = tidy.serialize(div);
385             
386             
387             if(Roo.isSafari){
388                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
389                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
390                 if(m && m[1]){
391                     html = '<div style="'+m[0]+'">' + html + '</div>';
392                 }
393             }
394             html = this.cleanHtml(html);
395             // fix up the special chars.. normaly like back quotes in word...
396             // however we do not want to do this with chinese..
397             html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
398                 
399                 var cc = match.charCodeAt();
400
401                 // Get the character value, handling surrogate pairs
402                 if (match.length == 2) {
403                     // It's a surrogate pair, calculate the Unicode code point
404                     var high = match.charCodeAt(0) - 0xD800;
405                     var low  = match.charCodeAt(1) - 0xDC00;
406                     cc = (high * 0x400) + low + 0x10000;
407                 }  else if (
408                     (cc >= 0x4E00 && cc < 0xA000 ) ||
409                     (cc >= 0x3400 && cc < 0x4E00 ) ||
410                     (cc >= 0xf900 && cc < 0xfb00 )
411                 ) {
412                         return match;
413                 }  
414          
415                 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
416                 return "&#" + cc + ";";
417                 
418                 
419             });
420             
421             
422              
423             if(this.owner.fireEvent('beforesync', this, html) !== false){
424                 this.el.dom.value = html;
425                 this.owner.fireEvent('sync', this, html);
426             }
427         }
428     },
429
430     /**
431      * TEXTAREA -> EDITABLE
432      * Protected method that will not generally be called directly. Pushes the value of the textarea
433      * into the iframe editor.
434      */
435     pushValue : function()
436     {
437         //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
438         if(this.initialized){
439             var v = this.el.dom.value.trim();
440             
441             
442             if(this.owner.fireEvent('beforepush', this, v) !== false){
443                 var d = (this.doc.body || this.doc.documentElement);
444                 d.innerHTML = v;
445                  
446                 this.el.dom.value = d.innerHTML;
447                 this.owner.fireEvent('push', this, v);
448             }
449             if (this.autoClean) {
450                 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
451                 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
452             }
453             if (this.enableBlocks) {
454                 Roo.htmleditor.Block.initAll(this.doc.body);
455             }
456             
457             this.updateLanguage();
458             
459             var lc = this.doc.body.lastChild;
460             if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
461                 // add an extra line at the end.
462                 this.doc.body.appendChild(this.doc.createElement('br'));
463             }
464             
465             
466         }
467     },
468
469     // private
470     deferFocus : function(){
471         this.focus.defer(10, this);
472     },
473
474     // doc'ed in Field
475     focus : function(){
476         if(this.win && !this.sourceEditMode){
477             this.win.focus();
478         }else{
479             this.el.focus();
480         }
481     },
482     
483     assignDocWin: function()
484     {
485         var iframe = this.iframe;
486         
487          if(Roo.isIE){
488             this.doc = iframe.contentWindow.document;
489             this.win = iframe.contentWindow;
490         } else {
491 //            if (!Roo.get(this.frameId)) {
492 //                return;
493 //            }
494 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
495 //            this.win = Roo.get(this.frameId).dom.contentWindow;
496             
497             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
498                 return;
499             }
500             
501             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
502             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
503         }
504     },
505     
506     // private
507     initEditor : function(){
508         //console.log("INIT EDITOR");
509         this.assignDocWin();
510         
511         
512         
513         this.doc.designMode="on";
514         this.doc.open();
515         this.doc.write(this.getDocMarkup());
516         this.doc.close();
517         
518         var dbody = (this.doc.body || this.doc.documentElement);
519         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
520         // this copies styles from the containing element into thsi one..
521         // not sure why we need all of this..
522         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
523         
524         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
525         //ss['background-attachment'] = 'fixed'; // w3c
526         dbody.bgProperties = 'fixed'; // ie
527         dbody.setAttribute("translate", "no");
528         
529         //Roo.DomHelper.applyStyles(dbody, ss);
530         Roo.EventManager.on(this.doc, {
531              
532             'mouseup': this.onEditorEvent,
533             'dblclick': this.onEditorEvent,
534             'click': this.onEditorEvent,
535             'keyup': this.onEditorEvent,
536             
537             buffer:100,
538             scope: this
539         });
540         Roo.EventManager.on(this.doc, {
541             'paste': this.onPasteEvent,
542             scope : this
543         });
544         if(Roo.isGecko){
545             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
546         }
547         //??? needed???
548         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
549             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
550         }
551         this.initialized = true;
552
553         
554         // initialize special key events - enter
555         new Roo.htmleditor.KeyEnter({core : this});
556         
557          
558         
559         this.owner.fireEvent('initialize', this);
560         this.pushValue();
561     },
562     // this is to prevent a href clicks resulting in a redirect?
563    
564     onPasteEvent : function(e,v)
565     {
566         // I think we better assume paste is going to be a dirty load of rubish from word..
567         
568         // even pasting into a 'email version' of this widget will have to clean up that mess.
569         var cd = (e.browserEvent.clipboardData || window.clipboardData);
570         
571         // check what type of paste - if it's an image, then handle it differently.
572         if (cd.files && cd.files.length > 0) {
573             // pasting images?
574             var urlAPI = (window.createObjectURL && window) || 
575                 (window.URL && URL.revokeObjectURL && URL) || 
576                 (window.webkitURL && webkitURL);
577     
578             var url = urlAPI.createObjectURL( cd.files[0]);
579             this.insertAtCursor('<img src=" + url + ">');
580             return false;
581         }
582         if (cd.types.indexOf('text/html') < 0 ) {
583             return false;
584         }
585         var images = [];
586         var html = cd.getData('text/html'); // clipboard event
587         if (cd.types.indexOf('text/rtf') > -1) {
588             var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
589             images = parser.doc ? parser.doc.getElementsByType('pict') : [];
590         }
591         Roo.log(images);
592         //Roo.log(imgs);
593         // fixme..
594         images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
595                        .map(function(g) { return g.toDataURL(); })
596                        .filter(function(g) { return g != 'about:blank'; });
597         
598         
599         html = this.cleanWordChars(html);
600         
601         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
602         
603         
604         var sn = this.getParentElement();
605         // check if d contains a table, and prevent nesting??
606         //Roo.log(d.getElementsByTagName('table'));
607         //Roo.log(sn);
608         //Roo.log(sn.closest('table'));
609         if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
610             e.preventDefault();
611             this.insertAtCursor("You can not nest tables");
612             //Roo.log("prevent?"); // fixme - 
613             return false;
614         }
615         
616         if (images.length > 0) {
617             Roo.each(d.getElementsByTagName('img'), function(img, i) {
618                 img.setAttribute('src', images[i]);
619             });
620         }
621         if (this.autoClean) {
622             new Roo.htmleditor.FilterStyleToTag({ node : d });
623             new Roo.htmleditor.FilterAttributes({
624                 node : d,
625                 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display'],
626                 attrib_clean : ['href', 'src' ] 
627             });
628             new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
629             // should be fonts..
630             new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
631             new Roo.htmleditor.FilterParagraph({ node : d });
632             new Roo.htmleditor.FilterSpan({ node : d });
633             new Roo.htmleditor.FilterLongBr({ node : d });
634             new Roo.htmleditor.FilterComment({ node : d });
635         }
636         if (this.enableBlocks) {
637                 
638             Array.from(d.getElementsByTagName('img')).forEach(function(img) {
639                 if (img.closest('figure')) { // assume!! that it's aready
640                     return;
641                 }
642                 var fig  = new Roo.htmleditor.BlockFigure({
643                     image_src  : img.src
644                 });
645                 fig.updateElement(img); // replace it..
646                 
647             });
648         }
649         
650         
651         this.insertAtCursor(d.innerHTML.replace(/&nbsp;/g,' '));
652         if (this.enableBlocks) {
653             Roo.htmleditor.Block.initAll(this.doc.body);
654         }
655         
656         
657         e.preventDefault();
658         return false;
659         // default behaveiour should be our local cleanup paste? (optional?)
660         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
661         //this.owner.fireEvent('paste', e, v);
662     },
663     // private
664     onDestroy : function(){
665         
666         
667         
668         if(this.rendered){
669             
670             //for (var i =0; i < this.toolbars.length;i++) {
671             //    // fixme - ask toolbars for heights?
672             //    this.toolbars[i].onDestroy();
673            // }
674             
675             //this.wrap.dom.innerHTML = '';
676             //this.wrap.remove();
677         }
678     },
679
680     // private
681     onFirstFocus : function(){
682         
683         this.assignDocWin();
684         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
685         
686         this.activated = true;
687          
688     
689         if(Roo.isGecko){ // prevent silly gecko errors
690             this.win.focus();
691             var s = this.win.getSelection();
692             if(!s.focusNode || s.focusNode.nodeType != 3){
693                 var r = s.getRangeAt(0);
694                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
695                 r.collapse(true);
696                 this.deferFocus();
697             }
698             try{
699                 this.execCmd('useCSS', true);
700                 this.execCmd('styleWithCSS', false);
701             }catch(e){}
702         }
703         this.owner.fireEvent('activate', this);
704     },
705
706     // private
707     adjustFont: function(btn){
708         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
709         //if(Roo.isSafari){ // safari
710         //    adjust *= 2;
711        // }
712         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
713         if(Roo.isSafari){ // safari
714             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
715             v =  (v < 10) ? 10 : v;
716             v =  (v > 48) ? 48 : v;
717             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
718             
719         }
720         
721         
722         v = Math.max(1, v+adjust);
723         
724         this.execCmd('FontSize', v  );
725     },
726
727     onEditorEvent : function(e)
728     {
729          
730         
731         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
732             return; // we do not handle this.. (undo manager does..)
733         }
734         // in theory this detects if the last element is not a br, then we try and do that.
735         // its so clicking in space at bottom triggers adding a br and moving the cursor.
736         if (e &&
737             e.target.nodeName == 'BODY' &&
738             e.type == "mouseup" &&
739             this.doc.body.lastChild
740            ) {
741             var lc = this.doc.body.lastChild;
742             // gtx-trans is google translate plugin adding crap.
743             while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
744                 lc = lc.previousSibling;
745             }
746             if (lc.nodeType == 1 && lc.nodeName != 'BR') {
747             // if last element is <BR> - then dont do anything.
748             
749                 var ns = this.doc.createElement('br');
750                 this.doc.body.appendChild(ns);
751                 range = this.doc.createRange();
752                 range.setStartAfter(ns);
753                 range.collapse(true);
754                 var sel = this.win.getSelection();
755                 sel.removeAllRanges();
756                 sel.addRange(range);
757             }
758         }
759         
760         
761         
762         this.fireEditorEvent(e);
763       //  this.updateToolbar();
764         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
765     },
766     
767     fireEditorEvent: function(e)
768     {
769         this.owner.fireEvent('editorevent', this, e);
770     },
771
772     insertTag : function(tg)
773     {
774         // could be a bit smarter... -> wrap the current selected tRoo..
775         if (tg.toLowerCase() == 'span' ||
776             tg.toLowerCase() == 'code' ||
777             tg.toLowerCase() == 'sup' ||
778             tg.toLowerCase() == 'sub' 
779             ) {
780             
781             range = this.createRange(this.getSelection());
782             var wrappingNode = this.doc.createElement(tg.toLowerCase());
783             wrappingNode.appendChild(range.extractContents());
784             range.insertNode(wrappingNode);
785
786             return;
787             
788             
789             
790         }
791         this.execCmd("formatblock",   tg);
792         this.undoManager.addEvent(); 
793     },
794     
795     insertText : function(txt)
796     {
797         
798         
799         var range = this.createRange();
800         range.deleteContents();
801                //alert(Sender.getAttribute('label'));
802                
803         range.insertNode(this.doc.createTextNode(txt));
804         this.undoManager.addEvent();
805     } ,
806     
807      
808
809     /**
810      * Executes a Midas editor command on the editor document and performs necessary focus and
811      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
812      * @param {String} cmd The Midas command
813      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
814      */
815     relayCmd : function(cmd, value)
816     {
817         
818         switch (cmd) {
819             case 'justifyleft':
820             case 'justifyright':
821             case 'justifycenter':
822                 // if we are in a cell, then we will adjust the
823                 var n = this.getParentElement();
824                 var td = n.closest('td');
825                 if (td) {
826                     var bl = Roo.htmleditor.Block.factory(td);
827                     bl.textAlign = cmd.replace('justify','');
828                     bl.updateElement();
829                     this.owner.fireEvent('editorevent', this);
830                     return;
831                 }
832                 this.execCmd('styleWithCSS', true); // 
833                 break;
834             case 'bold':
835             case 'italic':
836                 // if there is no selection, then we insert, and set the curson inside it..
837                 this.execCmd('styleWithCSS', false); 
838                 break;
839                 
840         
841             default:
842                 break;
843         }
844         
845         
846         this.win.focus();
847         this.execCmd(cmd, value);
848         this.owner.fireEvent('editorevent', this);
849         //this.updateToolbar();
850         this.owner.deferFocus();
851     },
852
853     /**
854      * Executes a Midas editor command directly on the editor document.
855      * For visual commands, you should use {@link #relayCmd} instead.
856      * <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     execCmd : function(cmd, value){
861         this.doc.execCommand(cmd, false, value === undefined ? null : value);
862         this.syncValue();
863     },
864  
865  
866    
867     /**
868      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
869      * to insert tRoo.
870      * @param {String} text | dom node.. 
871      */
872     insertAtCursor : function(text)
873     {
874         
875         if(!this.activated){
876             return;
877         }
878          
879         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
880             this.win.focus();
881             
882             
883             // from jquery ui (MIT licenced)
884             var range, node;
885             var win = this.win;
886             
887             if (win.getSelection && win.getSelection().getRangeAt) {
888                 
889                 // delete the existing?
890                 
891                 this.createRange(this.getSelection()).deleteContents();
892                 range = win.getSelection().getRangeAt(0);
893                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
894                 range.insertNode(node);
895                 range = range.cloneRange();
896                 range.collapse(false);
897                  
898                 win.getSelection().removeAllRanges();
899                 win.getSelection().addRange(range);
900                 
901                 
902                 
903             } else if (win.document.selection && win.document.selection.createRange) {
904                 // no firefox support
905                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
906                 win.document.selection.createRange().pasteHTML(txt);
907             
908             } else {
909                 // no firefox support
910                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
911                 this.execCmd('InsertHTML', txt);
912             } 
913             this.syncValue();
914             
915             this.deferFocus();
916         }
917     },
918  // private
919     mozKeyPress : function(e){
920         if(e.ctrlKey){
921             var c = e.getCharCode(), cmd;
922           
923             if(c > 0){
924                 c = String.fromCharCode(c).toLowerCase();
925                 switch(c){
926                     case 'b':
927                         cmd = 'bold';
928                         break;
929                     case 'i':
930                         cmd = 'italic';
931                         break;
932                     
933                     case 'u':
934                         cmd = 'underline';
935                         break;
936                     
937                     //case 'v':
938                       //  this.cleanUpPaste.defer(100, this);
939                       //  return;
940                         
941                 }
942                 if(cmd){
943                     
944                     this.relayCmd(cmd);
945                     //this.win.focus();
946                     //this.execCmd(cmd);
947                     //this.deferFocus();
948                     e.preventDefault();
949                 }
950                 
951             }
952         }
953     },
954
955     // private
956     fixKeys : function(){ // load time branching for fastest keydown performance
957         
958         
959         if(Roo.isIE){
960             return function(e){
961                 var k = e.getKey(), r;
962                 if(k == e.TAB){
963                     e.stopEvent();
964                     r = this.doc.selection.createRange();
965                     if(r){
966                         r.collapse(true);
967                         r.pasteHTML('&#160;&#160;&#160;&#160;');
968                         this.deferFocus();
969                     }
970                     return;
971                 }
972                 /// this is handled by Roo.htmleditor.KeyEnter
973                  /*
974                 if(k == e.ENTER){
975                     r = this.doc.selection.createRange();
976                     if(r){
977                         var target = r.parentElement();
978                         if(!target || target.tagName.toLowerCase() != 'li'){
979                             e.stopEvent();
980                             r.pasteHTML('<br/>');
981                             r.collapse(false);
982                             r.select();
983                         }
984                     }
985                 }
986                 */
987                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
988                 //    this.cleanUpPaste.defer(100, this);
989                 //    return;
990                 //}
991                 
992                 
993             };
994         }else if(Roo.isOpera){
995             return function(e){
996                 var k = e.getKey();
997                 if(k == e.TAB){
998                     e.stopEvent();
999                     this.win.focus();
1000                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
1001                     this.deferFocus();
1002                 }
1003                
1004                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1005                 //    this.cleanUpPaste.defer(100, this);
1006                  //   return;
1007                 //}
1008                 
1009             };
1010         }else if(Roo.isSafari){
1011             return function(e){
1012                 var k = e.getKey();
1013                 
1014                 if(k == e.TAB){
1015                     e.stopEvent();
1016                     this.execCmd('InsertText','\t');
1017                     this.deferFocus();
1018                     return;
1019                 }
1020                  this.mozKeyPress(e);
1021                 
1022                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1023                  //   this.cleanUpPaste.defer(100, this);
1024                  //   return;
1025                // }
1026                 
1027              };
1028         }
1029     }(),
1030     
1031     getAllAncestors: function()
1032     {
1033         var p = this.getSelectedNode();
1034         var a = [];
1035         if (!p) {
1036             a.push(p); // push blank onto stack..
1037             p = this.getParentElement();
1038         }
1039         
1040         
1041         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1042             a.push(p);
1043             p = p.parentNode;
1044         }
1045         a.push(this.doc.body);
1046         return a;
1047     },
1048     lastSel : false,
1049     lastSelNode : false,
1050     
1051     
1052     getSelection : function() 
1053     {
1054         this.assignDocWin();
1055         return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1056     },
1057     /**
1058      * Select a dom node
1059      * @param {DomElement} node the node to select
1060      */
1061     selectNode : function(node, collapse)
1062     {
1063         var nodeRange = node.ownerDocument.createRange();
1064         try {
1065             nodeRange.selectNode(node);
1066         } catch (e) {
1067             nodeRange.selectNodeContents(node);
1068         }
1069         if (collapse === true) {
1070             nodeRange.collapse(true);
1071         }
1072         //
1073         var s = this.win.getSelection();
1074         s.removeAllRanges();
1075         s.addRange(nodeRange);
1076     },
1077     
1078     getSelectedNode: function() 
1079     {
1080         // this may only work on Gecko!!!
1081         
1082         // should we cache this!!!!
1083         
1084          
1085          
1086         var range = this.createRange(this.getSelection()).cloneRange();
1087         
1088         if (Roo.isIE) {
1089             var parent = range.parentElement();
1090             while (true) {
1091                 var testRange = range.duplicate();
1092                 testRange.moveToElementText(parent);
1093                 if (testRange.inRange(range)) {
1094                     break;
1095                 }
1096                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1097                     break;
1098                 }
1099                 parent = parent.parentElement;
1100             }
1101             return parent;
1102         }
1103         
1104         // is ancestor a text element.
1105         var ac =  range.commonAncestorContainer;
1106         if (ac.nodeType == 3) {
1107             ac = ac.parentNode;
1108         }
1109         
1110         var ar = ac.childNodes;
1111          
1112         var nodes = [];
1113         var other_nodes = [];
1114         var has_other_nodes = false;
1115         for (var i=0;i<ar.length;i++) {
1116             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1117                 continue;
1118             }
1119             // fullly contained node.
1120             
1121             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1122                 nodes.push(ar[i]);
1123                 continue;
1124             }
1125             
1126             // probably selected..
1127             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1128                 other_nodes.push(ar[i]);
1129                 continue;
1130             }
1131             // outer..
1132             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1133                 continue;
1134             }
1135             
1136             
1137             has_other_nodes = true;
1138         }
1139         if (!nodes.length && other_nodes.length) {
1140             nodes= other_nodes;
1141         }
1142         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1143             return false;
1144         }
1145         
1146         return nodes[0];
1147     },
1148     
1149     
1150     createRange: function(sel)
1151     {
1152         // this has strange effects when using with 
1153         // top toolbar - not sure if it's a great idea.
1154         //this.editor.contentWindow.focus();
1155         if (typeof sel != "undefined") {
1156             try {
1157                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1158             } catch(e) {
1159                 return this.doc.createRange();
1160             }
1161         } else {
1162             return this.doc.createRange();
1163         }
1164     },
1165     getParentElement: function()
1166     {
1167         
1168         this.assignDocWin();
1169         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1170         
1171         var range = this.createRange(sel);
1172          
1173         try {
1174             var p = range.commonAncestorContainer;
1175             while (p.nodeType == 3) { // text node
1176                 p = p.parentNode;
1177             }
1178             return p;
1179         } catch (e) {
1180             return null;
1181         }
1182     
1183     },
1184     /***
1185      *
1186      * Range intersection.. the hard stuff...
1187      *  '-1' = before
1188      *  '0' = hits..
1189      *  '1' = after.
1190      *         [ -- selected range --- ]
1191      *   [fail]                        [fail]
1192      *
1193      *    basically..
1194      *      if end is before start or  hits it. fail.
1195      *      if start is after end or hits it fail.
1196      *
1197      *   if either hits (but other is outside. - then it's not 
1198      *   
1199      *    
1200      **/
1201     
1202     
1203     // @see http://www.thismuchiknow.co.uk/?p=64.
1204     rangeIntersectsNode : function(range, node)
1205     {
1206         var nodeRange = node.ownerDocument.createRange();
1207         try {
1208             nodeRange.selectNode(node);
1209         } catch (e) {
1210             nodeRange.selectNodeContents(node);
1211         }
1212     
1213         var rangeStartRange = range.cloneRange();
1214         rangeStartRange.collapse(true);
1215     
1216         var rangeEndRange = range.cloneRange();
1217         rangeEndRange.collapse(false);
1218     
1219         var nodeStartRange = nodeRange.cloneRange();
1220         nodeStartRange.collapse(true);
1221     
1222         var nodeEndRange = nodeRange.cloneRange();
1223         nodeEndRange.collapse(false);
1224     
1225         return rangeStartRange.compareBoundaryPoints(
1226                  Range.START_TO_START, nodeEndRange) == -1 &&
1227                rangeEndRange.compareBoundaryPoints(
1228                  Range.START_TO_START, nodeStartRange) == 1;
1229         
1230          
1231     },
1232     rangeCompareNode : function(range, node)
1233     {
1234         var nodeRange = node.ownerDocument.createRange();
1235         try {
1236             nodeRange.selectNode(node);
1237         } catch (e) {
1238             nodeRange.selectNodeContents(node);
1239         }
1240         
1241         
1242         range.collapse(true);
1243     
1244         nodeRange.collapse(true);
1245      
1246         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1247         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1248          
1249         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1250         
1251         var nodeIsBefore   =  ss == 1;
1252         var nodeIsAfter    = ee == -1;
1253         
1254         if (nodeIsBefore && nodeIsAfter) {
1255             return 0; // outer
1256         }
1257         if (!nodeIsBefore && nodeIsAfter) {
1258             return 1; //right trailed.
1259         }
1260         
1261         if (nodeIsBefore && !nodeIsAfter) {
1262             return 2;  // left trailed.
1263         }
1264         // fully contined.
1265         return 3;
1266     },
1267  
1268     cleanWordChars : function(input) {// change the chars to hex code
1269         
1270        var swapCodes  = [ 
1271             [    8211, "&#8211;" ], 
1272             [    8212, "&#8212;" ], 
1273             [    8216,  "'" ],  
1274             [    8217, "'" ],  
1275             [    8220, '"' ],  
1276             [    8221, '"' ],  
1277             [    8226, "*" ],  
1278             [    8230, "..." ]
1279         ]; 
1280         var output = input;
1281         Roo.each(swapCodes, function(sw) { 
1282             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1283             
1284             output = output.replace(swapper, sw[1]);
1285         });
1286         
1287         return output;
1288     },
1289     
1290      
1291     
1292         
1293     
1294     cleanUpChild : function (node)
1295     {
1296         
1297         new Roo.htmleditor.FilterComment({node : node});
1298         new Roo.htmleditor.FilterAttributes({
1299                 node : node,
1300                 attrib_black : this.ablack,
1301                 attrib_clean : this.aclean,
1302                 style_white : this.cwhite,
1303                 style_black : this.cblack
1304         });
1305         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1306         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1307          
1308         
1309     },
1310     
1311     /**
1312      * Clean up MS wordisms...
1313      * @deprecated - use filter directly
1314      */
1315     cleanWord : function(node)
1316     {
1317         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1318         
1319     },
1320    
1321     
1322     /**
1323
1324      * @deprecated - use filters
1325      */
1326     cleanTableWidths : function(node)
1327     {
1328         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1329         
1330  
1331     },
1332     
1333      
1334         
1335     applyBlacklists : function()
1336     {
1337         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1338         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1339         
1340         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1341         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1342         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1343         
1344         this.white = [];
1345         this.black = [];
1346         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1347             if (b.indexOf(tag) > -1) {
1348                 return;
1349             }
1350             this.white.push(tag);
1351             
1352         }, this);
1353         
1354         Roo.each(w, function(tag) {
1355             if (b.indexOf(tag) > -1) {
1356                 return;
1357             }
1358             if (this.white.indexOf(tag) > -1) {
1359                 return;
1360             }
1361             this.white.push(tag);
1362             
1363         }, this);
1364         
1365         
1366         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1367             if (w.indexOf(tag) > -1) {
1368                 return;
1369             }
1370             this.black.push(tag);
1371             
1372         }, this);
1373         
1374         Roo.each(b, function(tag) {
1375             if (w.indexOf(tag) > -1) {
1376                 return;
1377             }
1378             if (this.black.indexOf(tag) > -1) {
1379                 return;
1380             }
1381             this.black.push(tag);
1382             
1383         }, this);
1384         
1385         
1386         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1387         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1388         
1389         this.cwhite = [];
1390         this.cblack = [];
1391         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1392             if (b.indexOf(tag) > -1) {
1393                 return;
1394             }
1395             this.cwhite.push(tag);
1396             
1397         }, this);
1398         
1399         Roo.each(w, function(tag) {
1400             if (b.indexOf(tag) > -1) {
1401                 return;
1402             }
1403             if (this.cwhite.indexOf(tag) > -1) {
1404                 return;
1405             }
1406             this.cwhite.push(tag);
1407             
1408         }, this);
1409         
1410         
1411         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1412             if (w.indexOf(tag) > -1) {
1413                 return;
1414             }
1415             this.cblack.push(tag);
1416             
1417         }, this);
1418         
1419         Roo.each(b, function(tag) {
1420             if (w.indexOf(tag) > -1) {
1421                 return;
1422             }
1423             if (this.cblack.indexOf(tag) > -1) {
1424                 return;
1425             }
1426             this.cblack.push(tag);
1427             
1428         }, this);
1429     },
1430     
1431     setStylesheets : function(stylesheets)
1432     {
1433         if(typeof(stylesheets) == 'string'){
1434             Roo.get(this.iframe.contentDocument.head).createChild({
1435                 tag : 'link',
1436                 rel : 'stylesheet',
1437                 type : 'text/css',
1438                 href : stylesheets
1439             });
1440             
1441             return;
1442         }
1443         var _this = this;
1444      
1445         Roo.each(stylesheets, function(s) {
1446             if(!s.length){
1447                 return;
1448             }
1449             
1450             Roo.get(_this.iframe.contentDocument.head).createChild({
1451                 tag : 'link',
1452                 rel : 'stylesheet',
1453                 type : 'text/css',
1454                 href : s
1455             });
1456         });
1457
1458         
1459     },
1460     
1461     
1462     updateLanguage : function()
1463     {
1464         if (!this.iframe || !this.iframe.contentDocument) {
1465             return;
1466         }
1467         Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1468     },
1469     
1470     
1471     removeStylesheets : function()
1472     {
1473         var _this = this;
1474         
1475         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1476             s.remove();
1477         });
1478     },
1479     
1480     setStyle : function(style)
1481     {
1482         Roo.get(this.iframe.contentDocument.head).createChild({
1483             tag : 'style',
1484             type : 'text/css',
1485             html : style
1486         });
1487
1488         return;
1489     }
1490     
1491     // hide stuff that is not compatible
1492     /**
1493      * @event blur
1494      * @hide
1495      */
1496     /**
1497      * @event change
1498      * @hide
1499      */
1500     /**
1501      * @event focus
1502      * @hide
1503      */
1504     /**
1505      * @event specialkey
1506      * @hide
1507      */
1508     /**
1509      * @cfg {String} fieldClass @hide
1510      */
1511     /**
1512      * @cfg {String} focusClass @hide
1513      */
1514     /**
1515      * @cfg {String} autoCreate @hide
1516      */
1517     /**
1518      * @cfg {String} inputType @hide
1519      */
1520     /**
1521      * @cfg {String} invalidClass @hide
1522      */
1523     /**
1524      * @cfg {String} invalidText @hide
1525      */
1526     /**
1527      * @cfg {String} msgFx @hide
1528      */
1529     /**
1530      * @cfg {String} validateOnBlur @hide
1531      */
1532 });
1533
1534 Roo.HtmlEditorCore.white = [
1535         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1536         
1537        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1538        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1539        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1540        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1541        'TABLE',   'UL',         'XMP', 
1542        
1543        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1544       'THEAD',   'TR', 
1545      
1546       'DIR', 'MENU', 'OL', 'UL', 'DL',
1547        
1548       'EMBED',  'OBJECT'
1549 ];
1550
1551
1552 Roo.HtmlEditorCore.black = [
1553     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1554         'APPLET', // 
1555         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1556         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1557         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1558         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1559         //'FONT' // CLEAN LATER..
1560         'COLGROUP', 'COL'   // messy tables.
1561         
1562         
1563 ];
1564 Roo.HtmlEditorCore.clean = [ // ?? needed???
1565      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1566 ];
1567 Roo.HtmlEditorCore.tag_remove = [
1568     'FONT', 'TBODY'  
1569 ];
1570 // attributes..
1571
1572 Roo.HtmlEditorCore.ablack = [
1573     'on'
1574 ];
1575     
1576 Roo.HtmlEditorCore.aclean = [ 
1577     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1578 ];
1579
1580 // protocols..
1581 Roo.HtmlEditorCore.pwhite= [
1582         'http',  'https',  'mailto'
1583 ];
1584
1585 // white listed style attributes.
1586 Roo.HtmlEditorCore.cwhite= [
1587       //  'text-align', /// default is to allow most things..
1588       
1589          
1590 //        'font-size'//??
1591 ];
1592
1593 // black listed style attributes.
1594 Roo.HtmlEditorCore.cblack= [
1595       //  'font-size' -- this can be set by the project 
1596 ];
1597
1598
1599
1600
1601