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