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