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