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             //'mousedown': this.onEditorEvent,
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     
545     onPasteEvent : function(e,v)
546     {
547         // I think we better assume paste is going to be a dirty load of rubish from word..
548         
549         // even pasting into a 'email version' of this widget will have to clean up that mess.
550         var cd = (e.browserEvent.clipboardData || window.clipboardData);
551         
552         // check what type of paste - if it's an image, then handle it differently.
553         if (cd.files.length > 0) {
554             // pasting images?
555             var urlAPI = (window.createObjectURL && window) || 
556                 (window.URL && URL.revokeObjectURL && URL) || 
557                 (window.webkitURL && webkitURL);
558     
559             var url = urlAPI.createObjectURL( cd.files[0]);
560             this.insertAtCursor('<img src=" + url + ">');
561             return false;
562         }
563         
564         var html = cd.getData('text/html'); // clipboard event
565         var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
566         var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
567         Roo.log(images);
568         //Roo.log(imgs);
569         // fixme..
570         images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
571                        .map(function(g) { return g.toDataURL(); });
572         
573         
574         html = this.cleanWordChars(html);
575         
576         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
577         
578         
579         var sn = this.getParentElement();
580         // check if d contains a table, and prevent nesting??
581         //Roo.log(d.getElementsByTagName('table'));
582         //Roo.log(sn);
583         //Roo.log(sn.closest('table'));
584         if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
585             e.preventDefault();
586             this.insertAtCursor("You can not nest tables");
587             //Roo.log("prevent?"); // fixme - 
588             return false;
589         }
590         
591         if (images.length > 0) {
592             Roo.each(d.getElementsByTagName('img'), function(img, i) {
593                 img.setAttribute('src', images[i]);
594             });
595         }
596         if (this.autoClean) {
597             new Roo.htmleditor.FilterStyleToTag({ node : d });
598             new Roo.htmleditor.FilterAttributes({
599                 node : d,
600                 attrib_white : ['href', 'src', 'name', 'align'],
601                 attrib_clean : ['href', 'src' ] 
602             });
603             new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
604             // should be fonts..
605             new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
606             new Roo.htmleditor.FilterParagraph({ node : d });
607             new Roo.htmleditor.FilterSpan({ node : d });
608             new Roo.htmleditor.FilterLongBr({ node : d });
609         }
610         if (this.enableBlocks) {
611                 
612             Array.from(d.getElementsByTagName('img')).forEach(function(img) {
613                 if (img.closest('figure')) { // assume!! that it's aready
614                     return;
615                 }
616                 var fig  = new Roo.htmleditor.BlockFigure({
617                     image_src  : img.src
618                 });
619                 fig.updateElement(img); // replace it..
620                 
621             });
622         }
623         
624         
625         this.insertAtCursor(d.innerHTML.replace(/&nbsp;/g,' '));
626         if (this.enableBlocks) {
627             Roo.htmleditor.Block.initAll(this.doc.body);
628         }
629         
630         
631         e.preventDefault();
632         return false;
633         // default behaveiour should be our local cleanup paste? (optional?)
634         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
635         //this.owner.fireEvent('paste', e, v);
636     },
637     // private
638     onDestroy : function(){
639         
640         
641         
642         if(this.rendered){
643             
644             //for (var i =0; i < this.toolbars.length;i++) {
645             //    // fixme - ask toolbars for heights?
646             //    this.toolbars[i].onDestroy();
647            // }
648             
649             //this.wrap.dom.innerHTML = '';
650             //this.wrap.remove();
651         }
652     },
653
654     // private
655     onFirstFocus : function(){
656         
657         this.assignDocWin();
658         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
659         
660         this.activated = true;
661          
662     
663         if(Roo.isGecko){ // prevent silly gecko errors
664             this.win.focus();
665             var s = this.win.getSelection();
666             if(!s.focusNode || s.focusNode.nodeType != 3){
667                 var r = s.getRangeAt(0);
668                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
669                 r.collapse(true);
670                 this.deferFocus();
671             }
672             try{
673                 this.execCmd('useCSS', true);
674                 this.execCmd('styleWithCSS', false);
675             }catch(e){}
676         }
677         this.owner.fireEvent('activate', this);
678     },
679
680     // private
681     adjustFont: function(btn){
682         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
683         //if(Roo.isSafari){ // safari
684         //    adjust *= 2;
685        // }
686         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
687         if(Roo.isSafari){ // safari
688             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
689             v =  (v < 10) ? 10 : v;
690             v =  (v > 48) ? 48 : v;
691             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
692             
693         }
694         
695         
696         v = Math.max(1, v+adjust);
697         
698         this.execCmd('FontSize', v  );
699     },
700
701     onEditorEvent : function(e)
702     {
703         
704         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
705             return; // we do not handle this.. (undo manager does..)
706         }
707         // in theory this detects if the last element is not a br, then we try and do that.
708         // its so clicking in space at bottom triggers adding a br and moving the cursor.
709         if (e &&
710             e.target.nodeName == 'BODY' &&
711             e.type == "mouseup" &&
712             this.doc.body.lastChild
713            ) {
714             var lc = this.doc.body.lastChild;
715             // gtx-trans is google translate plugin adding crap.
716             while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
717                 lc = lc.previousSibling;
718             }
719             if (lc.nodeType == 1 && lc.nodeName != 'BR') {
720             // if last element is <BR> - then dont do anything.
721             
722                 var ns = this.doc.createElement('br');
723                 this.doc.body.appendChild(ns);
724                 range = this.doc.createRange();
725                 range.setStartAfter(ns);
726                 range.collapse(true);
727                 var sel = this.win.getSelection();
728                 sel.removeAllRanges();
729                 sel.addRange(range);
730             }
731         }
732         
733         
734         
735         this.fireEditorEvent(e);
736       //  this.updateToolbar();
737         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
738     },
739     
740     fireEditorEvent: function(e)
741     {
742         this.owner.fireEvent('editorevent', this, e);
743     },
744
745     insertTag : function(tg)
746     {
747         // could be a bit smarter... -> wrap the current selected tRoo..
748         if (tg.toLowerCase() == 'span' ||
749             tg.toLowerCase() == 'code' ||
750             tg.toLowerCase() == 'sup' ||
751             tg.toLowerCase() == 'sub' 
752             ) {
753             
754             range = this.createRange(this.getSelection());
755             var wrappingNode = this.doc.createElement(tg.toLowerCase());
756             wrappingNode.appendChild(range.extractContents());
757             range.insertNode(wrappingNode);
758
759             return;
760             
761             
762             
763         }
764         this.execCmd("formatblock",   tg);
765         this.undoManager.addEvent(); 
766     },
767     
768     insertText : function(txt)
769     {
770         
771         
772         var range = this.createRange();
773         range.deleteContents();
774                //alert(Sender.getAttribute('label'));
775                
776         range.insertNode(this.doc.createTextNode(txt));
777         this.undoManager.addEvent();
778     } ,
779     
780      
781
782     /**
783      * Executes a Midas editor command on the editor document and performs necessary focus and
784      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
785      * @param {String} cmd The Midas command
786      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
787      */
788     relayCmd : function(cmd, value)
789     {
790         
791         switch (cmd) {
792             case 'justifyleft':
793             case 'justifyright':
794             case 'justifycenter':
795                 // if we are in a cell, then we will adjust the
796                 var n = this.getParentElement();
797                 var td = n.closest('td');
798                 if (td) {
799                     var bl = Roo.htmleditor.Block.factory(td);
800                     bl.textAlign = cmd.replace('justify','');
801                     bl.updateElement();
802                     this.owner.fireEvent('editorevent', this);
803                     return;
804                 }
805                 this.execCmd('styleWithCSS', true); // 
806                 break;
807             case 'bold':
808             case 'italic':
809                 // if there is no selection, then we insert, and set the curson inside it..
810                 this.execCmd('styleWithCSS', false); 
811                 break;
812                 
813         
814             default:
815                 break;
816         }
817         
818         
819         this.win.focus();
820         this.execCmd(cmd, value);
821         this.owner.fireEvent('editorevent', this);
822         //this.updateToolbar();
823         this.owner.deferFocus();
824     },
825
826     /**
827      * Executes a Midas editor command directly on the editor document.
828      * For visual commands, you should use {@link #relayCmd} instead.
829      * <b>This should only be called after the editor is initialized.</b>
830      * @param {String} cmd The Midas command
831      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
832      */
833     execCmd : function(cmd, value){
834         this.doc.execCommand(cmd, false, value === undefined ? null : value);
835         this.syncValue();
836     },
837  
838  
839    
840     /**
841      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
842      * to insert tRoo.
843      * @param {String} text | dom node.. 
844      */
845     insertAtCursor : function(text)
846     {
847         
848         if(!this.activated){
849             return;
850         }
851          
852         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
853             this.win.focus();
854             
855             
856             // from jquery ui (MIT licenced)
857             var range, node;
858             var win = this.win;
859             
860             if (win.getSelection && win.getSelection().getRangeAt) {
861                 
862                 // delete the existing?
863                 
864                 this.createRange(this.getSelection()).deleteContents();
865                 range = win.getSelection().getRangeAt(0);
866                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
867                 range.insertNode(node);
868                 range = range.cloneRange();
869                 range.collapse(false);
870                  
871                 win.getSelection().removeAllRanges();
872                 win.getSelection().addRange(range);
873                 
874                 
875                 
876             } else if (win.document.selection && win.document.selection.createRange) {
877                 // no firefox support
878                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
879                 win.document.selection.createRange().pasteHTML(txt);
880             
881             } else {
882                 // no firefox support
883                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
884                 this.execCmd('InsertHTML', txt);
885             } 
886             this.syncValue();
887             
888             this.deferFocus();
889         }
890     },
891  // private
892     mozKeyPress : function(e){
893         if(e.ctrlKey){
894             var c = e.getCharCode(), cmd;
895           
896             if(c > 0){
897                 c = String.fromCharCode(c).toLowerCase();
898                 switch(c){
899                     case 'b':
900                         cmd = 'bold';
901                         break;
902                     case 'i':
903                         cmd = 'italic';
904                         break;
905                     
906                     case 'u':
907                         cmd = 'underline';
908                         break;
909                     
910                     //case 'v':
911                       //  this.cleanUpPaste.defer(100, this);
912                       //  return;
913                         
914                 }
915                 if(cmd){
916                     
917                     this.relayCmd(cmd);
918                     //this.win.focus();
919                     //this.execCmd(cmd);
920                     //this.deferFocus();
921                     e.preventDefault();
922                 }
923                 
924             }
925         }
926     },
927
928     // private
929     fixKeys : function(){ // load time branching for fastest keydown performance
930         
931         
932         if(Roo.isIE){
933             return function(e){
934                 var k = e.getKey(), r;
935                 if(k == e.TAB){
936                     e.stopEvent();
937                     r = this.doc.selection.createRange();
938                     if(r){
939                         r.collapse(true);
940                         r.pasteHTML('&#160;&#160;&#160;&#160;');
941                         this.deferFocus();
942                     }
943                     return;
944                 }
945                 /// this is handled by Roo.htmleditor.KeyEnter
946                  /*
947                 if(k == e.ENTER){
948                     r = this.doc.selection.createRange();
949                     if(r){
950                         var target = r.parentElement();
951                         if(!target || target.tagName.toLowerCase() != 'li'){
952                             e.stopEvent();
953                             r.pasteHTML('<br/>');
954                             r.collapse(false);
955                             r.select();
956                         }
957                     }
958                 }
959                 */
960                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
961                 //    this.cleanUpPaste.defer(100, this);
962                 //    return;
963                 //}
964                 
965                 
966             };
967         }else if(Roo.isOpera){
968             return function(e){
969                 var k = e.getKey();
970                 if(k == e.TAB){
971                     e.stopEvent();
972                     this.win.focus();
973                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
974                     this.deferFocus();
975                 }
976                
977                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
978                 //    this.cleanUpPaste.defer(100, this);
979                  //   return;
980                 //}
981                 
982             };
983         }else if(Roo.isSafari){
984             return function(e){
985                 var k = e.getKey();
986                 
987                 if(k == e.TAB){
988                     e.stopEvent();
989                     this.execCmd('InsertText','\t');
990                     this.deferFocus();
991                     return;
992                 }
993                  this.mozKeyPress(e);
994                 
995                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
996                  //   this.cleanUpPaste.defer(100, this);
997                  //   return;
998                // }
999                 
1000              };
1001         }
1002     }(),
1003     
1004     getAllAncestors: function()
1005     {
1006         var p = this.getSelectedNode();
1007         var a = [];
1008         if (!p) {
1009             a.push(p); // push blank onto stack..
1010             p = this.getParentElement();
1011         }
1012         
1013         
1014         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1015             a.push(p);
1016             p = p.parentNode;
1017         }
1018         a.push(this.doc.body);
1019         return a;
1020     },
1021     lastSel : false,
1022     lastSelNode : false,
1023     
1024     
1025     getSelection : function() 
1026     {
1027         this.assignDocWin();
1028         return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1029     },
1030     /**
1031      * Select a dom node
1032      * @param {DomElement} node the node to select
1033      */
1034     selectNode : function(node, collapse)
1035     {
1036         var nodeRange = node.ownerDocument.createRange();
1037         try {
1038             nodeRange.selectNode(node);
1039         } catch (e) {
1040             nodeRange.selectNodeContents(node);
1041         }
1042         if (collapse === true) {
1043             nodeRange.collapse(true);
1044         }
1045         //
1046         var s = this.win.getSelection();
1047         s.removeAllRanges();
1048         s.addRange(nodeRange);
1049     },
1050     
1051     getSelectedNode: function() 
1052     {
1053         // this may only work on Gecko!!!
1054         
1055         // should we cache this!!!!
1056         
1057          
1058          
1059         var range = this.createRange(this.getSelection()).cloneRange();
1060         
1061         if (Roo.isIE) {
1062             var parent = range.parentElement();
1063             while (true) {
1064                 var testRange = range.duplicate();
1065                 testRange.moveToElementText(parent);
1066                 if (testRange.inRange(range)) {
1067                     break;
1068                 }
1069                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1070                     break;
1071                 }
1072                 parent = parent.parentElement;
1073             }
1074             return parent;
1075         }
1076         
1077         // is ancestor a text element.
1078         var ac =  range.commonAncestorContainer;
1079         if (ac.nodeType == 3) {
1080             ac = ac.parentNode;
1081         }
1082         
1083         var ar = ac.childNodes;
1084          
1085         var nodes = [];
1086         var other_nodes = [];
1087         var has_other_nodes = false;
1088         for (var i=0;i<ar.length;i++) {
1089             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1090                 continue;
1091             }
1092             // fullly contained node.
1093             
1094             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1095                 nodes.push(ar[i]);
1096                 continue;
1097             }
1098             
1099             // probably selected..
1100             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1101                 other_nodes.push(ar[i]);
1102                 continue;
1103             }
1104             // outer..
1105             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1106                 continue;
1107             }
1108             
1109             
1110             has_other_nodes = true;
1111         }
1112         if (!nodes.length && other_nodes.length) {
1113             nodes= other_nodes;
1114         }
1115         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1116             return false;
1117         }
1118         
1119         return nodes[0];
1120     },
1121     
1122     
1123     createRange: function(sel)
1124     {
1125         // this has strange effects when using with 
1126         // top toolbar - not sure if it's a great idea.
1127         //this.editor.contentWindow.focus();
1128         if (typeof sel != "undefined") {
1129             try {
1130                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1131             } catch(e) {
1132                 return this.doc.createRange();
1133             }
1134         } else {
1135             return this.doc.createRange();
1136         }
1137     },
1138     getParentElement: function()
1139     {
1140         
1141         this.assignDocWin();
1142         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1143         
1144         var range = this.createRange(sel);
1145          
1146         try {
1147             var p = range.commonAncestorContainer;
1148             while (p.nodeType == 3) { // text node
1149                 p = p.parentNode;
1150             }
1151             return p;
1152         } catch (e) {
1153             return null;
1154         }
1155     
1156     },
1157     /***
1158      *
1159      * Range intersection.. the hard stuff...
1160      *  '-1' = before
1161      *  '0' = hits..
1162      *  '1' = after.
1163      *         [ -- selected range --- ]
1164      *   [fail]                        [fail]
1165      *
1166      *    basically..
1167      *      if end is before start or  hits it. fail.
1168      *      if start is after end or hits it fail.
1169      *
1170      *   if either hits (but other is outside. - then it's not 
1171      *   
1172      *    
1173      **/
1174     
1175     
1176     // @see http://www.thismuchiknow.co.uk/?p=64.
1177     rangeIntersectsNode : function(range, node)
1178     {
1179         var nodeRange = node.ownerDocument.createRange();
1180         try {
1181             nodeRange.selectNode(node);
1182         } catch (e) {
1183             nodeRange.selectNodeContents(node);
1184         }
1185     
1186         var rangeStartRange = range.cloneRange();
1187         rangeStartRange.collapse(true);
1188     
1189         var rangeEndRange = range.cloneRange();
1190         rangeEndRange.collapse(false);
1191     
1192         var nodeStartRange = nodeRange.cloneRange();
1193         nodeStartRange.collapse(true);
1194     
1195         var nodeEndRange = nodeRange.cloneRange();
1196         nodeEndRange.collapse(false);
1197     
1198         return rangeStartRange.compareBoundaryPoints(
1199                  Range.START_TO_START, nodeEndRange) == -1 &&
1200                rangeEndRange.compareBoundaryPoints(
1201                  Range.START_TO_START, nodeStartRange) == 1;
1202         
1203          
1204     },
1205     rangeCompareNode : function(range, node)
1206     {
1207         var nodeRange = node.ownerDocument.createRange();
1208         try {
1209             nodeRange.selectNode(node);
1210         } catch (e) {
1211             nodeRange.selectNodeContents(node);
1212         }
1213         
1214         
1215         range.collapse(true);
1216     
1217         nodeRange.collapse(true);
1218      
1219         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1220         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1221          
1222         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1223         
1224         var nodeIsBefore   =  ss == 1;
1225         var nodeIsAfter    = ee == -1;
1226         
1227         if (nodeIsBefore && nodeIsAfter) {
1228             return 0; // outer
1229         }
1230         if (!nodeIsBefore && nodeIsAfter) {
1231             return 1; //right trailed.
1232         }
1233         
1234         if (nodeIsBefore && !nodeIsAfter) {
1235             return 2;  // left trailed.
1236         }
1237         // fully contined.
1238         return 3;
1239     },
1240  
1241     cleanWordChars : function(input) {// change the chars to hex code
1242         
1243        var swapCodes  = [ 
1244             [    8211, "&#8211;" ], 
1245             [    8212, "&#8212;" ], 
1246             [    8216,  "'" ],  
1247             [    8217, "'" ],  
1248             [    8220, '"' ],  
1249             [    8221, '"' ],  
1250             [    8226, "*" ],  
1251             [    8230, "..." ]
1252         ]; 
1253         var output = input;
1254         Roo.each(swapCodes, function(sw) { 
1255             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1256             
1257             output = output.replace(swapper, sw[1]);
1258         });
1259         
1260         return output;
1261     },
1262     
1263      
1264     
1265         
1266     
1267     cleanUpChild : function (node)
1268     {
1269         
1270         new Roo.htmleditor.FilterComment({node : node});
1271         new Roo.htmleditor.FilterAttributes({
1272                 node : node,
1273                 attrib_black : this.ablack,
1274                 attrib_clean : this.aclean,
1275                 style_white : this.cwhite,
1276                 style_black : this.cblack
1277         });
1278         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1279         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1280          
1281         
1282     },
1283     
1284     /**
1285      * Clean up MS wordisms...
1286      * @deprecated - use filter directly
1287      */
1288     cleanWord : function(node)
1289     {
1290         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1291         
1292     },
1293    
1294     
1295     /**
1296
1297      * @deprecated - use filters
1298      */
1299     cleanTableWidths : function(node)
1300     {
1301         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1302         
1303  
1304     },
1305     
1306      
1307         
1308     applyBlacklists : function()
1309     {
1310         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1311         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1312         
1313         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1314         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1315         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1316         
1317         this.white = [];
1318         this.black = [];
1319         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1320             if (b.indexOf(tag) > -1) {
1321                 return;
1322             }
1323             this.white.push(tag);
1324             
1325         }, this);
1326         
1327         Roo.each(w, function(tag) {
1328             if (b.indexOf(tag) > -1) {
1329                 return;
1330             }
1331             if (this.white.indexOf(tag) > -1) {
1332                 return;
1333             }
1334             this.white.push(tag);
1335             
1336         }, this);
1337         
1338         
1339         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1340             if (w.indexOf(tag) > -1) {
1341                 return;
1342             }
1343             this.black.push(tag);
1344             
1345         }, this);
1346         
1347         Roo.each(b, function(tag) {
1348             if (w.indexOf(tag) > -1) {
1349                 return;
1350             }
1351             if (this.black.indexOf(tag) > -1) {
1352                 return;
1353             }
1354             this.black.push(tag);
1355             
1356         }, this);
1357         
1358         
1359         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1360         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1361         
1362         this.cwhite = [];
1363         this.cblack = [];
1364         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1365             if (b.indexOf(tag) > -1) {
1366                 return;
1367             }
1368             this.cwhite.push(tag);
1369             
1370         }, this);
1371         
1372         Roo.each(w, function(tag) {
1373             if (b.indexOf(tag) > -1) {
1374                 return;
1375             }
1376             if (this.cwhite.indexOf(tag) > -1) {
1377                 return;
1378             }
1379             this.cwhite.push(tag);
1380             
1381         }, this);
1382         
1383         
1384         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1385             if (w.indexOf(tag) > -1) {
1386                 return;
1387             }
1388             this.cblack.push(tag);
1389             
1390         }, this);
1391         
1392         Roo.each(b, function(tag) {
1393             if (w.indexOf(tag) > -1) {
1394                 return;
1395             }
1396             if (this.cblack.indexOf(tag) > -1) {
1397                 return;
1398             }
1399             this.cblack.push(tag);
1400             
1401         }, this);
1402     },
1403     
1404     setStylesheets : function(stylesheets)
1405     {
1406         if(typeof(stylesheets) == 'string'){
1407             Roo.get(this.iframe.contentDocument.head).createChild({
1408                 tag : 'link',
1409                 rel : 'stylesheet',
1410                 type : 'text/css',
1411                 href : stylesheets
1412             });
1413             
1414             return;
1415         }
1416         var _this = this;
1417      
1418         Roo.each(stylesheets, function(s) {
1419             if(!s.length){
1420                 return;
1421             }
1422             
1423             Roo.get(_this.iframe.contentDocument.head).createChild({
1424                 tag : 'link',
1425                 rel : 'stylesheet',
1426                 type : 'text/css',
1427                 href : s
1428             });
1429         });
1430
1431         
1432     },
1433     
1434     
1435     updateLanguage : function()
1436     {
1437         if (!this.iframe) {
1438             return;
1439         }
1440         Roo.get(this.iframe.content.body).attr("lang", this.language);
1441     },
1442     
1443     
1444     removeStylesheets : function()
1445     {
1446         var _this = this;
1447         
1448         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1449             s.remove();
1450         });
1451     },
1452     
1453     setStyle : function(style)
1454     {
1455         Roo.get(this.iframe.contentDocument.head).createChild({
1456             tag : 'style',
1457             type : 'text/css',
1458             html : style
1459         });
1460
1461         return;
1462     }
1463     
1464     // hide stuff that is not compatible
1465     /**
1466      * @event blur
1467      * @hide
1468      */
1469     /**
1470      * @event change
1471      * @hide
1472      */
1473     /**
1474      * @event focus
1475      * @hide
1476      */
1477     /**
1478      * @event specialkey
1479      * @hide
1480      */
1481     /**
1482      * @cfg {String} fieldClass @hide
1483      */
1484     /**
1485      * @cfg {String} focusClass @hide
1486      */
1487     /**
1488      * @cfg {String} autoCreate @hide
1489      */
1490     /**
1491      * @cfg {String} inputType @hide
1492      */
1493     /**
1494      * @cfg {String} invalidClass @hide
1495      */
1496     /**
1497      * @cfg {String} invalidText @hide
1498      */
1499     /**
1500      * @cfg {String} msgFx @hide
1501      */
1502     /**
1503      * @cfg {String} validateOnBlur @hide
1504      */
1505 });
1506
1507 Roo.HtmlEditorCore.white = [
1508         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1509         
1510        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1511        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1512        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1513        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1514        'TABLE',   'UL',         'XMP', 
1515        
1516        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1517       'THEAD',   'TR', 
1518      
1519       'DIR', 'MENU', 'OL', 'UL', 'DL',
1520        
1521       'EMBED',  'OBJECT'
1522 ];
1523
1524
1525 Roo.HtmlEditorCore.black = [
1526     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1527         'APPLET', // 
1528         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1529         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1530         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1531         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1532         //'FONT' // CLEAN LATER..
1533         'COLGROUP', 'COL'  // messy tables.
1534         
1535 ];
1536 Roo.HtmlEditorCore.clean = [ // ?? needed???
1537      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1538 ];
1539 Roo.HtmlEditorCore.tag_remove = [
1540     'FONT', 'TBODY'  
1541 ];
1542 // attributes..
1543
1544 Roo.HtmlEditorCore.ablack = [
1545     'on'
1546 ];
1547     
1548 Roo.HtmlEditorCore.aclean = [ 
1549     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1550 ];
1551
1552 // protocols..
1553 Roo.HtmlEditorCore.pwhite= [
1554         'http',  'https',  'mailto'
1555 ];
1556
1557 // white listed style attributes.
1558 Roo.HtmlEditorCore.cwhite= [
1559       //  'text-align', /// default is to allow most things..
1560       
1561          
1562 //        'font-size'//??
1563 ];
1564
1565 // black listed style attributes.
1566 Roo.HtmlEditorCore.cblack= [
1567       //  'font-size' -- this can be set by the project 
1568 ];
1569
1570
1571
1572
1573