sync
[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         var cd = (e.browserEvent.clipboardData || window.clipboardData);
531         
532         var html = cd.getData('text/html'); // clipboard event
533         html = this.cleanWordChars(html);
534         
535         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
536          Roo.log(cd.getData('text/rtf'));
537          Roo.log(cd.getData('text/richtext'));
538         
539         Roo.each(cd.items, function(item) {
540             Roo.log(item);
541         });
542         new Roo.htmleditor.FilterStyleToTag({ node : d });
543         new Roo.htmleditor.FilterAttributes({
544             node : d,
545             attrib_white : ['href', 'src', 'name'],
546             attrib_clean : ['href', 'src', 'name'] 
547         });
548         new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
549         // should be fonts..
550         new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
551         new Roo.htmleditor.FilterParagraph({ node : d });
552         new Roo.htmleditor.FilterSpan({ node : d });
553         new Roo.htmleditor.FilterLongBr({ node : d });
554         
555         
556         
557         this.insertAtCursor(d.innerHTML);
558         
559         e.preventDefault();
560         return false;
561         // default behaveiour should be our local cleanup paste? (optional?)
562         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
563         //this.owner.fireEvent('paste', e, v);
564     },
565     // private
566     onDestroy : function(){
567         
568         
569         
570         if(this.rendered){
571             
572             //for (var i =0; i < this.toolbars.length;i++) {
573             //    // fixme - ask toolbars for heights?
574             //    this.toolbars[i].onDestroy();
575            // }
576             
577             //this.wrap.dom.innerHTML = '';
578             //this.wrap.remove();
579         }
580     },
581
582     // private
583     onFirstFocus : function(){
584         
585         this.assignDocWin();
586         
587         
588         this.activated = true;
589          
590     
591         if(Roo.isGecko){ // prevent silly gecko errors
592             this.win.focus();
593             var s = this.win.getSelection();
594             if(!s.focusNode || s.focusNode.nodeType != 3){
595                 var r = s.getRangeAt(0);
596                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
597                 r.collapse(true);
598                 this.deferFocus();
599             }
600             try{
601                 this.execCmd('useCSS', true);
602                 this.execCmd('styleWithCSS', false);
603             }catch(e){}
604         }
605         this.owner.fireEvent('activate', this);
606     },
607
608     // private
609     adjustFont: function(btn){
610         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
611         //if(Roo.isSafari){ // safari
612         //    adjust *= 2;
613        // }
614         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
615         if(Roo.isSafari){ // safari
616             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
617             v =  (v < 10) ? 10 : v;
618             v =  (v > 48) ? 48 : v;
619             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
620             
621         }
622         
623         
624         v = Math.max(1, v+adjust);
625         
626         this.execCmd('FontSize', v  );
627     },
628
629     onEditorEvent : function(e)
630     {
631         this.owner.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' ||
640             tg.toLowerCase() == 'code' ||
641             tg.toLowerCase() == 'sup' ||
642             tg.toLowerCase() == 'sub' 
643             ) {
644             
645             range = this.createRange(this.getSelection());
646             var wrappingNode = this.doc.createElement(tg.toLowerCase());
647             wrappingNode.appendChild(range.extractContents());
648             range.insertNode(wrappingNode);
649
650             return;
651             
652             
653             
654         }
655         this.execCmd("formatblock",   tg);
656         
657     },
658     
659     insertText : function(txt)
660     {
661         
662         
663         var range = this.createRange();
664         range.deleteContents();
665                //alert(Sender.getAttribute('label'));
666                
667         range.insertNode(this.doc.createTextNode(txt));
668     } ,
669     
670      
671
672     /**
673      * Executes a Midas editor command on the editor document and performs necessary focus and
674      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
675      * @param {String} cmd The Midas command
676      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
677      */
678     relayCmd : function(cmd, value){
679         this.win.focus();
680         this.execCmd(cmd, value);
681         this.owner.fireEvent('editorevent', this);
682         //this.updateToolbar();
683         this.owner.deferFocus();
684     },
685
686     /**
687      * Executes a Midas editor command directly on the editor document.
688      * For visual commands, you should use {@link #relayCmd} instead.
689      * <b>This should only be called after the editor is initialized.</b>
690      * @param {String} cmd The Midas command
691      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
692      */
693     execCmd : function(cmd, value){
694         this.doc.execCommand(cmd, false, value === undefined ? null : value);
695         this.syncValue();
696     },
697  
698  
699    
700     /**
701      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
702      * to insert tRoo.
703      * @param {String} text | dom node.. 
704      */
705     insertAtCursor : function(text)
706     {
707         
708         if(!this.activated){
709             return;
710         }
711          
712         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
713             this.win.focus();
714             
715             
716             // from jquery ui (MIT licenced)
717             var range, node;
718             var win = this.win;
719             
720             if (win.getSelection && win.getSelection().getRangeAt) {
721                 
722                 // delete the existing?
723                 
724                 this.createRange(this.getSelection()).deleteContents();
725                 range = win.getSelection().getRangeAt(0);
726                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
727                 range.insertNode(node);
728             } else if (win.document.selection && win.document.selection.createRange) {
729                 // no firefox support
730                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
731                 win.document.selection.createRange().pasteHTML(txt);
732             } else {
733                 // no firefox support
734                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
735                 this.execCmd('InsertHTML', txt);
736             } 
737             
738             this.syncValue();
739             
740             this.deferFocus();
741         }
742     },
743  // private
744     mozKeyPress : function(e){
745         if(e.ctrlKey){
746             var c = e.getCharCode(), cmd;
747           
748             if(c > 0){
749                 c = String.fromCharCode(c).toLowerCase();
750                 switch(c){
751                     case 'b':
752                         cmd = 'bold';
753                         break;
754                     case 'i':
755                         cmd = 'italic';
756                         break;
757                     
758                     case 'u':
759                         cmd = 'underline';
760                         break;
761                     
762                     //case 'v':
763                       //  this.cleanUpPaste.defer(100, this);
764                       //  return;
765                         
766                 }
767                 if(cmd){
768                     this.win.focus();
769                     this.execCmd(cmd);
770                     this.deferFocus();
771                     e.preventDefault();
772                 }
773                 
774             }
775         }
776     },
777
778     // private
779     fixKeys : function(){ // load time branching for fastest keydown performance
780         if(Roo.isIE){
781             return function(e){
782                 var k = e.getKey(), r;
783                 if(k == e.TAB){
784                     e.stopEvent();
785                     r = this.doc.selection.createRange();
786                     if(r){
787                         r.collapse(true);
788                         r.pasteHTML('&#160;&#160;&#160;&#160;');
789                         this.deferFocus();
790                     }
791                     return;
792                 }
793                 
794                 if(k == e.ENTER){
795                     r = this.doc.selection.createRange();
796                     if(r){
797                         var target = r.parentElement();
798                         if(!target || target.tagName.toLowerCase() != 'li'){
799                             e.stopEvent();
800                             r.pasteHTML('<br/>');
801                             r.collapse(false);
802                             r.select();
803                         }
804                     }
805                 }
806                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
807                 //    this.cleanUpPaste.defer(100, this);
808                 //    return;
809                 //}
810                 
811                 
812             };
813         }else if(Roo.isOpera){
814             return function(e){
815                 var k = e.getKey();
816                 if(k == e.TAB){
817                     e.stopEvent();
818                     this.win.focus();
819                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
820                     this.deferFocus();
821                 }
822                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
823                 //    this.cleanUpPaste.defer(100, this);
824                  //   return;
825                 //}
826                 
827             };
828         }else if(Roo.isSafari){
829             return function(e){
830                 var k = e.getKey();
831                 
832                 if(k == e.TAB){
833                     e.stopEvent();
834                     this.execCmd('InsertText','\t');
835                     this.deferFocus();
836                     return;
837                 }
838                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
839                  //   this.cleanUpPaste.defer(100, this);
840                  //   return;
841                // }
842                 
843              };
844         }
845     }(),
846     
847     getAllAncestors: function()
848     {
849         var p = this.getSelectedNode();
850         var a = [];
851         if (!p) {
852             a.push(p); // push blank onto stack..
853             p = this.getParentElement();
854         }
855         
856         
857         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
858             a.push(p);
859             p = p.parentNode;
860         }
861         a.push(this.doc.body);
862         return a;
863     },
864     lastSel : false,
865     lastSelNode : false,
866     
867     
868     getSelection : function() 
869     {
870         this.assignDocWin();
871         return Roo.isIE ? this.doc.selection : this.win.getSelection();
872     },
873     /**
874      * Select a dom node
875      * @param {DomElement} node the node to select
876      */
877     selectNode : function(node)
878     {
879         
880             var nodeRange = node.ownerDocument.createRange();
881             try {
882                 nodeRange.selectNode(node);
883             } catch (e) {
884                 nodeRange.selectNodeContents(node);
885             }
886             //nodeRange.collapse(true);
887             var s = this.win.getSelection();
888             s.removeAllRanges();
889             s.addRange(nodeRange);
890     },
891     
892     getSelectedNode: function() 
893     {
894         // this may only work on Gecko!!!
895         
896         // should we cache this!!!!
897         
898         
899         
900          
901         var range = this.createRange(this.getSelection()).cloneRange();
902         
903         if (Roo.isIE) {
904             var parent = range.parentElement();
905             while (true) {
906                 var testRange = range.duplicate();
907                 testRange.moveToElementText(parent);
908                 if (testRange.inRange(range)) {
909                     break;
910                 }
911                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
912                     break;
913                 }
914                 parent = parent.parentElement;
915             }
916             return parent;
917         }
918         
919         // is ancestor a text element.
920         var ac =  range.commonAncestorContainer;
921         if (ac.nodeType == 3) {
922             ac = ac.parentNode;
923         }
924         
925         var ar = ac.childNodes;
926          
927         var nodes = [];
928         var other_nodes = [];
929         var has_other_nodes = false;
930         for (var i=0;i<ar.length;i++) {
931             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
932                 continue;
933             }
934             // fullly contained node.
935             
936             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
937                 nodes.push(ar[i]);
938                 continue;
939             }
940             
941             // probably selected..
942             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
943                 other_nodes.push(ar[i]);
944                 continue;
945             }
946             // outer..
947             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
948                 continue;
949             }
950             
951             
952             has_other_nodes = true;
953         }
954         if (!nodes.length && other_nodes.length) {
955             nodes= other_nodes;
956         }
957         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
958             return false;
959         }
960         
961         return nodes[0];
962     },
963     createRange: function(sel)
964     {
965         // this has strange effects when using with 
966         // top toolbar - not sure if it's a great idea.
967         //this.editor.contentWindow.focus();
968         if (typeof sel != "undefined") {
969             try {
970                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
971             } catch(e) {
972                 return this.doc.createRange();
973             }
974         } else {
975             return this.doc.createRange();
976         }
977     },
978     getParentElement: function()
979     {
980         
981         this.assignDocWin();
982         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
983         
984         var range = this.createRange(sel);
985          
986         try {
987             var p = range.commonAncestorContainer;
988             while (p.nodeType == 3) { // text node
989                 p = p.parentNode;
990             }
991             return p;
992         } catch (e) {
993             return null;
994         }
995     
996     },
997     /***
998      *
999      * Range intersection.. the hard stuff...
1000      *  '-1' = before
1001      *  '0' = hits..
1002      *  '1' = after.
1003      *         [ -- selected range --- ]
1004      *   [fail]                        [fail]
1005      *
1006      *    basically..
1007      *      if end is before start or  hits it. fail.
1008      *      if start is after end or hits it fail.
1009      *
1010      *   if either hits (but other is outside. - then it's not 
1011      *   
1012      *    
1013      **/
1014     
1015     
1016     // @see http://www.thismuchiknow.co.uk/?p=64.
1017     rangeIntersectsNode : function(range, node)
1018     {
1019         var nodeRange = node.ownerDocument.createRange();
1020         try {
1021             nodeRange.selectNode(node);
1022         } catch (e) {
1023             nodeRange.selectNodeContents(node);
1024         }
1025     
1026         var rangeStartRange = range.cloneRange();
1027         rangeStartRange.collapse(true);
1028     
1029         var rangeEndRange = range.cloneRange();
1030         rangeEndRange.collapse(false);
1031     
1032         var nodeStartRange = nodeRange.cloneRange();
1033         nodeStartRange.collapse(true);
1034     
1035         var nodeEndRange = nodeRange.cloneRange();
1036         nodeEndRange.collapse(false);
1037     
1038         return rangeStartRange.compareBoundaryPoints(
1039                  Range.START_TO_START, nodeEndRange) == -1 &&
1040                rangeEndRange.compareBoundaryPoints(
1041                  Range.START_TO_START, nodeStartRange) == 1;
1042         
1043          
1044     },
1045     rangeCompareNode : function(range, node)
1046     {
1047         var nodeRange = node.ownerDocument.createRange();
1048         try {
1049             nodeRange.selectNode(node);
1050         } catch (e) {
1051             nodeRange.selectNodeContents(node);
1052         }
1053         
1054         
1055         range.collapse(true);
1056     
1057         nodeRange.collapse(true);
1058      
1059         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1060         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1061          
1062         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1063         
1064         var nodeIsBefore   =  ss == 1;
1065         var nodeIsAfter    = ee == -1;
1066         
1067         if (nodeIsBefore && nodeIsAfter) {
1068             return 0; // outer
1069         }
1070         if (!nodeIsBefore && nodeIsAfter) {
1071             return 1; //right trailed.
1072         }
1073         
1074         if (nodeIsBefore && !nodeIsAfter) {
1075             return 2;  // left trailed.
1076         }
1077         // fully contined.
1078         return 3;
1079     },
1080  
1081     cleanWordChars : function(input) {// change the chars to hex code
1082         
1083        var swapCodes  = [ 
1084             [    8211, "&#8211;" ], 
1085             [    8212, "&#8212;" ], 
1086             [    8216,  "'" ],  
1087             [    8217, "'" ],  
1088             [    8220, '"' ],  
1089             [    8221, '"' ],  
1090             [    8226, "*" ],  
1091             [    8230, "..." ]
1092         ]; 
1093         var output = input;
1094         Roo.each(swapCodes, function(sw) { 
1095             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1096             
1097             output = output.replace(swapper, sw[1]);
1098         });
1099         
1100         return output;
1101     },
1102     
1103      
1104     
1105         
1106     
1107     cleanUpChild : function (node)
1108     {
1109         
1110         new Roo.htmleditor.FilterComment({node : node});
1111         new Roo.htmleditor.FilterAttributes({
1112                 node : node,
1113                 attrib_black : this.ablack,
1114                 attrib_clean : this.aclean,
1115                 style_white : this.cwhite,
1116                 style_black : this.cblack
1117         });
1118         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1119         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1120          
1121         
1122     },
1123     
1124     /**
1125      * Clean up MS wordisms...
1126      * @deprecated - use filter directly
1127      */
1128     cleanWord : function(node)
1129     {
1130         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1131         
1132     },
1133    
1134     
1135     /**
1136
1137      * @deprecated - use filters
1138      */
1139     cleanTableWidths : function(node)
1140     {
1141         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1142         
1143  
1144     },
1145     
1146      
1147         
1148     applyBlacklists : function()
1149     {
1150         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1151         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1152         
1153         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1154         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1155         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1156         
1157         this.white = [];
1158         this.black = [];
1159         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1160             if (b.indexOf(tag) > -1) {
1161                 return;
1162             }
1163             this.white.push(tag);
1164             
1165         }, this);
1166         
1167         Roo.each(w, function(tag) {
1168             if (b.indexOf(tag) > -1) {
1169                 return;
1170             }
1171             if (this.white.indexOf(tag) > -1) {
1172                 return;
1173             }
1174             this.white.push(tag);
1175             
1176         }, this);
1177         
1178         
1179         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1180             if (w.indexOf(tag) > -1) {
1181                 return;
1182             }
1183             this.black.push(tag);
1184             
1185         }, this);
1186         
1187         Roo.each(b, function(tag) {
1188             if (w.indexOf(tag) > -1) {
1189                 return;
1190             }
1191             if (this.black.indexOf(tag) > -1) {
1192                 return;
1193             }
1194             this.black.push(tag);
1195             
1196         }, this);
1197         
1198         
1199         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1200         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1201         
1202         this.cwhite = [];
1203         this.cblack = [];
1204         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1205             if (b.indexOf(tag) > -1) {
1206                 return;
1207             }
1208             this.cwhite.push(tag);
1209             
1210         }, this);
1211         
1212         Roo.each(w, function(tag) {
1213             if (b.indexOf(tag) > -1) {
1214                 return;
1215             }
1216             if (this.cwhite.indexOf(tag) > -1) {
1217                 return;
1218             }
1219             this.cwhite.push(tag);
1220             
1221         }, this);
1222         
1223         
1224         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1225             if (w.indexOf(tag) > -1) {
1226                 return;
1227             }
1228             this.cblack.push(tag);
1229             
1230         }, this);
1231         
1232         Roo.each(b, function(tag) {
1233             if (w.indexOf(tag) > -1) {
1234                 return;
1235             }
1236             if (this.cblack.indexOf(tag) > -1) {
1237                 return;
1238             }
1239             this.cblack.push(tag);
1240             
1241         }, this);
1242     },
1243     
1244     setStylesheets : function(stylesheets)
1245     {
1246         if(typeof(stylesheets) == 'string'){
1247             Roo.get(this.iframe.contentDocument.head).createChild({
1248                 tag : 'link',
1249                 rel : 'stylesheet',
1250                 type : 'text/css',
1251                 href : stylesheets
1252             });
1253             
1254             return;
1255         }
1256         var _this = this;
1257      
1258         Roo.each(stylesheets, function(s) {
1259             if(!s.length){
1260                 return;
1261             }
1262             
1263             Roo.get(_this.iframe.contentDocument.head).createChild({
1264                 tag : 'link',
1265                 rel : 'stylesheet',
1266                 type : 'text/css',
1267                 href : s
1268             });
1269         });
1270
1271         
1272     },
1273     
1274     removeStylesheets : function()
1275     {
1276         var _this = this;
1277         
1278         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1279             s.remove();
1280         });
1281     },
1282     
1283     setStyle : function(style)
1284     {
1285         Roo.get(this.iframe.contentDocument.head).createChild({
1286             tag : 'style',
1287             type : 'text/css',
1288             html : style
1289         });
1290
1291         return;
1292     }
1293     
1294     // hide stuff that is not compatible
1295     /**
1296      * @event blur
1297      * @hide
1298      */
1299     /**
1300      * @event change
1301      * @hide
1302      */
1303     /**
1304      * @event focus
1305      * @hide
1306      */
1307     /**
1308      * @event specialkey
1309      * @hide
1310      */
1311     /**
1312      * @cfg {String} fieldClass @hide
1313      */
1314     /**
1315      * @cfg {String} focusClass @hide
1316      */
1317     /**
1318      * @cfg {String} autoCreate @hide
1319      */
1320     /**
1321      * @cfg {String} inputType @hide
1322      */
1323     /**
1324      * @cfg {String} invalidClass @hide
1325      */
1326     /**
1327      * @cfg {String} invalidText @hide
1328      */
1329     /**
1330      * @cfg {String} msgFx @hide
1331      */
1332     /**
1333      * @cfg {String} validateOnBlur @hide
1334      */
1335 });
1336
1337 Roo.HtmlEditorCore.white = [
1338         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1339         
1340        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1341        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1342        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1343        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1344        'TABLE',   'UL',         'XMP', 
1345        
1346        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1347       'THEAD',   'TR', 
1348      
1349       'DIR', 'MENU', 'OL', 'UL', 'DL',
1350        
1351       'EMBED',  'OBJECT'
1352 ];
1353
1354
1355 Roo.HtmlEditorCore.black = [
1356     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1357         'APPLET', // 
1358         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1359         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1360         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1361         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1362         //'FONT' // CLEAN LATER..
1363         'COLGROUP', 'COL'  // messy tables.
1364         
1365 ];
1366 Roo.HtmlEditorCore.clean = [ // ?? needed???
1367      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1368 ];
1369 Roo.HtmlEditorCore.tag_remove = [
1370     'FONT', 'TBODY'  
1371 ];
1372 // attributes..
1373
1374 Roo.HtmlEditorCore.ablack = [
1375     'on'
1376 ];
1377     
1378 Roo.HtmlEditorCore.aclean = [ 
1379     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1380 ];
1381
1382 // protocols..
1383 Roo.HtmlEditorCore.pwhite= [
1384         'http',  'https',  'mailto'
1385 ];
1386
1387 // white listed style attributes.
1388 Roo.HtmlEditorCore.cwhite= [
1389       //  'text-align', /// default is to allow most things..
1390       
1391          
1392 //        'font-size'//??
1393 ];
1394
1395 // black listed style attributes.
1396 Roo.HtmlEditorCore.cblack= [
1397       //  'font-size' -- this can be set by the project 
1398 ];
1399
1400
1401
1402
1403