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