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