Changed 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             
637         }
638         if (this.enableBlocks) {
639                 
640             Array.from(d.getElementsByTagName('img')).forEach(function(img) {
641                 if (img.closest('figure')) { // assume!! that it's aready
642                     return;
643                 }
644                 var fig  = new Roo.htmleditor.BlockFigure({
645                     image_src  : img.src
646                 });
647                 fig.updateElement(img); // replace it..
648                 
649             });
650         }
651         
652         
653         this.insertAtCursor(d.innerHTML.replace(/&nbsp;/g,' '));
654         if (this.enableBlocks) {
655             Roo.htmleditor.Block.initAll(this.doc.body);
656         }
657         
658         try {
659             new Roo.htmleditor.FilterFileWarning({ node : d });
660         } catch(e) {
661             Roo.MessageBox.alert("Invalid URLS in content", "The pasted Content contains file:// URLS - you probably want to check all the links in this file");
662         }
663         
664         e.preventDefault();
665         return false;
666         // default behaveiour should be our local cleanup paste? (optional?)
667         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
668         //this.owner.fireEvent('paste', e, v);
669     },
670     // private
671     onDestroy : function(){
672         
673         
674         
675         if(this.rendered){
676             
677             //for (var i =0; i < this.toolbars.length;i++) {
678             //    // fixme - ask toolbars for heights?
679             //    this.toolbars[i].onDestroy();
680            // }
681             
682             //this.wrap.dom.innerHTML = '';
683             //this.wrap.remove();
684         }
685     },
686
687     // private
688     onFirstFocus : function(){
689         
690         this.assignDocWin();
691         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
692         
693         this.activated = true;
694          
695     
696         if(Roo.isGecko){ // prevent silly gecko errors
697             this.win.focus();
698             var s = this.win.getSelection();
699             if(!s.focusNode || s.focusNode.nodeType != 3){
700                 var r = s.getRangeAt(0);
701                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
702                 r.collapse(true);
703                 this.deferFocus();
704             }
705             try{
706                 this.execCmd('useCSS', true);
707                 this.execCmd('styleWithCSS', false);
708             }catch(e){}
709         }
710         this.owner.fireEvent('activate', this);
711     },
712
713     // private
714     adjustFont: function(btn){
715         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
716         //if(Roo.isSafari){ // safari
717         //    adjust *= 2;
718        // }
719         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
720         if(Roo.isSafari){ // safari
721             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
722             v =  (v < 10) ? 10 : v;
723             v =  (v > 48) ? 48 : v;
724             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
725             
726         }
727         
728         
729         v = Math.max(1, v+adjust);
730         
731         this.execCmd('FontSize', v  );
732     },
733
734     onEditorEvent : function(e)
735     {
736          
737         
738         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
739             return; // we do not handle this.. (undo manager does..)
740         }
741         // in theory this detects if the last element is not a br, then we try and do that.
742         // its so clicking in space at bottom triggers adding a br and moving the cursor.
743         if (e &&
744             e.target.nodeName == 'BODY' &&
745             e.type == "mouseup" &&
746             this.doc.body.lastChild
747            ) {
748             var lc = this.doc.body.lastChild;
749             // gtx-trans is google translate plugin adding crap.
750             while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
751                 lc = lc.previousSibling;
752             }
753             if (lc.nodeType == 1 && lc.nodeName != 'BR') {
754             // if last element is <BR> - then dont do anything.
755             
756                 var ns = this.doc.createElement('br');
757                 this.doc.body.appendChild(ns);
758                 range = this.doc.createRange();
759                 range.setStartAfter(ns);
760                 range.collapse(true);
761                 var sel = this.win.getSelection();
762                 sel.removeAllRanges();
763                 sel.addRange(range);
764             }
765         }
766         
767         
768         
769         this.fireEditorEvent(e);
770       //  this.updateToolbar();
771         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
772     },
773     
774     fireEditorEvent: function(e)
775     {
776         this.owner.fireEvent('editorevent', this, e);
777     },
778
779     insertTag : function(tg)
780     {
781         // could be a bit smarter... -> wrap the current selected tRoo..
782         if (tg.toLowerCase() == 'span' ||
783             tg.toLowerCase() == 'code' ||
784             tg.toLowerCase() == 'sup' ||
785             tg.toLowerCase() == 'sub' 
786             ) {
787             
788             range = this.createRange(this.getSelection());
789             var wrappingNode = this.doc.createElement(tg.toLowerCase());
790             wrappingNode.appendChild(range.extractContents());
791             range.insertNode(wrappingNode);
792
793             return;
794             
795             
796             
797         }
798         this.execCmd("formatblock",   tg);
799         this.undoManager.addEvent(); 
800     },
801     
802     insertText : function(txt)
803     {
804         
805         
806         var range = this.createRange();
807         range.deleteContents();
808                //alert(Sender.getAttribute('label'));
809                
810         range.insertNode(this.doc.createTextNode(txt));
811         this.undoManager.addEvent();
812     } ,
813     
814      
815
816     /**
817      * Executes a Midas editor command on the editor document and performs necessary focus and
818      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
819      * @param {String} cmd The Midas command
820      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
821      */
822     relayCmd : function(cmd, value)
823     {
824         
825         switch (cmd) {
826             case 'justifyleft':
827             case 'justifyright':
828             case 'justifycenter':
829                 // if we are in a cell, then we will adjust the
830                 var n = this.getParentElement();
831                 var td = n.closest('td');
832                 if (td) {
833                     var bl = Roo.htmleditor.Block.factory(td);
834                     bl.textAlign = cmd.replace('justify','');
835                     bl.updateElement();
836                     this.owner.fireEvent('editorevent', this);
837                     return;
838                 }
839                 this.execCmd('styleWithCSS', true); // 
840                 break;
841             case 'bold':
842             case 'italic':
843                 // if there is no selection, then we insert, and set the curson inside it..
844                 this.execCmd('styleWithCSS', false); 
845                 break;
846                 
847         
848             default:
849                 break;
850         }
851         
852         
853         this.win.focus();
854         this.execCmd(cmd, value);
855         this.owner.fireEvent('editorevent', this);
856         //this.updateToolbar();
857         this.owner.deferFocus();
858     },
859
860     /**
861      * Executes a Midas editor command directly on the editor document.
862      * For visual commands, you should use {@link #relayCmd} instead.
863      * <b>This should only be called after the editor is initialized.</b>
864      * @param {String} cmd The Midas command
865      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
866      */
867     execCmd : function(cmd, value){
868         this.doc.execCommand(cmd, false, value === undefined ? null : value);
869         this.syncValue();
870     },
871  
872  
873    
874     /**
875      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
876      * to insert tRoo.
877      * @param {String} text | dom node.. 
878      */
879     insertAtCursor : function(text)
880     {
881         
882         if(!this.activated){
883             return;
884         }
885          
886         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
887             this.win.focus();
888             
889             
890             // from jquery ui (MIT licenced)
891             var range, node;
892             var win = this.win;
893             
894             if (win.getSelection && win.getSelection().getRangeAt) {
895                 
896                 // delete the existing?
897                 
898                 this.createRange(this.getSelection()).deleteContents();
899                 range = win.getSelection().getRangeAt(0);
900                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
901                 range.insertNode(node);
902                 range = range.cloneRange();
903                 range.collapse(false);
904                  
905                 win.getSelection().removeAllRanges();
906                 win.getSelection().addRange(range);
907                 
908                 
909                 
910             } else if (win.document.selection && win.document.selection.createRange) {
911                 // no firefox support
912                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
913                 win.document.selection.createRange().pasteHTML(txt);
914             
915             } else {
916                 // no firefox support
917                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
918                 this.execCmd('InsertHTML', txt);
919             } 
920             this.syncValue();
921             
922             this.deferFocus();
923         }
924     },
925  // private
926     mozKeyPress : function(e){
927         if(e.ctrlKey){
928             var c = e.getCharCode(), cmd;
929           
930             if(c > 0){
931                 c = String.fromCharCode(c).toLowerCase();
932                 switch(c){
933                     case 'b':
934                         cmd = 'bold';
935                         break;
936                     case 'i':
937                         cmd = 'italic';
938                         break;
939                     
940                     case 'u':
941                         cmd = 'underline';
942                         break;
943                     
944                     //case 'v':
945                       //  this.cleanUpPaste.defer(100, this);
946                       //  return;
947                         
948                 }
949                 if(cmd){
950                     
951                     this.relayCmd(cmd);
952                     //this.win.focus();
953                     //this.execCmd(cmd);
954                     //this.deferFocus();
955                     e.preventDefault();
956                 }
957                 
958             }
959         }
960     },
961
962     // private
963     fixKeys : function(){ // load time branching for fastest keydown performance
964         
965         
966         if(Roo.isIE){
967             return function(e){
968                 var k = e.getKey(), r;
969                 if(k == e.TAB){
970                     e.stopEvent();
971                     r = this.doc.selection.createRange();
972                     if(r){
973                         r.collapse(true);
974                         r.pasteHTML('&#160;&#160;&#160;&#160;');
975                         this.deferFocus();
976                     }
977                     return;
978                 }
979                 /// this is handled by Roo.htmleditor.KeyEnter
980                  /*
981                 if(k == e.ENTER){
982                     r = this.doc.selection.createRange();
983                     if(r){
984                         var target = r.parentElement();
985                         if(!target || target.tagName.toLowerCase() != 'li'){
986                             e.stopEvent();
987                             r.pasteHTML('<br/>');
988                             r.collapse(false);
989                             r.select();
990                         }
991                     }
992                 }
993                 */
994                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
995                 //    this.cleanUpPaste.defer(100, this);
996                 //    return;
997                 //}
998                 
999                 
1000             };
1001         }else if(Roo.isOpera){
1002             return function(e){
1003                 var k = e.getKey();
1004                 if(k == e.TAB){
1005                     e.stopEvent();
1006                     this.win.focus();
1007                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
1008                     this.deferFocus();
1009                 }
1010                
1011                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1012                 //    this.cleanUpPaste.defer(100, this);
1013                  //   return;
1014                 //}
1015                 
1016             };
1017         }else if(Roo.isSafari){
1018             return function(e){
1019                 var k = e.getKey();
1020                 
1021                 if(k == e.TAB){
1022                     e.stopEvent();
1023                     this.execCmd('InsertText','\t');
1024                     this.deferFocus();
1025                     return;
1026                 }
1027                  this.mozKeyPress(e);
1028                 
1029                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1030                  //   this.cleanUpPaste.defer(100, this);
1031                  //   return;
1032                // }
1033                 
1034              };
1035         }
1036     }(),
1037     
1038     getAllAncestors: function()
1039     {
1040         var p = this.getSelectedNode();
1041         var a = [];
1042         if (!p) {
1043             a.push(p); // push blank onto stack..
1044             p = this.getParentElement();
1045         }
1046         
1047         
1048         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1049             a.push(p);
1050             p = p.parentNode;
1051         }
1052         a.push(this.doc.body);
1053         return a;
1054     },
1055     lastSel : false,
1056     lastSelNode : false,
1057     
1058     
1059     getSelection : function() 
1060     {
1061         this.assignDocWin();
1062         return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1063     },
1064     /**
1065      * Select a dom node
1066      * @param {DomElement} node the node to select
1067      */
1068     selectNode : function(node, collapse)
1069     {
1070         var nodeRange = node.ownerDocument.createRange();
1071         try {
1072             nodeRange.selectNode(node);
1073         } catch (e) {
1074             nodeRange.selectNodeContents(node);
1075         }
1076         if (collapse === true) {
1077             nodeRange.collapse(true);
1078         }
1079         //
1080         var s = this.win.getSelection();
1081         s.removeAllRanges();
1082         s.addRange(nodeRange);
1083     },
1084     
1085     getSelectedNode: function() 
1086     {
1087         // this may only work on Gecko!!!
1088         
1089         // should we cache this!!!!
1090         
1091          
1092          
1093         var range = this.createRange(this.getSelection()).cloneRange();
1094         
1095         if (Roo.isIE) {
1096             var parent = range.parentElement();
1097             while (true) {
1098                 var testRange = range.duplicate();
1099                 testRange.moveToElementText(parent);
1100                 if (testRange.inRange(range)) {
1101                     break;
1102                 }
1103                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1104                     break;
1105                 }
1106                 parent = parent.parentElement;
1107             }
1108             return parent;
1109         }
1110         
1111         // is ancestor a text element.
1112         var ac =  range.commonAncestorContainer;
1113         if (ac.nodeType == 3) {
1114             ac = ac.parentNode;
1115         }
1116         
1117         var ar = ac.childNodes;
1118          
1119         var nodes = [];
1120         var other_nodes = [];
1121         var has_other_nodes = false;
1122         for (var i=0;i<ar.length;i++) {
1123             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1124                 continue;
1125             }
1126             // fullly contained node.
1127             
1128             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1129                 nodes.push(ar[i]);
1130                 continue;
1131             }
1132             
1133             // probably selected..
1134             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1135                 other_nodes.push(ar[i]);
1136                 continue;
1137             }
1138             // outer..
1139             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1140                 continue;
1141             }
1142             
1143             
1144             has_other_nodes = true;
1145         }
1146         if (!nodes.length && other_nodes.length) {
1147             nodes= other_nodes;
1148         }
1149         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1150             return false;
1151         }
1152         
1153         return nodes[0];
1154     },
1155     
1156     
1157     createRange: function(sel)
1158     {
1159         // this has strange effects when using with 
1160         // top toolbar - not sure if it's a great idea.
1161         //this.editor.contentWindow.focus();
1162         if (typeof sel != "undefined") {
1163             try {
1164                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1165             } catch(e) {
1166                 return this.doc.createRange();
1167             }
1168         } else {
1169             return this.doc.createRange();
1170         }
1171     },
1172     getParentElement: function()
1173     {
1174         
1175         this.assignDocWin();
1176         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1177         
1178         var range = this.createRange(sel);
1179          
1180         try {
1181             var p = range.commonAncestorContainer;
1182             while (p.nodeType == 3) { // text node
1183                 p = p.parentNode;
1184             }
1185             return p;
1186         } catch (e) {
1187             return null;
1188         }
1189     
1190     },
1191     /***
1192      *
1193      * Range intersection.. the hard stuff...
1194      *  '-1' = before
1195      *  '0' = hits..
1196      *  '1' = after.
1197      *         [ -- selected range --- ]
1198      *   [fail]                        [fail]
1199      *
1200      *    basically..
1201      *      if end is before start or  hits it. fail.
1202      *      if start is after end or hits it fail.
1203      *
1204      *   if either hits (but other is outside. - then it's not 
1205      *   
1206      *    
1207      **/
1208     
1209     
1210     // @see http://www.thismuchiknow.co.uk/?p=64.
1211     rangeIntersectsNode : function(range, node)
1212     {
1213         var nodeRange = node.ownerDocument.createRange();
1214         try {
1215             nodeRange.selectNode(node);
1216         } catch (e) {
1217             nodeRange.selectNodeContents(node);
1218         }
1219     
1220         var rangeStartRange = range.cloneRange();
1221         rangeStartRange.collapse(true);
1222     
1223         var rangeEndRange = range.cloneRange();
1224         rangeEndRange.collapse(false);
1225     
1226         var nodeStartRange = nodeRange.cloneRange();
1227         nodeStartRange.collapse(true);
1228     
1229         var nodeEndRange = nodeRange.cloneRange();
1230         nodeEndRange.collapse(false);
1231     
1232         return rangeStartRange.compareBoundaryPoints(
1233                  Range.START_TO_START, nodeEndRange) == -1 &&
1234                rangeEndRange.compareBoundaryPoints(
1235                  Range.START_TO_START, nodeStartRange) == 1;
1236         
1237          
1238     },
1239     rangeCompareNode : function(range, node)
1240     {
1241         var nodeRange = node.ownerDocument.createRange();
1242         try {
1243             nodeRange.selectNode(node);
1244         } catch (e) {
1245             nodeRange.selectNodeContents(node);
1246         }
1247         
1248         
1249         range.collapse(true);
1250     
1251         nodeRange.collapse(true);
1252      
1253         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1254         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1255          
1256         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1257         
1258         var nodeIsBefore   =  ss == 1;
1259         var nodeIsAfter    = ee == -1;
1260         
1261         if (nodeIsBefore && nodeIsAfter) {
1262             return 0; // outer
1263         }
1264         if (!nodeIsBefore && nodeIsAfter) {
1265             return 1; //right trailed.
1266         }
1267         
1268         if (nodeIsBefore && !nodeIsAfter) {
1269             return 2;  // left trailed.
1270         }
1271         // fully contined.
1272         return 3;
1273     },
1274  
1275     cleanWordChars : function(input) {// change the chars to hex code
1276         
1277        var swapCodes  = [ 
1278             [    8211, "&#8211;" ], 
1279             [    8212, "&#8212;" ], 
1280             [    8216,  "'" ],  
1281             [    8217, "'" ],  
1282             [    8220, '"' ],  
1283             [    8221, '"' ],  
1284             [    8226, "*" ],  
1285             [    8230, "..." ]
1286         ]; 
1287         var output = input;
1288         Roo.each(swapCodes, function(sw) { 
1289             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1290             
1291             output = output.replace(swapper, sw[1]);
1292         });
1293         
1294         return output;
1295     },
1296     
1297      
1298     
1299         
1300     
1301     cleanUpChild : function (node)
1302     {
1303         
1304         new Roo.htmleditor.FilterComment({node : node});
1305         new Roo.htmleditor.FilterAttributes({
1306                 node : node,
1307                 attrib_black : this.ablack,
1308                 attrib_clean : this.aclean,
1309                 style_white : this.cwhite,
1310                 style_black : this.cblack
1311         });
1312         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1313         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1314          
1315         
1316     },
1317     
1318     /**
1319      * Clean up MS wordisms...
1320      * @deprecated - use filter directly
1321      */
1322     cleanWord : function(node)
1323     {
1324         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1325         
1326     },
1327    
1328     
1329     /**
1330
1331      * @deprecated - use filters
1332      */
1333     cleanTableWidths : function(node)
1334     {
1335         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1336         
1337  
1338     },
1339     
1340      
1341         
1342     applyBlacklists : function()
1343     {
1344         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1345         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1346         
1347         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1348         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1349         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1350         
1351         this.white = [];
1352         this.black = [];
1353         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1354             if (b.indexOf(tag) > -1) {
1355                 return;
1356             }
1357             this.white.push(tag);
1358             
1359         }, this);
1360         
1361         Roo.each(w, function(tag) {
1362             if (b.indexOf(tag) > -1) {
1363                 return;
1364             }
1365             if (this.white.indexOf(tag) > -1) {
1366                 return;
1367             }
1368             this.white.push(tag);
1369             
1370         }, this);
1371         
1372         
1373         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1374             if (w.indexOf(tag) > -1) {
1375                 return;
1376             }
1377             this.black.push(tag);
1378             
1379         }, this);
1380         
1381         Roo.each(b, function(tag) {
1382             if (w.indexOf(tag) > -1) {
1383                 return;
1384             }
1385             if (this.black.indexOf(tag) > -1) {
1386                 return;
1387             }
1388             this.black.push(tag);
1389             
1390         }, this);
1391         
1392         
1393         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1394         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1395         
1396         this.cwhite = [];
1397         this.cblack = [];
1398         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1399             if (b.indexOf(tag) > -1) {
1400                 return;
1401             }
1402             this.cwhite.push(tag);
1403             
1404         }, this);
1405         
1406         Roo.each(w, function(tag) {
1407             if (b.indexOf(tag) > -1) {
1408                 return;
1409             }
1410             if (this.cwhite.indexOf(tag) > -1) {
1411                 return;
1412             }
1413             this.cwhite.push(tag);
1414             
1415         }, this);
1416         
1417         
1418         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1419             if (w.indexOf(tag) > -1) {
1420                 return;
1421             }
1422             this.cblack.push(tag);
1423             
1424         }, this);
1425         
1426         Roo.each(b, function(tag) {
1427             if (w.indexOf(tag) > -1) {
1428                 return;
1429             }
1430             if (this.cblack.indexOf(tag) > -1) {
1431                 return;
1432             }
1433             this.cblack.push(tag);
1434             
1435         }, this);
1436     },
1437     
1438     setStylesheets : function(stylesheets)
1439     {
1440         if(typeof(stylesheets) == 'string'){
1441             Roo.get(this.iframe.contentDocument.head).createChild({
1442                 tag : 'link',
1443                 rel : 'stylesheet',
1444                 type : 'text/css',
1445                 href : stylesheets
1446             });
1447             
1448             return;
1449         }
1450         var _this = this;
1451      
1452         Roo.each(stylesheets, function(s) {
1453             if(!s.length){
1454                 return;
1455             }
1456             
1457             Roo.get(_this.iframe.contentDocument.head).createChild({
1458                 tag : 'link',
1459                 rel : 'stylesheet',
1460                 type : 'text/css',
1461                 href : s
1462             });
1463         });
1464
1465         
1466     },
1467     
1468     
1469     updateLanguage : function()
1470     {
1471         if (!this.iframe || !this.iframe.contentDocument) {
1472             return;
1473         }
1474         Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1475     },
1476     
1477     
1478     removeStylesheets : function()
1479     {
1480         var _this = this;
1481         
1482         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1483             s.remove();
1484         });
1485     },
1486     
1487     setStyle : function(style)
1488     {
1489         Roo.get(this.iframe.contentDocument.head).createChild({
1490             tag : 'style',
1491             type : 'text/css',
1492             html : style
1493         });
1494
1495         return;
1496     }
1497     
1498     // hide stuff that is not compatible
1499     /**
1500      * @event blur
1501      * @hide
1502      */
1503     /**
1504      * @event change
1505      * @hide
1506      */
1507     /**
1508      * @event focus
1509      * @hide
1510      */
1511     /**
1512      * @event specialkey
1513      * @hide
1514      */
1515     /**
1516      * @cfg {String} fieldClass @hide
1517      */
1518     /**
1519      * @cfg {String} focusClass @hide
1520      */
1521     /**
1522      * @cfg {String} autoCreate @hide
1523      */
1524     /**
1525      * @cfg {String} inputType @hide
1526      */
1527     /**
1528      * @cfg {String} invalidClass @hide
1529      */
1530     /**
1531      * @cfg {String} invalidText @hide
1532      */
1533     /**
1534      * @cfg {String} msgFx @hide
1535      */
1536     /**
1537      * @cfg {String} validateOnBlur @hide
1538      */
1539 });
1540
1541 Roo.HtmlEditorCore.white = [
1542         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1543         
1544        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1545        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1546        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1547        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1548        'TABLE',   'UL',         'XMP', 
1549        
1550        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1551       'THEAD',   'TR', 
1552      
1553       'DIR', 'MENU', 'OL', 'UL', 'DL',
1554        
1555       'EMBED',  'OBJECT'
1556 ];
1557
1558
1559 Roo.HtmlEditorCore.black = [
1560     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1561         'APPLET', // 
1562         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1563         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1564         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1565         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1566         //'FONT' // CLEAN LATER..
1567         'COLGROUP', 'COL'   // messy tables.
1568         
1569         
1570 ];
1571 Roo.HtmlEditorCore.clean = [ // ?? needed???
1572      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1573 ];
1574 Roo.HtmlEditorCore.tag_remove = [
1575     'FONT', 'TBODY'  
1576 ];
1577 // attributes..
1578
1579 Roo.HtmlEditorCore.ablack = [
1580     'on'
1581 ];
1582     
1583 Roo.HtmlEditorCore.aclean = [ 
1584     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1585 ];
1586
1587 // protocols..
1588 Roo.HtmlEditorCore.pwhite= [
1589         'http',  'https',  'mailto'
1590 ];
1591
1592 // white listed style attributes.
1593 Roo.HtmlEditorCore.cwhite= [
1594       //  'text-align', /// default is to allow most things..
1595       
1596          
1597 //        'font-size'//??
1598 ];
1599
1600 // black listed style attributes.
1601 Roo.HtmlEditorCore.cblack= [
1602       //  'font-size' -- this can be set by the project 
1603 ];
1604
1605
1606
1607
1608