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