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             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
393                 return;
394             }
395             
396             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
397             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
398         }
399     },
400     
401     // private
402     initEditor : function(){
403         //console.log("INIT EDITOR");
404         this.assignDocWin();
405         
406         
407         
408         this.doc.designMode="on";
409         this.doc.open();
410         this.doc.write(this.getDocMarkup());
411         this.doc.close();
412         
413         var dbody = (this.doc.body || this.doc.documentElement);
414         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
415         // this copies styles from the containing element into thsi one..
416         // not sure why we need all of this..
417         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
418         
419         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
420         //ss['background-attachment'] = 'fixed'; // w3c
421         dbody.bgProperties = 'fixed'; // ie
422         //Roo.DomHelper.applyStyles(dbody, ss);
423         Roo.EventManager.on(this.doc, {
424             //'mousedown': this.onEditorEvent,
425             'mouseup': this.onEditorEvent,
426             'dblclick': this.onEditorEvent,
427             'click': this.onEditorEvent,
428             'keyup': this.onEditorEvent,
429             buffer:100,
430             scope: this
431         });
432         if(Roo.isGecko){
433             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
434         }
435         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
436             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
437         }
438         this.initialized = true;
439
440         this.owner.fireEvent('initialize', this);
441         this.pushValue();
442     },
443
444     // private
445     onDestroy : function(){
446         
447         
448         
449         if(this.rendered){
450             
451             //for (var i =0; i < this.toolbars.length;i++) {
452             //    // fixme - ask toolbars for heights?
453             //    this.toolbars[i].onDestroy();
454            // }
455             
456             //this.wrap.dom.innerHTML = '';
457             //this.wrap.remove();
458         }
459     },
460
461     // private
462     onFirstFocus : function(){
463         
464         this.assignDocWin();
465         
466         
467         this.activated = true;
468          
469     
470         if(Roo.isGecko){ // prevent silly gecko errors
471             this.win.focus();
472             var s = this.win.getSelection();
473             if(!s.focusNode || s.focusNode.nodeType != 3){
474                 var r = s.getRangeAt(0);
475                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
476                 r.collapse(true);
477                 this.deferFocus();
478             }
479             try{
480                 this.execCmd('useCSS', true);
481                 this.execCmd('styleWithCSS', false);
482             }catch(e){}
483         }
484         this.owner.fireEvent('activate', this);
485     },
486
487     // private
488     adjustFont: function(btn){
489         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
490         //if(Roo.isSafari){ // safari
491         //    adjust *= 2;
492        // }
493         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
494         if(Roo.isSafari){ // safari
495             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
496             v =  (v < 10) ? 10 : v;
497             v =  (v > 48) ? 48 : v;
498             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
499             
500         }
501         
502         
503         v = Math.max(1, v+adjust);
504         
505         this.execCmd('FontSize', v  );
506     },
507
508     onEditorEvent : function(e){
509         this.owner.fireEvent('editorevent', this, e);
510       //  this.updateToolbar();
511         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
512     },
513
514     insertTag : function(tg)
515     {
516         // could be a bit smarter... -> wrap the current selected tRoo..
517         if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
518             
519             range = this.createRange(this.getSelection());
520             var wrappingNode = this.doc.createElement(tg.toLowerCase());
521             wrappingNode.appendChild(range.extractContents());
522             range.insertNode(wrappingNode);
523
524             return;
525             
526             
527             
528         }
529         this.execCmd("formatblock",   tg);
530         
531     },
532     
533     insertText : function(txt)
534     {
535         
536         
537         var range = this.createRange();
538         range.deleteContents();
539                //alert(Sender.getAttribute('label'));
540                
541         range.insertNode(this.doc.createTextNode(txt));
542     } ,
543     
544      
545
546     /**
547      * Executes a Midas editor command on the editor document and performs necessary focus and
548      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
549      * @param {String} cmd The Midas command
550      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
551      */
552     relayCmd : function(cmd, value){
553         this.win.focus();
554         this.execCmd(cmd, value);
555         this.owner.fireEvent('editorevent', this);
556         //this.updateToolbar();
557         this.owner.deferFocus();
558     },
559
560     /**
561      * Executes a Midas editor command directly on the editor document.
562      * For visual commands, you should use {@link #relayCmd} instead.
563      * <b>This should only be called after the editor is initialized.</b>
564      * @param {String} cmd The Midas command
565      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
566      */
567     execCmd : function(cmd, value){
568         this.doc.execCommand(cmd, false, value === undefined ? null : value);
569         this.syncValue();
570     },
571  
572  
573    
574     /**
575      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
576      * to insert tRoo.
577      * @param {String} text | dom node.. 
578      */
579     insertAtCursor : function(text)
580     {
581         
582         
583         
584         if(!this.activated){
585             return;
586         }
587         /*
588         if(Roo.isIE){
589             this.win.focus();
590             var r = this.doc.selection.createRange();
591             if(r){
592                 r.collapse(true);
593                 r.pasteHTML(text);
594                 this.syncValue();
595                 this.deferFocus();
596             
597             }
598             return;
599         }
600         */
601         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
602             this.win.focus();
603             
604             
605             // from jquery ui (MIT licenced)
606             var range, node;
607             var win = this.win;
608             
609             if (win.getSelection && win.getSelection().getRangeAt) {
610                 range = win.getSelection().getRangeAt(0);
611                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
612                 range.insertNode(node);
613             } else if (win.document.selection && win.document.selection.createRange) {
614                 // no firefox support
615                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
616                 win.document.selection.createRange().pasteHTML(txt);
617             } else {
618                 // no firefox support
619                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
620                 this.execCmd('InsertHTML', txt);
621             } 
622             
623             this.syncValue();
624             
625             this.deferFocus();
626         }
627     },
628  // private
629     mozKeyPress : function(e){
630         if(e.ctrlKey){
631             var c = e.getCharCode(), cmd;
632           
633             if(c > 0){
634                 c = String.fromCharCode(c).toLowerCase();
635                 switch(c){
636                     case 'b':
637                         cmd = 'bold';
638                         break;
639                     case 'i':
640                         cmd = 'italic';
641                         break;
642                     
643                     case 'u':
644                         cmd = 'underline';
645                         break;
646                     
647                     case 'v':
648                         this.cleanUpPaste.defer(100, this);
649                         return;
650                         
651                 }
652                 if(cmd){
653                     this.win.focus();
654                     this.execCmd(cmd);
655                     this.deferFocus();
656                     e.preventDefault();
657                 }
658                 
659             }
660         }
661     },
662
663     // private
664     fixKeys : function(){ // load time branching for fastest keydown performance
665         if(Roo.isIE){
666             return function(e){
667                 var k = e.getKey(), r;
668                 if(k == e.TAB){
669                     e.stopEvent();
670                     r = this.doc.selection.createRange();
671                     if(r){
672                         r.collapse(true);
673                         r.pasteHTML('&#160;&#160;&#160;&#160;');
674                         this.deferFocus();
675                     }
676                     return;
677                 }
678                 
679                 if(k == e.ENTER){
680                     r = this.doc.selection.createRange();
681                     if(r){
682                         var target = r.parentElement();
683                         if(!target || target.tagName.toLowerCase() != 'li'){
684                             e.stopEvent();
685                             r.pasteHTML('<br />');
686                             r.collapse(false);
687                             r.select();
688                         }
689                     }
690                 }
691                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
692                     this.cleanUpPaste.defer(100, this);
693                     return;
694                 }
695                 
696                 
697             };
698         }else if(Roo.isOpera){
699             return function(e){
700                 var k = e.getKey();
701                 if(k == e.TAB){
702                     e.stopEvent();
703                     this.win.focus();
704                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
705                     this.deferFocus();
706                 }
707                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
708                     this.cleanUpPaste.defer(100, this);
709                     return;
710                 }
711                 
712             };
713         }else if(Roo.isSafari){
714             return function(e){
715                 var k = e.getKey();
716                 
717                 if(k == e.TAB){
718                     e.stopEvent();
719                     this.execCmd('InsertText','\t');
720                     this.deferFocus();
721                     return;
722                 }
723                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
724                     this.cleanUpPaste.defer(100, this);
725                     return;
726                 }
727                 
728              };
729         }
730     }(),
731     
732     getAllAncestors: function()
733     {
734         var p = this.getSelectedNode();
735         var a = [];
736         if (!p) {
737             a.push(p); // push blank onto stack..
738             p = this.getParentElement();
739         }
740         
741         
742         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
743             a.push(p);
744             p = p.parentNode;
745         }
746         a.push(this.doc.body);
747         return a;
748     },
749     lastSel : false,
750     lastSelNode : false,
751     
752     
753     getSelection : function() 
754     {
755         this.assignDocWin();
756         return Roo.isIE ? this.doc.selection : this.win.getSelection();
757     },
758     
759     getSelectedNode: function() 
760     {
761         // this may only work on Gecko!!!
762         
763         // should we cache this!!!!
764         
765         
766         
767          
768         var range = this.createRange(this.getSelection()).cloneRange();
769         
770         if (Roo.isIE) {
771             var parent = range.parentElement();
772             while (true) {
773                 var testRange = range.duplicate();
774                 testRange.moveToElementText(parent);
775                 if (testRange.inRange(range)) {
776                     break;
777                 }
778                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
779                     break;
780                 }
781                 parent = parent.parentElement;
782             }
783             return parent;
784         }
785         
786         // is ancestor a text element.
787         var ac =  range.commonAncestorContainer;
788         if (ac.nodeType == 3) {
789             ac = ac.parentNode;
790         }
791         
792         var ar = ac.childNodes;
793          
794         var nodes = [];
795         var other_nodes = [];
796         var has_other_nodes = false;
797         for (var i=0;i<ar.length;i++) {
798             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
799                 continue;
800             }
801             // fullly contained node.
802             
803             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
804                 nodes.push(ar[i]);
805                 continue;
806             }
807             
808             // probably selected..
809             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
810                 other_nodes.push(ar[i]);
811                 continue;
812             }
813             // outer..
814             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
815                 continue;
816             }
817             
818             
819             has_other_nodes = true;
820         }
821         if (!nodes.length && other_nodes.length) {
822             nodes= other_nodes;
823         }
824         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
825             return false;
826         }
827         
828         return nodes[0];
829     },
830     createRange: function(sel)
831     {
832         // this has strange effects when using with 
833         // top toolbar - not sure if it's a great idea.
834         //this.editor.contentWindow.focus();
835         if (typeof sel != "undefined") {
836             try {
837                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
838             } catch(e) {
839                 return this.doc.createRange();
840             }
841         } else {
842             return this.doc.createRange();
843         }
844     },
845     getParentElement: function()
846     {
847         
848         this.assignDocWin();
849         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
850         
851         var range = this.createRange(sel);
852          
853         try {
854             var p = range.commonAncestorContainer;
855             while (p.nodeType == 3) { // text node
856                 p = p.parentNode;
857             }
858             return p;
859         } catch (e) {
860             return null;
861         }
862     
863     },
864     /***
865      *
866      * Range intersection.. the hard stuff...
867      *  '-1' = before
868      *  '0' = hits..
869      *  '1' = after.
870      *         [ -- selected range --- ]
871      *   [fail]                        [fail]
872      *
873      *    basically..
874      *      if end is before start or  hits it. fail.
875      *      if start is after end or hits it fail.
876      *
877      *   if either hits (but other is outside. - then it's not 
878      *   
879      *    
880      **/
881     
882     
883     // @see http://www.thismuchiknow.co.uk/?p=64.
884     rangeIntersectsNode : function(range, node)
885     {
886         var nodeRange = node.ownerDocument.createRange();
887         try {
888             nodeRange.selectNode(node);
889         } catch (e) {
890             nodeRange.selectNodeContents(node);
891         }
892     
893         var rangeStartRange = range.cloneRange();
894         rangeStartRange.collapse(true);
895     
896         var rangeEndRange = range.cloneRange();
897         rangeEndRange.collapse(false);
898     
899         var nodeStartRange = nodeRange.cloneRange();
900         nodeStartRange.collapse(true);
901     
902         var nodeEndRange = nodeRange.cloneRange();
903         nodeEndRange.collapse(false);
904     
905         return rangeStartRange.compareBoundaryPoints(
906                  Range.START_TO_START, nodeEndRange) == -1 &&
907                rangeEndRange.compareBoundaryPoints(
908                  Range.START_TO_START, nodeStartRange) == 1;
909         
910          
911     },
912     rangeCompareNode : function(range, node)
913     {
914         var nodeRange = node.ownerDocument.createRange();
915         try {
916             nodeRange.selectNode(node);
917         } catch (e) {
918             nodeRange.selectNodeContents(node);
919         }
920         
921         
922         range.collapse(true);
923     
924         nodeRange.collapse(true);
925      
926         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
927         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
928          
929         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
930         
931         var nodeIsBefore   =  ss == 1;
932         var nodeIsAfter    = ee == -1;
933         
934         if (nodeIsBefore && nodeIsAfter)
935             return 0; // outer
936         if (!nodeIsBefore && nodeIsAfter)
937             return 1; //right trailed.
938         
939         if (nodeIsBefore && !nodeIsAfter)
940             return 2;  // left trailed.
941         // fully contined.
942         return 3;
943     },
944
945     // private? - in a new class?
946     cleanUpPaste :  function()
947     {
948         // cleans up the whole document..
949         Roo.log('cleanuppaste');
950         
951         this.cleanUpChildren(this.doc.body);
952         var clean = this.cleanWordChars(this.doc.body.innerHTML);
953         if (clean != this.doc.body.innerHTML) {
954             this.doc.body.innerHTML = clean;
955         }
956         
957     },
958     
959     cleanWordChars : function(input) {// change the chars to hex code
960         var he = Roo.HtmlEditorCore;
961         
962         var output = input;
963         Roo.each(he.swapCodes, function(sw) { 
964             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
965             
966             output = output.replace(swapper, sw[1]);
967         });
968         
969         return output;
970     },
971     
972     
973     cleanUpChildren : function (n)
974     {
975         if (!n.childNodes.length) {
976             return;
977         }
978         for (var i = n.childNodes.length-1; i > -1 ; i--) {
979            this.cleanUpChild(n.childNodes[i]);
980         }
981     },
982     
983     
984         
985     
986     cleanUpChild : function (node)
987     {
988         var ed = this;
989         //console.log(node);
990         if (node.nodeName == "#text") {
991             // clean up silly Windows -- stuff?
992             return; 
993         }
994         if (node.nodeName == "#comment") {
995             node.parentNode.removeChild(node);
996             // clean up silly Windows -- stuff?
997             return; 
998         }
999         
1000         if (Roo.HtmlEditorCore.black.indexOf(node.tagName.toLowerCase()) > -1 && this.clearUp) {
1001             // remove node.
1002             node.parentNode.removeChild(node);
1003             return;
1004             
1005         }
1006         
1007         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1008         
1009         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1010         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1011         
1012         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1013         //    remove_keep_children = true;
1014         //}
1015         
1016         if (remove_keep_children) {
1017             this.cleanUpChildren(node);
1018             // inserts everything just before this node...
1019             while (node.childNodes.length) {
1020                 var cn = node.childNodes[0];
1021                 node.removeChild(cn);
1022                 node.parentNode.insertBefore(cn, node);
1023             }
1024             node.parentNode.removeChild(node);
1025             return;
1026         }
1027         
1028         if (!node.attributes || !node.attributes.length) {
1029             this.cleanUpChildren(node);
1030             return;
1031         }
1032         
1033         function cleanAttr(n,v)
1034         {
1035             
1036             if (v.match(/^\./) || v.match(/^\//)) {
1037                 return;
1038             }
1039             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1040                 return;
1041             }
1042             if (v.match(/^#/)) {
1043                 return;
1044             }
1045 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1046             node.removeAttribute(n);
1047             
1048         }
1049         
1050         function cleanStyle(n,v)
1051         {
1052             if (v.match(/expression/)) { //XSS?? should we even bother..
1053                 node.removeAttribute(n);
1054                 return;
1055             }
1056             var cwhite = typeof(ed.cwhite) != 'undefined' && ed.cwhite !== false ? ed.cwhite : Roo.HtmlEditorCore.cwhite;
1057             var cblack = typeof(ed.cblack) != 'undefined' && ed.cwhite !== false ? ed.cblack : Roo.HtmlEditorCore.cblack;
1058             
1059             
1060             var parts = v.split(/;/);
1061             var clean = [];
1062             
1063             Roo.each(parts, function(p) {
1064                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1065                 if (!p.length) {
1066                     return true;
1067                 }
1068                 var l = p.split(':').shift().replace(/\s+/g,'');
1069                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1070                 
1071                 if ( cblack.indexOf(l) > -1) {
1072 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1073                     //node.removeAttribute(n);
1074                     return true;
1075                 }
1076                 //Roo.log()
1077                 // only allow 'c whitelisted system attributes'
1078                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1079 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1080                     //node.removeAttribute(n);
1081                     return true;
1082                 }
1083                 
1084                 
1085                  
1086                 
1087                 clean.push(p);
1088                 return true;
1089             });
1090             if (clean.length) { 
1091                 node.setAttribute(n, clean.join(';'));
1092             } else {
1093                 node.removeAttribute(n);
1094             }
1095             
1096         }
1097         
1098         
1099         for (var i = node.attributes.length-1; i > -1 ; i--) {
1100             var a = node.attributes[i];
1101             //console.log(a);
1102             
1103             if (a.name.toLowerCase().substr(0,2)=='on')  {
1104                 node.removeAttribute(a.name);
1105                 continue;
1106             }
1107             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1108                 node.removeAttribute(a.name);
1109                 continue;
1110             }
1111             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1112                 cleanAttr(a.name,a.value); // fixme..
1113                 continue;
1114             }
1115             if (a.name == 'style') {
1116                 cleanStyle(a.name,a.value);
1117                 continue;
1118             }
1119             /// clean up MS crap..
1120             // tecnically this should be a list of valid class'es..
1121             
1122             
1123             if (a.name == 'class') {
1124                 if (a.value.match(/^Mso/)) {
1125                     node.className = '';
1126                 }
1127                 
1128                 if (a.value.match(/body/)) {
1129                     node.className = '';
1130                 }
1131                 continue;
1132             }
1133             
1134             // style cleanup!?
1135             // class cleanup?
1136             
1137         }
1138         
1139         
1140         this.cleanUpChildren(node);
1141         
1142         
1143     },
1144     /**
1145      * Clean up MS wordisms...
1146      */
1147     cleanWord : function(node)
1148     {
1149         var _t = this;
1150         var cleanWordChildren = function()
1151         {
1152             if (!node.childNodes.length) {
1153                 return;
1154             }
1155             for (var i = node.childNodes.length-1; i > -1 ; i--) {
1156                _t.cleanWord(node.childNodes[i]);
1157             }
1158         }
1159         
1160         
1161         if (!node) {
1162             this.cleanWord(this.doc.body);
1163             return;
1164         }
1165         if (node.nodeName == "#text") {
1166             // clean up silly Windows -- stuff?
1167             return; 
1168         }
1169         if (node.nodeName == "#comment") {
1170             node.parentNode.removeChild(node);
1171             // clean up silly Windows -- stuff?
1172             return; 
1173         }
1174         
1175         if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1176             node.parentNode.removeChild(node);
1177             return;
1178         }
1179         
1180         // remove - but keep children..
1181         if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1182             while (node.childNodes.length) {
1183                 var cn = node.childNodes[0];
1184                 node.removeChild(cn);
1185                 node.parentNode.insertBefore(cn, node);
1186             }
1187             node.parentNode.removeChild(node);
1188             cleanWordChildren();
1189             return;
1190         }
1191         // clean styles
1192         if (node.className.length) {
1193             
1194             var cn = node.className.split(/\W+/);
1195             var cna = [];
1196             Roo.each(cn, function(cls) {
1197                 if (cls.match(/Mso[a-zA-Z]+/)) {
1198                     return;
1199                 }
1200                 cna.push(cls);
1201             });
1202             node.className = cna.length ? cna.join(' ') : '';
1203             if (!cna.length) {
1204                 node.removeAttribute("class");
1205             }
1206         }
1207         
1208         if (node.hasAttribute("lang")) {
1209             node.removeAttribute("lang");
1210         }
1211         
1212         if (node.hasAttribute("style")) {
1213             
1214             var styles = node.getAttribute("style").split(";");
1215             var nstyle = [];
1216             Roo.each(styles, function(s) {
1217                 if (!s.match(/:/)) {
1218                     return;
1219                 }
1220                 var kv = s.split(":");
1221                 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1222                     return;
1223                 }
1224                 // what ever is left... we allow.
1225                 nstyle.push(s);
1226             });
1227             node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1228             if (!nstyle.length) {
1229                 node.removeAttribute('style');
1230             }
1231         }
1232         
1233         cleanWordChildren();
1234         
1235         
1236     },
1237     domToHTML : function(currentElement, depth, nopadtext) {
1238         
1239             depth = depth || 0;
1240             nopadtext = nopadtext || false;
1241         
1242             if (!currentElement) {
1243                 return this.domToHTML(this.doc.body);
1244             }
1245             
1246             //Roo.log(currentElement);
1247             var j;
1248             var allText = false;
1249             var nodeName = currentElement.nodeName;
1250             var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1251             
1252             if  (nodeName == '#text') {
1253                 return currentElement.nodeValue;
1254             }
1255             
1256             
1257             var ret = '';
1258             if (nodeName != 'BODY') {
1259                  
1260                 var i = 0;
1261                 // Prints the node tagName, such as <A>, <IMG>, etc
1262                 if (tagName) {
1263                     var attr = [];
1264                     for(i = 0; i < currentElement.attributes.length;i++) {
1265                         // quoting?
1266                         var aname = currentElement.attributes.item(i).name;
1267                         if (!currentElement.attributes.item(i).value.length) {
1268                             continue;
1269                         }
1270                         attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1271                     }
1272                     
1273                     ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1274                 } 
1275                 else {
1276                     
1277                     // eack
1278                 }
1279             } else {
1280                 tagName = false;
1281             }
1282             if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1283                 return ret;
1284             }
1285             if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1286                 nopadtext = true;
1287             }
1288             
1289             
1290             // Traverse the tree
1291             i = 0;
1292             var currentElementChild = currentElement.childNodes.item(i);
1293             var allText = true;
1294             var innerHTML  = '';
1295             lastnode = '';
1296             while (currentElementChild) {
1297                 // Formatting code (indent the tree so it looks nice on the screen)
1298                 var nopad = nopadtext;
1299                 if (lastnode == 'SPAN') {
1300                     nopad  = true;
1301                 }
1302                 // text
1303                 if  (currentElementChild.nodeName == '#text') {
1304                     var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1305                     if (!nopad && toadd.length > 80) {
1306                         innerHTML  += "\n" + (new Array( depth + 1 )).join( "  "  );
1307                     }
1308                     innerHTML  += toadd;
1309                     
1310                     i++;
1311                     currentElementChild = currentElement.childNodes.item(i);
1312                     lastNode = '';
1313                     continue;
1314                 }
1315                 allText = false;
1316                 
1317                 innerHTML  += nopad ? '' : "\n" + (new Array( depth + 1 )).join( "  "  );
1318                     
1319                 // Recursively traverse the tree structure of the child node
1320                 innerHTML   += this.domToHTML(currentElementChild, depth+1, nopadtext);
1321                 lastnode = currentElementChild.nodeName;
1322                 i++;
1323                 currentElementChild=currentElement.childNodes.item(i);
1324             }
1325             
1326             ret += innerHTML;
1327             
1328             if (!allText) {
1329                     // The remaining code is mostly for formatting the tree
1330                 ret+= nopadtext ? '' : "\n" + (new Array( depth  )).join( "  "  );
1331             }
1332             
1333             
1334             if (tagName) {
1335                 ret+= "</"+tagName+">";
1336             }
1337             return ret;
1338             
1339         }
1340     
1341     // hide stuff that is not compatible
1342     /**
1343      * @event blur
1344      * @hide
1345      */
1346     /**
1347      * @event change
1348      * @hide
1349      */
1350     /**
1351      * @event focus
1352      * @hide
1353      */
1354     /**
1355      * @event specialkey
1356      * @hide
1357      */
1358     /**
1359      * @cfg {String} fieldClass @hide
1360      */
1361     /**
1362      * @cfg {String} focusClass @hide
1363      */
1364     /**
1365      * @cfg {String} autoCreate @hide
1366      */
1367     /**
1368      * @cfg {String} inputType @hide
1369      */
1370     /**
1371      * @cfg {String} invalidClass @hide
1372      */
1373     /**
1374      * @cfg {String} invalidText @hide
1375      */
1376     /**
1377      * @cfg {String} msgFx @hide
1378      */
1379     /**
1380      * @cfg {String} validateOnBlur @hide
1381      */
1382 });
1383
1384 Roo.HtmlEditorCore.white = [
1385         'area', 'br', 'img', 'input', 'hr', 'wbr',
1386         
1387        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1388        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1389        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1390        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1391        'table',   'ul',         'xmp', 
1392        
1393        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1394       'thead',   'tr', 
1395      
1396       'dir', 'menu', 'ol', 'ul', 'dl',
1397        
1398       'embed',  'object'
1399 ];
1400
1401
1402 Roo.HtmlEditorCore.black = [
1403     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1404         'applet', // 
1405         'base',   'basefont', 'bgsound', 'blink',  'body', 
1406         'frame',  'frameset', 'head',    'html',   'ilayer', 
1407         'iframe', 'layer',  'link',     'meta',    'object',   
1408         'script', 'style' ,'title',  'xml' // clean later..
1409 ];
1410 Roo.HtmlEditorCore.clean = [
1411     'script', 'style', 'title', 'xml'
1412 ];
1413 Roo.HtmlEditorCore.remove = [
1414     'font'
1415 ];
1416 // attributes..
1417
1418 Roo.HtmlEditorCore.ablack = [
1419     'on'
1420 ];
1421     
1422 Roo.HtmlEditorCore.aclean = [ 
1423     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1424 ];
1425
1426 // protocols..
1427 Roo.HtmlEditorCore.pwhite= [
1428         'http',  'https',  'mailto'
1429 ];
1430
1431 // white listed style attributes.
1432 Roo.HtmlEditorCore.cwhite= [
1433       //  'text-align', /// default is to allow most things..
1434       
1435          
1436 //        'font-size'//??
1437 ];
1438
1439 // black listed style attributes.
1440 Roo.HtmlEditorCore.cblack= [
1441       //  'font-size' -- this can be set by the project 
1442 ];
1443
1444
1445 Roo.HtmlEditorCore.swapCodes   =[ 
1446     [    8211, "--" ], 
1447     [    8212, "--" ], 
1448     [    8216,  "'" ],  
1449     [    8217, "'" ],  
1450     [    8220, '"' ],  
1451     [    8221, '"' ],  
1452     [    8226, "*" ],  
1453     [    8230, "..." ]
1454 ]; 
1455
1456