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