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