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