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