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