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