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     // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
78     
79     // defaults : white / black...
80     this.applyBlacklists();
81     
82     
83     
84 };
85
86
87 Roo.extend(Roo.HtmlEditorCore, Roo.Component,  {
88
89
90      /**
91      * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field 
92      */
93     
94     owner : false,
95     
96      /**
97      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
98      *                        Roo.resizable.
99      */
100     resizable : false,
101      /**
102      * @cfg {Number} height (in pixels)
103      */   
104     height: 300,
105    /**
106      * @cfg {Number} width (in pixels)
107      */   
108     width: 500,
109     
110     /**
111      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
112      * 
113      */
114     stylesheets: false,
115     
116     // id of frame..
117     frameId: false,
118     
119     // private properties
120     validationEvent : false,
121     deferHeight: true,
122     initialized : false,
123     activated : false,
124     sourceEditMode : false,
125     onFocus : Roo.emptyFn,
126     iframePad:3,
127     hideMode:'offsets',
128     
129     clearUp: true,
130     
131     // blacklist + whitelisted elements..
132     black: false,
133     white: false,
134      
135     
136
137     /**
138      * Protected method that will not generally be called directly. It
139      * is called when the editor initializes the iframe with HTML contents. Override this method if you
140      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
141      */
142     getDocMarkup : function(){
143         // body styles..
144         var st = '';
145         Roo.log(this.stylesheets);
146         
147         // inherit styels from page...?? 
148         if (this.stylesheets === false) {
149             
150             Roo.get(document.head).select('style').each(function(node) {
151                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
152             });
153             
154             Roo.get(document.head).select('link').each(function(node) { 
155                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
156             });
157             
158         } else if (!this.stylesheets.length) {
159                 // simple..
160                 st = '<style type="text/css">' +
161                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
162                    '</style>';
163         } else {
164             Roo.each(this.stylesheets, function(s) {
165                 st += '<link rel="stylesheet" type="text/css" href="' + s +'" />'
166             });
167             
168         }
169         
170         st +=  '<style type="text/css">' +
171             'IMG { cursor: pointer } ' +
172         '</style>';
173
174         
175         return '<html><head>' + st  +
176             //<style type="text/css">' +
177             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
178             //'</style>' +
179             ' </head><body class="roo-htmleditor-body"></body></html>';
180     },
181
182     // private
183     onRender : function(ct, position)
184     {
185         var _t = this;
186         //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
187         this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
188         
189         
190         this.el.dom.style.border = '0 none';
191         this.el.dom.setAttribute('tabIndex', -1);
192         this.el.addClass('x-hidden hide');
193         
194         
195         
196         if(Roo.isIE){ // fix IE 1px bogus margin
197             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
198         }
199        
200         
201         this.frameId = Roo.id();
202         
203          
204         
205         var iframe = this.owner.wrap.createChild({
206             tag: 'iframe',
207             cls: 'form-control', // bootstrap..
208             id: this.frameId,
209             name: this.frameId,
210             frameBorder : 'no',
211             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
212         }, this.el
213         );
214         
215         
216         this.iframe = iframe.dom;
217
218          this.assignDocWin();
219         
220         this.doc.designMode = 'on';
221        
222         this.doc.open();
223         this.doc.write(this.getDocMarkup());
224         this.doc.close();
225
226         
227         var task = { // must defer to wait for browser to be ready
228             run : function(){
229                 //console.log("run task?" + this.doc.readyState);
230                 this.assignDocWin();
231                 if(this.doc.body || this.doc.readyState == 'complete'){
232                     try {
233                         this.doc.designMode="on";
234                     } catch (e) {
235                         return;
236                     }
237                     Roo.TaskMgr.stop(task);
238                     this.initEditor.defer(10, this);
239                 }
240             },
241             interval : 10,
242             duration: 10000,
243             scope: this
244         };
245         Roo.TaskMgr.start(task);
246
247         
248          
249     },
250
251     // private
252     onResize : function(w, h)
253     {
254          Roo.log('resize: ' +w + ',' + h );
255         //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
256         if(!this.iframe){
257             return;
258         }
259         if(typeof w == 'number'){
260             
261             this.iframe.style.width = w + 'px';
262         }
263         if(typeof h == 'number'){
264             
265             this.iframe.style.height = h + 'px';
266             if(this.doc){
267                 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
268             }
269         }
270         
271     },
272
273     /**
274      * Toggles the editor between standard and source edit mode.
275      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
276      */
277     toggleSourceEdit : function(sourceEditMode){
278         
279         this.sourceEditMode = sourceEditMode === true;
280         
281         if(this.sourceEditMode){
282  
283             Roo.get(this.iframe).addClass(['x-hidden','hide']);     //FIXME - what's the BS styles for these
284             
285         }else{
286             Roo.get(this.iframe).removeClass(['x-hidden','hide']);
287             //this.iframe.className = '';
288             this.deferFocus();
289         }
290         //this.setSize(this.owner.wrap.getSize());
291         //this.fireEvent('editmodechange', this, this.sourceEditMode);
292     },
293
294     
295   
296
297     /**
298      * Protected method that will not generally be called directly. If you need/want
299      * custom HTML cleanup, this is the method you should override.
300      * @param {String} html The HTML to be cleaned
301      * return {String} The cleaned HTML
302      */
303     cleanHtml : function(html){
304         html = String(html);
305         if(html.length > 5){
306             if(Roo.isSafari){ // strip safari nonsense
307                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
308             }
309         }
310         if(html == '&nbsp;'){
311             html = '';
312         }
313         return html;
314     },
315
316     /**
317      * HTML Editor -> Textarea
318      * Protected method that will not generally be called directly. Syncs the contents
319      * of the editor iframe with the textarea.
320      */
321     syncValue : function(){
322         if(this.initialized){
323             var bd = (this.doc.body || this.doc.documentElement);
324             //this.cleanUpPaste(); -- this is done else where and causes havoc..
325             var html = bd.innerHTML;
326             if(Roo.isSafari){
327                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
328                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
329                 if(m && m[1]){
330                     html = '<div style="'+m[0]+'">' + html + '</div>';
331                 }
332             }
333             html = this.cleanHtml(html);
334             // fix up the special chars.. normaly like back quotes in word...
335             // however we do not want to do this with chinese..
336             html = html.replace(/([\x80-\uffff])/g, function (a, b) {
337                 var cc = b.charCodeAt();
338                 if (
339                     (cc >= 0x4E00 && cc < 0xA000 ) ||
340                     (cc >= 0x3400 && cc < 0x4E00 ) ||
341                     (cc >= 0xf900 && cc < 0xfb00 )
342                 ) {
343                         return b;
344                 }
345                 return "&#"+cc+";" 
346             });
347             if(this.owner.fireEvent('beforesync', this, html) !== false){
348                 this.el.dom.value = html;
349                 this.owner.fireEvent('sync', this, html);
350             }
351         }
352     },
353
354     /**
355      * Protected method that will not generally be called directly. Pushes the value of the textarea
356      * into the iframe editor.
357      */
358     pushValue : function(){
359         if(this.initialized){
360             var v = this.el.dom.value.trim();
361             
362 //            if(v.length < 1){
363 //                v = '&#160;';
364 //            }
365             
366             if(this.owner.fireEvent('beforepush', this, v) !== false){
367                 var d = (this.doc.body || this.doc.documentElement);
368                 d.innerHTML = v;
369                 this.cleanUpPaste();
370                 this.el.dom.value = d.innerHTML;
371                 this.owner.fireEvent('push', this, v);
372             }
373         }
374     },
375
376     // private
377     deferFocus : function(){
378         this.focus.defer(10, this);
379     },
380
381     // doc'ed in Field
382     focus : function(){
383         if(this.win && !this.sourceEditMode){
384             this.win.focus();
385         }else{
386             this.el.focus();
387         }
388     },
389     
390     assignDocWin: function()
391     {
392         var iframe = this.iframe;
393         
394          if(Roo.isIE){
395             this.doc = iframe.contentWindow.document;
396             this.win = iframe.contentWindow;
397         } else {
398 //            if (!Roo.get(this.frameId)) {
399 //                return;
400 //            }
401 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
402 //            this.win = Roo.get(this.frameId).dom.contentWindow;
403             
404             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
405                 return;
406             }
407             
408             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
409             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
410         }
411     },
412     
413     // private
414     initEditor : function(){
415         //console.log("INIT EDITOR");
416         this.assignDocWin();
417         
418         
419         
420         this.doc.designMode="on";
421         this.doc.open();
422         this.doc.write(this.getDocMarkup());
423         this.doc.close();
424         
425         var dbody = (this.doc.body || this.doc.documentElement);
426         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
427         // this copies styles from the containing element into thsi one..
428         // not sure why we need all of this..
429         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
430         
431         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
432         //ss['background-attachment'] = 'fixed'; // w3c
433         dbody.bgProperties = 'fixed'; // ie
434         //Roo.DomHelper.applyStyles(dbody, ss);
435         Roo.EventManager.on(this.doc, {
436             //'mousedown': this.onEditorEvent,
437             'mouseup': this.onEditorEvent,
438             'dblclick': this.onEditorEvent,
439             'click': this.onEditorEvent,
440             'keyup': this.onEditorEvent,
441             buffer:100,
442             scope: this
443         });
444         if(Roo.isGecko){
445             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
446         }
447         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
448             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
449         }
450         this.initialized = true;
451
452         this.owner.fireEvent('initialize', this);
453         this.pushValue();
454     },
455
456     // private
457     onDestroy : function(){
458         
459         
460         
461         if(this.rendered){
462             
463             //for (var i =0; i < this.toolbars.length;i++) {
464             //    // fixme - ask toolbars for heights?
465             //    this.toolbars[i].onDestroy();
466            // }
467             
468             //this.wrap.dom.innerHTML = '';
469             //this.wrap.remove();
470         }
471     },
472
473     // private
474     onFirstFocus : function(){
475         
476         this.assignDocWin();
477         
478         
479         this.activated = true;
480          
481     
482         if(Roo.isGecko){ // prevent silly gecko errors
483             this.win.focus();
484             var s = this.win.getSelection();
485             if(!s.focusNode || s.focusNode.nodeType != 3){
486                 var r = s.getRangeAt(0);
487                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
488                 r.collapse(true);
489                 this.deferFocus();
490             }
491             try{
492                 this.execCmd('useCSS', true);
493                 this.execCmd('styleWithCSS', false);
494             }catch(e){}
495         }
496         this.owner.fireEvent('activate', this);
497     },
498
499     // private
500     adjustFont: function(btn){
501         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
502         //if(Roo.isSafari){ // safari
503         //    adjust *= 2;
504        // }
505         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
506         if(Roo.isSafari){ // safari
507             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
508             v =  (v < 10) ? 10 : v;
509             v =  (v > 48) ? 48 : v;
510             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
511             
512         }
513         
514         
515         v = Math.max(1, v+adjust);
516         
517         this.execCmd('FontSize', v  );
518     },
519
520     onEditorEvent : function(e){
521         this.owner.fireEvent('editorevent', this, e);
522       //  this.updateToolbar();
523         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
524     },
525
526     insertTag : function(tg)
527     {
528         // could be a bit smarter... -> wrap the current selected tRoo..
529         if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
530             
531             range = this.createRange(this.getSelection());
532             var wrappingNode = this.doc.createElement(tg.toLowerCase());
533             wrappingNode.appendChild(range.extractContents());
534             range.insertNode(wrappingNode);
535
536             return;
537             
538             
539             
540         }
541         this.execCmd("formatblock",   tg);
542         
543     },
544     
545     insertText : function(txt)
546     {
547         
548         
549         var range = this.createRange();
550         range.deleteContents();
551                //alert(Sender.getAttribute('label'));
552                
553         range.insertNode(this.doc.createTextNode(txt));
554     } ,
555     
556      
557
558     /**
559      * Executes a Midas editor command on the editor document and performs necessary focus and
560      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
561      * @param {String} cmd The Midas command
562      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
563      */
564     relayCmd : function(cmd, value){
565         this.win.focus();
566         this.execCmd(cmd, value);
567         this.owner.fireEvent('editorevent', this);
568         //this.updateToolbar();
569         this.owner.deferFocus();
570     },
571
572     /**
573      * Executes a Midas editor command directly on the editor document.
574      * For visual commands, you should use {@link #relayCmd} instead.
575      * <b>This should only be called after the editor is initialized.</b>
576      * @param {String} cmd The Midas command
577      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
578      */
579     execCmd : function(cmd, value){
580         this.doc.execCommand(cmd, false, value === undefined ? null : value);
581         this.syncValue();
582     },
583  
584  
585    
586     /**
587      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
588      * to insert tRoo.
589      * @param {String} text | dom node.. 
590      */
591     insertAtCursor : function(text)
592     {
593         
594         
595         
596         if(!this.activated){
597             return;
598         }
599         /*
600         if(Roo.isIE){
601             this.win.focus();
602             var r = this.doc.selection.createRange();
603             if(r){
604                 r.collapse(true);
605                 r.pasteHTML(text);
606                 this.syncValue();
607                 this.deferFocus();
608             
609             }
610             return;
611         }
612         */
613         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
614             this.win.focus();
615             
616             
617             // from jquery ui (MIT licenced)
618             var range, node;
619             var win = this.win;
620             
621             if (win.getSelection && win.getSelection().getRangeAt) {
622                 range = win.getSelection().getRangeAt(0);
623                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
624                 range.insertNode(node);
625             } else if (win.document.selection && win.document.selection.createRange) {
626                 // no firefox support
627                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
628                 win.document.selection.createRange().pasteHTML(txt);
629             } else {
630                 // no firefox support
631                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
632                 this.execCmd('InsertHTML', txt);
633             } 
634             
635             this.syncValue();
636             
637             this.deferFocus();
638         }
639     },
640  // private
641     mozKeyPress : function(e){
642         if(e.ctrlKey){
643             var c = e.getCharCode(), cmd;
644           
645             if(c > 0){
646                 c = String.fromCharCode(c).toLowerCase();
647                 switch(c){
648                     case 'b':
649                         cmd = 'bold';
650                         break;
651                     case 'i':
652                         cmd = 'italic';
653                         break;
654                     
655                     case 'u':
656                         cmd = 'underline';
657                         break;
658                     
659                     case 'v':
660                         this.cleanUpPaste.defer(100, this);
661                         return;
662                         
663                 }
664                 if(cmd){
665                     this.win.focus();
666                     this.execCmd(cmd);
667                     this.deferFocus();
668                     e.preventDefault();
669                 }
670                 
671             }
672         }
673     },
674
675     // private
676     fixKeys : function(){ // load time branching for fastest keydown performance
677         if(Roo.isIE){
678             return function(e){
679                 var k = e.getKey(), r;
680                 if(k == e.TAB){
681                     e.stopEvent();
682                     r = this.doc.selection.createRange();
683                     if(r){
684                         r.collapse(true);
685                         r.pasteHTML('&#160;&#160;&#160;&#160;');
686                         this.deferFocus();
687                     }
688                     return;
689                 }
690                 
691                 if(k == e.ENTER){
692                     r = this.doc.selection.createRange();
693                     if(r){
694                         var target = r.parentElement();
695                         if(!target || target.tagName.toLowerCase() != 'li'){
696                             e.stopEvent();
697                             r.pasteHTML('<br />');
698                             r.collapse(false);
699                             r.select();
700                         }
701                     }
702                 }
703                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
704                     this.cleanUpPaste.defer(100, this);
705                     return;
706                 }
707                 
708                 
709             };
710         }else if(Roo.isOpera){
711             return function(e){
712                 var k = e.getKey();
713                 if(k == e.TAB){
714                     e.stopEvent();
715                     this.win.focus();
716                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
717                     this.deferFocus();
718                 }
719                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
720                     this.cleanUpPaste.defer(100, this);
721                     return;
722                 }
723                 
724             };
725         }else if(Roo.isSafari){
726             return function(e){
727                 var k = e.getKey();
728                 
729                 if(k == e.TAB){
730                     e.stopEvent();
731                     this.execCmd('InsertText','\t');
732                     this.deferFocus();
733                     return;
734                 }
735                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
736                     this.cleanUpPaste.defer(100, this);
737                     return;
738                 }
739                 
740              };
741         }
742     }(),
743     
744     getAllAncestors: function()
745     {
746         var p = this.getSelectedNode();
747         var a = [];
748         if (!p) {
749             a.push(p); // push blank onto stack..
750             p = this.getParentElement();
751         }
752         
753         
754         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
755             a.push(p);
756             p = p.parentNode;
757         }
758         a.push(this.doc.body);
759         return a;
760     },
761     lastSel : false,
762     lastSelNode : false,
763     
764     
765     getSelection : function() 
766     {
767         this.assignDocWin();
768         return Roo.isIE ? this.doc.selection : this.win.getSelection();
769     },
770     
771     getSelectedNode: function() 
772     {
773         // this may only work on Gecko!!!
774         
775         // should we cache this!!!!
776         
777         
778         
779          
780         var range = this.createRange(this.getSelection()).cloneRange();
781         
782         if (Roo.isIE) {
783             var parent = range.parentElement();
784             while (true) {
785                 var testRange = range.duplicate();
786                 testRange.moveToElementText(parent);
787                 if (testRange.inRange(range)) {
788                     break;
789                 }
790                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
791                     break;
792                 }
793                 parent = parent.parentElement;
794             }
795             return parent;
796         }
797         
798         // is ancestor a text element.
799         var ac =  range.commonAncestorContainer;
800         if (ac.nodeType == 3) {
801             ac = ac.parentNode;
802         }
803         
804         var ar = ac.childNodes;
805          
806         var nodes = [];
807         var other_nodes = [];
808         var has_other_nodes = false;
809         for (var i=0;i<ar.length;i++) {
810             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
811                 continue;
812             }
813             // fullly contained node.
814             
815             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
816                 nodes.push(ar[i]);
817                 continue;
818             }
819             
820             // probably selected..
821             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
822                 other_nodes.push(ar[i]);
823                 continue;
824             }
825             // outer..
826             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
827                 continue;
828             }
829             
830             
831             has_other_nodes = true;
832         }
833         if (!nodes.length && other_nodes.length) {
834             nodes= other_nodes;
835         }
836         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
837             return false;
838         }
839         
840         return nodes[0];
841     },
842     createRange: function(sel)
843     {
844         // this has strange effects when using with 
845         // top toolbar - not sure if it's a great idea.
846         //this.editor.contentWindow.focus();
847         if (typeof sel != "undefined") {
848             try {
849                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
850             } catch(e) {
851                 return this.doc.createRange();
852             }
853         } else {
854             return this.doc.createRange();
855         }
856     },
857     getParentElement: function()
858     {
859         
860         this.assignDocWin();
861         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
862         
863         var range = this.createRange(sel);
864          
865         try {
866             var p = range.commonAncestorContainer;
867             while (p.nodeType == 3) { // text node
868                 p = p.parentNode;
869             }
870             return p;
871         } catch (e) {
872             return null;
873         }
874     
875     },
876     /***
877      *
878      * Range intersection.. the hard stuff...
879      *  '-1' = before
880      *  '0' = hits..
881      *  '1' = after.
882      *         [ -- selected range --- ]
883      *   [fail]                        [fail]
884      *
885      *    basically..
886      *      if end is before start or  hits it. fail.
887      *      if start is after end or hits it fail.
888      *
889      *   if either hits (but other is outside. - then it's not 
890      *   
891      *    
892      **/
893     
894     
895     // @see http://www.thismuchiknow.co.uk/?p=64.
896     rangeIntersectsNode : function(range, node)
897     {
898         var nodeRange = node.ownerDocument.createRange();
899         try {
900             nodeRange.selectNode(node);
901         } catch (e) {
902             nodeRange.selectNodeContents(node);
903         }
904     
905         var rangeStartRange = range.cloneRange();
906         rangeStartRange.collapse(true);
907     
908         var rangeEndRange = range.cloneRange();
909         rangeEndRange.collapse(false);
910     
911         var nodeStartRange = nodeRange.cloneRange();
912         nodeStartRange.collapse(true);
913     
914         var nodeEndRange = nodeRange.cloneRange();
915         nodeEndRange.collapse(false);
916     
917         return rangeStartRange.compareBoundaryPoints(
918                  Range.START_TO_START, nodeEndRange) == -1 &&
919                rangeEndRange.compareBoundaryPoints(
920                  Range.START_TO_START, nodeStartRange) == 1;
921         
922          
923     },
924     rangeCompareNode : function(range, node)
925     {
926         var nodeRange = node.ownerDocument.createRange();
927         try {
928             nodeRange.selectNode(node);
929         } catch (e) {
930             nodeRange.selectNodeContents(node);
931         }
932         
933         
934         range.collapse(true);
935     
936         nodeRange.collapse(true);
937      
938         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
939         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
940          
941         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
942         
943         var nodeIsBefore   =  ss == 1;
944         var nodeIsAfter    = ee == -1;
945         
946         if (nodeIsBefore && nodeIsAfter)
947             return 0; // outer
948         if (!nodeIsBefore && nodeIsAfter)
949             return 1; //right trailed.
950         
951         if (nodeIsBefore && !nodeIsAfter)
952             return 2;  // left trailed.
953         // fully contined.
954         return 3;
955     },
956
957     // private? - in a new class?
958     cleanUpPaste :  function()
959     {
960         // cleans up the whole document..
961         Roo.log('cleanuppaste');
962         
963         this.cleanUpChildren(this.doc.body);
964         var clean = this.cleanWordChars(this.doc.body.innerHTML);
965         if (clean != this.doc.body.innerHTML) {
966             this.doc.body.innerHTML = clean;
967         }
968         
969     },
970     
971     cleanWordChars : function(input) {// change the chars to hex code
972         var he = Roo.HtmlEditorCore;
973         
974         var output = input;
975         Roo.each(he.swapCodes, function(sw) { 
976             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
977             
978             output = output.replace(swapper, sw[1]);
979         });
980         
981         return output;
982     },
983     
984     
985     cleanUpChildren : function (n)
986     {
987         if (!n.childNodes.length) {
988             return;
989         }
990         for (var i = n.childNodes.length-1; i > -1 ; i--) {
991            this.cleanUpChild(n.childNodes[i]);
992         }
993     },
994     
995     
996         
997     
998     cleanUpChild : function (node)
999     {
1000         var ed = this;
1001         //console.log(node);
1002         if (node.nodeName == "#text") {
1003             // clean up silly Windows -- stuff?
1004             return; 
1005         }
1006         if (node.nodeName == "#comment") {
1007             node.parentNode.removeChild(node);
1008             // clean up silly Windows -- stuff?
1009             return; 
1010         }
1011         var lcname = node.tagName.toLowerCase();
1012         // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1013         // whitelist of tags..
1014         
1015         if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1016             // remove node.
1017             node.parentNode.removeChild(node);
1018             return;
1019             
1020         }
1021         
1022         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1023         
1024         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1025         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1026         
1027         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1028         //    remove_keep_children = true;
1029         //}
1030         
1031         if (remove_keep_children) {
1032             this.cleanUpChildren(node);
1033             // inserts everything just before this node...
1034             while (node.childNodes.length) {
1035                 var cn = node.childNodes[0];
1036                 node.removeChild(cn);
1037                 node.parentNode.insertBefore(cn, node);
1038             }
1039             node.parentNode.removeChild(node);
1040             return;
1041         }
1042         
1043         if (!node.attributes || !node.attributes.length) {
1044             this.cleanUpChildren(node);
1045             return;
1046         }
1047         
1048         function cleanAttr(n,v)
1049         {
1050             
1051             if (v.match(/^\./) || v.match(/^\//)) {
1052                 return;
1053             }
1054             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1055                 return;
1056             }
1057             if (v.match(/^#/)) {
1058                 return;
1059             }
1060 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1061             node.removeAttribute(n);
1062             
1063         }
1064         
1065         function cleanStyle(n,v)
1066         {
1067             if (v.match(/expression/)) { //XSS?? should we even bother..
1068                 node.removeAttribute(n);
1069                 return;
1070             }
1071             var cwhite = this.cwhite;
1072             var cblack = this.cblack;
1073             
1074             
1075             var parts = v.split(/;/);
1076             var clean = [];
1077             
1078             Roo.each(parts, function(p) {
1079                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1080                 if (!p.length) {
1081                     return true;
1082                 }
1083                 var l = p.split(':').shift().replace(/\s+/g,'');
1084                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1085                 
1086                 if ( cblack.indexOf(l) > -1) {
1087 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1088                     //node.removeAttribute(n);
1089                     return true;
1090                 }
1091                 //Roo.log()
1092                 // only allow 'c whitelisted system attributes'
1093                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1094 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1095                     //node.removeAttribute(n);
1096                     return true;
1097                 }
1098                 
1099                 
1100                  
1101                 
1102                 clean.push(p);
1103                 return true;
1104             });
1105             if (clean.length) { 
1106                 node.setAttribute(n, clean.join(';'));
1107             } else {
1108                 node.removeAttribute(n);
1109             }
1110             
1111         }
1112         
1113         
1114         for (var i = node.attributes.length-1; i > -1 ; i--) {
1115             var a = node.attributes[i];
1116             //console.log(a);
1117             
1118             if (a.name.toLowerCase().substr(0,2)=='on')  {
1119                 node.removeAttribute(a.name);
1120                 continue;
1121             }
1122             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1123                 node.removeAttribute(a.name);
1124                 continue;
1125             }
1126             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1127                 cleanAttr(a.name,a.value); // fixme..
1128                 continue;
1129             }
1130             if (a.name == 'style') {
1131                 cleanStyle(a.name,a.value);
1132                 continue;
1133             }
1134             /// clean up MS crap..
1135             // tecnically this should be a list of valid class'es..
1136             
1137             
1138             if (a.name == 'class') {
1139                 if (a.value.match(/^Mso/)) {
1140                     node.className = '';
1141                 }
1142                 
1143                 if (a.value.match(/body/)) {
1144                     node.className = '';
1145                 }
1146                 continue;
1147             }
1148             
1149             // style cleanup!?
1150             // class cleanup?
1151             
1152         }
1153         
1154         
1155         this.cleanUpChildren(node);
1156         
1157         
1158     },
1159     /**
1160      * Clean up MS wordisms...
1161      */
1162     cleanWord : function(node)
1163     {
1164         var _t = this;
1165         var cleanWordChildren = function()
1166         {
1167             if (!node.childNodes.length) {
1168                 return;
1169             }
1170             for (var i = node.childNodes.length-1; i > -1 ; i--) {
1171                _t.cleanWord(node.childNodes[i]);
1172             }
1173         }
1174         
1175         
1176         if (!node) {
1177             this.cleanWord(this.doc.body);
1178             return;
1179         }
1180         if (node.nodeName == "#text") {
1181             // clean up silly Windows -- stuff?
1182             return; 
1183         }
1184         if (node.nodeName == "#comment") {
1185             node.parentNode.removeChild(node);
1186             // clean up silly Windows -- stuff?
1187             return; 
1188         }
1189         
1190         if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1191             node.parentNode.removeChild(node);
1192             return;
1193         }
1194         
1195         // remove - but keep children..
1196         if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1197             while (node.childNodes.length) {
1198                 var cn = node.childNodes[0];
1199                 node.removeChild(cn);
1200                 node.parentNode.insertBefore(cn, node);
1201             }
1202             node.parentNode.removeChild(node);
1203             cleanWordChildren();
1204             return;
1205         }
1206         // clean styles
1207         if (node.className.length) {
1208             
1209             var cn = node.className.split(/\W+/);
1210             var cna = [];
1211             Roo.each(cn, function(cls) {
1212                 if (cls.match(/Mso[a-zA-Z]+/)) {
1213                     return;
1214                 }
1215                 cna.push(cls);
1216             });
1217             node.className = cna.length ? cna.join(' ') : '';
1218             if (!cna.length) {
1219                 node.removeAttribute("class");
1220             }
1221         }
1222         
1223         if (node.hasAttribute("lang")) {
1224             node.removeAttribute("lang");
1225         }
1226         
1227         if (node.hasAttribute("style")) {
1228             
1229             var styles = node.getAttribute("style").split(";");
1230             var nstyle = [];
1231             Roo.each(styles, function(s) {
1232                 if (!s.match(/:/)) {
1233                     return;
1234                 }
1235                 var kv = s.split(":");
1236                 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1237                     return;
1238                 }
1239                 // what ever is left... we allow.
1240                 nstyle.push(s);
1241             });
1242             node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1243             if (!nstyle.length) {
1244                 node.removeAttribute('style');
1245             }
1246         }
1247         
1248         cleanWordChildren();
1249         
1250         
1251     },
1252     domToHTML : function(currentElement, depth, nopadtext) {
1253         
1254         depth = depth || 0;
1255         nopadtext = nopadtext || false;
1256     
1257         if (!currentElement) {
1258             return this.domToHTML(this.doc.body);
1259         }
1260         
1261         //Roo.log(currentElement);
1262         var j;
1263         var allText = false;
1264         var nodeName = currentElement.nodeName;
1265         var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1266         
1267         if  (nodeName == '#text') {
1268             return currentElement.nodeValue;
1269         }
1270         
1271         
1272         var ret = '';
1273         if (nodeName != 'BODY') {
1274              
1275             var i = 0;
1276             // Prints the node tagName, such as <A>, <IMG>, etc
1277             if (tagName) {
1278                 var attr = [];
1279                 for(i = 0; i < currentElement.attributes.length;i++) {
1280                     // quoting?
1281                     var aname = currentElement.attributes.item(i).name;
1282                     if (!currentElement.attributes.item(i).value.length) {
1283                         continue;
1284                     }
1285                     attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1286                 }
1287                 
1288                 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1289             } 
1290             else {
1291                 
1292                 // eack
1293             }
1294         } else {
1295             tagName = false;
1296         }
1297         if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1298             return ret;
1299         }
1300         if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1301             nopadtext = true;
1302         }
1303         
1304         
1305         // Traverse the tree
1306         i = 0;
1307         var currentElementChild = currentElement.childNodes.item(i);
1308         var allText = true;
1309         var innerHTML  = '';
1310         lastnode = '';
1311         while (currentElementChild) {
1312             // Formatting code (indent the tree so it looks nice on the screen)
1313             var nopad = nopadtext;
1314             if (lastnode == 'SPAN') {
1315                 nopad  = true;
1316             }
1317             // text
1318             if  (currentElementChild.nodeName == '#text') {
1319                 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1320                 if (!nopad && toadd.length > 80) {
1321                     innerHTML  += "\n" + (new Array( depth + 1 )).join( "  "  );
1322                 }
1323                 innerHTML  += toadd;
1324                 
1325                 i++;
1326                 currentElementChild = currentElement.childNodes.item(i);
1327                 lastNode = '';
1328                 continue;
1329             }
1330             allText = false;
1331             
1332             innerHTML  += nopad ? '' : "\n" + (new Array( depth + 1 )).join( "  "  );
1333                 
1334             // Recursively traverse the tree structure of the child node
1335             innerHTML   += this.domToHTML(currentElementChild, depth+1, nopadtext);
1336             lastnode = currentElementChild.nodeName;
1337             i++;
1338             currentElementChild=currentElement.childNodes.item(i);
1339         }
1340         
1341         ret += innerHTML;
1342         
1343         if (!allText) {
1344                 // The remaining code is mostly for formatting the tree
1345             ret+= nopadtext ? '' : "\n" + (new Array( depth  )).join( "  "  );
1346         }
1347         
1348         
1349         if (tagName) {
1350             ret+= "</"+tagName+">";
1351         }
1352         return ret;
1353         
1354     },
1355         
1356     applyBlacklists : function()
1357     {
1358         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1359         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1360         
1361         this.white = [];
1362         this.black = [];
1363         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1364             if (b.indexOf(tag) > -1) {
1365                 return;
1366             }
1367             this.white.push(tag);
1368             
1369         }, this);
1370         
1371         Roo.each(w, function(tag) {
1372             if (b.indexOf(tag) > -1) {
1373                 return;
1374             }
1375             if (this.white.indexOf(tag) > -1) {
1376                 return;
1377             }
1378             this.white.push(tag);
1379             
1380         }, this);
1381         Roo.log(this.white);
1382         
1383         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1384             if (w.indexOf(tag) > -1) {
1385                 return;
1386             }
1387             this.black.push(tag);
1388             
1389         }, this);
1390         
1391         Roo.each(b, function(tag) {
1392             if (w.indexOf(tag) > -1) {
1393                 return;
1394             }
1395             if (this.black.indexOf(tag) > -1) {
1396                 return;
1397             }
1398             this.black.push(tag);
1399             
1400         }, this);
1401         
1402         
1403         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1404         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1405         
1406         this.cwhite = [];
1407         this.cblack = [];
1408         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1409             if (b.indexOf(tag) > -1) {
1410                 return;
1411             }
1412             this.cwhite.push(tag);
1413             
1414         }, this);
1415         
1416         Roo.each(w, function(tag) {
1417             if (b.indexOf(tag) > -1) {
1418                 return;
1419             }
1420             if (this.cwhite.indexOf(tag) > -1) {
1421                 return;
1422             }
1423             this.cwhite.push(tag);
1424             
1425         }, this);
1426         
1427         
1428         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1429             if (w.indexOf(tag) > -1) {
1430                 return;
1431             }
1432             this.cblack.push(tag);
1433             
1434         }, this);
1435         
1436         Roo.each(b, function(tag) {
1437             if (w.indexOf(tag) > -1) {
1438                 return;
1439             }
1440             if (this.cblack.indexOf(tag) > -1) {
1441                 return;
1442             }
1443             this.cblack.push(tag);
1444             
1445         }, this);
1446     }
1447     
1448     // hide stuff that is not compatible
1449     /**
1450      * @event blur
1451      * @hide
1452      */
1453     /**
1454      * @event change
1455      * @hide
1456      */
1457     /**
1458      * @event focus
1459      * @hide
1460      */
1461     /**
1462      * @event specialkey
1463      * @hide
1464      */
1465     /**
1466      * @cfg {String} fieldClass @hide
1467      */
1468     /**
1469      * @cfg {String} focusClass @hide
1470      */
1471     /**
1472      * @cfg {String} autoCreate @hide
1473      */
1474     /**
1475      * @cfg {String} inputType @hide
1476      */
1477     /**
1478      * @cfg {String} invalidClass @hide
1479      */
1480     /**
1481      * @cfg {String} invalidText @hide
1482      */
1483     /**
1484      * @cfg {String} msgFx @hide
1485      */
1486     /**
1487      * @cfg {String} validateOnBlur @hide
1488      */
1489 });
1490
1491 Roo.HtmlEditorCore.white = [
1492         'area', 'br', 'img', 'input', 'hr', 'wbr',
1493         
1494        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1495        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1496        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1497        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1498        'table',   'ul',         'xmp', 
1499        
1500        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1501       'thead',   'tr', 
1502      
1503       'dir', 'menu', 'ol', 'ul', 'dl',
1504        
1505       'embed',  'object'
1506 ];
1507
1508
1509 Roo.HtmlEditorCore.black = [
1510     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1511         'applet', // 
1512         'base',   'basefont', 'bgsound', 'blink',  'body', 
1513         'frame',  'frameset', 'head',    'html',   'ilayer', 
1514         'iframe', 'layer',  'link',     'meta',    'object',   
1515         'script', 'style' ,'title',  'xml' // clean later..
1516 ];
1517 Roo.HtmlEditorCore.clean = [
1518     'script', 'style', 'title', 'xml'
1519 ];
1520 Roo.HtmlEditorCore.remove = [
1521     'font'
1522 ];
1523 // attributes..
1524
1525 Roo.HtmlEditorCore.ablack = [
1526     'on'
1527 ];
1528     
1529 Roo.HtmlEditorCore.aclean = [ 
1530     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1531 ];
1532
1533 // protocols..
1534 Roo.HtmlEditorCore.pwhite= [
1535         'http',  'https',  'mailto'
1536 ];
1537
1538 // white listed style attributes.
1539 Roo.HtmlEditorCore.cwhite= [
1540       //  'text-align', /// default is to allow most things..
1541       
1542          
1543 //        'font-size'//??
1544 ];
1545
1546 // black listed style attributes.
1547 Roo.HtmlEditorCore.cblack= [
1548       //  'font-size' -- this can be set by the project 
1549 ];
1550
1551
1552 Roo.HtmlEditorCore.swapCodes   =[ 
1553     [    8211, "--" ], 
1554     [    8212, "--" ], 
1555     [    8216,  "'" ],  
1556     [    8217, "'" ],  
1557     [    8220, '"' ],  
1558     [    8221, '"' ],  
1559     [    8226, "*" ],  
1560     [    8230, "..." ]
1561 ]; 
1562
1563