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