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