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