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