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