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