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