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