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