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