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     undoManager : false,
144     /**
145      * Protected method that will not generally be called directly. It
146      * is called when the editor initializes the iframe with HTML contents. Override this method if you
147      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
148      */
149     getDocMarkup : function(){
150         // body styles..
151         var st = '';
152         
153         // inherit styels from page...?? 
154         if (this.stylesheets === false) {
155             
156             Roo.get(document.head).select('style').each(function(node) {
157                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158             });
159             
160             Roo.get(document.head).select('link').each(function(node) { 
161                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
162             });
163             
164         } else if (!this.stylesheets.length) {
165                 // simple..
166                 st = '<style type="text/css">' +
167                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
168                    '</style>';
169         } else {
170             for (var i in this.stylesheets) {
171                 if (typeof(this.stylesheets[i]) != 'string') {
172                     continue;
173                 }
174                 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
175             }
176             
177         }
178         
179         st +=  '<style type="text/css">' +
180             'IMG { cursor: pointer } ' +
181         '</style>';
182
183         var cls = 'roo-htmleditor-body';
184         
185         if(this.bodyCls.length){
186             cls += ' ' + this.bodyCls;
187         }
188         
189         return '<html><head>' + st  +
190             //<style type="text/css">' +
191             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
192             //'</style>' +
193             ' </head><body contenteditable="true" data-enable-grammerly="true" class="' +  cls + '"></body></html>';
194     },
195
196     // private
197     onRender : function(ct, position)
198     {
199         var _t = this;
200         //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
201         this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
202         
203         
204         this.el.dom.style.border = '0 none';
205         this.el.dom.setAttribute('tabIndex', -1);
206         this.el.addClass('x-hidden hide');
207         
208         
209         
210         if(Roo.isIE){ // fix IE 1px bogus margin
211             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
212         }
213        
214         
215         this.frameId = Roo.id();
216         
217          
218         
219         var iframe = this.owner.wrap.createChild({
220             tag: 'iframe',
221             cls: 'form-control', // bootstrap..
222             id: this.frameId,
223             name: this.frameId,
224             frameBorder : 'no',
225             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
226         }, this.el
227         );
228         
229         
230         this.iframe = iframe.dom;
231
232         this.assignDocWin();
233         
234         this.doc.designMode = 'on';
235        
236         this.doc.open();
237         this.doc.write(this.getDocMarkup());
238         this.doc.close();
239
240         
241         var task = { // must defer to wait for browser to be ready
242             run : function(){
243                 //console.log("run task?" + this.doc.readyState);
244                 this.assignDocWin();
245                 if(this.doc.body || this.doc.readyState == 'complete'){
246                     try {
247                         this.doc.designMode="on";
248                         
249                     } catch (e) {
250                         return;
251                     }
252                     Roo.TaskMgr.stop(task);
253                     this.initEditor.defer(10, this);
254                 }
255             },
256             interval : 10,
257             duration: 10000,
258             scope: this
259         };
260         Roo.TaskMgr.start(task);
261
262     },
263
264     // private
265     onResize : function(w, h)
266     {
267          Roo.log('resize: ' +w + ',' + h );
268         //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
269         if(!this.iframe){
270             return;
271         }
272         if(typeof w == 'number'){
273             
274             this.iframe.style.width = w + 'px';
275         }
276         if(typeof h == 'number'){
277             
278             this.iframe.style.height = h + 'px';
279             if(this.doc){
280                 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
281             }
282         }
283         
284     },
285
286     /**
287      * Toggles the editor between standard and source edit mode.
288      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
289      */
290     toggleSourceEdit : function(sourceEditMode){
291         
292         this.sourceEditMode = sourceEditMode === true;
293         
294         if(this.sourceEditMode){
295  
296             Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']);     //FIXME - what's the BS styles for these
297             
298         }else{
299             Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
300             //this.iframe.className = '';
301             this.deferFocus();
302         }
303         //this.setSize(this.owner.wrap.getSize());
304         //this.fireEvent('editmodechange', this, this.sourceEditMode);
305     },
306
307     
308   
309
310     /**
311      * Protected method that will not generally be called directly. If you need/want
312      * custom HTML cleanup, this is the method you should override.
313      * @param {String} html The HTML to be cleaned
314      * return {String} The cleaned HTML
315      */
316     cleanHtml : function(html){
317         html = String(html);
318         if(html.length > 5){
319             if(Roo.isSafari){ // strip safari nonsense
320                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
321             }
322         }
323         if(html == '&nbsp;'){
324             html = '';
325         }
326         return html;
327     },
328
329     /**
330      * HTML Editor -> Textarea
331      * Protected method that will not generally be called directly. Syncs the contents
332      * of the editor iframe with the textarea.
333      */
334     syncValue : function()
335     {
336         Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
337         if(this.initialized){
338             
339             this.undoManager.addEvent();
340
341             
342             var bd = (this.doc.body || this.doc.documentElement);
343             //this.cleanUpPaste(); -- this is done else where and causes havoc..
344             
345             // not sure if this is really the place for this
346             // the blocks are synced occasionaly - since we currently dont add listeners on the blocks
347             // this has to update attributes that get duped.. like alt and caption..
348             
349             
350             //Roo.each(Roo.get(this.doc.body).query('*[data-block]'), function(e) {
351             //     Roo.htmleditor.Block.factory(e);
352             //},this);
353             
354             
355             var div = document.createElement('div');
356             div.innerHTML = bd.innerHTML;
357             // remove content editable. (blocks)
358             
359            
360             new Roo.htmleditor.FilterAttributes({node : div, attrib_black: [ 'contenteditable' ] });
361             //?? tidy?
362             var html = div.innerHTML;
363             if(Roo.isSafari){
364                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
365                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
366                 if(m && m[1]){
367                     html = '<div style="'+m[0]+'">' + html + '</div>';
368                 }
369             }
370             html = this.cleanHtml(html);
371             // fix up the special chars.. normaly like back quotes in word...
372             // however we do not want to do this with chinese..
373             html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
374                 
375                 var cc = match.charCodeAt();
376
377                 // Get the character value, handling surrogate pairs
378                 if (match.length == 2) {
379                     // It's a surrogate pair, calculate the Unicode code point
380                     var high = match.charCodeAt(0) - 0xD800;
381                     var low  = match.charCodeAt(1) - 0xDC00;
382                     cc = (high * 0x400) + low + 0x10000;
383                 }  else if (
384                     (cc >= 0x4E00 && cc < 0xA000 ) ||
385                     (cc >= 0x3400 && cc < 0x4E00 ) ||
386                     (cc >= 0xf900 && cc < 0xfb00 )
387                 ) {
388                         return match;
389                 }  
390          
391                 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
392                 return "&#" + cc + ";";
393                 
394                 
395             });
396             
397             
398              
399             if(this.owner.fireEvent('beforesync', this, html) !== false){
400                 this.el.dom.value = html;
401                 this.owner.fireEvent('sync', this, html);
402             }
403         }
404     },
405
406     /**
407      * TEXTAREA -> EDITABLE
408      * Protected method that will not generally be called directly. Pushes the value of the textarea
409      * into the iframe editor.
410      */
411     pushValue : function()
412     {
413         Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
414         if(this.initialized){
415             var v = this.el.dom.value.trim();
416             
417             
418             if(this.owner.fireEvent('beforepush', this, v) !== false){
419                 var d = (this.doc.body || this.doc.documentElement);
420                 d.innerHTML = v;
421                  
422                 this.el.dom.value = d.innerHTML;
423                 this.owner.fireEvent('push', this, v);
424             }
425             
426             Roo.each(Roo.get(this.doc.body).query('*[data-block]'), function(e) {
427                 
428                 Roo.htmleditor.Block.factory(e);
429                 
430             },this);
431             var lc = this.doc.body.lastChild;
432             if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
433                 // add an extra line at the end.
434                 this.doc.body.appendChild(this.doc.createElement('br'));
435             }
436             
437             
438         }
439     },
440
441     // private
442     deferFocus : function(){
443         this.focus.defer(10, this);
444     },
445
446     // doc'ed in Field
447     focus : function(){
448         if(this.win && !this.sourceEditMode){
449             this.win.focus();
450         }else{
451             this.el.focus();
452         }
453     },
454     
455     assignDocWin: function()
456     {
457         var iframe = this.iframe;
458         
459          if(Roo.isIE){
460             this.doc = iframe.contentWindow.document;
461             this.win = iframe.contentWindow;
462         } else {
463 //            if (!Roo.get(this.frameId)) {
464 //                return;
465 //            }
466 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
467 //            this.win = Roo.get(this.frameId).dom.contentWindow;
468             
469             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
470                 return;
471             }
472             
473             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
474             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
475         }
476     },
477     
478     // private
479     initEditor : function(){
480         //console.log("INIT EDITOR");
481         this.assignDocWin();
482         
483         
484         
485         this.doc.designMode="on";
486         this.doc.open();
487         this.doc.write(this.getDocMarkup());
488         this.doc.close();
489         
490         var dbody = (this.doc.body || this.doc.documentElement);
491         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
492         // this copies styles from the containing element into thsi one..
493         // not sure why we need all of this..
494         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
495         
496         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
497         //ss['background-attachment'] = 'fixed'; // w3c
498         dbody.bgProperties = 'fixed'; // ie
499         //Roo.DomHelper.applyStyles(dbody, ss);
500         Roo.EventManager.on(this.doc, {
501             //'mousedown': this.onEditorEvent,
502             'mouseup': this.onEditorEvent,
503             'dblclick': this.onEditorEvent,
504             'click': this.onEditorEvent,
505             'keyup': this.onEditorEvent,
506             
507             buffer:100,
508             scope: this
509         });
510         Roo.EventManager.on(this.doc, {
511             'paste': this.onPasteEvent,
512             scope : this
513         });
514         if(Roo.isGecko){
515             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
516         }
517         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
518             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
519         }
520         this.initialized = true;
521
522         
523         // initialize special key events - enter
524         new Roo.htmleditor.KeyEnter({core : this});
525         
526          
527         
528         this.owner.fireEvent('initialize', this);
529         this.pushValue();
530     },
531     
532     onPasteEvent : function(e,v)
533     {
534         // I think we better assume paste is going to be a dirty load of rubish from word..
535         
536         // even pasting into a 'email version' of this widget will have to clean up that mess.
537         var cd = (e.browserEvent.clipboardData || window.clipboardData);
538         
539         // check what type of paste - if it's an image, then handle it differently.
540         if (cd.files.length > 0) {
541             // pasting images?
542             var urlAPI = (window.createObjectURL && window) || 
543                 (window.URL && URL.revokeObjectURL && URL) || 
544                 (window.webkitURL && webkitURL);
545     
546             var url = urlAPI.createObjectURL( cd.files[0]);
547             this.insertAtCursor('<img src=" + url + ">');
548             return false;
549         }
550         
551         var html = cd.getData('text/html'); // clipboard event
552         var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
553         var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
554         Roo.log(images);
555         //Roo.log(imgs);
556         // fixme..
557         images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
558                        .map(function(g) { return g.toDataURL(); });
559         
560         
561         html = this.cleanWordChars(html);
562         
563         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
564         
565         if (images.length > 0) {
566             Roo.each(d.getElementsByTagName('img'), function(img, i) {
567                 img.setAttribute('src', images[i]);
568             });
569         }
570         
571       
572         new Roo.htmleditor.FilterStyleToTag({ node : d });
573         new Roo.htmleditor.FilterAttributes({
574             node : d,
575             attrib_white : ['href', 'src', 'name', 'align'],
576             attrib_clean : ['href', 'src' ] 
577         });
578         new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
579         // should be fonts..
580         new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
581         new Roo.htmleditor.FilterParagraph({ node : d });
582         new Roo.htmleditor.FilterSpan({ node : d });
583         new Roo.htmleditor.FilterLongBr({ node : d });
584         
585         
586         
587         this.insertAtCursor(d.innerHTML);
588         
589         e.preventDefault();
590         return false;
591         // default behaveiour should be our local cleanup paste? (optional?)
592         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
593         //this.owner.fireEvent('paste', e, v);
594     },
595     // private
596     onDestroy : function(){
597         
598         
599         
600         if(this.rendered){
601             
602             //for (var i =0; i < this.toolbars.length;i++) {
603             //    // fixme - ask toolbars for heights?
604             //    this.toolbars[i].onDestroy();
605            // }
606             
607             //this.wrap.dom.innerHTML = '';
608             //this.wrap.remove();
609         }
610     },
611
612     // private
613     onFirstFocus : function(){
614         
615         this.assignDocWin();
616         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
617         
618         this.activated = true;
619          
620     
621         if(Roo.isGecko){ // prevent silly gecko errors
622             this.win.focus();
623             var s = this.win.getSelection();
624             if(!s.focusNode || s.focusNode.nodeType != 3){
625                 var r = s.getRangeAt(0);
626                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
627                 r.collapse(true);
628                 this.deferFocus();
629             }
630             try{
631                 this.execCmd('useCSS', true);
632                 this.execCmd('styleWithCSS', false);
633             }catch(e){}
634         }
635         this.owner.fireEvent('activate', this);
636     },
637
638     // private
639     adjustFont: function(btn){
640         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
641         //if(Roo.isSafari){ // safari
642         //    adjust *= 2;
643        // }
644         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
645         if(Roo.isSafari){ // safari
646             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
647             v =  (v < 10) ? 10 : v;
648             v =  (v > 48) ? 48 : v;
649             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
650             
651         }
652         
653         
654         v = Math.max(1, v+adjust);
655         
656         this.execCmd('FontSize', v  );
657     },
658
659     onEditorEvent : function(e)
660     {
661         
662         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
663             return; // we do not handle this.. (undo manager does..)
664         }
665         this.owner.fireEvent('editorevent', this, e);
666       //  this.updateToolbar();
667         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
668     },
669
670     insertTag : function(tg)
671     {
672         // could be a bit smarter... -> wrap the current selected tRoo..
673         if (tg.toLowerCase() == 'span' ||
674             tg.toLowerCase() == 'code' ||
675             tg.toLowerCase() == 'sup' ||
676             tg.toLowerCase() == 'sub' 
677             ) {
678             
679             range = this.createRange(this.getSelection());
680             var wrappingNode = this.doc.createElement(tg.toLowerCase());
681             wrappingNode.appendChild(range.extractContents());
682             range.insertNode(wrappingNode);
683
684             return;
685             
686             
687             
688         }
689         this.execCmd("formatblock",   tg);
690         this.undoManager.addEvent(); 
691     },
692     
693     insertText : function(txt)
694     {
695         
696         
697         var range = this.createRange();
698         range.deleteContents();
699                //alert(Sender.getAttribute('label'));
700                
701         range.insertNode(this.doc.createTextNode(txt));
702         this.undoManager.addEvent();
703     } ,
704     
705      
706
707     /**
708      * Executes a Midas editor command on the editor document and performs necessary focus and
709      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
710      * @param {String} cmd The Midas command
711      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
712      */
713     relayCmd : function(cmd, value){
714         this.win.focus();
715         this.execCmd(cmd, value);
716         this.owner.fireEvent('editorevent', this);
717         //this.updateToolbar();
718         this.owner.deferFocus();
719     },
720
721     /**
722      * Executes a Midas editor command directly on the editor document.
723      * For visual commands, you should use {@link #relayCmd} instead.
724      * <b>This should only be called after the editor is initialized.</b>
725      * @param {String} cmd The Midas command
726      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
727      */
728     execCmd : function(cmd, value){
729         this.doc.execCommand(cmd, false, value === undefined ? null : value);
730         this.syncValue();
731     },
732  
733  
734    
735     /**
736      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
737      * to insert tRoo.
738      * @param {String} text | dom node.. 
739      */
740     insertAtCursor : function(text)
741     {
742         
743         if(!this.activated){
744             return;
745         }
746          
747         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
748             this.win.focus();
749             
750             
751             // from jquery ui (MIT licenced)
752             var range, node;
753             var win = this.win;
754             
755             if (win.getSelection && win.getSelection().getRangeAt) {
756                 
757                 // delete the existing?
758                 
759                 this.createRange(this.getSelection()).deleteContents();
760                 range = win.getSelection().getRangeAt(0);
761                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
762                 range.insertNode(node);
763                 range = range.cloneRange();
764                 range.collapse(false);
765                  
766                 win.getSelection().removeAllRanges();
767                 win.getSelection().addRange(range);
768                 
769                 
770                 
771             } else if (win.document.selection && win.document.selection.createRange) {
772                 // no firefox support
773                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
774                 win.document.selection.createRange().pasteHTML(txt);
775             
776             } else {
777                 // no firefox support
778                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
779                 this.execCmd('InsertHTML', txt);
780             } 
781             this.syncValue();
782             
783             this.deferFocus();
784         }
785     },
786  // private
787     mozKeyPress : function(e){
788         if(e.ctrlKey){
789             var c = e.getCharCode(), cmd;
790           
791             if(c > 0){
792                 c = String.fromCharCode(c).toLowerCase();
793                 switch(c){
794                     case 'b':
795                         cmd = 'bold';
796                         break;
797                     case 'i':
798                         cmd = 'italic';
799                         break;
800                     
801                     case 'u':
802                         cmd = 'underline';
803                         break;
804                     
805                     //case 'v':
806                       //  this.cleanUpPaste.defer(100, this);
807                       //  return;
808                         
809                 }
810                 if(cmd){
811                     this.win.focus();
812                     this.execCmd(cmd);
813                     this.deferFocus();
814                     e.preventDefault();
815                 }
816                 
817             }
818         }
819     },
820
821     // private
822     fixKeys : function(){ // load time branching for fastest keydown performance
823         if(Roo.isIE){
824             return function(e){
825                 var k = e.getKey(), r;
826                 if(k == e.TAB){
827                     e.stopEvent();
828                     r = this.doc.selection.createRange();
829                     if(r){
830                         r.collapse(true);
831                         r.pasteHTML('&#160;&#160;&#160;&#160;');
832                         this.deferFocus();
833                     }
834                     return;
835                 }
836                 
837                 if(k == e.ENTER){
838                     r = this.doc.selection.createRange();
839                     if(r){
840                         var target = r.parentElement();
841                         if(!target || target.tagName.toLowerCase() != 'li'){
842                             e.stopEvent();
843                             r.pasteHTML('<br/>');
844                             r.collapse(false);
845                             r.select();
846                         }
847                     }
848                 }
849                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
850                 //    this.cleanUpPaste.defer(100, this);
851                 //    return;
852                 //}
853                 
854                 
855             };
856         }else if(Roo.isOpera){
857             return function(e){
858                 var k = e.getKey();
859                 if(k == e.TAB){
860                     e.stopEvent();
861                     this.win.focus();
862                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
863                     this.deferFocus();
864                 }
865                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
866                 //    this.cleanUpPaste.defer(100, this);
867                  //   return;
868                 //}
869                 
870             };
871         }else if(Roo.isSafari){
872             return function(e){
873                 var k = e.getKey();
874                 
875                 if(k == e.TAB){
876                     e.stopEvent();
877                     this.execCmd('InsertText','\t');
878                     this.deferFocus();
879                     return;
880                 }
881                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
882                  //   this.cleanUpPaste.defer(100, this);
883                  //   return;
884                // }
885                 
886              };
887         }
888     }(),
889     
890     getAllAncestors: function()
891     {
892         var p = this.getSelectedNode();
893         var a = [];
894         if (!p) {
895             a.push(p); // push blank onto stack..
896             p = this.getParentElement();
897         }
898         
899         
900         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
901             a.push(p);
902             p = p.parentNode;
903         }
904         a.push(this.doc.body);
905         return a;
906     },
907     lastSel : false,
908     lastSelNode : false,
909     
910     
911     getSelection : function() 
912     {
913         this.assignDocWin();
914         return Roo.isIE ? this.doc.selection : this.win.getSelection();
915     },
916     /**
917      * Select a dom node
918      * @param {DomElement} node the node to select
919      */
920     selectNode : function(node)
921     {
922         var nodeRange = node.ownerDocument.createRange();
923         try {
924             nodeRange.selectNode(node);
925         } catch (e) {
926             nodeRange.selectNodeContents(node);
927         }
928         //nodeRange.collapse(true);
929         var s = this.win.getSelection();
930         s.removeAllRanges();
931         s.addRange(nodeRange);
932     },
933     
934     getSelectedNode: function() 
935     {
936         // this may only work on Gecko!!!
937         
938         // should we cache this!!!!
939         
940         
941         
942          
943         var range = this.createRange(this.getSelection()).cloneRange();
944         
945         if (Roo.isIE) {
946             var parent = range.parentElement();
947             while (true) {
948                 var testRange = range.duplicate();
949                 testRange.moveToElementText(parent);
950                 if (testRange.inRange(range)) {
951                     break;
952                 }
953                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
954                     break;
955                 }
956                 parent = parent.parentElement;
957             }
958             return parent;
959         }
960         
961         // is ancestor a text element.
962         var ac =  range.commonAncestorContainer;
963         if (ac.nodeType == 3) {
964             ac = ac.parentNode;
965         }
966         
967         var ar = ac.childNodes;
968          
969         var nodes = [];
970         var other_nodes = [];
971         var has_other_nodes = false;
972         for (var i=0;i<ar.length;i++) {
973             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
974                 continue;
975             }
976             // fullly contained node.
977             
978             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
979                 nodes.push(ar[i]);
980                 continue;
981             }
982             
983             // probably selected..
984             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
985                 other_nodes.push(ar[i]);
986                 continue;
987             }
988             // outer..
989             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
990                 continue;
991             }
992             
993             
994             has_other_nodes = true;
995         }
996         if (!nodes.length && other_nodes.length) {
997             nodes= other_nodes;
998         }
999         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1000             return false;
1001         }
1002         
1003         return nodes[0];
1004     },
1005     createRange: function(sel)
1006     {
1007         // this has strange effects when using with 
1008         // top toolbar - not sure if it's a great idea.
1009         //this.editor.contentWindow.focus();
1010         if (typeof sel != "undefined") {
1011             try {
1012                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1013             } catch(e) {
1014                 return this.doc.createRange();
1015             }
1016         } else {
1017             return this.doc.createRange();
1018         }
1019     },
1020     getParentElement: function()
1021     {
1022         
1023         this.assignDocWin();
1024         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1025         
1026         var range = this.createRange(sel);
1027          
1028         try {
1029             var p = range.commonAncestorContainer;
1030             while (p.nodeType == 3) { // text node
1031                 p = p.parentNode;
1032             }
1033             return p;
1034         } catch (e) {
1035             return null;
1036         }
1037     
1038     },
1039     /***
1040      *
1041      * Range intersection.. the hard stuff...
1042      *  '-1' = before
1043      *  '0' = hits..
1044      *  '1' = after.
1045      *         [ -- selected range --- ]
1046      *   [fail]                        [fail]
1047      *
1048      *    basically..
1049      *      if end is before start or  hits it. fail.
1050      *      if start is after end or hits it fail.
1051      *
1052      *   if either hits (but other is outside. - then it's not 
1053      *   
1054      *    
1055      **/
1056     
1057     
1058     // @see http://www.thismuchiknow.co.uk/?p=64.
1059     rangeIntersectsNode : function(range, node)
1060     {
1061         var nodeRange = node.ownerDocument.createRange();
1062         try {
1063             nodeRange.selectNode(node);
1064         } catch (e) {
1065             nodeRange.selectNodeContents(node);
1066         }
1067     
1068         var rangeStartRange = range.cloneRange();
1069         rangeStartRange.collapse(true);
1070     
1071         var rangeEndRange = range.cloneRange();
1072         rangeEndRange.collapse(false);
1073     
1074         var nodeStartRange = nodeRange.cloneRange();
1075         nodeStartRange.collapse(true);
1076     
1077         var nodeEndRange = nodeRange.cloneRange();
1078         nodeEndRange.collapse(false);
1079     
1080         return rangeStartRange.compareBoundaryPoints(
1081                  Range.START_TO_START, nodeEndRange) == -1 &&
1082                rangeEndRange.compareBoundaryPoints(
1083                  Range.START_TO_START, nodeStartRange) == 1;
1084         
1085          
1086     },
1087     rangeCompareNode : function(range, node)
1088     {
1089         var nodeRange = node.ownerDocument.createRange();
1090         try {
1091             nodeRange.selectNode(node);
1092         } catch (e) {
1093             nodeRange.selectNodeContents(node);
1094         }
1095         
1096         
1097         range.collapse(true);
1098     
1099         nodeRange.collapse(true);
1100      
1101         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1102         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1103          
1104         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1105         
1106         var nodeIsBefore   =  ss == 1;
1107         var nodeIsAfter    = ee == -1;
1108         
1109         if (nodeIsBefore && nodeIsAfter) {
1110             return 0; // outer
1111         }
1112         if (!nodeIsBefore && nodeIsAfter) {
1113             return 1; //right trailed.
1114         }
1115         
1116         if (nodeIsBefore && !nodeIsAfter) {
1117             return 2;  // left trailed.
1118         }
1119         // fully contined.
1120         return 3;
1121     },
1122  
1123     cleanWordChars : function(input) {// change the chars to hex code
1124         
1125        var swapCodes  = [ 
1126             [    8211, "&#8211;" ], 
1127             [    8212, "&#8212;" ], 
1128             [    8216,  "'" ],  
1129             [    8217, "'" ],  
1130             [    8220, '"' ],  
1131             [    8221, '"' ],  
1132             [    8226, "*" ],  
1133             [    8230, "..." ]
1134         ]; 
1135         var output = input;
1136         Roo.each(swapCodes, function(sw) { 
1137             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1138             
1139             output = output.replace(swapper, sw[1]);
1140         });
1141         
1142         return output;
1143     },
1144     
1145      
1146     
1147         
1148     
1149     cleanUpChild : function (node)
1150     {
1151         
1152         new Roo.htmleditor.FilterComment({node : node});
1153         new Roo.htmleditor.FilterAttributes({
1154                 node : node,
1155                 attrib_black : this.ablack,
1156                 attrib_clean : this.aclean,
1157                 style_white : this.cwhite,
1158                 style_black : this.cblack
1159         });
1160         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1161         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1162          
1163         
1164     },
1165     
1166     /**
1167      * Clean up MS wordisms...
1168      * @deprecated - use filter directly
1169      */
1170     cleanWord : function(node)
1171     {
1172         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1173         
1174     },
1175    
1176     
1177     /**
1178
1179      * @deprecated - use filters
1180      */
1181     cleanTableWidths : function(node)
1182     {
1183         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1184         
1185  
1186     },
1187     
1188      
1189         
1190     applyBlacklists : function()
1191     {
1192         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1193         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1194         
1195         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1196         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1197         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1198         
1199         this.white = [];
1200         this.black = [];
1201         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1202             if (b.indexOf(tag) > -1) {
1203                 return;
1204             }
1205             this.white.push(tag);
1206             
1207         }, this);
1208         
1209         Roo.each(w, function(tag) {
1210             if (b.indexOf(tag) > -1) {
1211                 return;
1212             }
1213             if (this.white.indexOf(tag) > -1) {
1214                 return;
1215             }
1216             this.white.push(tag);
1217             
1218         }, this);
1219         
1220         
1221         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1222             if (w.indexOf(tag) > -1) {
1223                 return;
1224             }
1225             this.black.push(tag);
1226             
1227         }, this);
1228         
1229         Roo.each(b, function(tag) {
1230             if (w.indexOf(tag) > -1) {
1231                 return;
1232             }
1233             if (this.black.indexOf(tag) > -1) {
1234                 return;
1235             }
1236             this.black.push(tag);
1237             
1238         }, this);
1239         
1240         
1241         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1242         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1243         
1244         this.cwhite = [];
1245         this.cblack = [];
1246         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1247             if (b.indexOf(tag) > -1) {
1248                 return;
1249             }
1250             this.cwhite.push(tag);
1251             
1252         }, this);
1253         
1254         Roo.each(w, function(tag) {
1255             if (b.indexOf(tag) > -1) {
1256                 return;
1257             }
1258             if (this.cwhite.indexOf(tag) > -1) {
1259                 return;
1260             }
1261             this.cwhite.push(tag);
1262             
1263         }, this);
1264         
1265         
1266         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1267             if (w.indexOf(tag) > -1) {
1268                 return;
1269             }
1270             this.cblack.push(tag);
1271             
1272         }, this);
1273         
1274         Roo.each(b, function(tag) {
1275             if (w.indexOf(tag) > -1) {
1276                 return;
1277             }
1278             if (this.cblack.indexOf(tag) > -1) {
1279                 return;
1280             }
1281             this.cblack.push(tag);
1282             
1283         }, this);
1284     },
1285     
1286     setStylesheets : function(stylesheets)
1287     {
1288         if(typeof(stylesheets) == 'string'){
1289             Roo.get(this.iframe.contentDocument.head).createChild({
1290                 tag : 'link',
1291                 rel : 'stylesheet',
1292                 type : 'text/css',
1293                 href : stylesheets
1294             });
1295             
1296             return;
1297         }
1298         var _this = this;
1299      
1300         Roo.each(stylesheets, function(s) {
1301             if(!s.length){
1302                 return;
1303             }
1304             
1305             Roo.get(_this.iframe.contentDocument.head).createChild({
1306                 tag : 'link',
1307                 rel : 'stylesheet',
1308                 type : 'text/css',
1309                 href : s
1310             });
1311         });
1312
1313         
1314     },
1315     
1316     removeStylesheets : function()
1317     {
1318         var _this = this;
1319         
1320         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1321             s.remove();
1322         });
1323     },
1324     
1325     setStyle : function(style)
1326     {
1327         Roo.get(this.iframe.contentDocument.head).createChild({
1328             tag : 'style',
1329             type : 'text/css',
1330             html : style
1331         });
1332
1333         return;
1334     }
1335     
1336     // hide stuff that is not compatible
1337     /**
1338      * @event blur
1339      * @hide
1340      */
1341     /**
1342      * @event change
1343      * @hide
1344      */
1345     /**
1346      * @event focus
1347      * @hide
1348      */
1349     /**
1350      * @event specialkey
1351      * @hide
1352      */
1353     /**
1354      * @cfg {String} fieldClass @hide
1355      */
1356     /**
1357      * @cfg {String} focusClass @hide
1358      */
1359     /**
1360      * @cfg {String} autoCreate @hide
1361      */
1362     /**
1363      * @cfg {String} inputType @hide
1364      */
1365     /**
1366      * @cfg {String} invalidClass @hide
1367      */
1368     /**
1369      * @cfg {String} invalidText @hide
1370      */
1371     /**
1372      * @cfg {String} msgFx @hide
1373      */
1374     /**
1375      * @cfg {String} validateOnBlur @hide
1376      */
1377 });
1378
1379 Roo.HtmlEditorCore.white = [
1380         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1381         
1382        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1383        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1384        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1385        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1386        'TABLE',   'UL',         'XMP', 
1387        
1388        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1389       'THEAD',   'TR', 
1390      
1391       'DIR', 'MENU', 'OL', 'UL', 'DL',
1392        
1393       'EMBED',  'OBJECT'
1394 ];
1395
1396
1397 Roo.HtmlEditorCore.black = [
1398     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1399         'APPLET', // 
1400         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1401         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1402         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1403         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1404         //'FONT' // CLEAN LATER..
1405         'COLGROUP', 'COL'  // messy tables.
1406         
1407 ];
1408 Roo.HtmlEditorCore.clean = [ // ?? needed???
1409      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1410 ];
1411 Roo.HtmlEditorCore.tag_remove = [
1412     'FONT', 'TBODY'  
1413 ];
1414 // attributes..
1415
1416 Roo.HtmlEditorCore.ablack = [
1417     'on'
1418 ];
1419     
1420 Roo.HtmlEditorCore.aclean = [ 
1421     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1422 ];
1423
1424 // protocols..
1425 Roo.HtmlEditorCore.pwhite= [
1426         'http',  'https',  'mailto'
1427 ];
1428
1429 // white listed style attributes.
1430 Roo.HtmlEditorCore.cwhite= [
1431       //  'text-align', /// default is to allow most things..
1432       
1433          
1434 //        'font-size'//??
1435 ];
1436
1437 // black listed style attributes.
1438 Roo.HtmlEditorCore.cblack= [
1439       //  'font-size' -- this can be set by the project 
1440 ];
1441
1442
1443
1444
1445