roojs-ui.js
[roojs1] / Roo / HtmlEditorCore.js
1 //<script type="text/javascript">
2
3 /*
4  * Based  Ext JS Library 1.1.1
5  * Copyright(c) 2006-2007, Ext JS, LLC.
6  * LGPL
7  *
8  */
9  
10 /**
11  * @class Roo.HtmlEditorCore
12  * @extends Roo.Component
13  * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
14  *
15  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
16  */
17
18 Roo.HtmlEditorCore = function(config){
19     
20     
21     Roo.HtmlEditorCore.superclass.constructor.call(this, config);
22     
23     
24     this.addEvents({
25         /**
26          * @event initialize
27          * Fires when the editor is fully initialized (including the iframe)
28          * @param {Roo.HtmlEditorCore} this
29          */
30         initialize: true,
31         /**
32          * @event activate
33          * Fires when the editor is first receives the focus. Any insertion must wait
34          * until after this event.
35          * @param {Roo.HtmlEditorCore} this
36          */
37         activate: true,
38          /**
39          * @event beforesync
40          * Fires before the textarea is updated with content from the editor iframe. Return false
41          * to cancel the sync.
42          * @param {Roo.HtmlEditorCore} this
43          * @param {String} html
44          */
45         beforesync: true,
46          /**
47          * @event beforepush
48          * Fires before the iframe editor is updated with content from the textarea. Return false
49          * to cancel the push.
50          * @param {Roo.HtmlEditorCore} this
51          * @param {String} html
52          */
53         beforepush: true,
54          /**
55          * @event sync
56          * Fires when the textarea is updated with content from the editor iframe.
57          * @param {Roo.HtmlEditorCore} this
58          * @param {String} html
59          */
60         sync: true,
61          /**
62          * @event push
63          * Fires when the iframe editor is updated with content from the textarea.
64          * @param {Roo.HtmlEditorCore} this
65          * @param {String} html
66          */
67         push: true,
68         
69         /**
70          * @event editorevent
71          * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72          * @param {Roo.HtmlEditorCore} this
73          */
74         editorevent: true
75         
76     });
77     
78     // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
79     
80     // defaults : white / black...
81     this.applyBlacklists();
82     
83     
84     
85 };
86
87
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component,  {
89
90
91      /**
92      * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field 
93      */
94     
95     owner : false,
96     
97      /**
98      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
99      *                        Roo.resizable.
100      */
101     resizable : false,
102      /**
103      * @cfg {Number} height (in pixels)
104      */   
105     height: 300,
106    /**
107      * @cfg {Number} width (in pixels)
108      */   
109     width: 500,
110     
111     /**
112      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
113      * 
114      */
115     stylesheets: false,
116     
117     /**
118      * @cfg {boolean} allowComments - default false - allow comments in HTML source - by default they are stripped - if you are editing email you may need this.
119      */
120     allowComments: false,
121     // id of frame..
122     frameId: false,
123     
124     // private properties
125     validationEvent : false,
126     deferHeight: true,
127     initialized : false,
128     activated : false,
129     sourceEditMode : false,
130     onFocus : Roo.emptyFn,
131     iframePad:3,
132     hideMode:'offsets',
133     
134     clearUp: true,
135     
136     // blacklist + whitelisted elements..
137     black: false,
138     white: false,
139      
140     bodyCls : '',
141
142     /**
143      * Protected method that will not generally be called directly. It
144      * is called when the editor initializes the iframe with HTML contents. Override this method if you
145      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
146      */
147     getDocMarkup : function(){
148         // body styles..
149         var st = '';
150         
151         // inherit styels from page...?? 
152         if (this.stylesheets === false) {
153             
154             Roo.get(document.head).select('style').each(function(node) {
155                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
156             });
157             
158             Roo.get(document.head).select('link').each(function(node) { 
159                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
160             });
161             
162         } else if (!this.stylesheets.length) {
163                 // simple..
164                 st = '<style type="text/css">' +
165                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
166                    '</style>';
167         } else {
168             for (var i in this.stylesheets) {
169                 if (typeof(this.stylesheets[i]) != 'string') {
170                     continue;
171                 }
172                 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
173             }
174             
175         }
176         
177         st +=  '<style type="text/css">' +
178             'IMG { cursor: pointer } ' +
179         '</style>';
180
181         var cls = 'roo-htmleditor-body';
182         
183         if(this.bodyCls.length){
184             cls += ' ' + this.bodyCls;
185         }
186         
187         return '<html><head>' + st  +
188             //<style type="text/css">' +
189             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
190             //'</style>' +
191             ' </head><body contenteditable="true" data-enable-grammerly="true" class="' +  cls + '"></body></html>';
192     },
193
194     // private
195     onRender : function(ct, position)
196     {
197         var _t = this;
198         //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
199         this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
200         
201         
202         this.el.dom.style.border = '0 none';
203         this.el.dom.setAttribute('tabIndex', -1);
204         this.el.addClass('x-hidden hide');
205         
206         
207         
208         if(Roo.isIE){ // fix IE 1px bogus margin
209             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
210         }
211        
212         
213         this.frameId = Roo.id();
214         
215          
216         
217         var iframe = this.owner.wrap.createChild({
218             tag: 'iframe',
219             cls: 'form-control', // bootstrap..
220             id: this.frameId,
221             name: this.frameId,
222             frameBorder : 'no',
223             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
224         }, this.el
225         );
226         
227         
228         this.iframe = iframe.dom;
229
230          this.assignDocWin();
231         
232         this.doc.designMode = 'on';
233        
234         this.doc.open();
235         this.doc.write(this.getDocMarkup());
236         this.doc.close();
237
238         
239         var task = { // must defer to wait for browser to be ready
240             run : function(){
241                 //console.log("run task?" + this.doc.readyState);
242                 this.assignDocWin();
243                 if(this.doc.body || this.doc.readyState == 'complete'){
244                     try {
245                         this.doc.designMode="on";
246                     } catch (e) {
247                         return;
248                     }
249                     Roo.TaskMgr.stop(task);
250                     this.initEditor.defer(10, this);
251                 }
252             },
253             interval : 10,
254             duration: 10000,
255             scope: this
256         };
257         Roo.TaskMgr.start(task);
258
259     },
260
261     // private
262     onResize : function(w, h)
263     {
264          Roo.log('resize: ' +w + ',' + h );
265         //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
266         if(!this.iframe){
267             return;
268         }
269         if(typeof w == 'number'){
270             
271             this.iframe.style.width = w + 'px';
272         }
273         if(typeof h == 'number'){
274             
275             this.iframe.style.height = h + 'px';
276             if(this.doc){
277                 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
278             }
279         }
280         
281     },
282
283     /**
284      * Toggles the editor between standard and source edit mode.
285      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
286      */
287     toggleSourceEdit : function(sourceEditMode){
288         
289         this.sourceEditMode = sourceEditMode === true;
290         
291         if(this.sourceEditMode){
292  
293             Roo.get(this.iframe).addClass(['x-hidden','hide']);     //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     applyBlacklists : function()
1068     {
1069         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1070         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1071         
1072         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1073         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1074         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1075         
1076         this.white = [];
1077         this.black = [];
1078         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1079             if (b.indexOf(tag) > -1) {
1080                 return;
1081             }
1082             this.white.push(tag);
1083             
1084         }, this);
1085         
1086         Roo.each(w, function(tag) {
1087             if (b.indexOf(tag) > -1) {
1088                 return;
1089             }
1090             if (this.white.indexOf(tag) > -1) {
1091                 return;
1092             }
1093             this.white.push(tag);
1094             
1095         }, this);
1096         
1097         
1098         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1099             if (w.indexOf(tag) > -1) {
1100                 return;
1101             }
1102             this.black.push(tag);
1103             
1104         }, this);
1105         
1106         Roo.each(b, function(tag) {
1107             if (w.indexOf(tag) > -1) {
1108                 return;
1109             }
1110             if (this.black.indexOf(tag) > -1) {
1111                 return;
1112             }
1113             this.black.push(tag);
1114             
1115         }, this);
1116         
1117         
1118         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1119         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1120         
1121         this.cwhite = [];
1122         this.cblack = [];
1123         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1124             if (b.indexOf(tag) > -1) {
1125                 return;
1126             }
1127             this.cwhite.push(tag);
1128             
1129         }, this);
1130         
1131         Roo.each(w, function(tag) {
1132             if (b.indexOf(tag) > -1) {
1133                 return;
1134             }
1135             if (this.cwhite.indexOf(tag) > -1) {
1136                 return;
1137             }
1138             this.cwhite.push(tag);
1139             
1140         }, this);
1141         
1142         
1143         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1144             if (w.indexOf(tag) > -1) {
1145                 return;
1146             }
1147             this.cblack.push(tag);
1148             
1149         }, this);
1150         
1151         Roo.each(b, function(tag) {
1152             if (w.indexOf(tag) > -1) {
1153                 return;
1154             }
1155             if (this.cblack.indexOf(tag) > -1) {
1156                 return;
1157             }
1158             this.cblack.push(tag);
1159             
1160         }, this);
1161     },
1162     
1163     setStylesheets : function(stylesheets)
1164     {
1165         if(typeof(stylesheets) == 'string'){
1166             Roo.get(this.iframe.contentDocument.head).createChild({
1167                 tag : 'link',
1168                 rel : 'stylesheet',
1169                 type : 'text/css',
1170                 href : stylesheets
1171             });
1172             
1173             return;
1174         }
1175         var _this = this;
1176      
1177         Roo.each(stylesheets, function(s) {
1178             if(!s.length){
1179                 return;
1180             }
1181             
1182             Roo.get(_this.iframe.contentDocument.head).createChild({
1183                 tag : 'link',
1184                 rel : 'stylesheet',
1185                 type : 'text/css',
1186                 href : s
1187             });
1188         });
1189
1190         
1191     },
1192     
1193     removeStylesheets : function()
1194     {
1195         var _this = this;
1196         
1197         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1198             s.remove();
1199         });
1200     },
1201     
1202     setStyle : function(style)
1203     {
1204         Roo.get(this.iframe.contentDocument.head).createChild({
1205             tag : 'style',
1206             type : 'text/css',
1207             html : style
1208         });
1209
1210         return;
1211     }
1212     
1213     // hide stuff that is not compatible
1214     /**
1215      * @event blur
1216      * @hide
1217      */
1218     /**
1219      * @event change
1220      * @hide
1221      */
1222     /**
1223      * @event focus
1224      * @hide
1225      */
1226     /**
1227      * @event specialkey
1228      * @hide
1229      */
1230     /**
1231      * @cfg {String} fieldClass @hide
1232      */
1233     /**
1234      * @cfg {String} focusClass @hide
1235      */
1236     /**
1237      * @cfg {String} autoCreate @hide
1238      */
1239     /**
1240      * @cfg {String} inputType @hide
1241      */
1242     /**
1243      * @cfg {String} invalidClass @hide
1244      */
1245     /**
1246      * @cfg {String} invalidText @hide
1247      */
1248     /**
1249      * @cfg {String} msgFx @hide
1250      */
1251     /**
1252      * @cfg {String} validateOnBlur @hide
1253      */
1254 });
1255
1256 Roo.HtmlEditorCore.white = [
1257         'area', 'br', 'img', 'input', 'hr', 'wbr',
1258         
1259        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1260        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1261        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1262        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1263        'table',   'ul',         'xmp', 
1264        
1265        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1266       'thead',   'tr', 
1267      
1268       'dir', 'menu', 'ol', 'ul', 'dl',
1269        
1270       'embed',  'object'
1271 ];
1272
1273
1274 Roo.HtmlEditorCore.black = [
1275     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1276         'applet', // 
1277         'base',   'basefont', 'bgsound', 'blink',  'body', 
1278         'frame',  'frameset', 'head',    'html',   'ilayer', 
1279         'iframe', 'layer',  'link',     'meta',    'object',   
1280         'script', 'style' ,'title',  'xml' // clean later..
1281 ];
1282 Roo.HtmlEditorCore.clean = [
1283     'script', 'style', 'title', 'xml'
1284 ];
1285 Roo.HtmlEditorCore.tag_remove = [
1286     'font'
1287 ];
1288 // attributes..
1289
1290 Roo.HtmlEditorCore.ablack = [
1291     'on'
1292 ];
1293     
1294 Roo.HtmlEditorCore.aclean = [ 
1295     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1296 ];
1297
1298 // protocols..
1299 Roo.HtmlEditorCore.pwhite= [
1300         'http',  'https',  'mailto'
1301 ];
1302
1303 // white listed style attributes.
1304 Roo.HtmlEditorCore.cwhite= [
1305       //  'text-align', /// default is to allow most things..
1306       
1307          
1308 //        'font-size'//??
1309 ];
1310
1311 // black listed style attributes.
1312 Roo.HtmlEditorCore.cblack= [
1313       //  'font-size' -- this can be set by the project 
1314 ];
1315
1316
1317 Roo.HtmlEditorCore.swapCodes   =[ 
1318     [    8211, "&#8211;" ], 
1319     [    8212, "&#8212;" ], 
1320     [    8216,  "'" ],  
1321     [    8217, "'" ],  
1322     [    8220, '"' ],  
1323     [    8221, '"' ],  
1324     [    8226, "*" ],  
1325     [    8230, "..." ]
1326 ]; 
1327
1328