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             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                 //this.cleanUpPaste();
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          // default behaveiour should be our local cleanup paste? (optional?)
523          // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
524          this.owner.fireEvent('paste', e, v);
525     },
526     // private
527     onDestroy : function(){
528         
529         
530         
531         if(this.rendered){
532             
533             //for (var i =0; i < this.toolbars.length;i++) {
534             //    // fixme - ask toolbars for heights?
535             //    this.toolbars[i].onDestroy();
536            // }
537             
538             //this.wrap.dom.innerHTML = '';
539             //this.wrap.remove();
540         }
541     },
542
543     // private
544     onFirstFocus : function(){
545         
546         this.assignDocWin();
547         
548         
549         this.activated = true;
550          
551     
552         if(Roo.isGecko){ // prevent silly gecko errors
553             this.win.focus();
554             var s = this.win.getSelection();
555             if(!s.focusNode || s.focusNode.nodeType != 3){
556                 var r = s.getRangeAt(0);
557                 r.selectNodeContents((this.doc.body || this.doc.documentElement));
558                 r.collapse(true);
559                 this.deferFocus();
560             }
561             try{
562                 this.execCmd('useCSS', true);
563                 this.execCmd('styleWithCSS', false);
564             }catch(e){}
565         }
566         this.owner.fireEvent('activate', this);
567     },
568
569     // private
570     adjustFont: function(btn){
571         var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
572         //if(Roo.isSafari){ // safari
573         //    adjust *= 2;
574        // }
575         var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
576         if(Roo.isSafari){ // safari
577             var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
578             v =  (v < 10) ? 10 : v;
579             v =  (v > 48) ? 48 : v;
580             v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
581             
582         }
583         
584         
585         v = Math.max(1, v+adjust);
586         
587         this.execCmd('FontSize', v  );
588     },
589
590     onEditorEvent : function(e)
591     {
592         this.owner.fireEvent('editorevent', this, e);
593       //  this.updateToolbar();
594         this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
595     },
596
597     insertTag : function(tg)
598     {
599         // could be a bit smarter... -> wrap the current selected tRoo..
600         if (tg.toLowerCase() == 'span' ||
601             tg.toLowerCase() == 'code' ||
602             tg.toLowerCase() == 'sup' ||
603             tg.toLowerCase() == 'sub' 
604             ) {
605             
606             range = this.createRange(this.getSelection());
607             var wrappingNode = this.doc.createElement(tg.toLowerCase());
608             wrappingNode.appendChild(range.extractContents());
609             range.insertNode(wrappingNode);
610
611             return;
612             
613             
614             
615         }
616         this.execCmd("formatblock",   tg);
617         
618     },
619     
620     insertText : function(txt)
621     {
622         
623         
624         var range = this.createRange();
625         range.deleteContents();
626                //alert(Sender.getAttribute('label'));
627                
628         range.insertNode(this.doc.createTextNode(txt));
629     } ,
630     
631      
632
633     /**
634      * Executes a Midas editor command on the editor document and performs necessary focus and
635      * toolbar updates. <b>This should only be called after the editor is initialized.</b>
636      * @param {String} cmd The Midas command
637      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
638      */
639     relayCmd : function(cmd, value){
640         this.win.focus();
641         this.execCmd(cmd, value);
642         this.owner.fireEvent('editorevent', this);
643         //this.updateToolbar();
644         this.owner.deferFocus();
645     },
646
647     /**
648      * Executes a Midas editor command directly on the editor document.
649      * For visual commands, you should use {@link #relayCmd} instead.
650      * <b>This should only be called after the editor is initialized.</b>
651      * @param {String} cmd The Midas command
652      * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
653      */
654     execCmd : function(cmd, value){
655         this.doc.execCommand(cmd, false, value === undefined ? null : value);
656         this.syncValue();
657     },
658  
659  
660    
661     /**
662      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
663      * to insert tRoo.
664      * @param {String} text | dom node.. 
665      */
666     insertAtCursor : function(text)
667     {
668         
669         if(!this.activated){
670             return;
671         }
672         /*
673         if(Roo.isIE){
674             this.win.focus();
675             var r = this.doc.selection.createRange();
676             if(r){
677                 r.collapse(true);
678                 r.pasteHTML(text);
679                 this.syncValue();
680                 this.deferFocus();
681             
682             }
683             return;
684         }
685         */
686         if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
687             this.win.focus();
688             
689             
690             // from jquery ui (MIT licenced)
691             var range, node;
692             var win = this.win;
693             
694             if (win.getSelection && win.getSelection().getRangeAt) {
695                 range = win.getSelection().getRangeAt(0);
696                 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
697                 range.insertNode(node);
698             } else if (win.document.selection && win.document.selection.createRange) {
699                 // no firefox support
700                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
701                 win.document.selection.createRange().pasteHTML(txt);
702             } else {
703                 // no firefox support
704                 var txt = typeof(text) == 'string' ? text : text.outerHTML;
705                 this.execCmd('InsertHTML', txt);
706             } 
707             
708             this.syncValue();
709             
710             this.deferFocus();
711         }
712     },
713  // private
714     mozKeyPress : function(e){
715         if(e.ctrlKey){
716             var c = e.getCharCode(), cmd;
717           
718             if(c > 0){
719                 c = String.fromCharCode(c).toLowerCase();
720                 switch(c){
721                     case 'b':
722                         cmd = 'bold';
723                         break;
724                     case 'i':
725                         cmd = 'italic';
726                         break;
727                     
728                     case 'u':
729                         cmd = 'underline';
730                         break;
731                     
732                     //case 'v':
733                       //  this.cleanUpPaste.defer(100, this);
734                       //  return;
735                         
736                 }
737                 if(cmd){
738                     this.win.focus();
739                     this.execCmd(cmd);
740                     this.deferFocus();
741                     e.preventDefault();
742                 }
743                 
744             }
745         }
746     },
747
748     // private
749     fixKeys : function(){ // load time branching for fastest keydown performance
750         if(Roo.isIE){
751             return function(e){
752                 var k = e.getKey(), r;
753                 if(k == e.TAB){
754                     e.stopEvent();
755                     r = this.doc.selection.createRange();
756                     if(r){
757                         r.collapse(true);
758                         r.pasteHTML('&#160;&#160;&#160;&#160;');
759                         this.deferFocus();
760                     }
761                     return;
762                 }
763                 
764                 if(k == e.ENTER){
765                     r = this.doc.selection.createRange();
766                     if(r){
767                         var target = r.parentElement();
768                         if(!target || target.tagName.toLowerCase() != 'li'){
769                             e.stopEvent();
770                             r.pasteHTML('<br/>');
771                             r.collapse(false);
772                             r.select();
773                         }
774                     }
775                 }
776                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
777                 //    this.cleanUpPaste.defer(100, this);
778                 //    return;
779                 //}
780                 
781                 
782             };
783         }else if(Roo.isOpera){
784             return function(e){
785                 var k = e.getKey();
786                 if(k == e.TAB){
787                     e.stopEvent();
788                     this.win.focus();
789                     this.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');
790                     this.deferFocus();
791                 }
792                 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
793                 //    this.cleanUpPaste.defer(100, this);
794                  //   return;
795                 //}
796                 
797             };
798         }else if(Roo.isSafari){
799             return function(e){
800                 var k = e.getKey();
801                 
802                 if(k == e.TAB){
803                     e.stopEvent();
804                     this.execCmd('InsertText','\t');
805                     this.deferFocus();
806                     return;
807                 }
808                //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
809                  //   this.cleanUpPaste.defer(100, this);
810                  //   return;
811                // }
812                 
813              };
814         }
815     }(),
816     
817     getAllAncestors: function()
818     {
819         var p = this.getSelectedNode();
820         var a = [];
821         if (!p) {
822             a.push(p); // push blank onto stack..
823             p = this.getParentElement();
824         }
825         
826         
827         while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
828             a.push(p);
829             p = p.parentNode;
830         }
831         a.push(this.doc.body);
832         return a;
833     },
834     lastSel : false,
835     lastSelNode : false,
836     
837     
838     getSelection : function() 
839     {
840         this.assignDocWin();
841         return Roo.isIE ? this.doc.selection : this.win.getSelection();
842     },
843     /**
844      * Select a dom node
845      * @param {DomElement} node the node to select
846      */
847     selectNode : function(node)
848     {
849         
850             var nodeRange = node.ownerDocument.createRange();
851             try {
852                 nodeRange.selectNode(node);
853             } catch (e) {
854                 nodeRange.selectNodeContents(node);
855             }
856             //nodeRange.collapse(true);
857             var s = this.win.getSelection();
858             s.removeAllRanges();
859             s.addRange(nodeRange);
860     },
861     
862     getSelectedNode: function() 
863     {
864         // this may only work on Gecko!!!
865         
866         // should we cache this!!!!
867         
868         
869         
870          
871         var range = this.createRange(this.getSelection()).cloneRange();
872         
873         if (Roo.isIE) {
874             var parent = range.parentElement();
875             while (true) {
876                 var testRange = range.duplicate();
877                 testRange.moveToElementText(parent);
878                 if (testRange.inRange(range)) {
879                     break;
880                 }
881                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
882                     break;
883                 }
884                 parent = parent.parentElement;
885             }
886             return parent;
887         }
888         
889         // is ancestor a text element.
890         var ac =  range.commonAncestorContainer;
891         if (ac.nodeType == 3) {
892             ac = ac.parentNode;
893         }
894         
895         var ar = ac.childNodes;
896          
897         var nodes = [];
898         var other_nodes = [];
899         var has_other_nodes = false;
900         for (var i=0;i<ar.length;i++) {
901             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
902                 continue;
903             }
904             // fullly contained node.
905             
906             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
907                 nodes.push(ar[i]);
908                 continue;
909             }
910             
911             // probably selected..
912             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
913                 other_nodes.push(ar[i]);
914                 continue;
915             }
916             // outer..
917             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
918                 continue;
919             }
920             
921             
922             has_other_nodes = true;
923         }
924         if (!nodes.length && other_nodes.length) {
925             nodes= other_nodes;
926         }
927         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
928             return false;
929         }
930         
931         return nodes[0];
932     },
933     createRange: function(sel)
934     {
935         // this has strange effects when using with 
936         // top toolbar - not sure if it's a great idea.
937         //this.editor.contentWindow.focus();
938         if (typeof sel != "undefined") {
939             try {
940                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
941             } catch(e) {
942                 return this.doc.createRange();
943             }
944         } else {
945             return this.doc.createRange();
946         }
947     },
948     getParentElement: function()
949     {
950         
951         this.assignDocWin();
952         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
953         
954         var range = this.createRange(sel);
955          
956         try {
957             var p = range.commonAncestorContainer;
958             while (p.nodeType == 3) { // text node
959                 p = p.parentNode;
960             }
961             return p;
962         } catch (e) {
963             return null;
964         }
965     
966     },
967     /***
968      *
969      * Range intersection.. the hard stuff...
970      *  '-1' = before
971      *  '0' = hits..
972      *  '1' = after.
973      *         [ -- selected range --- ]
974      *   [fail]                        [fail]
975      *
976      *    basically..
977      *      if end is before start or  hits it. fail.
978      *      if start is after end or hits it fail.
979      *
980      *   if either hits (but other is outside. - then it's not 
981      *   
982      *    
983      **/
984     
985     
986     // @see http://www.thismuchiknow.co.uk/?p=64.
987     rangeIntersectsNode : function(range, node)
988     {
989         var nodeRange = node.ownerDocument.createRange();
990         try {
991             nodeRange.selectNode(node);
992         } catch (e) {
993             nodeRange.selectNodeContents(node);
994         }
995     
996         var rangeStartRange = range.cloneRange();
997         rangeStartRange.collapse(true);
998     
999         var rangeEndRange = range.cloneRange();
1000         rangeEndRange.collapse(false);
1001     
1002         var nodeStartRange = nodeRange.cloneRange();
1003         nodeStartRange.collapse(true);
1004     
1005         var nodeEndRange = nodeRange.cloneRange();
1006         nodeEndRange.collapse(false);
1007     
1008         return rangeStartRange.compareBoundaryPoints(
1009                  Range.START_TO_START, nodeEndRange) == -1 &&
1010                rangeEndRange.compareBoundaryPoints(
1011                  Range.START_TO_START, nodeStartRange) == 1;
1012         
1013          
1014     },
1015     rangeCompareNode : function(range, node)
1016     {
1017         var nodeRange = node.ownerDocument.createRange();
1018         try {
1019             nodeRange.selectNode(node);
1020         } catch (e) {
1021             nodeRange.selectNodeContents(node);
1022         }
1023         
1024         
1025         range.collapse(true);
1026     
1027         nodeRange.collapse(true);
1028      
1029         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1030         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1031          
1032         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1033         
1034         var nodeIsBefore   =  ss == 1;
1035         var nodeIsAfter    = ee == -1;
1036         
1037         if (nodeIsBefore && nodeIsAfter) {
1038             return 0; // outer
1039         }
1040         if (!nodeIsBefore && nodeIsAfter) {
1041             return 1; //right trailed.
1042         }
1043         
1044         if (nodeIsBefore && !nodeIsAfter) {
1045             return 2;  // left trailed.
1046         }
1047         // fully contined.
1048         return 3;
1049     },
1050 /*
1051     // private? - in a new class?
1052     cleanUpPaste :  function()
1053     {
1054         // cleans up the whole document..
1055         Roo.log('cleanuppaste');
1056         
1057         this.cleanUpChild(this.doc.body);
1058         var clean = this.cleanWordChars(this.doc.body.innerHTML);
1059         if (clean != this.doc.body.innerHTML) {
1060             this.doc.body.innerHTML = clean;
1061         }
1062         
1063     },
1064     */
1065     cleanWordChars : function(input) {// change the chars to hex code
1066         var he = Roo.HtmlEditorCore;
1067         
1068         var output = input;
1069         Roo.each(he.swapCodes, function(sw) { 
1070             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1071             
1072             output = output.replace(swapper, sw[1]);
1073         });
1074         
1075         return output;
1076     },
1077     
1078      
1079     
1080         
1081     
1082     cleanUpChild : function (node)
1083     {
1084         
1085         new Roo.htmleditor.FilterComment({node : node});
1086         new Roo.htmleditor.FilterAttributes({
1087                 node : node,
1088                 attrib_black : this.ablack,
1089                 attrib_clean : this.aclean,
1090                 style_white : this.cwhite,
1091                 style_black : this.cblack
1092         });
1093         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1094         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1095          
1096         
1097     },
1098     
1099     /**
1100      * Clean up MS wordisms...
1101      * @deprecated - use filter directly
1102      */
1103     cleanWord : function(node)
1104     {
1105         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1106         
1107     },
1108    
1109     
1110     /**
1111
1112      * @deprecated - use filters
1113      */
1114     cleanTableWidths : function(node)
1115     {
1116         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1117         
1118  
1119     },
1120     
1121      
1122         
1123     applyBlacklists : function()
1124     {
1125         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1126         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1127         
1128         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1129         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1130         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1131         
1132         this.white = [];
1133         this.black = [];
1134         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1135             if (b.indexOf(tag) > -1) {
1136                 return;
1137             }
1138             this.white.push(tag);
1139             
1140         }, this);
1141         
1142         Roo.each(w, function(tag) {
1143             if (b.indexOf(tag) > -1) {
1144                 return;
1145             }
1146             if (this.white.indexOf(tag) > -1) {
1147                 return;
1148             }
1149             this.white.push(tag);
1150             
1151         }, this);
1152         
1153         
1154         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1155             if (w.indexOf(tag) > -1) {
1156                 return;
1157             }
1158             this.black.push(tag);
1159             
1160         }, this);
1161         
1162         Roo.each(b, function(tag) {
1163             if (w.indexOf(tag) > -1) {
1164                 return;
1165             }
1166             if (this.black.indexOf(tag) > -1) {
1167                 return;
1168             }
1169             this.black.push(tag);
1170             
1171         }, this);
1172         
1173         
1174         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1175         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1176         
1177         this.cwhite = [];
1178         this.cblack = [];
1179         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1180             if (b.indexOf(tag) > -1) {
1181                 return;
1182             }
1183             this.cwhite.push(tag);
1184             
1185         }, this);
1186         
1187         Roo.each(w, function(tag) {
1188             if (b.indexOf(tag) > -1) {
1189                 return;
1190             }
1191             if (this.cwhite.indexOf(tag) > -1) {
1192                 return;
1193             }
1194             this.cwhite.push(tag);
1195             
1196         }, this);
1197         
1198         
1199         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1200             if (w.indexOf(tag) > -1) {
1201                 return;
1202             }
1203             this.cblack.push(tag);
1204             
1205         }, this);
1206         
1207         Roo.each(b, function(tag) {
1208             if (w.indexOf(tag) > -1) {
1209                 return;
1210             }
1211             if (this.cblack.indexOf(tag) > -1) {
1212                 return;
1213             }
1214             this.cblack.push(tag);
1215             
1216         }, this);
1217     },
1218     
1219     setStylesheets : function(stylesheets)
1220     {
1221         if(typeof(stylesheets) == 'string'){
1222             Roo.get(this.iframe.contentDocument.head).createChild({
1223                 tag : 'link',
1224                 rel : 'stylesheet',
1225                 type : 'text/css',
1226                 href : stylesheets
1227             });
1228             
1229             return;
1230         }
1231         var _this = this;
1232      
1233         Roo.each(stylesheets, function(s) {
1234             if(!s.length){
1235                 return;
1236             }
1237             
1238             Roo.get(_this.iframe.contentDocument.head).createChild({
1239                 tag : 'link',
1240                 rel : 'stylesheet',
1241                 type : 'text/css',
1242                 href : s
1243             });
1244         });
1245
1246         
1247     },
1248     
1249     removeStylesheets : function()
1250     {
1251         var _this = this;
1252         
1253         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1254             s.remove();
1255         });
1256     },
1257     
1258     setStyle : function(style)
1259     {
1260         Roo.get(this.iframe.contentDocument.head).createChild({
1261             tag : 'style',
1262             type : 'text/css',
1263             html : style
1264         });
1265
1266         return;
1267     }
1268     
1269     // hide stuff that is not compatible
1270     /**
1271      * @event blur
1272      * @hide
1273      */
1274     /**
1275      * @event change
1276      * @hide
1277      */
1278     /**
1279      * @event focus
1280      * @hide
1281      */
1282     /**
1283      * @event specialkey
1284      * @hide
1285      */
1286     /**
1287      * @cfg {String} fieldClass @hide
1288      */
1289     /**
1290      * @cfg {String} focusClass @hide
1291      */
1292     /**
1293      * @cfg {String} autoCreate @hide
1294      */
1295     /**
1296      * @cfg {String} inputType @hide
1297      */
1298     /**
1299      * @cfg {String} invalidClass @hide
1300      */
1301     /**
1302      * @cfg {String} invalidText @hide
1303      */
1304     /**
1305      * @cfg {String} msgFx @hide
1306      */
1307     /**
1308      * @cfg {String} validateOnBlur @hide
1309      */
1310 });
1311
1312 Roo.HtmlEditorCore.white = [
1313         'area', 'br', 'img', 'input', 'hr', 'wbr',
1314         
1315        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1316        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1317        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1318        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1319        'table',   'ul',         'xmp', 
1320        
1321        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1322       'thead',   'tr', 
1323      
1324       'dir', 'menu', 'ol', 'ul', 'dl',
1325        
1326       'embed',  'object'
1327 ];
1328
1329
1330 Roo.HtmlEditorCore.black = [
1331     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1332         'applet', // 
1333         'base',   'basefont', 'bgsound', 'blink',  'body', 
1334         'frame',  'frameset', 'head',    'html',   'ilayer', 
1335         'iframe', 'layer',  'link',     'meta',    'object',   
1336         'script', 'style' ,'title',  'xml' // clean later..
1337 ];
1338 Roo.HtmlEditorCore.clean = [
1339     'script', 'style', 'title', 'xml'
1340 ];
1341 Roo.HtmlEditorCore.tag_remove = [
1342     'font'
1343 ];
1344 // attributes..
1345
1346 Roo.HtmlEditorCore.ablack = [
1347     'on'
1348 ];
1349     
1350 Roo.HtmlEditorCore.aclean = [ 
1351     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1352 ];
1353
1354 // protocols..
1355 Roo.HtmlEditorCore.pwhite= [
1356         'http',  'https',  'mailto'
1357 ];
1358
1359 // white listed style attributes.
1360 Roo.HtmlEditorCore.cwhite= [
1361       //  'text-align', /// default is to allow most things..
1362       
1363          
1364 //        'font-size'//??
1365 ];
1366
1367 // black listed style attributes.
1368 Roo.HtmlEditorCore.cblack= [
1369       //  'font-size' -- this can be set by the project 
1370 ];
1371
1372
1373 Roo.HtmlEditorCore.swapCodes   =[ 
1374     [    8211, "&#8211;" ], 
1375     [    8212, "&#8212;" ], 
1376     [    8216,  "'" ],  
1377     [    8217, "'" ],  
1378     [    8220, '"' ],  
1379     [    8221, '"' ],  
1380     [    8226, "*" ],  
1381     [    8230, "..." ]
1382 ]; 
1383
1384