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