roojs-all.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     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             
361             //?? tidy?
362             new Roo.htmleditor.FilterBlock({ node : div });
363             
364             var html = div.innerHTML;
365             if(Roo.isSafari){
366                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
367                 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
368                 if(m && m[1]){
369                     html = '<div style="'+m[0]+'">' + html + '</div>';
370                 }
371             }
372             html = this.cleanHtml(html);
373             // fix up the special chars.. normaly like back quotes in word...
374             // however we do not want to do this with chinese..
375             html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
376                 
377                 var cc = match.charCodeAt();
378
379                 // Get the character value, handling surrogate pairs
380                 if (match.length == 2) {
381                     // It's a surrogate pair, calculate the Unicode code point
382                     var high = match.charCodeAt(0) - 0xD800;
383                     var low  = match.charCodeAt(1) - 0xDC00;
384                     cc = (high * 0x400) + low + 0x10000;
385                 }  else if (
386                     (cc >= 0x4E00 && cc < 0xA000 ) ||
387                     (cc >= 0x3400 && cc < 0x4E00 ) ||
388                     (cc >= 0xf900 && cc < 0xfb00 )
389                 ) {
390                         return match;
391                 }  
392          
393                 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
394                 return "&#" + cc + ";";
395                 
396                 
397             });
398             
399             
400              
401             if(this.owner.fireEvent('beforesync', this, html) !== false){
402                 this.el.dom.value = html;
403                 this.owner.fireEvent('sync', this, html);
404             }
405         }
406     },
407
408     /**
409      * TEXTAREA -> EDITABLE
410      * Protected method that will not generally be called directly. Pushes the value of the textarea
411      * into the iframe editor.
412      */
413     pushValue : function()
414     {
415         //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
416         if(this.initialized){
417             var v = this.el.dom.value.trim();
418             
419             
420             if(this.owner.fireEvent('beforepush', this, v) !== false){
421                 var d = (this.doc.body || this.doc.documentElement);
422                 d.innerHTML = v;
423                  
424                 this.el.dom.value = d.innerHTML;
425                 this.owner.fireEvent('push', this, v);
426             }
427             Roo.htmleditor.Block.initAll(this.doc.body);
428             
429             var lc = this.doc.body.lastChild;
430             if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
431                 // add an extra line at the end.
432                 this.doc.body.appendChild(this.doc.createElement('br'));
433             }
434             
435             
436         }
437     },
438
439     // private
440     deferFocus : function(){
441         this.focus.defer(10, this);
442     },
443
444     // doc'ed in Field
445     focus : function(){
446         if(this.win && !this.sourceEditMode){
447             this.win.focus();
448         }else{
449             this.el.focus();
450         }
451     },
452     
453     assignDocWin: function()
454     {
455         var iframe = this.iframe;
456         
457          if(Roo.isIE){
458             this.doc = iframe.contentWindow.document;
459             this.win = iframe.contentWindow;
460         } else {
461 //            if (!Roo.get(this.frameId)) {
462 //                return;
463 //            }
464 //            this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
465 //            this.win = Roo.get(this.frameId).dom.contentWindow;
466             
467             if (!Roo.get(this.frameId) && !iframe.contentDocument) {
468                 return;
469             }
470             
471             this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
472             this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
473         }
474     },
475     
476     // private
477     initEditor : function(){
478         //console.log("INIT EDITOR");
479         this.assignDocWin();
480         
481         
482         
483         this.doc.designMode="on";
484         this.doc.open();
485         this.doc.write(this.getDocMarkup());
486         this.doc.close();
487         
488         var dbody = (this.doc.body || this.doc.documentElement);
489         //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
490         // this copies styles from the containing element into thsi one..
491         // not sure why we need all of this..
492         //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
493         
494         //var ss = this.el.getStyles( 'background-image', 'background-repeat');
495         //ss['background-attachment'] = 'fixed'; // w3c
496         dbody.bgProperties = 'fixed'; // ie
497         //Roo.DomHelper.applyStyles(dbody, ss);
498         Roo.EventManager.on(this.doc, {
499             //'mousedown': this.onEditorEvent,
500             'mouseup': this.onEditorEvent,
501             'dblclick': this.onEditorEvent,
502             'click': this.onEditorEvent,
503             'keyup': this.onEditorEvent,
504             
505             buffer:100,
506             scope: this
507         });
508         Roo.EventManager.on(this.doc, {
509             'paste': this.onPasteEvent,
510             scope : this
511         });
512         if(Roo.isGecko){
513             Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
514         }
515         if(Roo.isIE || Roo.isSafari || Roo.isOpera){
516             Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
517         }
518         this.initialized = true;
519
520         
521         // initialize special key events - enter
522         new Roo.htmleditor.KeyEnter({core : this});
523         
524          
525         
526         this.owner.fireEvent('initialize', this);
527         this.pushValue();
528     },
529     
530     onPasteEvent : function(e,v)
531     {
532         // I think we better assume paste is going to be a dirty load of rubish from word..
533         
534         // even pasting into a 'email version' of this widget will have to clean up that mess.
535         var cd = (e.browserEvent.clipboardData || window.clipboardData);
536         
537         // check what type of paste - if it's an image, then handle it differently.
538         if (cd.files.length > 0) {
539             // pasting images?
540             var urlAPI = (window.createObjectURL && window) || 
541                 (window.URL && URL.revokeObjectURL && URL) || 
542                 (window.webkitURL && webkitURL);
543     
544             var url = urlAPI.createObjectURL( cd.files[0]);
545             this.insertAtCursor('<img src=" + url + ">');
546             return false;
547         }
548         
549         var html = cd.getData('text/html'); // clipboard event
550         var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
551         var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
552         Roo.log(images);
553         //Roo.log(imgs);
554         // fixme..
555         images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
556                        .map(function(g) { return g.toDataURL(); });
557         
558         
559         html = this.cleanWordChars(html);
560         
561         var d = (new DOMParser().parseFromString(html, 'text/html')).body;
562         
563         
564         var sn = this.getParentElement();
565         // check if d contains a table, and prevent nesting??
566         //Roo.log(d.getElementsByTagName('table'));
567         //Roo.log(sn);
568         //Roo.log(sn.closest('table'));
569         if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
570             e.preventDefault();
571             this.insertAtCursor("You can not nest tables");
572             //Roo.log("prevent?"); // fixme - 
573             return false;
574         }
575         
576         if (images.length > 0) {
577             Roo.each(d.getElementsByTagName('img'), function(img, i) {
578                 img.setAttribute('src', images[i]);
579             });
580         }
581         
582       
583         new Roo.htmleditor.FilterStyleToTag({ node : d });
584         new Roo.htmleditor.FilterAttributes({
585             node : d,
586             attrib_white : ['href', 'src', 'name', 'align'],
587             attrib_clean : ['href', 'src' ] 
588         });
589         new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
590         // should be fonts..
591         new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
592         new Roo.htmleditor.FilterParagraph({ node : d });
593         new Roo.htmleditor.FilterSpan({ node : d });
594         new Roo.htmleditor.FilterLongBr({ node : d });
595         
596         
597         
598         this.insertAtCursor(d.innerHTML);
599         Roo.htmleditor.Block.initAll(this.doc.body);
600         
601         
602         e.preventDefault();
603         return false;
604         // default behaveiour should be our local cleanup paste? (optional?)
605         // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
606         //this.owner.fireEvent('paste', e, v);
607     },
608     // private
609     onDestroy : function(){
610         
611         
612         
613         if(this.rendered){
614             
615             //for (var i =0; i < this.toolbars.length;i++) {
616             //    // fixme - ask toolbars for heights?
617             //    this.toolbars[i].onDestroy();
618            // }
619             
620             //this.wrap.dom.innerHTML = '';
621             //this.wrap.remove();
622         }
623     },
624
625     // private
626     onFirstFocus : function(){
627         
628         this.assignDocWin();
629         this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
630         
631         this.activated = true;
632          
633     
634         if(Roo.isGecko){ // prevent silly gecko errors
635             this.win.focus();
636             var s = this.win.getSelection();
637             if(!s.focusNode || s.focusNode.nodeType != 3){
638                 var r = s.getRangeAt(0);
639                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
640                 r.collapse(true);
641                 this.deferFocus();
642             }
643             try{
644                 this.execCmd('useCSS', true);
645                 this.execCmd('styleWithCSS', false);
646             }catch(e){}
647         }
648         this.owner.fireEvent('activate', this);
649     },
650
651     // private
652     adjustFont: function(btn){
653         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
654         //if(Roo.isSafari){ // safari
655         //    adjust *= 2;
656        // }
657         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
658         if(Roo.isSafari){ // safari
659             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
660             v =  (v < 10) ? 10 : v;
661             v =  (v > 48) ? 48 : v;
662             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
663             
664         }
665         
666         
667         v = Math.max(1, v+adjust);
668         
669         this.execCmd('FontSize', v  );
670     },
671
672     onEditorEvent : function(e)
673     {
674         
675         if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
676             return; // we do not handle this.. (undo manager does..)
677         }
678         // in theory this detects if the last element is not a br, then we try and do that.
679         // its so clicking in space at bottom triggers adding a br and moving the cursor.
680         if (e &&
681             e.target.nodeName == 'BODY' &&
682             e.type == "mouseup" &&
683             this.doc.body.lastChild
684            ) {
685             var lc = this.doc.body.lastChild;
686             while (lc.nodeType == 3 && lc.nodeValue == '') {
687                 lc = lc.previousSibling;
688             }
689             if (lc.nodeType == 1 && lc.nodeName != 'BR') {
690             // if last element is <BR> - then dont do anything.
691             
692                 var ns = this.doc.createElement('br');
693                 this.doc.body.appendChild(ns);
694                 range = this.doc.createRange();
695                 range.setStartAfter(ns);
696                 range.collapse(true);
697                 var sel = this.win.getSelection();
698                 sel.removeAllRanges();
699                 sel.addRange(range);
700             }
701         }
702         
703         
704         
705         this.owner.fireEvent('editorevent', this, e);
706       //  this.updateToolbar();
707         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
708     },
709
710     insertTag : function(tg)
711     {
712         // could be a bit smarter... -> wrap the current selected tRoo..
713         if (tg.toLowerCase() == 'span' ||
714             tg.toLowerCase() == 'code' ||
715             tg.toLowerCase() == 'sup' ||
716             tg.toLowerCase() == 'sub' 
717             ) {
718             
719             range = this.createRange(this.getSelection());
720             var wrappingNode = this.doc.createElement(tg.toLowerCase());
721             wrappingNode.appendChild(range.extractContents());
722             range.insertNode(wrappingNode);
723
724             return;
725             
726             
727             
728         }
729         this.execCmd("formatblock",   tg);
730         this.undoManager.addEvent(); 
731     },
732     
733     insertText : function(txt)
734     {
735         
736         
737         var range = this.createRange();
738         range.deleteContents();
739                //alert(Sender.getAttribute('label'));
740                
741         range.insertNode(this.doc.createTextNode(txt));
742         this.undoManager.addEvent();
743     } ,
744     
745      
746
747     /**
748      * Executes a Midas editor command on the editor document and performs necessary focus and
749      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
750      * @param {String} cmd The Midas command
751      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
752      */
753     relayCmd : function(cmd, value){
754         this.win.focus();
755         this.execCmd(cmd, value);
756         this.owner.fireEvent('editorevent', this);
757         //this.updateToolbar();
758         this.owner.deferFocus();
759     },
760
761     /**
762      * Executes a Midas editor command directly on the editor document.
763      * For visual commands, you should use {@link #relayCmd} instead.
764      * <b>This should only be called after the editor is initialized.</b>
765      * @param {String} cmd The Midas command
766      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
767      */
768     execCmd : function(cmd, value){
769         this.doc.execCommand(cmd, false, value === undefined ? null : value);
770         this.syncValue();
771     },
772  
773  
774    
775     /**
776      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
777      * to insert tRoo.
778      * @param {String} text | dom node.. 
779      */
780     insertAtCursor : function(text)
781     {
782         
783         if(!this.activated){
784             return;
785         }
786          
787         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
788             this.win.focus();
789             
790             
791             // from jquery ui (MIT licenced)
792             var range, node;
793             var win = this.win;
794             
795             if (win.getSelection && win.getSelection().getRangeAt) {
796                 
797                 // delete the existing?
798                 
799                 this.createRange(this.getSelection()).deleteContents();
800                 range = win.getSelection().getRangeAt(0);
801                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
802                 range.insertNode(node);
803                 range = range.cloneRange();
804                 range.collapse(false);
805                  
806                 win.getSelection().removeAllRanges();
807                 win.getSelection().addRange(range);
808                 
809                 
810                 
811             } else if (win.document.selection && win.document.selection.createRange) {
812                 // no firefox support
813                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
814                 win.document.selection.createRange().pasteHTML(txt);
815             
816             } else {
817                 // no firefox support
818                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
819                 this.execCmd('InsertHTML', txt);
820             } 
821             this.syncValue();
822             
823             this.deferFocus();
824         }
825     },
826  // private
827     mozKeyPress : function(e){
828         if(e.ctrlKey){
829             var c = e.getCharCode(), cmd;
830           
831             if(c > 0){
832                 c = String.fromCharCode(c).toLowerCase();
833                 switch(c){
834                     case 'b':
835                         cmd = 'bold';
836                         break;
837                     case 'i':
838                         cmd = 'italic';
839                         break;
840                     
841                     case 'u':
842                         cmd = 'underline';
843                         break;
844                     
845                     //case 'v':
846                       //  this.cleanUpPaste.defer(100, this);
847                       //  return;
848                         
849                 }
850                 if(cmd){
851                     this.win.focus();
852                     this.execCmd(cmd);
853                     this.deferFocus();
854                     e.preventDefault();
855                 }
856                 
857             }
858         }
859     },
860
861     // private
862     fixKeys : function(){ // load time branching for fastest keydown performance
863         if(Roo.isIE){
864             return function(e){
865                 var k = e.getKey(), r;
866                 if(k == e.TAB){
867                     e.stopEvent();
868                     r = this.doc.selection.createRange();
869                     if(r){
870                         r.collapse(true);
871                         r.pasteHTML('&#160;&#160;&#160;&#160;');
872                         this.deferFocus();
873                     }
874                     return;
875                 }
876                 /// this is handled by Roo.htmleditor.KeyEnter
877                  /*
878                 if(k == e.ENTER){
879                     r = this.doc.selection.createRange();
880                     if(r){
881                         var target = r.parentElement();
882                         if(!target || target.tagName.toLowerCase() != 'li'){
883                             e.stopEvent();
884                             r.pasteHTML('<br/>');
885                             r.collapse(false);
886                             r.select();
887                         }
888                     }
889                 }
890                 */
891                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
892                 //    this.cleanUpPaste.defer(100, this);
893                 //    return;
894                 //}
895                 
896                 
897             };
898         }else if(Roo.isOpera){
899             return function(e){
900                 var k = e.getKey();
901                 if(k == e.TAB){
902                     e.stopEvent();
903                     this.win.focus();
904                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
905                     this.deferFocus();
906                 }
907                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
908                 //    this.cleanUpPaste.defer(100, this);
909                  //   return;
910                 //}
911                 
912             };
913         }else if(Roo.isSafari){
914             return function(e){
915                 var k = e.getKey();
916                 
917                 if(k == e.TAB){
918                     e.stopEvent();
919                     this.execCmd('InsertText','\t');
920                     this.deferFocus();
921                     return;
922                 }
923                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
924                  //   this.cleanUpPaste.defer(100, this);
925                  //   return;
926                // }
927                 
928              };
929         }
930     }(),
931     
932     getAllAncestors: function()
933     {
934         var p = this.getSelectedNode();
935         var a = [];
936         if (!p) {
937             a.push(p); // push blank onto stack..
938             p = this.getParentElement();
939         }
940         
941         
942         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
943             a.push(p);
944             p = p.parentNode;
945         }
946         a.push(this.doc.body);
947         return a;
948     },
949     lastSel : false,
950     lastSelNode : false,
951     
952     
953     getSelection : function() 
954     {
955         this.assignDocWin();
956         return Roo.isIE ? this.doc.selection : this.win.getSelection();
957     },
958     /**
959      * Select a dom node
960      * @param {DomElement} node the node to select
961      */
962     selectNode : function(node)
963     {
964         var nodeRange = node.ownerDocument.createRange();
965         try {
966             nodeRange.selectNode(node);
967         } catch (e) {
968             nodeRange.selectNodeContents(node);
969         }
970         //nodeRange.collapse(true);
971         var s = this.win.getSelection();
972         s.removeAllRanges();
973         s.addRange(nodeRange);
974     },
975     
976     getSelectedNode: function() 
977     {
978         // this may only work on Gecko!!!
979         
980         // should we cache this!!!!
981         
982         
983         
984          
985         var range = this.createRange(this.getSelection()).cloneRange();
986         
987         if (Roo.isIE) {
988             var parent = range.parentElement();
989             while (true) {
990                 var testRange = range.duplicate();
991                 testRange.moveToElementText(parent);
992                 if (testRange.inRange(range)) {
993                     break;
994                 }
995                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
996                     break;
997                 }
998                 parent = parent.parentElement;
999             }
1000             return parent;
1001         }
1002         
1003         // is ancestor a text element.
1004         var ac =  range.commonAncestorContainer;
1005         if (ac.nodeType == 3) {
1006             ac = ac.parentNode;
1007         }
1008         
1009         var ar = ac.childNodes;
1010          
1011         var nodes = [];
1012         var other_nodes = [];
1013         var has_other_nodes = false;
1014         for (var i=0;i<ar.length;i++) {
1015             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
1016                 continue;
1017             }
1018             // fullly contained node.
1019             
1020             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1021                 nodes.push(ar[i]);
1022                 continue;
1023             }
1024             
1025             // probably selected..
1026             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1027                 other_nodes.push(ar[i]);
1028                 continue;
1029             }
1030             // outer..
1031             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
1032                 continue;
1033             }
1034             
1035             
1036             has_other_nodes = true;
1037         }
1038         if (!nodes.length && other_nodes.length) {
1039             nodes= other_nodes;
1040         }
1041         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1042             return false;
1043         }
1044         
1045         return nodes[0];
1046     },
1047     createRange: function(sel)
1048     {
1049         // this has strange effects when using with 
1050         // top toolbar - not sure if it's a great idea.
1051         //this.editor.contentWindow.focus();
1052         if (typeof sel != "undefined") {
1053             try {
1054                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1055             } catch(e) {
1056                 return this.doc.createRange();
1057             }
1058         } else {
1059             return this.doc.createRange();
1060         }
1061     },
1062     getParentElement: function()
1063     {
1064         
1065         this.assignDocWin();
1066         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1067         
1068         var range = this.createRange(sel);
1069          
1070         try {
1071             var p = range.commonAncestorContainer;
1072             while (p.nodeType == 3) { // text node
1073                 p = p.parentNode;
1074             }
1075             return p;
1076         } catch (e) {
1077             return null;
1078         }
1079     
1080     },
1081     /***
1082      *
1083      * Range intersection.. the hard stuff...
1084      *  '-1' = before
1085      *  '0' = hits..
1086      *  '1' = after.
1087      *         [ -- selected range --- ]
1088      *   [fail]                        [fail]
1089      *
1090      *    basically..
1091      *      if end is before start or  hits it. fail.
1092      *      if start is after end or hits it fail.
1093      *
1094      *   if either hits (but other is outside. - then it's not 
1095      *   
1096      *    
1097      **/
1098     
1099     
1100     // @see http://www.thismuchiknow.co.uk/?p=64.
1101     rangeIntersectsNode : function(range, node)
1102     {
1103         var nodeRange = node.ownerDocument.createRange();
1104         try {
1105             nodeRange.selectNode(node);
1106         } catch (e) {
1107             nodeRange.selectNodeContents(node);
1108         }
1109     
1110         var rangeStartRange = range.cloneRange();
1111         rangeStartRange.collapse(true);
1112     
1113         var rangeEndRange = range.cloneRange();
1114         rangeEndRange.collapse(false);
1115     
1116         var nodeStartRange = nodeRange.cloneRange();
1117         nodeStartRange.collapse(true);
1118     
1119         var nodeEndRange = nodeRange.cloneRange();
1120         nodeEndRange.collapse(false);
1121     
1122         return rangeStartRange.compareBoundaryPoints(
1123                  Range.START_TO_START, nodeEndRange) == -1 &&
1124                rangeEndRange.compareBoundaryPoints(
1125                  Range.START_TO_START, nodeStartRange) == 1;
1126         
1127          
1128     },
1129     rangeCompareNode : function(range, node)
1130     {
1131         var nodeRange = node.ownerDocument.createRange();
1132         try {
1133             nodeRange.selectNode(node);
1134         } catch (e) {
1135             nodeRange.selectNodeContents(node);
1136         }
1137         
1138         
1139         range.collapse(true);
1140     
1141         nodeRange.collapse(true);
1142      
1143         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1144         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1145          
1146         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1147         
1148         var nodeIsBefore   =  ss == 1;
1149         var nodeIsAfter    = ee == -1;
1150         
1151         if (nodeIsBefore && nodeIsAfter) {
1152             return 0; // outer
1153         }
1154         if (!nodeIsBefore && nodeIsAfter) {
1155             return 1; //right trailed.
1156         }
1157         
1158         if (nodeIsBefore && !nodeIsAfter) {
1159             return 2;  // left trailed.
1160         }
1161         // fully contined.
1162         return 3;
1163     },
1164  
1165     cleanWordChars : function(input) {// change the chars to hex code
1166         
1167        var swapCodes  = [ 
1168             [    8211, "&#8211;" ], 
1169             [    8212, "&#8212;" ], 
1170             [    8216,  "'" ],  
1171             [    8217, "'" ],  
1172             [    8220, '"' ],  
1173             [    8221, '"' ],  
1174             [    8226, "*" ],  
1175             [    8230, "..." ]
1176         ]; 
1177         var output = input;
1178         Roo.each(swapCodes, function(sw) { 
1179             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1180             
1181             output = output.replace(swapper, sw[1]);
1182         });
1183         
1184         return output;
1185     },
1186     
1187      
1188     
1189         
1190     
1191     cleanUpChild : function (node)
1192     {
1193         
1194         new Roo.htmleditor.FilterComment({node : node});
1195         new Roo.htmleditor.FilterAttributes({
1196                 node : node,
1197                 attrib_black : this.ablack,
1198                 attrib_clean : this.aclean,
1199                 style_white : this.cwhite,
1200                 style_black : this.cblack
1201         });
1202         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1203         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1204          
1205         
1206     },
1207     
1208     /**
1209      * Clean up MS wordisms...
1210      * @deprecated - use filter directly
1211      */
1212     cleanWord : function(node)
1213     {
1214         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1215         
1216     },
1217    
1218     
1219     /**
1220
1221      * @deprecated - use filters
1222      */
1223     cleanTableWidths : function(node)
1224     {
1225         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1226         
1227  
1228     },
1229     
1230      
1231         
1232     applyBlacklists : function()
1233     {
1234         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1235         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1236         
1237         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1238         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1239         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1240         
1241         this.white = [];
1242         this.black = [];
1243         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1244             if (b.indexOf(tag) > -1) {
1245                 return;
1246             }
1247             this.white.push(tag);
1248             
1249         }, this);
1250         
1251         Roo.each(w, function(tag) {
1252             if (b.indexOf(tag) > -1) {
1253                 return;
1254             }
1255             if (this.white.indexOf(tag) > -1) {
1256                 return;
1257             }
1258             this.white.push(tag);
1259             
1260         }, this);
1261         
1262         
1263         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1264             if (w.indexOf(tag) > -1) {
1265                 return;
1266             }
1267             this.black.push(tag);
1268             
1269         }, this);
1270         
1271         Roo.each(b, function(tag) {
1272             if (w.indexOf(tag) > -1) {
1273                 return;
1274             }
1275             if (this.black.indexOf(tag) > -1) {
1276                 return;
1277             }
1278             this.black.push(tag);
1279             
1280         }, this);
1281         
1282         
1283         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1284         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1285         
1286         this.cwhite = [];
1287         this.cblack = [];
1288         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1289             if (b.indexOf(tag) > -1) {
1290                 return;
1291             }
1292             this.cwhite.push(tag);
1293             
1294         }, this);
1295         
1296         Roo.each(w, function(tag) {
1297             if (b.indexOf(tag) > -1) {
1298                 return;
1299             }
1300             if (this.cwhite.indexOf(tag) > -1) {
1301                 return;
1302             }
1303             this.cwhite.push(tag);
1304             
1305         }, this);
1306         
1307         
1308         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1309             if (w.indexOf(tag) > -1) {
1310                 return;
1311             }
1312             this.cblack.push(tag);
1313             
1314         }, this);
1315         
1316         Roo.each(b, function(tag) {
1317             if (w.indexOf(tag) > -1) {
1318                 return;
1319             }
1320             if (this.cblack.indexOf(tag) > -1) {
1321                 return;
1322             }
1323             this.cblack.push(tag);
1324             
1325         }, this);
1326     },
1327     
1328     setStylesheets : function(stylesheets)
1329     {
1330         if(typeof(stylesheets) == 'string'){
1331             Roo.get(this.iframe.contentDocument.head).createChild({
1332                 tag : 'link',
1333                 rel : 'stylesheet',
1334                 type : 'text/css',
1335                 href : stylesheets
1336             });
1337             
1338             return;
1339         }
1340         var _this = this;
1341      
1342         Roo.each(stylesheets, function(s) {
1343             if(!s.length){
1344                 return;
1345             }
1346             
1347             Roo.get(_this.iframe.contentDocument.head).createChild({
1348                 tag : 'link',
1349                 rel : 'stylesheet',
1350                 type : 'text/css',
1351                 href : s
1352             });
1353         });
1354
1355         
1356     },
1357     
1358     removeStylesheets : function()
1359     {
1360         var _this = this;
1361         
1362         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1363             s.remove();
1364         });
1365     },
1366     
1367     setStyle : function(style)
1368     {
1369         Roo.get(this.iframe.contentDocument.head).createChild({
1370             tag : 'style',
1371             type : 'text/css',
1372             html : style
1373         });
1374
1375         return;
1376     }
1377     
1378     // hide stuff that is not compatible
1379     /**
1380      * @event blur
1381      * @hide
1382      */
1383     /**
1384      * @event change
1385      * @hide
1386      */
1387     /**
1388      * @event focus
1389      * @hide
1390      */
1391     /**
1392      * @event specialkey
1393      * @hide
1394      */
1395     /**
1396      * @cfg {String} fieldClass @hide
1397      */
1398     /**
1399      * @cfg {String} focusClass @hide
1400      */
1401     /**
1402      * @cfg {String} autoCreate @hide
1403      */
1404     /**
1405      * @cfg {String} inputType @hide
1406      */
1407     /**
1408      * @cfg {String} invalidClass @hide
1409      */
1410     /**
1411      * @cfg {String} invalidText @hide
1412      */
1413     /**
1414      * @cfg {String} msgFx @hide
1415      */
1416     /**
1417      * @cfg {String} validateOnBlur @hide
1418      */
1419 });
1420
1421 Roo.HtmlEditorCore.white = [
1422         'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1423         
1424        'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD',      'DIR',       'DIV', 
1425        'DL',      'DT',         'H1',     'H2',      'H3',        'H4', 
1426        'H5',      'H6',         'HR',     'ISINDEX', 'LISTING',   'MARQUEE', 
1427        'MENU',    'MULTICOL',   'OL',     'P',       'PLAINTEXT', 'PRE', 
1428        'TABLE',   'UL',         'XMP', 
1429        
1430        'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH', 
1431       'THEAD',   'TR', 
1432      
1433       'DIR', 'MENU', 'OL', 'UL', 'DL',
1434        
1435       'EMBED',  'OBJECT'
1436 ];
1437
1438
1439 Roo.HtmlEditorCore.black = [
1440     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1441         'APPLET', // 
1442         'BASE',   'BASEFONT', 'BGSOUND', 'BLINK',  'BODY', 
1443         'FRAME',  'FRAMESET', 'HEAD',    'HTML',   'ILAYER', 
1444         'IFRAME', 'LAYER',  'LINK',     'META',    'OBJECT',   
1445         'SCRIPT', 'STYLE' ,'TITLE',  'XML',
1446         //'FONT' // CLEAN LATER..
1447         'COLGROUP', 'COL'  // messy tables.
1448         
1449 ];
1450 Roo.HtmlEditorCore.clean = [ // ?? needed???
1451      'SCRIPT', 'STYLE', 'TITLE', 'XML'
1452 ];
1453 Roo.HtmlEditorCore.tag_remove = [
1454     'FONT', 'TBODY'  
1455 ];
1456 // attributes..
1457
1458 Roo.HtmlEditorCore.ablack = [
1459     'on'
1460 ];
1461     
1462 Roo.HtmlEditorCore.aclean = [ 
1463     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1464 ];
1465
1466 // protocols..
1467 Roo.HtmlEditorCore.pwhite= [
1468         'http',  'https',  'mailto'
1469 ];
1470
1471 // white listed style attributes.
1472 Roo.HtmlEditorCore.cwhite= [
1473       //  'text-align', /// default is to allow most things..
1474       
1475          
1476 //        'font-size'//??
1477 ];
1478
1479 // black listed style attributes.
1480 Roo.HtmlEditorCore.cblack= [
1481       //  'font-size' -- this can be set by the project 
1482 ];
1483
1484
1485
1486
1487