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