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