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