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