Roo/form/HtmlEditor.js
[roojs1] / Roo / form / HtmlEditor.js
1 //<script type="text/javascript">
2
3 /*
4  * Ext JS Library 1.1.1
5  * Copyright(c) 2006-2007, Ext JS, LLC.
6  * licensing@extjs.com
7  * 
8  * http://www.extjs.com/license
9  */
10  
11  /*
12   * 
13   * Known bugs:
14   * Default CSS appears to render it as fixed text by default (should really be Sans-Serif)
15   * - IE ? - no idea how much works there.
16   * 
17   * 
18   * 
19   */
20  
21
22 /**
23  * @class Ext.form.HtmlEditor
24  * @extends Ext.form.Field
25  * Provides a lightweight HTML Editor component.
26  * WARNING - THIS CURRENTlY ONLY WORKS ON FIREFOX - USE FCKeditor for a cross platform version
27  * 
28  * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
29  * supported by this editor.</b><br/><br/>
30  * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
31  * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
32  */
33 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
34       /**
35      * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
36      */
37     toolbars : false,
38     /**
39      * @cfg {String} createLinkText The default text for the create link prompt
40      */
41     createLinkText : 'Please enter the URL for the link:',
42     /**
43      * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
44      */
45     defaultLinkValue : 'http:/'+'/',
46    
47      /**
48      * @cfg {String} resizable  's' or 'se' or 'e' - wrapps the element in a
49      *                        Roo.resizable.
50      */
51     resizable : false,
52      /**
53      * @cfg {Number} height (in pixels)
54      */   
55     height: 300,
56    /**
57      * @cfg {Number} width (in pixels)
58      */   
59     width: 500,
60     
61     /**
62      * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
63      * 
64      */
65     stylesheets: false,
66     
67     // id of frame..
68     frameId: false,
69     
70     // private properties
71     validationEvent : false,
72     deferHeight: true,
73     initialized : false,
74     activated : false,
75     sourceEditMode : false,
76     onFocus : Roo.emptyFn,
77     iframePad:3,
78     hideMode:'offsets',
79     
80     defaultAutoCreate : { // modified by initCompnoent..
81         tag: "textarea",
82         style:"width:500px;height:300px;",
83         autocomplete: "off"
84     },
85
86     // private
87     initComponent : function(){
88         this.addEvents({
89             /**
90              * @event initialize
91              * Fires when the editor is fully initialized (including the iframe)
92              * @param {HtmlEditor} this
93              */
94             initialize: true,
95             /**
96              * @event activate
97              * Fires when the editor is first receives the focus. Any insertion must wait
98              * until after this event.
99              * @param {HtmlEditor} this
100              */
101             activate: true,
102              /**
103              * @event beforesync
104              * Fires before the textarea is updated with content from the editor iframe. Return false
105              * to cancel the sync.
106              * @param {HtmlEditor} this
107              * @param {String} html
108              */
109             beforesync: true,
110              /**
111              * @event beforepush
112              * Fires before the iframe editor is updated with content from the textarea. Return false
113              * to cancel the push.
114              * @param {HtmlEditor} this
115              * @param {String} html
116              */
117             beforepush: true,
118              /**
119              * @event sync
120              * Fires when the textarea is updated with content from the editor iframe.
121              * @param {HtmlEditor} this
122              * @param {String} html
123              */
124             sync: true,
125              /**
126              * @event push
127              * Fires when the iframe editor is updated with content from the textarea.
128              * @param {HtmlEditor} this
129              * @param {String} html
130              */
131             push: true,
132              /**
133              * @event editmodechange
134              * Fires when the editor switches edit modes
135              * @param {HtmlEditor} this
136              * @param {Boolean} sourceEdit True if source edit, false if standard editing.
137              */
138             editmodechange: true,
139             /**
140              * @event editorevent
141              * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
142              * @param {HtmlEditor} this
143              */
144             editorevent: true
145         });
146         this.defaultAutoCreate =  {
147             tag: "textarea",
148             style:'width: ' + this.width + 'px;height: ' + this.height + 'px;',
149             autocomplete: "off"
150         };
151     },
152
153     /**
154      * Protected method that will not generally be called directly. It
155      * is called when the editor creates its toolbar. Override this method if you need to
156      * add custom toolbar buttons.
157      * @param {HtmlEditor} editor
158      */
159     createToolbar : function(editor){
160         if (!editor.toolbars || !editor.toolbars.length) {
161             editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
162         }
163         
164         for (var i =0 ; i < editor.toolbars.length;i++) {
165             editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
166             editor.toolbars[i].init(editor);
167         }
168          
169         
170     },
171
172     /**
173      * Protected method that will not generally be called directly. It
174      * is called when the editor initializes the iframe with HTML contents. Override this method if you
175      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
176      */
177     getDocMarkup : function(){
178         // body styles..
179         var st = '';
180         if (this.stylesheets === false) {
181             
182             Roo.get(document.head).select('style').each(function(node) {
183                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
184             });
185             
186             Roo.get(document.head).select('link').each(function(node) { 
187                 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
188             });
189             
190         } else if (!this.stylesheets.length) {
191                 // simple..
192                 st = '<style type="text/css">' +
193                     'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
194                    '</style>';
195         } else {
196             Roo.each(this.stylesheets, function(s) {
197                 st += '<link rel="stylesheet" type="text/css" href="' + s +'" />'
198             });
199             
200         }
201         
202         return '<html><head>' + st  +
203             //<style type="text/css">' +
204             //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
205             //'</style>' +
206             ' </head><body></body></html>';
207     },
208
209     // private
210     onRender : function(ct, position)
211     {
212         var _t = this;
213         Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
214         this.el.dom.style.border = '0 none';
215         this.el.dom.setAttribute('tabIndex', -1);
216         this.el.addClass('x-hidden');
217         if(Roo.isIE){ // fix IE 1px bogus margin
218             this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
219         }
220         this.wrap = this.el.wrap({
221             cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
222         });
223         
224         if (this.resizable) {
225             this.resizeEl = new Roo.Resizable(this.wrap, {
226                 pinned : true,
227                 wrap: true,
228                 dynamic : true,
229                 minHeight : this.height,
230                 height: this.height,
231                 handles : this.resizable,
232                 width: this.width,
233                 listeners : {
234                     resize : function(r, w, h) {
235                         _t.onResize(w,h); // -something
236                     }
237                 }
238             });
239             
240         }
241
242         this.frameId = Roo.id();
243         
244         this.createToolbar(this);
245         
246       
247         
248         var iframe = this.wrap.createChild({
249             tag: 'iframe',
250             id: this.frameId,
251             name: this.frameId,
252             frameBorder : 'no',
253             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
254         }, this.el
255         );
256         
257        // console.log(iframe);
258         //this.wrap.dom.appendChild(iframe);
259
260         this.iframe = iframe.dom;
261
262          this.assignDocWin();
263         
264         this.doc.designMode = 'on';
265        
266         this.doc.open();
267         this.doc.write(this.getDocMarkup());
268         this.doc.close();
269
270         
271         var task = { // must defer to wait for browser to be ready
272             run : function(){
273                 //console.log("run task?" + this.doc.readyState);
274                 this.assignDocWin();
275                 if(this.doc.body || this.doc.readyState == 'complete'){
276                     try {
277                         this.doc.designMode="on";
278                     } catch (e) {
279                         return;
280                     }
281                     Roo.TaskMgr.stop(task);
282                     this.initEditor.defer(10, this);
283                 }
284             },
285             interval : 10,
286             duration:10000,
287             scope: this
288         };
289         Roo.TaskMgr.start(task);
290
291         if(!this.width){
292             this.setSize(this.wrap.getSize());
293         }
294         if (this.resizeEl) {
295             this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
296             // should trigger onReize..
297         }
298     },
299
300     // private
301     onResize : function(w, h)
302     {
303         //Roo.log('resize: ' +w + ',' + h );
304         Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
305         if(this.el && this.iframe){
306             if(typeof w == 'number'){
307                 var aw = w - this.wrap.getFrameWidth('lr');
308                 this.el.setWidth(this.adjustWidth('textarea', aw));
309                 this.iframe.style.width = aw + 'px';
310             }
311             if(typeof h == 'number'){
312                 var tbh = 0;
313                 for (var i =0; i < this.toolbars.length;i++) {
314                     // fixme - ask toolbars for heights?
315                     tbh += this.toolbars[i].tb.el.getHeight();
316                     if (this.toolbars[i].footer) {
317                         tbh += this.toolbars[i].footer.el.getHeight();
318                     }
319                 }
320                 
321                 
322                 
323                 
324                 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
325                 ah -= 5; // knock a few pixes off for look..
326                 this.el.setHeight(this.adjustWidth('textarea', ah));
327                 this.iframe.style.height = ah + 'px';
328                 if(this.doc){
329                     (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
330                 }
331             }
332         }
333     },
334
335     /**
336      * Toggles the editor between standard and source edit mode.
337      * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
338      */
339     toggleSourceEdit : function(sourceEditMode){
340         
341         this.sourceEditMode = sourceEditMode === true;
342         
343         if(this.sourceEditMode){
344           
345             this.syncValue();
346             this.iframe.className = 'x-hidden';
347             this.el.removeClass('x-hidden');
348             this.el.dom.removeAttribute('tabIndex');
349             this.el.focus();
350         }else{
351              
352             this.pushValue();
353             this.iframe.className = '';
354             this.el.addClass('x-hidden');
355             this.el.dom.setAttribute('tabIndex', -1);
356             this.deferFocus();
357         }
358         this.setSize(this.wrap.getSize());
359         this.fireEvent('editmodechange', this, this.sourceEditMode);
360     },
361
362     // private used internally
363     createLink : function(){
364         var url = prompt(this.createLinkText, this.defaultLinkValue);
365         if(url && url != 'http:/'+'/'){
366             this.relayCmd('createlink', url);
367         }
368     },
369
370     // private (for BoxComponent)
371     adjustSize : Roo.BoxComponent.prototype.adjustSize,
372
373     // private (for BoxComponent)
374     getResizeEl : function(){
375         return this.wrap;
376     },
377
378     // private (for BoxComponent)
379     getPositionEl : function(){
380         return this.wrap;
381     },
382
383     // private
384     initEvents : function(){
385         this.originalValue = this.getValue();
386     },
387
388     /**
389      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
390      * @method
391      */
392     markInvalid : Roo.emptyFn,
393     /**
394      * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
395      * @method
396      */
397     clearInvalid : Roo.emptyFn,
398
399     setValue : function(v){
400         Roo.form.HtmlEditor.superclass.setValue.call(this, v);
401         this.pushValue();
402     },
403
404     /**
405      * Protected method that will not generally be called directly. If you need/want
406      * custom HTML cleanup, this is the method you should override.
407      * @param {String} html The HTML to be cleaned
408      * return {String} The cleaned HTML
409      */
410     cleanHtml : function(html){
411         html = String(html);
412         if(html.length > 5){
413             if(Roo.isSafari){ // strip safari nonsense
414                 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
415             }
416         }
417         if(html == '&nbsp;'){
418             html = '';
419         }
420         return html;
421     },
422
423     /**
424      * Protected method that will not generally be called directly. Syncs the contents
425      * of the editor iframe with the textarea.
426      */
427     syncValue : function(){
428         if(this.initialized){
429             var bd = (this.doc.body || this.doc.documentElement);
430             //this.cleanUpPaste();
431             var html = bd.innerHTML;
432             if(Roo.isSafari){
433                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
434                 var m = bs.match(/text-align:(.*?);/i);
435                 if(m && m[1]){
436                     html = '<div style="'+m[0]+'">' + html + '</div>';
437                 }
438             }
439             html = this.cleanHtml(html);
440             // fix up the special chars..
441             html.replace(/([\x80-\uffff])/g, function (a, b) {
442                 return "&#"+b.charCodeAt()+";" 
443             });
444             if(this.fireEvent('beforesync', this, html) !== false){
445                 this.el.dom.value = html;
446                 this.fireEvent('sync', this, html);
447             }
448         }
449     },
450
451     /**
452      * Protected method that will not generally be called directly. Pushes the value of the textarea
453      * into the iframe editor.
454      */
455     pushValue : function(){
456         if(this.initialized){
457             var v = this.el.dom.value;
458             if(v.length < 1){
459                 v = '&#160;';
460             }
461             
462             if(this.fireEvent('beforepush', this, v) !== false){
463                 var d = (this.doc.body || this.doc.documentElement);
464                 d.innerHTML = v;
465                 this.cleanUpPaste();
466                 this.el.dom.value = d.innerHTML;
467                 this.fireEvent('push', this, v);
468             }
469         }
470     },
471
472     // private
473     deferFocus : function(){
474         this.focus.defer(10, this);
475     },
476
477     // doc'ed in Field
478     focus : function(){
479         if(this.win && !this.sourceEditMode){
480             this.win.focus();
481         }else{
482             this.el.focus();
483         }
484     },
485     
486     assignDocWin: function()
487     {
488         var iframe = this.iframe;
489         
490          if(Roo.isIE){
491             this.doc = iframe.contentWindow.document;
492             this.win = iframe.contentWindow;
493         } else {
494             if (!Roo.get(this.frameId)) {
495                 return;
496             }
497             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
498             this.win = Roo.get(this.frameId).dom.contentWindow;
499         }
500     },
501     
502     // private
503     initEditor : function(){
504         //console.log("INIT EDITOR");
505         this.assignDocWin();
506         
507         
508         
509         this.doc.designMode="on";
510         this.doc.open();
511         this.doc.write(this.getDocMarkup());
512         this.doc.close();
513         
514         var dbody = (this.doc.body || this.doc.documentElement);
515         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
516         // this copies styles from the containing element into thsi one..
517         // not sure why we need all of this..
518         var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
519         ss['background-attachment'] = 'fixed'; // w3c
520         dbody.bgProperties = 'fixed'; // ie
521         Roo.DomHelper.applyStyles(dbody, ss);
522         Roo.EventManager.on(this.doc, {
523             //'mousedown': this.onEditorEvent,
524             'mouseup': this.onEditorEvent,
525             'dblclick': this.onEditorEvent,
526             'click': this.onEditorEvent,
527             'keyup': this.onEditorEvent,
528             buffer:100,
529             scope: this
530         });
531         if(Roo.isGecko){
532             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
533         }
534         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
535             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
536         }
537         this.initialized = true;
538
539         this.fireEvent('initialize', this);
540         this.pushValue();
541     },
542
543     // private
544     onDestroy : function(){
545         
546         
547         
548         if(this.rendered){
549             
550             for (var i =0; i < this.toolbars.length;i++) {
551                 // fixme - ask toolbars for heights?
552                 this.toolbars[i].onDestroy();
553             }
554             
555             this.wrap.dom.innerHTML = '';
556             this.wrap.remove();
557         }
558     },
559
560     // private
561     onFirstFocus : function(){
562         
563         this.assignDocWin();
564         
565         
566         this.activated = true;
567         for (var i =0; i < this.toolbars.length;i++) {
568             this.toolbars[i].onFirstFocus();
569         }
570        
571         if(Roo.isGecko){ // prevent silly gecko errors
572             this.win.focus();
573             var s = this.win.getSelection();
574             if(!s.focusNode || s.focusNode.nodeType != 3){
575                 var r = s.getRangeAt(0);
576                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
577                 r.collapse(true);
578                 this.deferFocus();
579             }
580             try{
581                 this.execCmd('useCSS', true);
582                 this.execCmd('styleWithCSS', false);
583             }catch(e){}
584         }
585         this.fireEvent('activate', this);
586     },
587
588     // private
589     adjustFont: function(btn){
590         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
591         //if(Roo.isSafari){ // safari
592         //    adjust *= 2;
593        // }
594         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
595         if(Roo.isSafari){ // safari
596             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
597             v =  (v < 10) ? 10 : v;
598             v =  (v > 48) ? 48 : v;
599             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
600             
601         }
602         
603         
604         v = Math.max(1, v+adjust);
605         
606         this.execCmd('FontSize', v  );
607     },
608
609     onEditorEvent : function(e){
610         this.fireEvent('editorevent', this, e);
611       //  this.updateToolbar();
612         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
613     },
614
615     insertTag : function(tg)
616     {
617         // could be a bit smarter... -> wrap the current selected tRoo..
618         
619         this.execCmd("formatblock",   tg);
620         
621     },
622     
623     insertText : function(txt)
624     {
625         
626         
627         range = this.createRange();
628         range.deleteContents();
629                //alert(Sender.getAttribute('label'));
630                
631         range.insertNode(this.doc.createTextNode(txt));
632     } ,
633     
634     // private
635     relayBtnCmd : function(btn){
636         this.relayCmd(btn.cmd);
637     },
638
639     /**
640      * Executes a Midas editor command on the editor document and performs necessary focus and
641      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
642      * @param {String} cmd The Midas command
643      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
644      */
645     relayCmd : function(cmd, value){
646         this.win.focus();
647         this.execCmd(cmd, value);
648         this.fireEvent('editorevent', this);
649         //this.updateToolbar();
650         this.deferFocus();
651     },
652
653     /**
654      * Executes a Midas editor command directly on the editor document.
655      * For visual commands, you should use {@link #relayCmd} instead.
656      * <b>This should only be called after the editor is initialized.</b>
657      * @param {String} cmd The Midas command
658      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
659      */
660     execCmd : function(cmd, value){
661         this.doc.execCommand(cmd, false, value === undefined ? null : value);
662         this.syncValue();
663     },
664
665    
666     /**
667      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
668      * to insert tRoo.
669      * @param {String} text | dom node.. 
670      */
671     insertAtCursor : function(text)
672     {
673         
674         
675         
676         if(!this.activated){
677             return;
678         }
679         /*
680         if(Roo.isIE){
681             this.win.focus();
682             var r = this.doc.selection.createRange();
683             if(r){
684                 r.collapse(true);
685                 r.pasteHTML(text);
686                 this.syncValue();
687                 this.deferFocus();
688             
689             }
690             return;
691         }
692         */
693         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
694             this.win.focus();
695             
696             
697             // from jquery ui (MIT licenced)
698             var range, node;
699             var win = this.win;
700             
701             if (win.getSelection && win.getSelection().getRangeAt) {
702                 range = win.getSelection().getRangeAt(0);
703                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
704                 range.insertNode(node);
705             } else if (win.document.selection && win.document.selection.createRange) {
706                 // no firefox support
707                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
708                 win.document.selection.createRange().pasteHTML(txt);
709             } else {
710                 // no firefox support
711                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
712                 this.execCmd('InsertHTML', txt);
713             } 
714             
715             this.syncValue();
716             
717             this.deferFocus();
718         }
719     },
720  // private
721     mozKeyPress : function(e){
722         if(e.ctrlKey){
723             var c = e.getCharCode(), cmd;
724           
725             if(c > 0){
726                 c = String.fromCharCode(c).toLowerCase();
727                 switch(c){
728                     case 'b':
729                         cmd = 'bold';
730                     break;
731                     case 'i':
732                         cmd = 'italic';
733                     break;
734                     case 'u':
735                         cmd = 'underline';
736                         break;
737                     case 'v':
738                         this.cleanUpPaste.defer(100, this);
739                         return;
740                     break;
741                 }
742                 if(cmd){
743                     this.win.focus();
744                     this.execCmd(cmd);
745                     this.deferFocus();
746                     e.preventDefault();
747                 }
748                 
749             }
750         }
751     },
752
753     // private
754     fixKeys : function(){ // load time branching for fastest keydown performance
755         if(Roo.isIE){
756             return function(e){
757                 var k = e.getKey(), r;
758                 if(k == e.TAB){
759                     e.stopEvent();
760                     r = this.doc.selection.createRange();
761                     if(r){
762                         r.collapse(true);
763                         r.pasteHTML('&#160;&#160;&#160;&#160;');
764                         this.deferFocus();
765                     }
766                     return;
767                 }
768                 
769                 if(k == e.ENTER){
770                     r = this.doc.selection.createRange();
771                     if(r){
772                         var target = r.parentElement();
773                         if(!target || target.tagName.toLowerCase() != 'li'){
774                             e.stopEvent();
775                             r.pasteHTML('<br />');
776                             r.collapse(false);
777                             r.select();
778                         }
779                     }
780                 }
781                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
782                     this.cleanUpPaste.defer(100, this);
783                     return;
784                 }
785                 
786                 
787             };
788         }else if(Roo.isOpera){
789             return function(e){
790                 var k = e.getKey();
791                 if(k == e.TAB){
792                     e.stopEvent();
793                     this.win.focus();
794                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
795                     this.deferFocus();
796                 }
797                 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
798                     this.cleanUpPaste.defer(100, this);
799                     return;
800                 }
801                 
802             };
803         }else if(Roo.isSafari){
804             return function(e){
805                 var k = e.getKey();
806                 
807                 if(k == e.TAB){
808                     e.stopEvent();
809                     this.execCmd('InsertText','\t');
810                     this.deferFocus();
811                     return;
812                 }
813                if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
814                     this.cleanUpPaste.defer(100, this);
815                     return;
816                 }
817                 
818              };
819         }
820     }(),
821     
822     getAllAncestors: function()
823     {
824         var p = this.getSelectedNode();
825         var a = [];
826         if (!p) {
827             a.push(p); // push blank onto stack..
828             p = this.getParentElement();
829         }
830         
831         
832         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
833             a.push(p);
834             p = p.parentNode;
835         }
836         a.push(this.doc.body);
837         return a;
838     },
839     lastSel : false,
840     lastSelNode : false,
841     
842     
843     getSelection : function() 
844     {
845         this.assignDocWin();
846         return Roo.isIE ? this.doc.selection : this.win.getSelection();
847     },
848     
849     getSelectedNode: function() 
850     {
851         // this may only work on Gecko!!!
852         
853         // should we cache this!!!!
854         
855         
856         
857          
858         var range = this.createRange(this.getSelection()).cloneRange();
859         
860         if (Roo.isIE) {
861             var parent = range.parentElement();
862             while (true) {
863                 var testRange = range.duplicate();
864                 testRange.moveToElementText(parent);
865                 if (testRange.inRange(range)) {
866                     break;
867                 }
868                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
869                     break;
870                 }
871                 parent = parent.parentElement;
872             }
873             return parent;
874         }
875         
876         // is ancestor a text element.
877         var ac =  range.commonAncestorContainer;
878         if (ac.nodeType == 3) {
879             ac = ac.parentNode;
880         }
881         
882         var ar = ac.childNodes;
883          
884         var nodes = [];
885         var other_nodes = [];
886         var has_other_nodes = false;
887         for (var i=0;i<ar.length;i++) {
888             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
889                 continue;
890             }
891             // fullly contained node.
892             
893             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
894                 nodes.push(ar[i]);
895                 continue;
896             }
897             
898             // probably selected..
899             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
900                 other_nodes.push(ar[i]);
901                 continue;
902             }
903             // outer..
904             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
905                 continue;
906             }
907             
908             
909             has_other_nodes = true;
910         }
911         if (!nodes.length && other_nodes.length) {
912             nodes= other_nodes;
913         }
914         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
915             return false;
916         }
917         
918         return nodes[0];
919     },
920     createRange: function(sel)
921     {
922         // this has strange effects when using with 
923         // top toolbar - not sure if it's a great idea.
924         //this.editor.contentWindow.focus();
925         if (typeof sel != "undefined") {
926             try {
927                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
928             } catch(e) {
929                 return this.doc.createRange();
930             }
931         } else {
932             return this.doc.createRange();
933         }
934     },
935     getParentElement: function()
936     {
937         
938         this.assignDocWin();
939         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
940         
941         var range = this.createRange(sel);
942          
943         try {
944             var p = range.commonAncestorContainer;
945             while (p.nodeType == 3) { // text node
946                 p = p.parentNode;
947             }
948             return p;
949         } catch (e) {
950             return null;
951         }
952     
953     },
954     /***
955      *
956      * Range intersection.. the hard stuff...
957      *  '-1' = before
958      *  '0' = hits..
959      *  '1' = after.
960      *         [ -- selected range --- ]
961      *   [fail]                        [fail]
962      *
963      *    basically..
964      *      if end is before start or  hits it. fail.
965      *      if start is after end or hits it fail.
966      *
967      *   if either hits (but other is outside. - then it's not 
968      *   
969      *    
970      **/
971     
972     
973     // @see http://www.thismuchiknow.co.uk/?p=64.
974     rangeIntersectsNode : function(range, node)
975     {
976         var nodeRange = node.ownerDocument.createRange();
977         try {
978             nodeRange.selectNode(node);
979         } catch (e) {
980             nodeRange.selectNodeContents(node);
981         }
982     
983         var rangeStartRange = range.cloneRange();
984         rangeStartRange.collapse(true);
985     
986         var rangeEndRange = range.cloneRange();
987         rangeEndRange.collapse(false);
988     
989         var nodeStartRange = nodeRange.cloneRange();
990         nodeStartRange.collapse(true);
991     
992         var nodeEndRange = nodeRange.cloneRange();
993         nodeEndRange.collapse(false);
994     
995         return rangeStartRange.compareBoundaryPoints(
996                  Range.START_TO_START, nodeEndRange) == -1 &&
997                rangeEndRange.compareBoundaryPoints(
998                  Range.START_TO_START, nodeStartRange) == 1;
999         
1000          
1001     },
1002     rangeCompareNode : function(range, node)
1003     {
1004         var nodeRange = node.ownerDocument.createRange();
1005         try {
1006             nodeRange.selectNode(node);
1007         } catch (e) {
1008             nodeRange.selectNodeContents(node);
1009         }
1010         
1011         
1012         range.collapse(true);
1013     
1014         nodeRange.collapse(true);
1015      
1016         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1017         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1018          
1019         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1020         
1021         var nodeIsBefore   =  ss == 1;
1022         var nodeIsAfter    = ee == -1;
1023         
1024         if (nodeIsBefore && nodeIsAfter)
1025             return 0; // outer
1026         if (!nodeIsBefore && nodeIsAfter)
1027             return 1; //right trailed.
1028         
1029         if (nodeIsBefore && !nodeIsAfter)
1030             return 2;  // left trailed.
1031         // fully contined.
1032         return 3;
1033     },
1034
1035     // private? - in a new class?
1036     cleanUpPaste :  function()
1037     {
1038         // cleans up the whole document..
1039          Roo.log('cleanuppaste');
1040         this.cleanUpChildren(this.doc.body);
1041         var clean = this.cleanWordChars(this.doc.body.innerHTML);
1042         if (clean != this.doc.body.innerHTML) {
1043             this.doc.body.innerHTML = clean;
1044         }
1045         
1046     },
1047     
1048     cleanWordChars : function(input) {
1049         var he = Roo.form.HtmlEditor;
1050     
1051         var output = input;
1052         Roo.each(he.swapCodes, function(sw) { 
1053         
1054             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1055             output = output.replace(swapper, sw[1]);
1056         });
1057         return output;
1058     },
1059     
1060     
1061     cleanUpChildren : function (n)
1062     {
1063         if (!n.childNodes.length) {
1064             return;
1065         }
1066         for (var i = n.childNodes.length-1; i > -1 ; i--) {
1067            this.cleanUpChild(n.childNodes[i]);
1068         }
1069     },
1070     
1071     
1072         
1073     
1074     cleanUpChild : function (node)
1075     {
1076         //console.log(node);
1077         if (node.nodeName == "#text") {
1078             // clean up silly Windows -- stuff?
1079             return; 
1080         }
1081         if (node.nodeName == "#comment") {
1082             node.parentNode.removeChild(node);
1083             // clean up silly Windows -- stuff?
1084             return; 
1085         }
1086         
1087         if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1088             // remove node.
1089             node.parentNode.removeChild(node);
1090             return;
1091             
1092         }
1093         
1094         var remove_keep_children= Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1;
1095         
1096         // remove <a name=....> as rendering on yahoo mailer is bored with this.
1097         
1098         if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1099             remove_keep_children = true;
1100         }
1101         
1102         if (remove_keep_children) {
1103             this.cleanUpChildren(node);
1104             // inserts everything just before this node...
1105             while (node.childNodes.length) {
1106                 var cn = node.childNodes[0];
1107                 node.removeChild(cn);
1108                 node.parentNode.insertBefore(cn, node);
1109             }
1110             node.parentNode.removeChild(node);
1111             return;
1112         }
1113         
1114         if (!node.attributes || !node.attributes.length) {
1115             this.cleanUpChildren(node);
1116             return;
1117         }
1118         
1119         function cleanAttr(n,v)
1120         {
1121             
1122             if (v.match(/^\./) || v.match(/^\//)) {
1123                 return;
1124             }
1125             if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1126                 return;
1127             }
1128             Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1129             node.removeAttribute(n);
1130             
1131         }
1132         
1133         function cleanStyle(n,v)
1134         {
1135             if (v.match(/expression/)) { //XSS?? should we even bother..
1136                 node.removeAttribute(n);
1137                 return;
1138             }
1139             
1140             
1141             var parts = v.split(/;/);
1142             Roo.each(parts, function(p) {
1143                 p = p.replace(/\s+/g,'');
1144                 if (!p.length) {
1145                     return true;
1146                 }
1147                 var l = p.split(':').shift().replace(/\s+/g,'');
1148                 
1149                 // only allow 'c whitelisted system attributes'
1150                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1151                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1152                     node.removeAttribute(n);
1153                     return false;
1154                 }
1155                 return true;
1156             });
1157             
1158             
1159         }
1160         
1161         
1162         for (var i = node.attributes.length-1; i > -1 ; i--) {
1163             var a = node.attributes[i];
1164             //console.log(a);
1165             if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1166                 node.removeAttribute(a.name);
1167                 return;
1168             }
1169             if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1170                 cleanAttr(a.name,a.value); // fixme..
1171                 return;
1172             }
1173             if (a.name == 'style') {
1174                 cleanStyle(a.name,a.value);
1175             }
1176             /// clean up MS crap..
1177             // tecnically this should be a list of valid class'es..
1178             
1179             
1180             if (a.name == 'class') {
1181                 if (a.value.match(/^Mso/)) {
1182                     node.className = '';
1183                 }
1184                 
1185                 if (a.value.match(/body/)) {
1186                     node.className = '';
1187                 }
1188             }
1189             
1190             // style cleanup!?
1191             // class cleanup?
1192             
1193         }
1194         
1195         
1196         this.cleanUpChildren(node);
1197         
1198         
1199     }
1200     
1201     
1202     // hide stuff that is not compatible
1203     /**
1204      * @event blur
1205      * @hide
1206      */
1207     /**
1208      * @event change
1209      * @hide
1210      */
1211     /**
1212      * @event focus
1213      * @hide
1214      */
1215     /**
1216      * @event specialkey
1217      * @hide
1218      */
1219     /**
1220      * @cfg {String} fieldClass @hide
1221      */
1222     /**
1223      * @cfg {String} focusClass @hide
1224      */
1225     /**
1226      * @cfg {String} autoCreate @hide
1227      */
1228     /**
1229      * @cfg {String} inputType @hide
1230      */
1231     /**
1232      * @cfg {String} invalidClass @hide
1233      */
1234     /**
1235      * @cfg {String} invalidText @hide
1236      */
1237     /**
1238      * @cfg {String} msgFx @hide
1239      */
1240     /**
1241      * @cfg {String} validateOnBlur @hide
1242      */
1243 });
1244
1245 Roo.form.HtmlEditor.white = [
1246         'area', 'br', 'img', 'input', 'hr', 'wbr',
1247         
1248        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1249        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1250        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1251        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1252        'table',   'ul',         'xmp', 
1253        
1254        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1255       'thead',   'tr', 
1256      
1257       'dir', 'menu', 'ol', 'ul', 'dl',
1258        
1259       'embed',  'object'
1260 ];
1261
1262
1263 Roo.form.HtmlEditor.black = [
1264     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1265         'applet', // 
1266         'base',   'basefont', 'bgsound', 'blink',  'body', 
1267         'frame',  'frameset', 'head',    'html',   'ilayer', 
1268         'iframe', 'layer',  'link',     'meta',    'object',   
1269         'script', 'style' ,'title',  'xml' // clean later..
1270 ];
1271 Roo.form.HtmlEditor.clean = [
1272     'script', 'style', 'title', 'xml'
1273 ];
1274 Roo.form.HtmlEditor.remove = [
1275     'font'
1276 ];
1277 // attributes..
1278
1279 Roo.form.HtmlEditor.ablack = [
1280     'on'
1281 ];
1282     
1283 Roo.form.HtmlEditor.aclean = [ 
1284     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1285 ];
1286
1287 // protocols..
1288 Roo.form.HtmlEditor.pwhite= [
1289         'http',  'https',  'mailto'
1290 ];
1291
1292 // white listed style attributes.
1293 Roo.form.HtmlEditor.cwhite= [
1294         'text-align',
1295         'font-size'
1296 ];
1297
1298
1299 Roo.form.HtmlEditor.swapCodes   =[ 
1300     [    8211, "--" ], 
1301     [    8212, "--" ], 
1302     [    8216,  "'" ],  
1303     [    8217, "'" ],  
1304     [    8220, '"' ],  
1305     [    8221, '"' ],  
1306     [    8226, "*" ],  
1307     [    8230, "..." ]
1308 ]; 
1309
1310