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