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