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         var nodeRange = node.ownerDocument.createRange();
919         try {
920             nodeRange.selectNode(node);
921         } catch (e) {
922             nodeRange.selectNodeContents(node);
923         }
924         //nodeRange.collapse(true);
925         var s = this.win.getSelection();
926         s.removeAllRanges();
927         s.addRange(nodeRange);
928     },
929     
930     getSelectedNode: function() 
931     {
932         // this may only work on Gecko!!!
933         
934         // should we cache this!!!!
935         
936         
937         
938          
939         var range = this.createRange(this.getSelection()).cloneRange();
940         
941         if (Roo.isIE) {
942             var parent = range.parentElement();
943             while (true) {
944                 var testRange = range.duplicate();
945                 testRange.moveToElementText(parent);
946                 if (testRange.inRange(range)) {
947                     break;
948                 }
949                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
950                     break;
951                 }
952                 parent = parent.parentElement;
953             }
954             return parent;
955         }
956         
957         // is ancestor a text element.
958         var ac =  range.commonAncestorContainer;
959         if (ac.nodeType == 3) {
960             ac = ac.parentNode;
961         }
962         
963         var ar = ac.childNodes;
964          
965         var nodes = [];
966         var other_nodes = [];
967         var has_other_nodes = false;
968         for (var i=0;i<ar.length;i++) {
969             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
970                 continue;
971             }
972             // fullly contained node.
973             
974             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
975                 nodes.push(ar[i]);
976                 continue;
977             }
978             
979             // probably selected..
980             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
981                 other_nodes.push(ar[i]);
982                 continue;
983             }
984             // outer..
985             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
986                 continue;
987             }
988             
989             
990             has_other_nodes = true;
991         }
992         if (!nodes.length && other_nodes.length) {
993             nodes= other_nodes;
994         }
995         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
996             return false;
997         }
998         
999         return nodes[0];
1000     },
1001     createRange: function(sel)
1002     {
1003         // this has strange effects when using with 
1004         // top toolbar - not sure if it's a great idea.
1005         //this.editor.contentWindow.focus();
1006         if (typeof sel != "undefined") {
1007             try {
1008                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1009             } catch(e) {
1010                 return this.doc.createRange();
1011             }
1012         } else {
1013             return this.doc.createRange();
1014         }
1015     },
1016     getParentElement: function()
1017     {
1018         
1019         this.assignDocWin();
1020         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1021         
1022         var range = this.createRange(sel);
1023          
1024         try {
1025             var p = range.commonAncestorContainer;
1026             while (p.nodeType == 3) { // text node
1027                 p = p.parentNode;
1028             }
1029             return p;
1030         } catch (e) {
1031             return null;
1032         }
1033     
1034     },
1035     /***
1036      *
1037      * Range intersection.. the hard stuff...
1038      *  '-1' = before
1039      *  '0' = hits..
1040      *  '1' = after.
1041      *         [ -- selected range --- ]
1042      *   [fail]                        [fail]
1043      *
1044      *    basically..
1045      *      if end is before start or  hits it. fail.
1046      *      if start is after end or hits it fail.
1047      *
1048      *   if either hits (but other is outside. - then it's not 
1049      *   
1050      *    
1051      **/
1052     
1053     
1054     // @see http://www.thismuchiknow.co.uk/?p=64.
1055     rangeIntersectsNode : function(range, node)
1056     {
1057         var nodeRange = node.ownerDocument.createRange();
1058         try {
1059             nodeRange.selectNode(node);
1060         } catch (e) {
1061             nodeRange.selectNodeContents(node);
1062         }
1063     
1064         var rangeStartRange = range.cloneRange();
1065         rangeStartRange.collapse(true);
1066     
1067         var rangeEndRange = range.cloneRange();
1068         rangeEndRange.collapse(false);
1069     
1070         var nodeStartRange = nodeRange.cloneRange();
1071         nodeStartRange.collapse(true);
1072     
1073         var nodeEndRange = nodeRange.cloneRange();
1074         nodeEndRange.collapse(false);
1075     
1076         return rangeStartRange.compareBoundaryPoints(
1077                  Range.START_TO_START, nodeEndRange) == -1 &&
1078                rangeEndRange.compareBoundaryPoints(
1079                  Range.START_TO_START, nodeStartRange) == 1;
1080         
1081          
1082     },
1083     rangeCompareNode : function(range, node)
1084     {
1085         var nodeRange = node.ownerDocument.createRange();
1086         try {
1087             nodeRange.selectNode(node);
1088         } catch (e) {
1089             nodeRange.selectNodeContents(node);
1090         }
1091         
1092         
1093         range.collapse(true);
1094     
1095         nodeRange.collapse(true);
1096      
1097         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1098         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1099          
1100         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1101         
1102         var nodeIsBefore   =  ss == 1;
1103         var nodeIsAfter    = ee == -1;
1104         
1105         if (nodeIsBefore && nodeIsAfter) {
1106             return 0; // outer
1107         }
1108         if (!nodeIsBefore && nodeIsAfter) {
1109             return 1; //right trailed.
1110         }
1111         
1112         if (nodeIsBefore && !nodeIsAfter) {
1113             return 2;  // left trailed.
1114         }
1115         // fully contined.
1116         return 3;
1117     },
1118  
1119     cleanWordChars : function(input) {// change the chars to hex code
1120         
1121        var swapCodes  = [ 
1122             [    8211, "&#8211;" ], 
1123             [    8212, "&#8212;" ], 
1124             [    8216,  "'" ],  
1125             [    8217, "'" ],  
1126             [    8220, '"' ],  
1127             [    8221, '"' ],  
1128             [    8226, "*" ],  
1129             [    8230, "..." ]
1130         ]; 
1131         var output = input;
1132         Roo.each(swapCodes, function(sw) { 
1133             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1134             
1135             output = output.replace(swapper, sw[1]);
1136         });
1137         
1138         return output;
1139     },
1140     
1141      
1142     
1143         
1144     
1145     cleanUpChild : function (node)
1146     {
1147         
1148         new Roo.htmleditor.FilterComment({node : node});
1149         new Roo.htmleditor.FilterAttributes({
1150                 node : node,
1151                 attrib_black : this.ablack,
1152                 attrib_clean : this.aclean,
1153                 style_white : this.cwhite,
1154                 style_black : this.cblack
1155         });
1156         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1157         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1158          
1159         
1160     },
1161     
1162     /**
1163      * Clean up MS wordisms...
1164      * @deprecated - use filter directly
1165      */
1166     cleanWord : function(node)
1167     {
1168         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1169         
1170     },
1171    
1172     
1173     /**
1174
1175      * @deprecated - use filters
1176      */
1177     cleanTableWidths : function(node)
1178     {
1179         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1180         
1181  
1182     },
1183     
1184      
1185         
1186     applyBlacklists : function()
1187     {
1188         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1189         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1190         
1191         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1192         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1193         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1194         
1195         this.white = [];
1196         this.black = [];
1197         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1198             if (b.indexOf(tag) > -1) {
1199                 return;
1200             }
1201             this.white.push(tag);
1202             
1203         }, this);
1204         
1205         Roo.each(w, function(tag) {
1206             if (b.indexOf(tag) > -1) {
1207                 return;
1208             }
1209             if (this.white.indexOf(tag) > -1) {
1210                 return;
1211             }
1212             this.white.push(tag);
1213             
1214         }, this);
1215         
1216         
1217         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1218             if (w.indexOf(tag) > -1) {
1219                 return;
1220             }
1221             this.black.push(tag);
1222             
1223         }, this);
1224         
1225         Roo.each(b, function(tag) {
1226             if (w.indexOf(tag) > -1) {
1227                 return;
1228             }
1229             if (this.black.indexOf(tag) > -1) {
1230                 return;
1231             }
1232             this.black.push(tag);
1233             
1234         }, this);
1235         
1236         
1237         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1238         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1239         
1240         this.cwhite = [];
1241         this.cblack = [];
1242         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1243             if (b.indexOf(tag) > -1) {
1244                 return;
1245             }
1246             this.cwhite.push(tag);
1247             
1248         }, this);
1249         
1250         Roo.each(w, function(tag) {
1251             if (b.indexOf(tag) > -1) {
1252                 return;
1253             }
1254             if (this.cwhite.indexOf(tag) > -1) {
1255                 return;
1256             }
1257             this.cwhite.push(tag);
1258             
1259         }, this);
1260         
1261         
1262         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1263             if (w.indexOf(tag) > -1) {
1264                 return;
1265             }
1266             this.cblack.push(tag);
1267             
1268         }, this);
1269         
1270         Roo.each(b, function(tag) {
1271             if (w.indexOf(tag) > -1) {
1272                 return;
1273             }
1274             if (this.cblack.indexOf(tag) > -1) {
1275                 return;
1276             }
1277             this.cblack.push(tag);
1278             
1279         }, this);
1280     },
1281     
1282     setStylesheets : function(stylesheets)
1283     {
1284         if(typeof(stylesheets) == 'string'){
1285             Roo.get(this.iframe.contentDocument.head).createChild({
1286                 tag : 'link',
1287                 rel : 'stylesheet',
1288                 type : 'text/css',
1289                 href : stylesheets
1290             });
1291             
1292             return;
1293         }
1294         var _this = this;
1295      
1296         Roo.each(stylesheets, function(s) {
1297             if(!s.length){
1298                 return;
1299             }
1300             
1301             Roo.get(_this.iframe.contentDocument.head).createChild({
1302                 tag : 'link',
1303                 rel : 'stylesheet',
1304                 type : 'text/css',
1305                 href : s
1306             });
1307         });
1308
1309         
1310     },
1311     
1312     removeStylesheets : function()
1313     {
1314         var _this = this;
1315         
1316         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1317             s.remove();
1318         });
1319     },
1320     
1321     setStyle : function(style)
1322     {
1323         Roo.get(this.iframe.contentDocument.head).createChild({
1324             tag : 'style',
1325             type : 'text/css',
1326             html : style
1327         });
1328
1329         return;
1330     }
1331     
1332     // hide stuff that is not compatible
1333     /**
1334      * @event blur
1335      * @hide
1336      */
1337     /**
1338      * @event change
1339      * @hide
1340      */
1341     /**
1342      * @event focus
1343      * @hide
1344      */
1345     /**
1346      * @event specialkey
1347      * @hide
1348      */
1349     /**
1350      * @cfg {String} fieldClass @hide
1351      */
1352     /**
1353      * @cfg {String} focusClass @hide
1354      */
1355     /**
1356      * @cfg {String} autoCreate @hide
1357      */
1358     /**
1359      * @cfg {String} inputType @hide
1360      */
1361     /**
1362      * @cfg {String} invalidClass @hide
1363      */
1364     /**
1365      * @cfg {String} invalidText @hide
1366      */
1367     /**
1368      * @cfg {String} msgFx @hide
1369      */
1370     /**
1371      * @cfg {String} validateOnBlur @hide
1372      */
1373 });
1374
1375 Roo.HtmlEditorCore.white = [
1376         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1377         
1378        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1379        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1380        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1381        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1382        'TABLE',   'UL',         'XMP', 
1383        
1384        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1385       'THEAD',   'TR', 
1386      
1387       'DIR', 'MENU', 'OL', 'UL', 'DL',
1388        
1389       'EMBED',  'OBJECT'
1390 ];
1391
1392
1393 Roo.HtmlEditorCore.black = [
1394     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1395         'APPLET', // 
1396         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1397         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1398         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1399         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1400         //'FONT' // CLEAN LATER..
1401         'COLGROUP', 'COL'  // messy tables.
1402         
1403 ];
1404 Roo.HtmlEditorCore.clean = [ // ?? needed???
1405      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1406 ];
1407 Roo.HtmlEditorCore.tag_remove = [
1408     'FONT', 'TBODY'  
1409 ];
1410 // attributes..
1411
1412 Roo.HtmlEditorCore.ablack = [
1413     'on'
1414 ];
1415     
1416 Roo.HtmlEditorCore.aclean = [ 
1417     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1418 ];
1419
1420 // protocols..
1421 Roo.HtmlEditorCore.pwhite= [
1422         'http',  'https',  'mailto'
1423 ];
1424
1425 // white listed style attributes.
1426 Roo.HtmlEditorCore.cwhite= [
1427       //  'text-align', /// default is to allow most things..
1428       
1429          
1430 //        'font-size'//??
1431 ];
1432
1433 // black listed style attributes.
1434 Roo.HtmlEditorCore.cblack= [
1435       //  'font-size' -- this can be set by the project 
1436 ];
1437
1438
1439
1440
1441