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