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