Roo/HtmlEditorCore.js
[roojs1] / Roo / HtmlEditorCore.js
1 //<script type="text/javascript">
2
3 /*
4  * Based  Ext JS Library 1.1.1
5  * Copyright(c) 2006-2007, Ext JS, LLC.
6  * LGPL
7  *
8  */
9  
10 /**
11  * @class Roo.HtmlEditorCore
12  * @extends Roo.Component
13  * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
14  *
15  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
16  */
17
18 Roo.HtmlEditorCore = function(config){
19     
20     
21     Roo.HtmlEditorCore.superclass.constructor.call(this, config);
22     
23     
24     this.addEvents({
25         /**
26          * @event initialize
27          * Fires when the editor is fully initialized (including the iframe)
28          * @param {Roo.HtmlEditorCore} this
29          */
30         initialize: true,
31         /**
32          * @event activate
33          * Fires when the editor is first receives the focus. Any insertion must wait
34          * until after this event.
35          * @param {Roo.HtmlEditorCore} this
36          */
37         activate: true,
38          /**
39          * @event beforesync
40          * Fires before the textarea is updated with content from the editor iframe. Return false
41          * to cancel the sync.
42          * @param {Roo.HtmlEditorCore} this
43          * @param {String} html
44          */
45         beforesync: true,
46          /**
47          * @event beforepush
48          * Fires before the iframe editor is updated with content from the textarea. Return false
49          * to cancel the push.
50          * @param {Roo.HtmlEditorCore} this
51          * @param {String} html
52          */
53         beforepush: true,
54          /**
55          * @event sync
56          * Fires when the textarea is updated with content from the editor iframe.
57          * @param {Roo.HtmlEditorCore} this
58          * @param {String} html
59          */
60         sync: true,
61          /**
62          * @event push
63          * Fires when the iframe editor is updated with content from the textarea.
64          * @param {Roo.HtmlEditorCore} this
65          * @param {String} html
66          */
67         push: true,
68         
69         /**
70          * @event editorevent
71          * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72          * @param {Roo.HtmlEditorCore} this
73          */
74         editorevent: true 
75          
76         
77     });
78     
79     // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80     
81     // defaults : white / black...
82     this.applyBlacklists();
83     
84     
85     
86 };
87
88
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component,  {
90
91
92      /**
93      * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field 
94      */
95     
96     owner : false,
97     
98      /**
99      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
100      *                        Roo.resizable.
101      */
102     resizable : false,
103      /**
104      * @cfg {Number} height (in pixels)
105      */   
106     height: 300,
107    /**
108      * @cfg {Number} width (in pixels)
109      */   
110     width: 500,
111      /**
112      * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113      *         if you are doing an email editor, this probably needs disabling, it's designed
114      */
115     autoClean: true,
116     
117     /**
118      * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
119      */
120     enableBlocks : true,
121     /**
122      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
123      * 
124      */
125     stylesheets: false,
126     
127     /**
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          
1054         var range = this.createRange(this.getSelection()).cloneRange();
1055         
1056         if (Roo.isIE) {
1057             var parent = range.parentElement();
1058             while (true) {
1059                 var testRange = range.duplicate();
1060                 testRange.moveToElementText(parent);
1061                 if (testRange.inRange(range)) {
1062                     break;
1063                 }
1064                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1065                     break;
1066                 }
1067                 parent = parent.parentElement;
1068             }
1069             return parent;
1070         }
1071         
1072         // is ancestor a text element.
1073         var ac =  range.commonAncestorContainer;
1074         if (ac.nodeType == 3) {
1075             ac = ac.parentNode;
1076         }
1077         
1078         var ar = ac.childNodes;
1079          
1080         var nodes = [];
1081         var other_nodes = [];
1082         var has_other_nodes = false;
1083         for (var i=0;i<ar.length;i++) {
1084             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1085                 continue;
1086             }
1087             // fullly contained node.
1088             
1089             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1090                 nodes.push(ar[i]);
1091                 continue;
1092             }
1093             
1094             // probably selected..
1095             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1096                 other_nodes.push(ar[i]);
1097                 continue;
1098             }
1099             // outer..
1100             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1101                 continue;
1102             }
1103             
1104             
1105             has_other_nodes = true;
1106         }
1107         if (!nodes.length && other_nodes.length) {
1108             nodes= other_nodes;
1109         }
1110         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1111             return false;
1112         }
1113         
1114         return nodes[0];
1115     },
1116     createRange: function(sel)
1117     {
1118         // this has strange effects when using with 
1119         // top toolbar - not sure if it's a great idea.
1120         //this.editor.contentWindow.focus();
1121         if (typeof sel != "undefined") {
1122             try {
1123                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1124             } catch(e) {
1125                 return this.doc.createRange();
1126             }
1127         } else {
1128             return this.doc.createRange();
1129         }
1130     },
1131     getParentElement: function()
1132     {
1133         
1134         this.assignDocWin();
1135         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1136         
1137         var range = this.createRange(sel);
1138          
1139         try {
1140             var p = range.commonAncestorContainer;
1141             while (p.nodeType == 3) { // text node
1142                 p = p.parentNode;
1143             }
1144             return p;
1145         } catch (e) {
1146             return null;
1147         }
1148     
1149     },
1150     /***
1151      *
1152      * Range intersection.. the hard stuff...
1153      *  '-1' = before
1154      *  '0' = hits..
1155      *  '1' = after.
1156      *         [ -- selected range --- ]
1157      *   [fail]                        [fail]
1158      *
1159      *    basically..
1160      *      if end is before start or  hits it. fail.
1161      *      if start is after end or hits it fail.
1162      *
1163      *   if either hits (but other is outside. - then it's not 
1164      *   
1165      *    
1166      **/
1167     
1168     
1169     // @see http://www.thismuchiknow.co.uk/?p=64.
1170     rangeIntersectsNode : function(range, node)
1171     {
1172         var nodeRange = node.ownerDocument.createRange();
1173         try {
1174             nodeRange.selectNode(node);
1175         } catch (e) {
1176             nodeRange.selectNodeContents(node);
1177         }
1178     
1179         var rangeStartRange = range.cloneRange();
1180         rangeStartRange.collapse(true);
1181     
1182         var rangeEndRange = range.cloneRange();
1183         rangeEndRange.collapse(false);
1184     
1185         var nodeStartRange = nodeRange.cloneRange();
1186         nodeStartRange.collapse(true);
1187     
1188         var nodeEndRange = nodeRange.cloneRange();
1189         nodeEndRange.collapse(false);
1190     
1191         return rangeStartRange.compareBoundaryPoints(
1192                  Range.START_TO_START, nodeEndRange) == -1 &&
1193                rangeEndRange.compareBoundaryPoints(
1194                  Range.START_TO_START, nodeStartRange) == 1;
1195         
1196          
1197     },
1198     rangeCompareNode : function(range, node)
1199     {
1200         var nodeRange = node.ownerDocument.createRange();
1201         try {
1202             nodeRange.selectNode(node);
1203         } catch (e) {
1204             nodeRange.selectNodeContents(node);
1205         }
1206         
1207         
1208         range.collapse(true);
1209     
1210         nodeRange.collapse(true);
1211      
1212         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1213         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1214          
1215         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1216         
1217         var nodeIsBefore   =  ss == 1;
1218         var nodeIsAfter    = ee == -1;
1219         
1220         if (nodeIsBefore && nodeIsAfter) {
1221             return 0; // outer
1222         }
1223         if (!nodeIsBefore && nodeIsAfter) {
1224             return 1; //right trailed.
1225         }
1226         
1227         if (nodeIsBefore && !nodeIsAfter) {
1228             return 2;  // left trailed.
1229         }
1230         // fully contined.
1231         return 3;
1232     },
1233  
1234     cleanWordChars : function(input) {// change the chars to hex code
1235         
1236        var swapCodes  = [ 
1237             [    8211, "&#8211;" ], 
1238             [    8212, "&#8212;" ], 
1239             [    8216,  "'" ],  
1240             [    8217, "'" ],  
1241             [    8220, '"' ],  
1242             [    8221, '"' ],  
1243             [    8226, "*" ],  
1244             [    8230, "..." ]
1245         ]; 
1246         var output = input;
1247         Roo.each(swapCodes, function(sw) { 
1248             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1249             
1250             output = output.replace(swapper, sw[1]);
1251         });
1252         
1253         return output;
1254     },
1255     
1256      
1257     
1258         
1259     
1260     cleanUpChild : function (node)
1261     {
1262         
1263         new Roo.htmleditor.FilterComment({node : node});
1264         new Roo.htmleditor.FilterAttributes({
1265                 node : node,
1266                 attrib_black : this.ablack,
1267                 attrib_clean : this.aclean,
1268                 style_white : this.cwhite,
1269                 style_black : this.cblack
1270         });
1271         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1272         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1273          
1274         
1275     },
1276     
1277     /**
1278      * Clean up MS wordisms...
1279      * @deprecated - use filter directly
1280      */
1281     cleanWord : function(node)
1282     {
1283         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1284         
1285     },
1286    
1287     
1288     /**
1289
1290      * @deprecated - use filters
1291      */
1292     cleanTableWidths : function(node)
1293     {
1294         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1295         
1296  
1297     },
1298     
1299      
1300         
1301     applyBlacklists : function()
1302     {
1303         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1304         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1305         
1306         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1307         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1308         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1309         
1310         this.white = [];
1311         this.black = [];
1312         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1313             if (b.indexOf(tag) > -1) {
1314                 return;
1315             }
1316             this.white.push(tag);
1317             
1318         }, this);
1319         
1320         Roo.each(w, function(tag) {
1321             if (b.indexOf(tag) > -1) {
1322                 return;
1323             }
1324             if (this.white.indexOf(tag) > -1) {
1325                 return;
1326             }
1327             this.white.push(tag);
1328             
1329         }, this);
1330         
1331         
1332         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1333             if (w.indexOf(tag) > -1) {
1334                 return;
1335             }
1336             this.black.push(tag);
1337             
1338         }, this);
1339         
1340         Roo.each(b, function(tag) {
1341             if (w.indexOf(tag) > -1) {
1342                 return;
1343             }
1344             if (this.black.indexOf(tag) > -1) {
1345                 return;
1346             }
1347             this.black.push(tag);
1348             
1349         }, this);
1350         
1351         
1352         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1353         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1354         
1355         this.cwhite = [];
1356         this.cblack = [];
1357         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1358             if (b.indexOf(tag) > -1) {
1359                 return;
1360             }
1361             this.cwhite.push(tag);
1362             
1363         }, this);
1364         
1365         Roo.each(w, function(tag) {
1366             if (b.indexOf(tag) > -1) {
1367                 return;
1368             }
1369             if (this.cwhite.indexOf(tag) > -1) {
1370                 return;
1371             }
1372             this.cwhite.push(tag);
1373             
1374         }, this);
1375         
1376         
1377         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1378             if (w.indexOf(tag) > -1) {
1379                 return;
1380             }
1381             this.cblack.push(tag);
1382             
1383         }, this);
1384         
1385         Roo.each(b, function(tag) {
1386             if (w.indexOf(tag) > -1) {
1387                 return;
1388             }
1389             if (this.cblack.indexOf(tag) > -1) {
1390                 return;
1391             }
1392             this.cblack.push(tag);
1393             
1394         }, this);
1395     },
1396     
1397     setStylesheets : function(stylesheets)
1398     {
1399         if(typeof(stylesheets) == 'string'){
1400             Roo.get(this.iframe.contentDocument.head).createChild({
1401                 tag : 'link',
1402                 rel : 'stylesheet',
1403                 type : 'text/css',
1404                 href : stylesheets
1405             });
1406             
1407             return;
1408         }
1409         var _this = this;
1410      
1411         Roo.each(stylesheets, function(s) {
1412             if(!s.length){
1413                 return;
1414             }
1415             
1416             Roo.get(_this.iframe.contentDocument.head).createChild({
1417                 tag : 'link',
1418                 rel : 'stylesheet',
1419                 type : 'text/css',
1420                 href : s
1421             });
1422         });
1423
1424         
1425     },
1426     
1427     removeStylesheets : function()
1428     {
1429         var _this = this;
1430         
1431         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1432             s.remove();
1433         });
1434     },
1435     
1436     setStyle : function(style)
1437     {
1438         Roo.get(this.iframe.contentDocument.head).createChild({
1439             tag : 'style',
1440             type : 'text/css',
1441             html : style
1442         });
1443
1444         return;
1445     }
1446     
1447     // hide stuff that is not compatible
1448     /**
1449      * @event blur
1450      * @hide
1451      */
1452     /**
1453      * @event change
1454      * @hide
1455      */
1456     /**
1457      * @event focus
1458      * @hide
1459      */
1460     /**
1461      * @event specialkey
1462      * @hide
1463      */
1464     /**
1465      * @cfg {String} fieldClass @hide
1466      */
1467     /**
1468      * @cfg {String} focusClass @hide
1469      */
1470     /**
1471      * @cfg {String} autoCreate @hide
1472      */
1473     /**
1474      * @cfg {String} inputType @hide
1475      */
1476     /**
1477      * @cfg {String} invalidClass @hide
1478      */
1479     /**
1480      * @cfg {String} invalidText @hide
1481      */
1482     /**
1483      * @cfg {String} msgFx @hide
1484      */
1485     /**
1486      * @cfg {String} validateOnBlur @hide
1487      */
1488 });
1489
1490 Roo.HtmlEditorCore.white = [
1491         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1492         
1493        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1494        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1495        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1496        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1497        'TABLE',   'UL',         'XMP', 
1498        
1499        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1500       'THEAD',   'TR', 
1501      
1502       'DIR', 'MENU', 'OL', 'UL', 'DL',
1503        
1504       'EMBED',  'OBJECT'
1505 ];
1506
1507
1508 Roo.HtmlEditorCore.black = [
1509     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1510         'APPLET', // 
1511         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1512         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1513         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1514         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1515         //'FONT' // CLEAN LATER..
1516         'COLGROUP', 'COL'  // messy tables.
1517         
1518 ];
1519 Roo.HtmlEditorCore.clean = [ // ?? needed???
1520      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1521 ];
1522 Roo.HtmlEditorCore.tag_remove = [
1523     'FONT', 'TBODY'  
1524 ];
1525 // attributes..
1526
1527 Roo.HtmlEditorCore.ablack = [
1528     'on'
1529 ];
1530     
1531 Roo.HtmlEditorCore.aclean = [ 
1532     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1533 ];
1534
1535 // protocols..
1536 Roo.HtmlEditorCore.pwhite= [
1537         'http',  'https',  'mailto'
1538 ];
1539
1540 // white listed style attributes.
1541 Roo.HtmlEditorCore.cwhite= [
1542       //  'text-align', /// default is to allow most things..
1543       
1544          
1545 //        'font-size'//??
1546 ];
1547
1548 // black listed style attributes.
1549 Roo.HtmlEditorCore.cblack= [
1550       //  'font-size' -- this can be set by the project 
1551 ];
1552
1553
1554
1555
1556