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