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