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