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         Roo.log(html);
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             
618             
619             
620             Roo.each(d.getElementsByTagName('img'), function(img, i) {
621                 img.setAttribute('src', images[i]);
622             });
623         }
624         if (this.autoClean) {
625             new Roo.htmleditor.FilterWord({ node : d });
626             
627             new Roo.htmleditor.FilterStyleToTag({ node : d });
628             new Roo.htmleditor.FilterAttributes({
629                 node : d,
630                 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width'],
631                 attrib_clean : ['href', 'src' ] 
632             });
633             new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
634             // should be fonts..
635             new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
636             new Roo.htmleditor.FilterParagraph({ node : d });
637             new Roo.htmleditor.FilterSpan({ node : d });
638             new Roo.htmleditor.FilterLongBr({ node : d });
639             new Roo.htmleditor.FilterComment({ node : d });
640             
641             
642         }
643         if (this.enableBlocks) {
644                 
645             Array.from(d.getElementsByTagName('img')).forEach(function(img) {
646                 if (img.closest('figure')) { // assume!! that it's aready
647                     return;
648                 }
649                 var fig  = new Roo.htmleditor.BlockFigure({
650                     image_src  : img.src
651                 });
652                 fig.updateElement(img); // replace it..
653                 
654             });
655         }
656         
657         
658         this.insertAtCursor(d.innerHTML.replace(/&nbsp;/g,' '));
659         if (this.enableBlocks) {
660             Roo.htmleditor.Block.initAll(this.doc.body);
661         }
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         new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1326         
1327     },
1328    
1329     
1330     /**
1331
1332      * @deprecated - use filters
1333      */
1334     cleanTableWidths : function(node)
1335     {
1336         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1337         
1338  
1339     },
1340     
1341      
1342         
1343     applyBlacklists : function()
1344     {
1345         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1346         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1347         
1348         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1349         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1350         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1351         
1352         this.white = [];
1353         this.black = [];
1354         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1355             if (b.indexOf(tag) > -1) {
1356                 return;
1357             }
1358             this.white.push(tag);
1359             
1360         }, this);
1361         
1362         Roo.each(w, function(tag) {
1363             if (b.indexOf(tag) > -1) {
1364                 return;
1365             }
1366             if (this.white.indexOf(tag) > -1) {
1367                 return;
1368             }
1369             this.white.push(tag);
1370             
1371         }, this);
1372         
1373         
1374         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1375             if (w.indexOf(tag) > -1) {
1376                 return;
1377             }
1378             this.black.push(tag);
1379             
1380         }, this);
1381         
1382         Roo.each(b, function(tag) {
1383             if (w.indexOf(tag) > -1) {
1384                 return;
1385             }
1386             if (this.black.indexOf(tag) > -1) {
1387                 return;
1388             }
1389             this.black.push(tag);
1390             
1391         }, this);
1392         
1393         
1394         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1395         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1396         
1397         this.cwhite = [];
1398         this.cblack = [];
1399         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1400             if (b.indexOf(tag) > -1) {
1401                 return;
1402             }
1403             this.cwhite.push(tag);
1404             
1405         }, this);
1406         
1407         Roo.each(w, function(tag) {
1408             if (b.indexOf(tag) > -1) {
1409                 return;
1410             }
1411             if (this.cwhite.indexOf(tag) > -1) {
1412                 return;
1413             }
1414             this.cwhite.push(tag);
1415             
1416         }, this);
1417         
1418         
1419         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1420             if (w.indexOf(tag) > -1) {
1421                 return;
1422             }
1423             this.cblack.push(tag);
1424             
1425         }, this);
1426         
1427         Roo.each(b, function(tag) {
1428             if (w.indexOf(tag) > -1) {
1429                 return;
1430             }
1431             if (this.cblack.indexOf(tag) > -1) {
1432                 return;
1433             }
1434             this.cblack.push(tag);
1435             
1436         }, this);
1437     },
1438     
1439     setStylesheets : function(stylesheets)
1440     {
1441         if(typeof(stylesheets) == 'string'){
1442             Roo.get(this.iframe.contentDocument.head).createChild({
1443                 tag : 'link',
1444                 rel : 'stylesheet',
1445                 type : 'text/css',
1446                 href : stylesheets
1447             });
1448             
1449             return;
1450         }
1451         var _this = this;
1452      
1453         Roo.each(stylesheets, function(s) {
1454             if(!s.length){
1455                 return;
1456             }
1457             
1458             Roo.get(_this.iframe.contentDocument.head).createChild({
1459                 tag : 'link',
1460                 rel : 'stylesheet',
1461                 type : 'text/css',
1462                 href : s
1463             });
1464         });
1465
1466         
1467     },
1468     
1469     
1470     updateLanguage : function()
1471     {
1472         if (!this.iframe || !this.iframe.contentDocument) {
1473             return;
1474         }
1475         Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1476     },
1477     
1478     
1479     removeStylesheets : function()
1480     {
1481         var _this = this;
1482         
1483         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1484             s.remove();
1485         });
1486     },
1487     
1488     setStyle : function(style)
1489     {
1490         Roo.get(this.iframe.contentDocument.head).createChild({
1491             tag : 'style',
1492             type : 'text/css',
1493             html : style
1494         });
1495
1496         return;
1497     }
1498     
1499     // hide stuff that is not compatible
1500     /**
1501      * @event blur
1502      * @hide
1503      */
1504     /**
1505      * @event change
1506      * @hide
1507      */
1508     /**
1509      * @event focus
1510      * @hide
1511      */
1512     /**
1513      * @event specialkey
1514      * @hide
1515      */
1516     /**
1517      * @cfg {String} fieldClass @hide
1518      */
1519     /**
1520      * @cfg {String} focusClass @hide
1521      */
1522     /**
1523      * @cfg {String} autoCreate @hide
1524      */
1525     /**
1526      * @cfg {String} inputType @hide
1527      */
1528     /**
1529      * @cfg {String} invalidClass @hide
1530      */
1531     /**
1532      * @cfg {String} invalidText @hide
1533      */
1534     /**
1535      * @cfg {String} msgFx @hide
1536      */
1537     /**
1538      * @cfg {String} validateOnBlur @hide
1539      */
1540 });
1541
1542 Roo.HtmlEditorCore.white = [
1543         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1544         
1545        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1546        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1547        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1548        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1549        'TABLE',   'UL',         'XMP', 
1550        
1551        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1552       'THEAD',   'TR', 
1553      
1554       'DIR', 'MENU', 'OL', 'UL', 'DL',
1555        
1556       'EMBED',  'OBJECT'
1557 ];
1558
1559
1560 Roo.HtmlEditorCore.black = [
1561     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1562         'APPLET', // 
1563         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1564         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1565         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1566         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1567         //'FONT' // CLEAN LATER..
1568         'COLGROUP', 'COL'   // messy tables.
1569         
1570         
1571 ];
1572 Roo.HtmlEditorCore.clean = [ // ?? needed???
1573      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1574 ];
1575 Roo.HtmlEditorCore.tag_remove = [
1576     'FONT', 'TBODY'  
1577 ];
1578 // attributes..
1579
1580 Roo.HtmlEditorCore.ablack = [
1581     'on'
1582 ];
1583     
1584 Roo.HtmlEditorCore.aclean = [ 
1585     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1586 ];
1587
1588 // protocols..
1589 Roo.HtmlEditorCore.pwhite= [
1590         'http',  'https',  'mailto'
1591 ];
1592
1593 // white listed style attributes.
1594 Roo.HtmlEditorCore.cwhite= [
1595       //  'text-align', /// default is to allow most things..
1596       
1597          
1598 //        'font-size'//??
1599 ];
1600
1601 // black listed style attributes.
1602 Roo.HtmlEditorCore.cblack= [
1603       //  'font-size' -- this can be set by the project 
1604 ];
1605
1606
1607
1608
1609