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