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