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