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