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