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