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