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