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         
1012         if (this.black.indexOf(node.tagName.toLowerCase()) > -1 && this.clearUp) {
1013             // remove node.
1014             node.parentNode.removeChild(node);
1015             return;
1016             
1017         }
1018         
1019         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1020         
1021         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1022         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1023         
1024         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1025         //    remove_keep_children = true;
1026         //}
1027         
1028         if (remove_keep_children) {
1029             this.cleanUpChildren(node);
1030             // inserts everything just before this node...
1031             while (node.childNodes.length) {
1032                 var cn = node.childNodes[0];
1033                 node.removeChild(cn);
1034                 node.parentNode.insertBefore(cn, node);
1035             }
1036             node.parentNode.removeChild(node);
1037             return;
1038         }
1039         
1040         if (!node.attributes || !node.attributes.length) {
1041             this.cleanUpChildren(node);
1042             return;
1043         }
1044         
1045         function cleanAttr(n,v)
1046         {
1047             
1048             if (v.match(/^\./) || v.match(/^\//)) {
1049                 return;
1050             }
1051             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1052                 return;
1053             }
1054             if (v.match(/^#/)) {
1055                 return;
1056             }
1057 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1058             node.removeAttribute(n);
1059             
1060         }
1061         
1062         function cleanStyle(n,v)
1063         {
1064             if (v.match(/expression/)) { //XSS?? should we even bother..
1065                 node.removeAttribute(n);
1066                 return;
1067             }
1068             var cwhite = typeof(ed.cwhite) != 'undefined' && ed.cwhite !== false ? ed.cwhite : Roo.HtmlEditorCore.cwhite;
1069             var cblack = typeof(ed.cblack) != 'undefined' && ed.cwhite !== false ? ed.cblack : Roo.HtmlEditorCore.cblack;
1070             
1071             
1072             var parts = v.split(/;/);
1073             var clean = [];
1074             
1075             Roo.each(parts, function(p) {
1076                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1077                 if (!p.length) {
1078                     return true;
1079                 }
1080                 var l = p.split(':').shift().replace(/\s+/g,'');
1081                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1082                 
1083                 if ( cblack.indexOf(l) > -1) {
1084 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1085                     //node.removeAttribute(n);
1086                     return true;
1087                 }
1088                 //Roo.log()
1089                 // only allow 'c whitelisted system attributes'
1090                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1091 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1092                     //node.removeAttribute(n);
1093                     return true;
1094                 }
1095                 
1096                 
1097                  
1098                 
1099                 clean.push(p);
1100                 return true;
1101             });
1102             if (clean.length) { 
1103                 node.setAttribute(n, clean.join(';'));
1104             } else {
1105                 node.removeAttribute(n);
1106             }
1107             
1108         }
1109         
1110         
1111         for (var i = node.attributes.length-1; i > -1 ; i--) {
1112             var a = node.attributes[i];
1113             //console.log(a);
1114             
1115             if (a.name.toLowerCase().substr(0,2)=='on')  {
1116                 node.removeAttribute(a.name);
1117                 continue;
1118             }
1119             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1120                 node.removeAttribute(a.name);
1121                 continue;
1122             }
1123             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1124                 cleanAttr(a.name,a.value); // fixme..
1125                 continue;
1126             }
1127             if (a.name == 'style') {
1128                 cleanStyle(a.name,a.value);
1129                 continue;
1130             }
1131             /// clean up MS crap..
1132             // tecnically this should be a list of valid class'es..
1133             
1134             
1135             if (a.name == 'class') {
1136                 if (a.value.match(/^Mso/)) {
1137                     node.className = '';
1138                 }
1139                 
1140                 if (a.value.match(/body/)) {
1141                     node.className = '';
1142                 }
1143                 continue;
1144             }
1145             
1146             // style cleanup!?
1147             // class cleanup?
1148             
1149         }
1150         
1151         
1152         this.cleanUpChildren(node);
1153         
1154         
1155     },
1156     /**
1157      * Clean up MS wordisms...
1158      */
1159     cleanWord : function(node)
1160     {
1161         var _t = this;
1162         var cleanWordChildren = function()
1163         {
1164             if (!node.childNodes.length) {
1165                 return;
1166             }
1167             for (var i = node.childNodes.length-1; i > -1 ; i--) {
1168                _t.cleanWord(node.childNodes[i]);
1169             }
1170         }
1171         
1172         
1173         if (!node) {
1174             this.cleanWord(this.doc.body);
1175             return;
1176         }
1177         if (node.nodeName == "#text") {
1178             // clean up silly Windows -- stuff?
1179             return; 
1180         }
1181         if (node.nodeName == "#comment") {
1182             node.parentNode.removeChild(node);
1183             // clean up silly Windows -- stuff?
1184             return; 
1185         }
1186         
1187         if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1188             node.parentNode.removeChild(node);
1189             return;
1190         }
1191         
1192         // remove - but keep children..
1193         if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1194             while (node.childNodes.length) {
1195                 var cn = node.childNodes[0];
1196                 node.removeChild(cn);
1197                 node.parentNode.insertBefore(cn, node);
1198             }
1199             node.parentNode.removeChild(node);
1200             cleanWordChildren();
1201             return;
1202         }
1203         // clean styles
1204         if (node.className.length) {
1205             
1206             var cn = node.className.split(/\W+/);
1207             var cna = [];
1208             Roo.each(cn, function(cls) {
1209                 if (cls.match(/Mso[a-zA-Z]+/)) {
1210                     return;
1211                 }
1212                 cna.push(cls);
1213             });
1214             node.className = cna.length ? cna.join(' ') : '';
1215             if (!cna.length) {
1216                 node.removeAttribute("class");
1217             }
1218         }
1219         
1220         if (node.hasAttribute("lang")) {
1221             node.removeAttribute("lang");
1222         }
1223         
1224         if (node.hasAttribute("style")) {
1225             
1226             var styles = node.getAttribute("style").split(";");
1227             var nstyle = [];
1228             Roo.each(styles, function(s) {
1229                 if (!s.match(/:/)) {
1230                     return;
1231                 }
1232                 var kv = s.split(":");
1233                 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1234                     return;
1235                 }
1236                 // what ever is left... we allow.
1237                 nstyle.push(s);
1238             });
1239             node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1240             if (!nstyle.length) {
1241                 node.removeAttribute('style');
1242             }
1243         }
1244         
1245         cleanWordChildren();
1246         
1247         
1248     },
1249     domToHTML : function(currentElement, depth, nopadtext) {
1250         
1251         depth = depth || 0;
1252         nopadtext = nopadtext || false;
1253     
1254         if (!currentElement) {
1255             return this.domToHTML(this.doc.body);
1256         }
1257         
1258         //Roo.log(currentElement);
1259         var j;
1260         var allText = false;
1261         var nodeName = currentElement.nodeName;
1262         var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1263         
1264         if  (nodeName == '#text') {
1265             return currentElement.nodeValue;
1266         }
1267         
1268         
1269         var ret = '';
1270         if (nodeName != 'BODY') {
1271              
1272             var i = 0;
1273             // Prints the node tagName, such as <A>, <IMG>, etc
1274             if (tagName) {
1275                 var attr = [];
1276                 for(i = 0; i < currentElement.attributes.length;i++) {
1277                     // quoting?
1278                     var aname = currentElement.attributes.item(i).name;
1279                     if (!currentElement.attributes.item(i).value.length) {
1280                         continue;
1281                     }
1282                     attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1283                 }
1284                 
1285                 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1286             } 
1287             else {
1288                 
1289                 // eack
1290             }
1291         } else {
1292             tagName = false;
1293         }
1294         if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1295             return ret;
1296         }
1297         if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1298             nopadtext = true;
1299         }
1300         
1301         
1302         // Traverse the tree
1303         i = 0;
1304         var currentElementChild = currentElement.childNodes.item(i);
1305         var allText = true;
1306         var innerHTML  = '';
1307         lastnode = '';
1308         while (currentElementChild) {
1309             // Formatting code (indent the tree so it looks nice on the screen)
1310             var nopad = nopadtext;
1311             if (lastnode == 'SPAN') {
1312                 nopad  = true;
1313             }
1314             // text
1315             if  (currentElementChild.nodeName == '#text') {
1316                 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1317                 if (!nopad && toadd.length > 80) {
1318                     innerHTML  += "\n" + (new Array( depth + 1 )).join( "  "  );
1319                 }
1320                 innerHTML  += toadd;
1321                 
1322                 i++;
1323                 currentElementChild = currentElement.childNodes.item(i);
1324                 lastNode = '';
1325                 continue;
1326             }
1327             allText = false;
1328             
1329             innerHTML  += nopad ? '' : "\n" + (new Array( depth + 1 )).join( "  "  );
1330                 
1331             // Recursively traverse the tree structure of the child node
1332             innerHTML   += this.domToHTML(currentElementChild, depth+1, nopadtext);
1333             lastnode = currentElementChild.nodeName;
1334             i++;
1335             currentElementChild=currentElement.childNodes.item(i);
1336         }
1337         
1338         ret += innerHTML;
1339         
1340         if (!allText) {
1341                 // The remaining code is mostly for formatting the tree
1342             ret+= nopadtext ? '' : "\n" + (new Array( depth  )).join( "  "  );
1343         }
1344         
1345         
1346         if (tagName) {
1347             ret+= "</"+tagName+">";
1348         }
1349         return ret;
1350         
1351     },
1352         
1353     applyBlacklists : function()
1354     {
1355         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1356         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1357         
1358         this.white = [];
1359         this.black = [];
1360         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1361             if (b.indexOf(tag) > -1) {
1362                 return;
1363             }
1364             this.white.push(tag);
1365             
1366         }, this);
1367         
1368         Roo.each(w, function(tag) {
1369             if (b.indexOf(tag) > -1) {
1370                 return;
1371             }
1372             if (this.white.indexOf(tag) > -1) {
1373                 return;
1374             }
1375             this.white.push(tag);
1376             
1377         }, this);
1378         
1379         
1380         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1381             if (w.indexOf(tag) > -1) {
1382                 return;
1383             }
1384             this.black.push(tag);
1385             
1386         }, this);
1387         
1388         Roo.each(b, function(tag) {
1389             if (w.indexOf(tag) > -1) {
1390                 return;
1391             }
1392             if (this.black.indexOf(tag) > -1) {
1393                 return;
1394             }
1395             this.black.push(tag);
1396             
1397         }, this);
1398         
1399         
1400         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1401         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1402         
1403         this.cwhite = [];
1404         this.cblack = [];
1405         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1406             if (b.indexOf(tag) > -1) {
1407                 return;
1408             }
1409             this.cwhite.push(tag);
1410             
1411         }, this);
1412         
1413         Roo.each(w, function(tag) {
1414             if (b.indexOf(tag) > -1) {
1415                 return;
1416             }
1417             if (this.cwhite.indexOf(tag) > -1) {
1418                 return;
1419             }
1420             this.cwhite.push(tag);
1421             
1422         }, this);
1423         
1424         
1425         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1426             if (w.indexOf(tag) > -1) {
1427                 return;
1428             }
1429             this.cblack.push(tag);
1430             
1431         }, this);
1432         
1433         Roo.each(b, function(tag) {
1434             if (w.indexOf(tag) > -1) {
1435                 return;
1436             }
1437             if (this.cblack.indexOf(tag) > -1) {
1438                 return;
1439             }
1440             this.cblack.push(tag);
1441             
1442         }, this);
1443     }
1444     
1445     // hide stuff that is not compatible
1446     /**
1447      * @event blur
1448      * @hide
1449      */
1450     /**
1451      * @event change
1452      * @hide
1453      */
1454     /**
1455      * @event focus
1456      * @hide
1457      */
1458     /**
1459      * @event specialkey
1460      * @hide
1461      */
1462     /**
1463      * @cfg {String} fieldClass @hide
1464      */
1465     /**
1466      * @cfg {String} focusClass @hide
1467      */
1468     /**
1469      * @cfg {String} autoCreate @hide
1470      */
1471     /**
1472      * @cfg {String} inputType @hide
1473      */
1474     /**
1475      * @cfg {String} invalidClass @hide
1476      */
1477     /**
1478      * @cfg {String} invalidText @hide
1479      */
1480     /**
1481      * @cfg {String} msgFx @hide
1482      */
1483     /**
1484      * @cfg {String} validateOnBlur @hide
1485      */
1486 });
1487
1488 Roo.HtmlEditorCore.white = [
1489         'area', 'br', 'img', 'input', 'hr', 'wbr',
1490         
1491        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1492        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1493        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1494        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1495        'table',   'ul',         'xmp', 
1496        
1497        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1498       'thead',   'tr', 
1499      
1500       'dir', 'menu', 'ol', 'ul', 'dl',
1501        
1502       'embed',  'object'
1503 ];
1504
1505
1506 Roo.HtmlEditorCore.black = [
1507     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1508         'applet', // 
1509         'base',   'basefont', 'bgsound', 'blink',  'body', 
1510         'frame',  'frameset', 'head',    'html',   'ilayer', 
1511         'iframe', 'layer',  'link',     'meta',    'object',   
1512         'script', 'style' ,'title',  'xml' // clean later..
1513 ];
1514 Roo.HtmlEditorCore.clean = [
1515     'script', 'style', 'title', 'xml'
1516 ];
1517 Roo.HtmlEditorCore.remove = [
1518     'font'
1519 ];
1520 // attributes..
1521
1522 Roo.HtmlEditorCore.ablack = [
1523     'on'
1524 ];
1525     
1526 Roo.HtmlEditorCore.aclean = [ 
1527     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1528 ];
1529
1530 // protocols..
1531 Roo.HtmlEditorCore.pwhite= [
1532         'http',  'https',  'mailto'
1533 ];
1534
1535 // white listed style attributes.
1536 Roo.HtmlEditorCore.cwhite= [
1537       //  'text-align', /// default is to allow most things..
1538       
1539          
1540 //        'font-size'//??
1541 ];
1542
1543 // black listed style attributes.
1544 Roo.HtmlEditorCore.cblack= [
1545       //  'font-size' -- this can be set by the project 
1546 ];
1547
1548
1549 Roo.HtmlEditorCore.swapCodes   =[ 
1550     [    8211, "--" ], 
1551     [    8212, "--" ], 
1552     [    8216,  "'" ],  
1553     [    8217, "'" ],  
1554     [    8220, '"' ],  
1555     [    8221, '"' ],  
1556     [    8226, "*" ],  
1557     [    8230, "..." ]
1558 ]; 
1559
1560