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     // id of frame..
118     frameId: false,
119     
120     // private properties
121     validationEvent : false,
122     deferHeight: true,
123     initialized : false,
124     activated : false,
125     sourceEditMode : false,
126     onFocus : Roo.emptyFn,
127     iframePad:3,
128     hideMode:'offsets',
129     
130     clearUp: true,
131     
132     // blacklist + whitelisted elements..
133     black: false,
134     white: false,
135      
136     
137
138     /**
139      * Protected method that will not generally be called directly. It
140      * is called when the editor initializes the iframe with HTML contents. Override this method if you
141      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
142      */
143     getDocMarkup : function(){
144         // body styles..
145         var st = '';
146         
147         // inherit styels from page...?? 
148         if (this.stylesheets === false) {
149             
150             Roo.get(document.head).select('style').each(function(node) {
151                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
152             });
153             
154             Roo.get(document.head).select('link').each(function(node) { 
155                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
156             });
157             
158         } else if (!this.stylesheets.length) {
159                 // simple..
160                 st = '<style type="text/css">' +
161                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
162                    '</style>';
163         } else { 
164             
165         }
166         
167         st +=  '<style type="text/css">' +
168             'IMG { cursor: pointer } ' +
169         '</style>';
170
171         
172         return '<html><head>' + st  +
173             //<style type="text/css">' +
174             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
175             //'</style>' +
176             ' </head><body class="roo-htmleditor-body"></body></html>';
177     },
178
179     // private
180     onRender : function(ct, position)
181     {
182         var _t = this;
183         //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
184         this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
185         
186         
187         this.el.dom.style.border = '0 none';
188         this.el.dom.setAttribute('tabIndex', -1);
189         this.el.addClass('x-hidden hide');
190         
191         
192         
193         if(Roo.isIE){ // fix IE 1px bogus margin
194             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
195         }
196        
197         
198         this.frameId = Roo.id();
199         
200          
201         
202         var iframe = this.owner.wrap.createChild({
203             tag: 'iframe',
204             cls: 'form-control', // bootstrap..
205             id: this.frameId,
206             name: this.frameId,
207             frameBorder : 'no',
208             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
209         }, this.el
210         );
211         
212         
213         this.iframe = iframe.dom;
214
215          this.assignDocWin();
216         
217         this.doc.designMode = 'on';
218        
219         this.doc.open();
220         this.doc.write(this.getDocMarkup());
221         this.doc.close();
222
223         
224         var task = { // must defer to wait for browser to be ready
225             run : function(){
226                 //console.log("run task?" + this.doc.readyState);
227                 this.assignDocWin();
228                 if(this.doc.body || this.doc.readyState == 'complete'){
229                     try {
230                         this.doc.designMode="on";
231                     } catch (e) {
232                         return;
233                     }
234                     Roo.TaskMgr.stop(task);
235                     this.initEditor.defer(10, this);
236                 }
237             },
238             interval : 10,
239             duration: 10000,
240             scope: this
241         };
242         Roo.TaskMgr.start(task);
243
244     },
245
246     // private
247     onResize : function(w, h)
248     {
249          Roo.log('resize: ' +w + ',' + h );
250         //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
251         if(!this.iframe){
252             return;
253         }
254         if(typeof w == 'number'){
255             
256             this.iframe.style.width = w + 'px';
257         }
258         if(typeof h == 'number'){
259             
260             this.iframe.style.height = h + 'px';
261             if(this.doc){
262                 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
263             }
264         }
265         
266     },
267
268     /**
269      * Toggles the editor between standard and source edit mode.
270      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
271      */
272     toggleSourceEdit : function(sourceEditMode){
273         
274         this.sourceEditMode = sourceEditMode === true;
275         
276         if(this.sourceEditMode){
277  
278             Roo.get(this.iframe).addClass(['x-hidden','hide']);     //FIXME - what's the BS styles for these
279             
280         }else{
281             Roo.get(this.iframe).removeClass(['x-hidden','hide']);
282             //this.iframe.className = '';
283             this.deferFocus();
284         }
285         //this.setSize(this.owner.wrap.getSize());
286         //this.fireEvent('editmodechange', this, this.sourceEditMode);
287     },
288
289     
290   
291
292     /**
293      * Protected method that will not generally be called directly. If you need/want
294      * custom HTML cleanup, this is the method you should override.
295      * @param {String} html The HTML to be cleaned
296      * return {String} The cleaned HTML
297      */
298     cleanHtml : function(html){
299         html = String(html);
300         if(html.length > 5){
301             if(Roo.isSafari){ // strip safari nonsense
302                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
303             }
304         }
305         if(html == '&nbsp;'){
306             html = '';
307         }
308         return html;
309     },
310
311     /**
312      * HTML Editor -> Textarea
313      * Protected method that will not generally be called directly. Syncs the contents
314      * of the editor iframe with the textarea.
315      */
316     syncValue : function(){
317         if(this.initialized){
318             var bd = (this.doc.body || this.doc.documentElement);
319             //this.cleanUpPaste(); -- this is done else where and causes havoc..
320             var html = bd.innerHTML;
321             if(Roo.isSafari){
322                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
323                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
324                 if(m && m[1]){
325                     html = '<div style="'+m[0]+'">' + html + '</div>';
326                 }
327             }
328             html = this.cleanHtml(html);
329             // fix up the special chars.. normaly like back quotes in word...
330             // however we do not want to do this with chinese..
331             html = html.replace(/([\x80-\uffff])/g, function (a, b) {
332                 var cc = b.charCodeAt();
333                 if (
334                     (cc >= 0x4E00 && cc < 0xA000 ) ||
335                     (cc >= 0x3400 && cc < 0x4E00 ) ||
336                     (cc >= 0xf900 && cc < 0xfb00 )
337                 ) {
338                         return b;
339                 }
340                 return "&#"+cc+";" 
341             });
342             if(this.owner.fireEvent('beforesync', this, html) !== false){
343                 this.el.dom.value = html;
344                 this.owner.fireEvent('sync', this, html);
345             }
346         }
347     },
348
349     /**
350      * Protected method that will not generally be called directly. Pushes the value of the textarea
351      * into the iframe editor.
352      */
353     pushValue : function(){
354         if(this.initialized){
355             var v = this.el.dom.value.trim();
356             
357 //            if(v.length < 1){
358 //                v = '&#160;';
359 //            }
360             
361             if(this.owner.fireEvent('beforepush', this, v) !== false){
362                 var d = (this.doc.body || this.doc.documentElement);
363                 d.innerHTML = v;
364                 this.cleanUpPaste();
365                 this.el.dom.value = d.innerHTML;
366                 this.owner.fireEvent('push', this, v);
367             }
368         }
369     },
370
371     // private
372     deferFocus : function(){
373         this.focus.defer(10, this);
374     },
375
376     // doc'ed in Field
377     focus : function(){
378         if(this.win && !this.sourceEditMode){
379             this.win.focus();
380         }else{
381             this.el.focus();
382         }
383     },
384     
385     assignDocWin: function()
386     {
387         var iframe = this.iframe;
388         
389          if(Roo.isIE){
390             this.doc = iframe.contentWindow.document;
391             this.win = iframe.contentWindow;
392         } else {
393 //            if (!Roo.get(this.frameId)) {
394 //                return;
395 //            }
396 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
397 //            this.win = Roo.get(this.frameId).dom.contentWindow;
398             
399             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
400                 return;
401             }
402             
403             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
404             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
405         }
406     },
407     
408     // private
409     initEditor : function(){
410         //console.log("INIT EDITOR");
411         this.assignDocWin();
412         
413         
414         
415         this.doc.designMode="on";
416         this.doc.open();
417         this.doc.write(this.getDocMarkup());
418         this.doc.close();
419         
420         var dbody = (this.doc.body || this.doc.documentElement);
421         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
422         // this copies styles from the containing element into thsi one..
423         // not sure why we need all of this..
424         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
425         
426         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
427         //ss['background-attachment'] = 'fixed'; // w3c
428         dbody.bgProperties = 'fixed'; // ie
429         //Roo.DomHelper.applyStyles(dbody, ss);
430         Roo.EventManager.on(this.doc, {
431             //'mousedown': this.onEditorEvent,
432             'mouseup': this.onEditorEvent,
433             'dblclick': this.onEditorEvent,
434             'click': this.onEditorEvent,
435             'keyup': this.onEditorEvent,
436             buffer:100,
437             scope: this
438         });
439         if(Roo.isGecko){
440             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
441         }
442         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
443             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
444         }
445         this.initialized = true;
446
447         this.owner.fireEvent('initialize', this);
448         this.pushValue();
449     },
450
451     // private
452     onDestroy : function(){
453         
454         
455         
456         if(this.rendered){
457             
458             //for (var i =0; i < this.toolbars.length;i++) {
459             //    // fixme - ask toolbars for heights?
460             //    this.toolbars[i].onDestroy();
461            // }
462             
463             //this.wrap.dom.innerHTML = '';
464             //this.wrap.remove();
465         }
466     },
467
468     // private
469     onFirstFocus : function(){
470         
471         this.assignDocWin();
472         
473         
474         this.activated = true;
475          
476     
477         if(Roo.isGecko){ // prevent silly gecko errors
478             this.win.focus();
479             var s = this.win.getSelection();
480             if(!s.focusNode || s.focusNode.nodeType != 3){
481                 var r = s.getRangeAt(0);
482                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
483                 r.collapse(true);
484                 this.deferFocus();
485             }
486             try{
487                 this.execCmd('useCSS', true);
488                 this.execCmd('styleWithCSS', false);
489             }catch(e){}
490         }
491         this.owner.fireEvent('activate', this);
492     },
493
494     // private
495     adjustFont: function(btn){
496         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
497         //if(Roo.isSafari){ // safari
498         //    adjust *= 2;
499        // }
500         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
501         if(Roo.isSafari){ // safari
502             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
503             v =  (v < 10) ? 10 : v;
504             v =  (v > 48) ? 48 : v;
505             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
506             
507         }
508         
509         
510         v = Math.max(1, v+adjust);
511         
512         this.execCmd('FontSize', v  );
513     },
514
515     onEditorEvent : function(e){
516         this.owner.fireEvent('editorevent', this, e);
517       //  this.updateToolbar();
518         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
519     },
520
521     insertTag : function(tg)
522     {
523         // could be a bit smarter... -> wrap the current selected tRoo..
524         if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
525             
526             range = this.createRange(this.getSelection());
527             var wrappingNode = this.doc.createElement(tg.toLowerCase());
528             wrappingNode.appendChild(range.extractContents());
529             range.insertNode(wrappingNode);
530
531             return;
532             
533             
534             
535         }
536         this.execCmd("formatblock",   tg);
537         
538     },
539     
540     insertText : function(txt)
541     {
542         
543         
544         var range = this.createRange();
545         range.deleteContents();
546                //alert(Sender.getAttribute('label'));
547                
548         range.insertNode(this.doc.createTextNode(txt));
549     } ,
550     
551      
552
553     /**
554      * Executes a Midas editor command on the editor document and performs necessary focus and
555      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
556      * @param {String} cmd The Midas command
557      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
558      */
559     relayCmd : function(cmd, value){
560         this.win.focus();
561         this.execCmd(cmd, value);
562         this.owner.fireEvent('editorevent', this);
563         //this.updateToolbar();
564         this.owner.deferFocus();
565     },
566
567     /**
568      * Executes a Midas editor command directly on the editor document.
569      * For visual commands, you should use {@link #relayCmd} instead.
570      * <b>This should only be called after the editor is initialized.</b>
571      * @param {String} cmd The Midas command
572      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
573      */
574     execCmd : function(cmd, value){
575         this.doc.execCommand(cmd, false, value === undefined ? null : value);
576         this.syncValue();
577     },
578  
579  
580    
581     /**
582      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
583      * to insert tRoo.
584      * @param {String} text | dom node.. 
585      */
586     insertAtCursor : function(text)
587     {
588         
589         
590         
591         if(!this.activated){
592             return;
593         }
594         /*
595         if(Roo.isIE){
596             this.win.focus();
597             var r = this.doc.selection.createRange();
598             if(r){
599                 r.collapse(true);
600                 r.pasteHTML(text);
601                 this.syncValue();
602                 this.deferFocus();
603             
604             }
605             return;
606         }
607         */
608         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
609             this.win.focus();
610             
611             
612             // from jquery ui (MIT licenced)
613             var range, node;
614             var win = this.win;
615             
616             if (win.getSelection && win.getSelection().getRangeAt) {
617                 range = win.getSelection().getRangeAt(0);
618                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
619                 range.insertNode(node);
620             } else if (win.document.selection && win.document.selection.createRange) {
621                 // no firefox support
622                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
623                 win.document.selection.createRange().pasteHTML(txt);
624             } else {
625                 // no firefox support
626                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
627                 this.execCmd('InsertHTML', txt);
628             } 
629             
630             this.syncValue();
631             
632             this.deferFocus();
633         }
634     },
635  // private
636     mozKeyPress : function(e){
637         if(e.ctrlKey){
638             var c = e.getCharCode(), cmd;
639           
640             if(c > 0){
641                 c = String.fromCharCode(c).toLowerCase();
642                 switch(c){
643                     case 'b':
644                         cmd = 'bold';
645                         break;
646                     case 'i':
647                         cmd = 'italic';
648                         break;
649                     
650                     case 'u':
651                         cmd = 'underline';
652                         break;
653                     
654                     case 'v':
655                         this.cleanUpPaste.defer(100, this);
656                         return;
657                         
658                 }
659                 if(cmd){
660                     this.win.focus();
661                     this.execCmd(cmd);
662                     this.deferFocus();
663                     e.preventDefault();
664                 }
665                 
666             }
667         }
668     },
669
670     // private
671     fixKeys : function(){ // load time branching for fastest keydown performance
672         if(Roo.isIE){
673             return function(e){
674                 var k = e.getKey(), r;
675                 if(k == e.TAB){
676                     e.stopEvent();
677                     r = this.doc.selection.createRange();
678                     if(r){
679                         r.collapse(true);
680                         r.pasteHTML('&#160;&#160;&#160;&#160;');
681                         this.deferFocus();
682                     }
683                     return;
684                 }
685                 
686                 if(k == e.ENTER){
687                     r = this.doc.selection.createRange();
688                     if(r){
689                         var target = r.parentElement();
690                         if(!target || target.tagName.toLowerCase() != 'li'){
691                             e.stopEvent();
692                             r.pasteHTML('<br />');
693                             r.collapse(false);
694                             r.select();
695                         }
696                     }
697                 }
698                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
699                     this.cleanUpPaste.defer(100, this);
700                     return;
701                 }
702                 
703                 
704             };
705         }else if(Roo.isOpera){
706             return function(e){
707                 var k = e.getKey();
708                 if(k == e.TAB){
709                     e.stopEvent();
710                     this.win.focus();
711                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
712                     this.deferFocus();
713                 }
714                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
715                     this.cleanUpPaste.defer(100, this);
716                     return;
717                 }
718                 
719             };
720         }else if(Roo.isSafari){
721             return function(e){
722                 var k = e.getKey();
723                 
724                 if(k == e.TAB){
725                     e.stopEvent();
726                     this.execCmd('InsertText','\t');
727                     this.deferFocus();
728                     return;
729                 }
730                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
731                     this.cleanUpPaste.defer(100, this);
732                     return;
733                 }
734                 
735              };
736         }
737     }(),
738     
739     getAllAncestors: function()
740     {
741         var p = this.getSelectedNode();
742         var a = [];
743         if (!p) {
744             a.push(p); // push blank onto stack..
745             p = this.getParentElement();
746         }
747         
748         
749         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
750             a.push(p);
751             p = p.parentNode;
752         }
753         a.push(this.doc.body);
754         return a;
755     },
756     lastSel : false,
757     lastSelNode : false,
758     
759     
760     getSelection : function() 
761     {
762         this.assignDocWin();
763         return Roo.isIE ? this.doc.selection : this.win.getSelection();
764     },
765     
766     getSelectedNode: function() 
767     {
768         // this may only work on Gecko!!!
769         
770         // should we cache this!!!!
771         
772         
773         
774          
775         var range = this.createRange(this.getSelection()).cloneRange();
776         
777         if (Roo.isIE) {
778             var parent = range.parentElement();
779             while (true) {
780                 var testRange = range.duplicate();
781                 testRange.moveToElementText(parent);
782                 if (testRange.inRange(range)) {
783                     break;
784                 }
785                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
786                     break;
787                 }
788                 parent = parent.parentElement;
789             }
790             return parent;
791         }
792         
793         // is ancestor a text element.
794         var ac =  range.commonAncestorContainer;
795         if (ac.nodeType == 3) {
796             ac = ac.parentNode;
797         }
798         
799         var ar = ac.childNodes;
800          
801         var nodes = [];
802         var other_nodes = [];
803         var has_other_nodes = false;
804         for (var i=0;i<ar.length;i++) {
805             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
806                 continue;
807             }
808             // fullly contained node.
809             
810             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
811                 nodes.push(ar[i]);
812                 continue;
813             }
814             
815             // probably selected..
816             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
817                 other_nodes.push(ar[i]);
818                 continue;
819             }
820             // outer..
821             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
822                 continue;
823             }
824             
825             
826             has_other_nodes = true;
827         }
828         if (!nodes.length && other_nodes.length) {
829             nodes= other_nodes;
830         }
831         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
832             return false;
833         }
834         
835         return nodes[0];
836     },
837     createRange: function(sel)
838     {
839         // this has strange effects when using with 
840         // top toolbar - not sure if it's a great idea.
841         //this.editor.contentWindow.focus();
842         if (typeof sel != "undefined") {
843             try {
844                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
845             } catch(e) {
846                 return this.doc.createRange();
847             }
848         } else {
849             return this.doc.createRange();
850         }
851     },
852     getParentElement: function()
853     {
854         
855         this.assignDocWin();
856         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
857         
858         var range = this.createRange(sel);
859          
860         try {
861             var p = range.commonAncestorContainer;
862             while (p.nodeType == 3) { // text node
863                 p = p.parentNode;
864             }
865             return p;
866         } catch (e) {
867             return null;
868         }
869     
870     },
871     /***
872      *
873      * Range intersection.. the hard stuff...
874      *  '-1' = before
875      *  '0' = hits..
876      *  '1' = after.
877      *         [ -- selected range --- ]
878      *   [fail]                        [fail]
879      *
880      *    basically..
881      *      if end is before start or  hits it. fail.
882      *      if start is after end or hits it fail.
883      *
884      *   if either hits (but other is outside. - then it's not 
885      *   
886      *    
887      **/
888     
889     
890     // @see http://www.thismuchiknow.co.uk/?p=64.
891     rangeIntersectsNode : function(range, node)
892     {
893         var nodeRange = node.ownerDocument.createRange();
894         try {
895             nodeRange.selectNode(node);
896         } catch (e) {
897             nodeRange.selectNodeContents(node);
898         }
899     
900         var rangeStartRange = range.cloneRange();
901         rangeStartRange.collapse(true);
902     
903         var rangeEndRange = range.cloneRange();
904         rangeEndRange.collapse(false);
905     
906         var nodeStartRange = nodeRange.cloneRange();
907         nodeStartRange.collapse(true);
908     
909         var nodeEndRange = nodeRange.cloneRange();
910         nodeEndRange.collapse(false);
911     
912         return rangeStartRange.compareBoundaryPoints(
913                  Range.START_TO_START, nodeEndRange) == -1 &&
914                rangeEndRange.compareBoundaryPoints(
915                  Range.START_TO_START, nodeStartRange) == 1;
916         
917          
918     },
919     rangeCompareNode : function(range, node)
920     {
921         var nodeRange = node.ownerDocument.createRange();
922         try {
923             nodeRange.selectNode(node);
924         } catch (e) {
925             nodeRange.selectNodeContents(node);
926         }
927         
928         
929         range.collapse(true);
930     
931         nodeRange.collapse(true);
932      
933         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
934         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
935          
936         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
937         
938         var nodeIsBefore   =  ss == 1;
939         var nodeIsAfter    = ee == -1;
940         
941         if (nodeIsBefore && nodeIsAfter)
942             return 0; // outer
943         if (!nodeIsBefore && nodeIsAfter)
944             return 1; //right trailed.
945         
946         if (nodeIsBefore && !nodeIsAfter)
947             return 2;  // left trailed.
948         // fully contined.
949         return 3;
950     },
951
952     // private? - in a new class?
953     cleanUpPaste :  function()
954     {
955         // cleans up the whole document..
956         Roo.log('cleanuppaste');
957         
958         this.cleanUpChildren(this.doc.body);
959         var clean = this.cleanWordChars(this.doc.body.innerHTML);
960         if (clean != this.doc.body.innerHTML) {
961             this.doc.body.innerHTML = clean;
962         }
963         
964     },
965     
966     cleanWordChars : function(input) {// change the chars to hex code
967         var he = Roo.HtmlEditorCore;
968         
969         var output = input;
970         Roo.each(he.swapCodes, function(sw) { 
971             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
972             
973             output = output.replace(swapper, sw[1]);
974         });
975         
976         return output;
977     },
978     
979     
980     cleanUpChildren : function (n)
981     {
982         if (!n.childNodes.length) {
983             return;
984         }
985         for (var i = n.childNodes.length-1; i > -1 ; i--) {
986            this.cleanUpChild(n.childNodes[i]);
987         }
988     },
989     
990     
991         
992     
993     cleanUpChild : function (node)
994     {
995         var ed = this;
996         //console.log(node);
997         if (node.nodeName == "#text") {
998             // clean up silly Windows -- stuff?
999             return; 
1000         }
1001         if (node.nodeName == "#comment") {
1002             node.parentNode.removeChild(node);
1003             // clean up silly Windows -- stuff?
1004             return; 
1005         }
1006         var lcname = node.tagName.toLowerCase();
1007         // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1008         // whitelist of tags..
1009         
1010         if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1011             // remove node.
1012             node.parentNode.removeChild(node);
1013             return;
1014             
1015         }
1016         
1017         var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1018         
1019         // remove <a name=....> as rendering on yahoo mailer is borked with this.
1020         // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1021         
1022         //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1023         //    remove_keep_children = true;
1024         //}
1025         
1026         if (remove_keep_children) {
1027             this.cleanUpChildren(node);
1028             // inserts everything just before this node...
1029             while (node.childNodes.length) {
1030                 var cn = node.childNodes[0];
1031                 node.removeChild(cn);
1032                 node.parentNode.insertBefore(cn, node);
1033             }
1034             node.parentNode.removeChild(node);
1035             return;
1036         }
1037         
1038         if (!node.attributes || !node.attributes.length) {
1039             this.cleanUpChildren(node);
1040             return;
1041         }
1042         
1043         function cleanAttr(n,v)
1044         {
1045             
1046             if (v.match(/^\./) || v.match(/^\//)) {
1047                 return;
1048             }
1049             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1050                 return;
1051             }
1052             if (v.match(/^#/)) {
1053                 return;
1054             }
1055 //            Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1056             node.removeAttribute(n);
1057             
1058         }
1059         
1060         var cwhite = this.cwhite;
1061         var cblack = this.cblack;
1062             
1063         function cleanStyle(n,v)
1064         {
1065             if (v.match(/expression/)) { //XSS?? should we even bother..
1066                 node.removeAttribute(n);
1067                 return;
1068             }
1069             
1070             var parts = v.split(/;/);
1071             var clean = [];
1072             
1073             Roo.each(parts, function(p) {
1074                 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1075                 if (!p.length) {
1076                     return true;
1077                 }
1078                 var l = p.split(':').shift().replace(/\s+/g,'');
1079                 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1080                 
1081                 if ( cwhite.length && cblack.indexOf(l) > -1) {
1082 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1083                     //node.removeAttribute(n);
1084                     return true;
1085                 }
1086                 //Roo.log()
1087                 // only allow 'c whitelisted system attributes'
1088                 if ( cwhite.length &&  cwhite.indexOf(l) < 0) {
1089 //                    Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1090                     //node.removeAttribute(n);
1091                     return true;
1092                 }
1093                 
1094                 
1095                  
1096                 
1097                 clean.push(p);
1098                 return true;
1099             });
1100             if (clean.length) { 
1101                 node.setAttribute(n, clean.join(';'));
1102             } else {
1103                 node.removeAttribute(n);
1104             }
1105             
1106         }
1107         
1108         
1109         for (var i = node.attributes.length-1; i > -1 ; i--) {
1110             var a = node.attributes[i];
1111             //console.log(a);
1112             
1113             if (a.name.toLowerCase().substr(0,2)=='on')  {
1114                 node.removeAttribute(a.name);
1115                 continue;
1116             }
1117             if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1118                 node.removeAttribute(a.name);
1119                 continue;
1120             }
1121             if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1122                 cleanAttr(a.name,a.value); // fixme..
1123                 continue;
1124             }
1125             if (a.name == 'style') {
1126                 cleanStyle(a.name,a.value);
1127                 continue;
1128             }
1129             /// clean up MS crap..
1130             // tecnically this should be a list of valid class'es..
1131             
1132             
1133             if (a.name == 'class') {
1134                 if (a.value.match(/^Mso/)) {
1135                     node.className = '';
1136                 }
1137                 
1138                 if (a.value.match(/body/)) {
1139                     node.className = '';
1140                 }
1141                 continue;
1142             }
1143             
1144             // style cleanup!?
1145             // class cleanup?
1146             
1147         }
1148         
1149         
1150         this.cleanUpChildren(node);
1151         
1152         
1153     },
1154     /**
1155      * Clean up MS wordisms...
1156      */
1157     cleanWord : function(node)
1158     {
1159         var _t = this;
1160         var cleanWordChildren = function()
1161         {
1162             if (!node.childNodes.length) {
1163                 return;
1164             }
1165             for (var i = node.childNodes.length-1; i > -1 ; i--) {
1166                _t.cleanWord(node.childNodes[i]);
1167             }
1168         }
1169         
1170         
1171         if (!node) {
1172             this.cleanWord(this.doc.body);
1173             return;
1174         }
1175         if (node.nodeName == "#text") {
1176             // clean up silly Windows -- stuff?
1177             return; 
1178         }
1179         if (node.nodeName == "#comment") {
1180             node.parentNode.removeChild(node);
1181             // clean up silly Windows -- stuff?
1182             return; 
1183         }
1184         
1185         if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1186             node.parentNode.removeChild(node);
1187             return;
1188         }
1189         
1190         // remove - but keep children..
1191         if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1192             while (node.childNodes.length) {
1193                 var cn = node.childNodes[0];
1194                 node.removeChild(cn);
1195                 node.parentNode.insertBefore(cn, node);
1196             }
1197             node.parentNode.removeChild(node);
1198             cleanWordChildren();
1199             return;
1200         }
1201         // clean styles
1202         if (node.className.length) {
1203             
1204             var cn = node.className.split(/\W+/);
1205             var cna = [];
1206             Roo.each(cn, function(cls) {
1207                 if (cls.match(/Mso[a-zA-Z]+/)) {
1208                     return;
1209                 }
1210                 cna.push(cls);
1211             });
1212             node.className = cna.length ? cna.join(' ') : '';
1213             if (!cna.length) {
1214                 node.removeAttribute("class");
1215             }
1216         }
1217         
1218         if (node.hasAttribute("lang")) {
1219             node.removeAttribute("lang");
1220         }
1221         
1222         if (node.hasAttribute("style")) {
1223             
1224             var styles = node.getAttribute("style").split(";");
1225             var nstyle = [];
1226             Roo.each(styles, function(s) {
1227                 if (!s.match(/:/)) {
1228                     return;
1229                 }
1230                 var kv = s.split(":");
1231                 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1232                     return;
1233                 }
1234                 // what ever is left... we allow.
1235                 nstyle.push(s);
1236             });
1237             node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1238             if (!nstyle.length) {
1239                 node.removeAttribute('style');
1240             }
1241         }
1242         
1243         cleanWordChildren();
1244         
1245         
1246     },
1247     domToHTML : function(currentElement, depth, nopadtext) {
1248         
1249         depth = depth || 0;
1250         nopadtext = nopadtext || false;
1251     
1252         if (!currentElement) {
1253             return this.domToHTML(this.doc.body);
1254         }
1255         
1256         //Roo.log(currentElement);
1257         var j;
1258         var allText = false;
1259         var nodeName = currentElement.nodeName;
1260         var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1261         
1262         if  (nodeName == '#text') {
1263             
1264             return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1265         }
1266         
1267         
1268         var ret = '';
1269         if (nodeName != 'BODY') {
1270              
1271             var i = 0;
1272             // Prints the node tagName, such as <A>, <IMG>, etc
1273             if (tagName) {
1274                 var attr = [];
1275                 for(i = 0; i < currentElement.attributes.length;i++) {
1276                     // quoting?
1277                     var aname = currentElement.attributes.item(i).name;
1278                     if (!currentElement.attributes.item(i).value.length) {
1279                         continue;
1280                     }
1281                     attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1282                 }
1283                 
1284                 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1285             } 
1286             else {
1287                 
1288                 // eack
1289             }
1290         } else {
1291             tagName = false;
1292         }
1293         if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1294             return ret;
1295         }
1296         if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1297             nopadtext = true;
1298         }
1299         
1300         
1301         // Traverse the tree
1302         i = 0;
1303         var currentElementChild = currentElement.childNodes.item(i);
1304         var allText = true;
1305         var innerHTML  = '';
1306         lastnode = '';
1307         while (currentElementChild) {
1308             // Formatting code (indent the tree so it looks nice on the screen)
1309             var nopad = nopadtext;
1310             if (lastnode == 'SPAN') {
1311                 nopad  = true;
1312             }
1313             // text
1314             if  (currentElementChild.nodeName == '#text') {
1315                 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1316                 toadd = nopadtext ? toadd : toadd.trim();
1317                 if (!nopad && toadd.length > 80) {
1318                     innerHTML  += "\n" + (new Array( depth + 1 )).join( "  "  );
1319                 }
1320                 innerHTML  += toadd;
1321                 
1322                 i++;
1323                 currentElementChild = currentElement.childNodes.item(i);
1324                 lastNode = '';
1325                 continue;
1326             }
1327             allText = false;
1328             
1329             innerHTML  += nopad ? '' : "\n" + (new Array( depth + 1 )).join( "  "  );
1330                 
1331             // Recursively traverse the tree structure of the child node
1332             innerHTML   += this.domToHTML(currentElementChild, depth+1, nopadtext);
1333             lastnode = currentElementChild.nodeName;
1334             i++;
1335             currentElementChild=currentElement.childNodes.item(i);
1336         }
1337         
1338         ret += innerHTML;
1339         
1340         if (!allText) {
1341                 // The remaining code is mostly for formatting the tree
1342             ret+= nopadtext ? '' : "\n" + (new Array( depth  )).join( "  "  );
1343         }
1344         
1345         
1346         if (tagName) {
1347             ret+= "</"+tagName+">";
1348         }
1349         return ret;
1350         
1351     },
1352         
1353     applyBlacklists : function()
1354     {
1355         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1356         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1357         
1358         this.white = [];
1359         this.black = [];
1360         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1361             if (b.indexOf(tag) > -1) {
1362                 return;
1363             }
1364             this.white.push(tag);
1365             
1366         }, this);
1367         
1368         Roo.each(w, function(tag) {
1369             if (b.indexOf(tag) > -1) {
1370                 return;
1371             }
1372             if (this.white.indexOf(tag) > -1) {
1373                 return;
1374             }
1375             this.white.push(tag);
1376             
1377         }, this);
1378         
1379         
1380         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1381             if (w.indexOf(tag) > -1) {
1382                 return;
1383             }
1384             this.black.push(tag);
1385             
1386         }, this);
1387         
1388         Roo.each(b, function(tag) {
1389             if (w.indexOf(tag) > -1) {
1390                 return;
1391             }
1392             if (this.black.indexOf(tag) > -1) {
1393                 return;
1394             }
1395             this.black.push(tag);
1396             
1397         }, this);
1398         
1399         
1400         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1401         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1402         
1403         this.cwhite = [];
1404         this.cblack = [];
1405         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1406             if (b.indexOf(tag) > -1) {
1407                 return;
1408             }
1409             this.cwhite.push(tag);
1410             
1411         }, this);
1412         
1413         Roo.each(w, function(tag) {
1414             if (b.indexOf(tag) > -1) {
1415                 return;
1416             }
1417             if (this.cwhite.indexOf(tag) > -1) {
1418                 return;
1419             }
1420             this.cwhite.push(tag);
1421             
1422         }, this);
1423         
1424         
1425         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1426             if (w.indexOf(tag) > -1) {
1427                 return;
1428             }
1429             this.cblack.push(tag);
1430             
1431         }, this);
1432         
1433         Roo.each(b, function(tag) {
1434             if (w.indexOf(tag) > -1) {
1435                 return;
1436             }
1437             if (this.cblack.indexOf(tag) > -1) {
1438                 return;
1439             }
1440             this.cblack.push(tag);
1441             
1442         }, this);
1443     },
1444     
1445     setStylesheets : function(stylesheets)
1446     {
1447         Roo.log('setting stylesheets!!!!!!!!!!!!!!!!!!!!!!!!!!');
1448         Roo.log(stylesheets);
1449         if(typeof(stylesheets) == 'string'){
1450             Roo.get(this.iframe.contentDocument.head).createChild({
1451                 tag : 'link',
1452                 rel : 'stylesheet',
1453                 type : 'text/css',
1454                 href : stylesheets
1455             });
1456             
1457             return;
1458         }
1459         var _this = this;
1460         
1461         Roo.each(stylesheets, function(s) {
1462             if(!s.length){
1463                 return;
1464             }
1465             
1466             Roo.get(_this.iframe.contentDocument.head).createChild({
1467                 tag : 'link',
1468                 rel : 'stylesheet',
1469                 type : 'text/css',
1470                 href : s
1471             });
1472         });
1473
1474         
1475     },
1476     
1477     removeStylesheets : function()
1478     {
1479         var _this = this;
1480         
1481         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1482             s.remove();
1483         });
1484     }
1485     
1486     // hide stuff that is not compatible
1487     /**
1488      * @event blur
1489      * @hide
1490      */
1491     /**
1492      * @event change
1493      * @hide
1494      */
1495     /**
1496      * @event focus
1497      * @hide
1498      */
1499     /**
1500      * @event specialkey
1501      * @hide
1502      */
1503     /**
1504      * @cfg {String} fieldClass @hide
1505      */
1506     /**
1507      * @cfg {String} focusClass @hide
1508      */
1509     /**
1510      * @cfg {String} autoCreate @hide
1511      */
1512     /**
1513      * @cfg {String} inputType @hide
1514      */
1515     /**
1516      * @cfg {String} invalidClass @hide
1517      */
1518     /**
1519      * @cfg {String} invalidText @hide
1520      */
1521     /**
1522      * @cfg {String} msgFx @hide
1523      */
1524     /**
1525      * @cfg {String} validateOnBlur @hide
1526      */
1527 });
1528
1529 Roo.HtmlEditorCore.white = [
1530         'area', 'br', 'img', 'input', 'hr', 'wbr',
1531         
1532        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1533        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1534        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1535        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1536        'table',   'ul',         'xmp', 
1537        
1538        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1539       'thead',   'tr', 
1540      
1541       'dir', 'menu', 'ol', 'ul', 'dl',
1542        
1543       'embed',  'object'
1544 ];
1545
1546
1547 Roo.HtmlEditorCore.black = [
1548     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1549         'applet', // 
1550         'base',   'basefont', 'bgsound', 'blink',  'body', 
1551         'frame',  'frameset', 'head',    'html',   'ilayer', 
1552         'iframe', 'layer',  'link',     'meta',    'object',   
1553         'script', 'style' ,'title',  'xml' // clean later..
1554 ];
1555 Roo.HtmlEditorCore.clean = [
1556     'script', 'style', 'title', 'xml'
1557 ];
1558 Roo.HtmlEditorCore.remove = [
1559     'font'
1560 ];
1561 // attributes..
1562
1563 Roo.HtmlEditorCore.ablack = [
1564     'on'
1565 ];
1566     
1567 Roo.HtmlEditorCore.aclean = [ 
1568     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1569 ];
1570
1571 // protocols..
1572 Roo.HtmlEditorCore.pwhite= [
1573         'http',  'https',  'mailto'
1574 ];
1575
1576 // white listed style attributes.
1577 Roo.HtmlEditorCore.cwhite= [
1578       //  'text-align', /// default is to allow most things..
1579       
1580          
1581 //        'font-size'//??
1582 ];
1583
1584 // black listed style attributes.
1585 Roo.HtmlEditorCore.cblack= [
1586       //  'font-size' -- this can be set by the project 
1587 ];
1588
1589
1590 Roo.HtmlEditorCore.swapCodes   =[ 
1591     [    8211, "--" ], 
1592     [    8212, "--" ], 
1593     [    8216,  "'" ],  
1594     [    8217, "'" ],  
1595     [    8220, '"' ],  
1596     [    8221, '"' ],  
1597     [    8226, "*" ],  
1598     [    8230, "..." ]
1599 ]; 
1600
1601