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