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