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