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