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