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