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