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     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
433     onFocus : function()
434     {
435         Roo.log('got here!!!!!!!!!!!!!!!!!!!!!!!!!!!!1');
436     },
437
438     // private
439     onDestroy : function(){
440         
441         
442         
443         if(this.rendered){
444             
445             //for (var i =0; i < this.toolbars.length;i++) {
446             //    // fixme - ask toolbars for heights?
447             //    this.toolbars[i].onDestroy();
448            // }
449             
450             //this.wrap.dom.innerHTML = '';
451             //this.wrap.remove();
452         }
453     },
454
455     // private
456     onFirstFocus : function(){
457         
458         this.assignDocWin();
459         
460         
461         this.activated = true;
462          
463     
464         if(Roo.isGecko){ // prevent silly gecko errors
465             this.win.focus();
466             var s = this.win.getSelection();
467             if(!s.focusNode || s.focusNode.nodeType != 3){
468                 var r = s.getRangeAt(0);
469                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
470                 r.collapse(true);
471                 this.deferFocus();
472             }
473             try{
474                 this.execCmd('useCSS', true);
475                 this.execCmd('styleWithCSS', false);
476             }catch(e){}
477         }
478         this.owner.fireEvent('activate', this);
479     },
480
481     // private
482     adjustFont: function(btn){
483         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
484         //if(Roo.isSafari){ // safari
485         //    adjust *= 2;
486        // }
487         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
488         if(Roo.isSafari){ // safari
489             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
490             v =  (v < 10) ? 10 : v;
491             v =  (v > 48) ? 48 : v;
492             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
493             
494         }
495         
496         
497         v = Math.max(1, v+adjust);
498         
499         this.execCmd('FontSize', v  );
500     },
501
502     onEditorEvent : function(e){
503         this.owner.fireEvent('editorevent', this, e);
504       //  this.updateToolbar();
505         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
506     },
507
508     insertTag : function(tg)
509     {
510         // could be a bit smarter... -> wrap the current selected tRoo..
511         if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
512             
513             range = this.createRange(this.getSelection());
514             var wrappingNode = this.doc.createElement(tg.toLowerCase());
515             wrappingNode.appendChild(range.extractContents());
516             range.insertNode(wrappingNode);
517
518             return;
519             
520             
521             
522         }
523         this.execCmd("formatblock",   tg);
524         
525     },
526     
527     insertText : function(txt)
528     {
529         
530         
531         var range = this.createRange();
532         range.deleteContents();
533                //alert(Sender.getAttribute('label'));
534                
535         range.insertNode(this.doc.createTextNode(txt));
536     } ,
537     
538      
539
540     /**
541      * Executes a Midas editor command on the editor document and performs necessary focus and
542      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
543      * @param {String} cmd The Midas command
544      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
545      */
546     relayCmd : function(cmd, value){
547         this.win.focus();
548         this.execCmd(cmd, value);
549         this.owner.fireEvent('editorevent', this);
550         //this.updateToolbar();
551         this.owner.deferFocus();
552     },
553
554     /**
555      * Executes a Midas editor command directly on the editor document.
556      * For visual commands, you should use {@link #relayCmd} instead.
557      * <b>This should only be called after the editor is initialized.</b>
558      * @param {String} cmd The Midas command
559      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
560      */
561     execCmd : function(cmd, value){
562         this.doc.execCommand(cmd, false, value === undefined ? null : value);
563         this.syncValue();
564     },
565  
566  
567    
568     /**
569      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
570      * to insert tRoo.
571      * @param {String} text | dom node.. 
572      */
573     insertAtCursor : function(text)
574     {
575         
576         
577         
578         if(!this.activated){
579             return;
580         }
581         /*
582         if(Roo.isIE){
583             this.win.focus();
584             var r = this.doc.selection.createRange();
585             if(r){
586                 r.collapse(true);
587                 r.pasteHTML(text);
588                 this.syncValue();
589                 this.deferFocus();
590             
591             }
592             return;
593         }
594         */
595         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
596             this.win.focus();
597             
598             
599             // from jquery ui (MIT licenced)
600             var range, node;
601             var win = this.win;
602             
603             if (win.getSelection && win.getSelection().getRangeAt) {
604                 range = win.getSelection().getRangeAt(0);
605                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
606                 range.insertNode(node);
607             } else if (win.document.selection && win.document.selection.createRange) {
608                 // no firefox support
609                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
610                 win.document.selection.createRange().pasteHTML(txt);
611             } else {
612                 // no firefox support
613                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
614                 this.execCmd('InsertHTML', txt);
615             } 
616             
617             this.syncValue();
618             
619             this.deferFocus();
620         }
621     },
622  // private
623     mozKeyPress : function(e){
624         if(e.ctrlKey){
625             var c = e.getCharCode(), cmd;
626           
627             if(c > 0){
628                 c = String.fromCharCode(c).toLowerCase();
629                 switch(c){
630                     case 'b':
631                         cmd = 'bold';
632                         break;
633                     case 'i':
634                         cmd = 'italic';
635                         break;
636                     
637                     case 'u':
638                         cmd = 'underline';
639                         break;
640                     
641                     case 'v':
642                         this.cleanUpPaste.defer(100, this);
643                         return;
644                         
645                 }
646                 if(cmd){
647                     this.win.focus();
648                     this.execCmd(cmd);
649                     this.deferFocus();
650                     e.preventDefault();
651                 }
652                 
653             }
654         }
655     },
656
657     // private
658     fixKeys : function(){ // load time branching for fastest keydown performance
659         if(Roo.isIE){
660             return function(e){
661                 var k = e.getKey(), r;
662                 if(k == e.TAB){
663                     e.stopEvent();
664                     r = this.doc.selection.createRange();
665                     if(r){
666                         r.collapse(true);
667                         r.pasteHTML('&#160;&#160;&#160;&#160;');
668                         this.deferFocus();
669                     }
670                     return;
671                 }
672                 
673                 if(k == e.ENTER){
674                     r = this.doc.selection.createRange();
675                     if(r){
676                         var target = r.parentElement();
677                         if(!target || target.tagName.toLowerCase() != 'li'){
678                             e.stopEvent();
679                             r.pasteHTML('<br />');
680                             r.collapse(false);
681                             r.select();
682                         }
683                     }
684                 }
685                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
686                     this.cleanUpPaste.defer(100, this);
687                     return;
688                 }
689                 
690                 
691             };
692         }else if(Roo.isOpera){
693             return function(e){
694                 var k = e.getKey();
695                 if(k == e.TAB){
696                     e.stopEvent();
697                     this.win.focus();
698                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
699                     this.deferFocus();
700                 }
701                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
702                     this.cleanUpPaste.defer(100, this);
703                     return;
704                 }
705                 
706             };
707         }else if(Roo.isSafari){
708             return function(e){
709                 var k = e.getKey();
710                 
711                 if(k == e.TAB){
712                     e.stopEvent();
713                     this.execCmd('InsertText','\t');
714                     this.deferFocus();
715                     return;
716                 }
717                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
718                     this.cleanUpPaste.defer(100, this);
719                     return;
720                 }
721                 
722              };
723         }
724     }(),
725     
726     getAllAncestors: function()
727     {
728         var p = this.getSelectedNode();
729         var a = [];
730         if (!p) {
731             a.push(p); // push blank onto stack..
732             p = this.getParentElement();
733         }
734         
735         
736         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
737             a.push(p);
738             p = p.parentNode;
739         }
740         a.push(this.doc.body);
741         return a;
742     },
743     lastSel : false,
744     lastSelNode : false,
745     
746     
747     getSelection : function() 
748     {
749         this.assignDocWin();
750         return Roo.isIE ? this.doc.selection : this.win.getSelection();
751     },
752     
753     getSelectedNode: function() 
754     {
755         // this may only work on Gecko!!!
756         
757         // should we cache this!!!!
758         
759         
760         
761          
762         var range = this.createRange(this.getSelection()).cloneRange();
763         
764         if (Roo.isIE) {
765             var parent = range.parentElement();
766             while (true) {
767                 var testRange = range.duplicate();
768                 testRange.moveToElementText(parent);
769                 if (testRange.inRange(range)) {
770                     break;
771                 }
772                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
773                     break;
774                 }
775                 parent = parent.parentElement;
776             }
777             return parent;
778         }
779         
780         // is ancestor a text element.
781         var ac =  range.commonAncestorContainer;
782         if (ac.nodeType == 3) {
783             ac = ac.parentNode;
784         }
785         
786         var ar = ac.childNodes;
787          
788         var nodes = [];
789         var other_nodes = [];
790         var has_other_nodes = false;
791         for (var i=0;i<ar.length;i++) {
792             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
793                 continue;
794             }
795             // fullly contained node.
796             
797             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
798                 nodes.push(ar[i]);
799                 continue;
800             }
801             
802             // probably selected..
803             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
804                 other_nodes.push(ar[i]);
805                 continue;
806             }
807             // outer..
808             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
809                 continue;
810             }
811             
812             
813             has_other_nodes = true;
814         }
815         if (!nodes.length && other_nodes.length) {
816             nodes= other_nodes;
817         }
818         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
819             return false;
820         }
821         
822         return nodes[0];
823     },
824     createRange: function(sel)
825     {
826         // this has strange effects when using with 
827         // top toolbar - not sure if it's a great idea.
828         //this.editor.contentWindow.focus();
829         if (typeof sel != "undefined") {
830             try {
831                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
832             } catch(e) {
833                 return this.doc.createRange();
834             }
835         } else {
836             return this.doc.createRange();
837         }
838     },
839     getParentElement: function()
840     {
841         
842         this.assignDocWin();
843         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
844         
845         var range = this.createRange(sel);
846          
847         try {
848             var p = range.commonAncestorContainer;
849             while (p.nodeType == 3) { // text node
850                 p = p.parentNode;
851             }
852             return p;
853         } catch (e) {
854             return null;
855         }
856     
857     },
858     /***
859      *
860      * Range intersection.. the hard stuff...
861      *  '-1' = before
862      *  '0' = hits..
863      *  '1' = after.
864      *         [ -- selected range --- ]
865      *   [fail]                        [fail]
866      *
867      *    basically..
868      *      if end is before start or  hits it. fail.
869      *      if start is after end or hits it fail.
870      *
871      *   if either hits (but other is outside. - then it's not 
872      *   
873      *    
874      **/
875     
876     
877     // @see http://www.thismuchiknow.co.uk/?p=64.
878     rangeIntersectsNode : function(range, node)
879     {
880         var nodeRange = node.ownerDocument.createRange();
881         try {
882             nodeRange.selectNode(node);
883         } catch (e) {
884             nodeRange.selectNodeContents(node);
885         }
886     
887         var rangeStartRange = range.cloneRange();
888         rangeStartRange.collapse(true);
889     
890         var rangeEndRange = range.cloneRange();
891         rangeEndRange.collapse(false);
892     
893         var nodeStartRange = nodeRange.cloneRange();
894         nodeStartRange.collapse(true);
895     
896         var nodeEndRange = nodeRange.cloneRange();
897         nodeEndRange.collapse(false);
898     
899         return rangeStartRange.compareBoundaryPoints(
900                  Range.START_TO_START, nodeEndRange) == -1 &&
901                rangeEndRange.compareBoundaryPoints(
902                  Range.START_TO_START, nodeStartRange) == 1;
903         
904          
905     },
906     rangeCompareNode : function(range, node)
907     {
908         var nodeRange = node.ownerDocument.createRange();
909         try {
910             nodeRange.selectNode(node);
911         } catch (e) {
912             nodeRange.selectNodeContents(node);
913         }
914         
915         
916         range.collapse(true);
917     
918         nodeRange.collapse(true);
919      
920         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
921         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
922          
923         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
924         
925         var nodeIsBefore   =  ss == 1;
926         var nodeIsAfter    = ee == -1;
927         
928         if (nodeIsBefore && nodeIsAfter)
929             return 0; // outer
930         if (!nodeIsBefore && nodeIsAfter)
931             return 1; //right trailed.
932         
933         if (nodeIsBefore && !nodeIsAfter)
934             return 2;  // left trailed.
935         // fully contined.
936         return 3;
937     },
938
939     // private? - in a new class?
940     cleanUpPaste :  function()
941     {
942         // cleans up the whole document..
943          Roo.log('cleanuppaste');
944         this.cleanUpChildren(this.doc.body);
945         var clean = this.cleanWordChars(this.doc.body.innerHTML);
946         if (clean != this.doc.body.innerHTML) {
947             this.doc.body.innerHTML = clean;
948         }
949         
950     },
951     
952     cleanWordChars : function(input) {// change the chars to hex code
953         var he = Roo.HtmlEditorCore;
954         
955         var output = input;
956         Roo.each(he.swapCodes, function(sw) { 
957             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
958             
959             output = output.replace(swapper, sw[1]);
960         });
961         
962         return output;
963     },
964     
965     
966     cleanUpChildren : function (n)
967     {
968         if (!n.childNodes.length) {
969             return;
970         }
971         for (var i = n.childNodes.length-1; i > -1 ; i--) {
972            this.cleanUpChild(n.childNodes[i]);
973         }
974     },
975     
976     
977         
978     
979     cleanUpChild : function (node)
980     {
981         var ed = this;
982         //console.log(node);
983         if (node.nodeName == "#text") {
984             // clean up silly Windows -- stuff?
985             return; 
986         }
987         if (node.nodeName == "#comment") {
988             node.parentNode.removeChild(node);
989             // clean up silly Windows -- stuff?
990             return; 
991         }
992         
993         if (Roo.HtmlEditorCore.black.indexOf(node.tagName.toLowerCase()) > -1) {
994             // remove node.
995             node.parentNode.removeChild(node);
996             return;
997             
998         }
999         
1000         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1001         
1002         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1003         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1004         
1005         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1006         //    remove_keep_children = true;
1007         //}
1008         
1009         if (remove_keep_children) {
1010             this.cleanUpChildren(node);
1011             // inserts everything just before this node...
1012             while (node.childNodes.length) {
1013                 var cn = node.childNodes[0];
1014                 node.removeChild(cn);
1015                 node.parentNode.insertBefore(cn, node);
1016             }
1017             node.parentNode.removeChild(node);
1018             return;
1019         }
1020         
1021         if (!node.attributes || !node.attributes.length) {
1022             this.cleanUpChildren(node);
1023             return;
1024         }
1025         
1026         function cleanAttr(n,v)
1027         {
1028             
1029             if (v.match(/^\./) || v.match(/^\//)) {
1030                 return;
1031             }
1032             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1033                 return;
1034             }
1035             if (v.match(/^#/)) {
1036                 return;
1037             }
1038 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1039             node.removeAttribute(n);
1040             
1041         }
1042         
1043         function cleanStyle(n,v)
1044         {
1045             if (v.match(/expression/)) { //XSS?? should we even bother..
1046                 node.removeAttribute(n);
1047                 return;
1048             }
1049             var cwhite = typeof(ed.cwhite) == 'undefined' ? Roo.HtmlEditorCore.cwhite : ed.cwhite;
1050             var cblack = typeof(ed.cblack) == 'undefined' ? Roo.HtmlEditorCore.cblack : ed.cblack;
1051             
1052             
1053             var parts = v.split(/;/);
1054             var clean = [];
1055             
1056             Roo.each(parts, function(p) {
1057                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1058                 if (!p.length) {
1059                     return true;
1060                 }
1061                 var l = p.split(':').shift().replace(/\s+/g,'');
1062                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1063                 
1064                 
1065                 if ( cblack.indexOf(l) > -1) {
1066 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1067                     //node.removeAttribute(n);
1068                     return true;
1069                 }
1070                 //Roo.log()
1071                 // only allow 'c whitelisted system attributes'
1072                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1073 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1074                     //node.removeAttribute(n);
1075                     return true;
1076                 }
1077                 
1078                 
1079                  
1080                 
1081                 clean.push(p);
1082                 return true;
1083             });
1084             if (clean.length) { 
1085                 node.setAttribute(n, clean.join(';'));
1086             } else {
1087                 node.removeAttribute(n);
1088             }
1089             
1090         }
1091         
1092         
1093         for (var i = node.attributes.length-1; i > -1 ; i--) {
1094             var a = node.attributes[i];
1095             //console.log(a);
1096             
1097             if (a.name.toLowerCase().substr(0,2)=='on')  {
1098                 node.removeAttribute(a.name);
1099                 continue;
1100             }
1101             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1102                 node.removeAttribute(a.name);
1103                 continue;
1104             }
1105             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1106                 cleanAttr(a.name,a.value); // fixme..
1107                 continue;
1108             }
1109             if (a.name == 'style') {
1110                 cleanStyle(a.name,a.value);
1111                 continue;
1112             }
1113             /// clean up MS crap..
1114             // tecnically this should be a list of valid class'es..
1115             
1116             
1117             if (a.name == 'class') {
1118                 if (a.value.match(/^Mso/)) {
1119                     node.className = '';
1120                 }
1121                 
1122                 if (a.value.match(/body/)) {
1123                     node.className = '';
1124                 }
1125                 continue;
1126             }
1127             
1128             // style cleanup!?
1129             // class cleanup?
1130             
1131         }
1132         
1133         
1134         this.cleanUpChildren(node);
1135         
1136         
1137     }
1138     
1139     
1140     // hide stuff that is not compatible
1141     /**
1142      * @event blur
1143      * @hide
1144      */
1145     /**
1146      * @event change
1147      * @hide
1148      */
1149     /**
1150      * @event focus
1151      * @hide
1152      */
1153     /**
1154      * @event specialkey
1155      * @hide
1156      */
1157     /**
1158      * @cfg {String} fieldClass @hide
1159      */
1160     /**
1161      * @cfg {String} focusClass @hide
1162      */
1163     /**
1164      * @cfg {String} autoCreate @hide
1165      */
1166     /**
1167      * @cfg {String} inputType @hide
1168      */
1169     /**
1170      * @cfg {String} invalidClass @hide
1171      */
1172     /**
1173      * @cfg {String} invalidText @hide
1174      */
1175     /**
1176      * @cfg {String} msgFx @hide
1177      */
1178     /**
1179      * @cfg {String} validateOnBlur @hide
1180      */
1181 });
1182
1183 Roo.HtmlEditorCore.white = [
1184         'area', 'br', 'img', 'input', 'hr', 'wbr',
1185         
1186        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1187        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1188        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1189        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1190        'table',   'ul',         'xmp', 
1191        
1192        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1193       'thead',   'tr', 
1194      
1195       'dir', 'menu', 'ol', 'ul', 'dl',
1196        
1197       'embed',  'object'
1198 ];
1199
1200
1201 Roo.HtmlEditorCore.black = [
1202     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1203         'applet', // 
1204         'base',   'basefont', 'bgsound', 'blink',  'body', 
1205         'frame',  'frameset', 'head',    'html',   'ilayer', 
1206         'iframe', 'layer',  'link',     'meta',    'object',   
1207         'script', 'style' ,'title',  'xml' // clean later..
1208 ];
1209 Roo.HtmlEditorCore.clean = [
1210     'script', 'style', 'title', 'xml'
1211 ];
1212 Roo.HtmlEditorCore.remove = [
1213     'font'
1214 ];
1215 // attributes..
1216
1217 Roo.HtmlEditorCore.ablack = [
1218     'on'
1219 ];
1220     
1221 Roo.HtmlEditorCore.aclean = [ 
1222     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1223 ];
1224
1225 // protocols..
1226 Roo.HtmlEditorCore.pwhite= [
1227         'http',  'https',  'mailto'
1228 ];
1229
1230 // white listed style attributes.
1231 Roo.HtmlEditorCore.cwhite= [
1232       //  'text-align', /// default is to allow most things..
1233       
1234          
1235 //        'font-size'//??
1236 ];
1237
1238 // black listed style attributes.
1239 Roo.HtmlEditorCore.cblack= [
1240       //  'font-size' -- this can be set by the project 
1241 ];
1242
1243
1244 Roo.HtmlEditorCore.swapCodes   =[ 
1245     [    8211, "--" ], 
1246     [    8212, "--" ], 
1247     [    8216,  "'" ],  
1248     [    8217, "'" ],  
1249     [    8220, '"' ],  
1250     [    8221, '"' ],  
1251     [    8226, "*" ],  
1252     [    8230, "..." ]
1253 ]; 
1254
1255