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