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