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