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