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