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