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         this.cleanUpChildren(this.doc.body);
943         var clean = this.cleanWordChars(this.doc.body.innerHTML);
944         if (clean != this.doc.body.innerHTML) {
945             this.doc.body.innerHTML = clean;
946         }
947         
948     },
949     
950     cleanWordChars : function(input) {// change the chars to hex code
951         var he = Roo.HtmlEditorCore;
952         
953         var output = input;
954         Roo.each(he.swapCodes, function(sw) { 
955             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
956             
957             output = output.replace(swapper, sw[1]);
958         });
959         
960         return output;
961     },
962     
963     
964     cleanUpChildren : function (n)
965     {
966         if (!n.childNodes.length) {
967             return;
968         }
969         for (var i = n.childNodes.length-1; i > -1 ; i--) {
970            this.cleanUpChild(n.childNodes[i]);
971         }
972     },
973     
974     
975         
976     
977     cleanUpChild : function (node)
978     {
979         var ed = this;
980         //console.log(node);
981         if (node.nodeName == "#text") {
982             // clean up silly Windows -- stuff?
983             return; 
984         }
985         if (node.nodeName == "#comment") {
986             node.parentNode.removeChild(node);
987             // clean up silly Windows -- stuff?
988             return; 
989         }
990         
991         if (Roo.HtmlEditorCore.black.indexOf(node.tagName.toLowerCase()) > -1 && this.clearUp) {
992             // remove node.
993             node.parentNode.removeChild(node);
994             return;
995             
996         }
997         
998         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
999         
1000         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1001         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1002         
1003         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1004         //    remove_keep_children = true;
1005         //}
1006         
1007         if (remove_keep_children) {
1008             this.cleanUpChildren(node);
1009             // inserts everything just before this node...
1010             while (node.childNodes.length) {
1011                 var cn = node.childNodes[0];
1012                 node.removeChild(cn);
1013                 node.parentNode.insertBefore(cn, node);
1014             }
1015             node.parentNode.removeChild(node);
1016             return;
1017         }
1018         
1019         if (!node.attributes || !node.attributes.length) {
1020             this.cleanUpChildren(node);
1021             return;
1022         }
1023         
1024         function cleanAttr(n,v)
1025         {
1026             
1027             if (v.match(/^\./) || v.match(/^\//)) {
1028                 return;
1029             }
1030             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1031                 return;
1032             }
1033             if (v.match(/^#/)) {
1034                 return;
1035             }
1036 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1037             node.removeAttribute(n);
1038             
1039         }
1040         
1041         function cleanStyle(n,v)
1042         {
1043             if (v.match(/expression/)) { //XSS?? should we even bother..
1044                 node.removeAttribute(n);
1045                 return;
1046             }
1047             var cwhite = typeof(ed.cwhite) == 'undefined' ? Roo.HtmlEditorCore.cwhite : ed.cwhite;
1048             var cblack = typeof(ed.cblack) == 'undefined' ? Roo.HtmlEditorCore.cblack : ed.cblack;
1049             
1050             
1051             var parts = v.split(/;/);
1052             var clean = [];
1053             
1054             Roo.each(parts, function(p) {
1055                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1056                 if (!p.length) {
1057                     return true;
1058                 }
1059                 var l = p.split(':').shift().replace(/\s+/g,'');
1060                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1061                 
1062                 if ( cblack.indexOf(l) > -1) {
1063 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1064                     //node.removeAttribute(n);
1065                     return true;
1066                 }
1067                 //Roo.log()
1068                 // only allow 'c whitelisted system attributes'
1069                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1070 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1071                     //node.removeAttribute(n);
1072                     return true;
1073                 }
1074                 
1075                 
1076                  
1077                 
1078                 clean.push(p);
1079                 return true;
1080             });
1081             if (clean.length) { 
1082                 node.setAttribute(n, clean.join(';'));
1083             } else {
1084                 node.removeAttribute(n);
1085             }
1086             
1087         }
1088         
1089         
1090         for (var i = node.attributes.length-1; i > -1 ; i--) {
1091             var a = node.attributes[i];
1092             //console.log(a);
1093             
1094             if (a.name.toLowerCase().substr(0,2)=='on')  {
1095                 node.removeAttribute(a.name);
1096                 continue;
1097             }
1098             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1099                 node.removeAttribute(a.name);
1100                 continue;
1101             }
1102             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1103                 cleanAttr(a.name,a.value); // fixme..
1104                 continue;
1105             }
1106             if (a.name == 'style') {
1107                 cleanStyle(a.name,a.value);
1108                 continue;
1109             }
1110             /// clean up MS crap..
1111             // tecnically this should be a list of valid class'es..
1112             
1113             
1114             if (a.name == 'class') {
1115                 if (a.value.match(/^Mso/)) {
1116                     node.className = '';
1117                 }
1118                 
1119                 if (a.value.match(/body/)) {
1120                     node.className = '';
1121                 }
1122                 continue;
1123             }
1124             
1125             // style cleanup!?
1126             // class cleanup?
1127             
1128         }
1129         
1130         
1131         this.cleanUpChildren(node);
1132         
1133         
1134     },
1135     /**
1136      * Clean up MS wordisms...
1137      */
1138     cleanWord : function(node)
1139     {
1140         var cleanWordChildren = function()
1141         {
1142             if (!node.childNodes.length) {
1143                 return;
1144             }
1145             for (var i = n.childNodes.length-1; i > -1 ; i--) {
1146                this.cleanWord(n.childNodes[i]);
1147             }
1148         }
1149         
1150         
1151         if (!node) {
1152             this.cleanWord(this.doc.body);
1153             return;
1154         }
1155         if (node.nodeName == "#text") {
1156             // clean up silly Windows -- stuff?
1157             return; 
1158         }
1159         if (node.nodeName == "#comment") {
1160             node.parentNode.removeChild(node);
1161             // clean up silly Windows -- stuff?
1162             return; 
1163         }
1164         
1165         if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1166             node.parentNode.removeChild(node);
1167             return;
1168         }
1169         
1170         // remove - but keep children..
1171         if (node.tagName.toLowerCase().match(/^(meta|link|span|\\?xml:|st1:|o:|font)/)) {
1172             while (node.childNodes.length) {
1173                 var cn = node.childNodes[0];
1174                 node.removeChild(cn);
1175                 node.parentNode.insertBefore(cn, node);
1176             }
1177             node.parentNode.removeChild(node);
1178             cleanWordChildren();
1179             return;
1180         }
1181         // clean styles
1182         if (node.className.length) {
1183             
1184             var cn = node.className.split(/\W+/);
1185             var cna = [];
1186             Roo.each(cn, function(cls) {
1187                 if (cls.match(/Mso[a-zA-Z]+/)) {
1188                     return;
1189                 }
1190                 cna.push(cn);
1191             });
1192             node.className = cna.length ? cna.join(' ') : '';
1193             if (!cna.length) {
1194                 node.removeAttribute("class");
1195             }
1196         }
1197         if (node.hasAttribute("lang")) {
1198             node.removeAttribute("lang");
1199         }
1200         if (node.getAttribute("style").length) {
1201             
1202             var styles = node.getAttribute("style").split(";");
1203             var nstyle = [];
1204             Roo.each(styles, function(s) {
1205                 if (!s.match(/:/)) {
1206                     return;
1207                 }
1208                 var kv = s.split(":");
1209                 if (kv[0].match(/^(mso-|line|font|background|margin|padding|/)) {
1210                     return;
1211                 }
1212                 // what ever is left... we allow.
1213                 nstyle.push(s);
1214             });
1215             node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1216             if (!nstyle.length) {
1217                 node.removeAttribute('style');
1218             }
1219         }
1220         
1221         
1222         
1223         
1224         
1225         
1226     }
1227     
1228     // hide stuff that is not compatible
1229     /**
1230      * @event blur
1231      * @hide
1232      */
1233     /**
1234      * @event change
1235      * @hide
1236      */
1237     /**
1238      * @event focus
1239      * @hide
1240      */
1241     /**
1242      * @event specialkey
1243      * @hide
1244      */
1245     /**
1246      * @cfg {String} fieldClass @hide
1247      */
1248     /**
1249      * @cfg {String} focusClass @hide
1250      */
1251     /**
1252      * @cfg {String} autoCreate @hide
1253      */
1254     /**
1255      * @cfg {String} inputType @hide
1256      */
1257     /**
1258      * @cfg {String} invalidClass @hide
1259      */
1260     /**
1261      * @cfg {String} invalidText @hide
1262      */
1263     /**
1264      * @cfg {String} msgFx @hide
1265      */
1266     /**
1267      * @cfg {String} validateOnBlur @hide
1268      */
1269 });
1270
1271 Roo.HtmlEditorCore.white = [
1272         'area', 'br', 'img', 'input', 'hr', 'wbr',
1273         
1274        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1275        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1276        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1277        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1278        'table',   'ul',         'xmp', 
1279        
1280        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1281       'thead',   'tr', 
1282      
1283       'dir', 'menu', 'ol', 'ul', 'dl',
1284        
1285       'embed',  'object'
1286 ];
1287
1288
1289 Roo.HtmlEditorCore.black = [
1290     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1291         'applet', // 
1292         'base',   'basefont', 'bgsound', 'blink',  'body', 
1293         'frame',  'frameset', 'head',    'html',   'ilayer', 
1294         'iframe', 'layer',  'link',     'meta',    'object',   
1295         'script', 'style' ,'title',  'xml' // clean later..
1296 ];
1297 Roo.HtmlEditorCore.clean = [
1298     'script', 'style', 'title', 'xml'
1299 ];
1300 Roo.HtmlEditorCore.remove = [
1301     'font'
1302 ];
1303 // attributes..
1304
1305 Roo.HtmlEditorCore.ablack = [
1306     'on'
1307 ];
1308     
1309 Roo.HtmlEditorCore.aclean = [ 
1310     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1311 ];
1312
1313 // protocols..
1314 Roo.HtmlEditorCore.pwhite= [
1315         'http',  'https',  'mailto'
1316 ];
1317
1318 // white listed style attributes.
1319 Roo.HtmlEditorCore.cwhite= [
1320       //  'text-align', /// default is to allow most things..
1321       
1322          
1323 //        'font-size'//??
1324 ];
1325
1326 // black listed style attributes.
1327 Roo.HtmlEditorCore.cblack= [
1328       //  'font-size' -- this can be set by the project 
1329 ];
1330
1331
1332 Roo.HtmlEditorCore.swapCodes   =[ 
1333     [    8211, "--" ], 
1334     [    8212, "--" ], 
1335     [    8216,  "'" ],  
1336     [    8217, "'" ],  
1337     [    8220, '"' ],  
1338     [    8221, '"' ],  
1339     [    8226, "*" ],  
1340     [    8230, "..." ]
1341 ]; 
1342
1343