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                 //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.createChild('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     getSelectedNode: function() 
845     {
846         // this may only work on Gecko!!!
847         
848         // should we cache this!!!!
849         
850         
851         
852          
853         var range = this.createRange(this.getSelection()).cloneRange();
854         
855         if (Roo.isIE) {
856             var parent = range.parentElement();
857             while (true) {
858                 var testRange = range.duplicate();
859                 testRange.moveToElementText(parent);
860                 if (testRange.inRange(range)) {
861                     break;
862                 }
863                 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
864                     break;
865                 }
866                 parent = parent.parentElement;
867             }
868             return parent;
869         }
870         
871         // is ancestor a text element.
872         var ac =  range.commonAncestorContainer;
873         if (ac.nodeType == 3) {
874             ac = ac.parentNode;
875         }
876         
877         var ar = ac.childNodes;
878          
879         var nodes = [];
880         var other_nodes = [];
881         var has_other_nodes = false;
882         for (var i=0;i<ar.length;i++) {
883             if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ? 
884                 continue;
885             }
886             // fullly contained node.
887             
888             if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
889                 nodes.push(ar[i]);
890                 continue;
891             }
892             
893             // probably selected..
894             if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
895                 other_nodes.push(ar[i]);
896                 continue;
897             }
898             // outer..
899             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
900                 continue;
901             }
902             
903             
904             has_other_nodes = true;
905         }
906         if (!nodes.length && other_nodes.length) {
907             nodes= other_nodes;
908         }
909         if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
910             return false;
911         }
912         
913         return nodes[0];
914     },
915     createRange: function(sel)
916     {
917         // this has strange effects when using with 
918         // top toolbar - not sure if it's a great idea.
919         //this.editor.contentWindow.focus();
920         if (typeof sel != "undefined") {
921             try {
922                 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
923             } catch(e) {
924                 return this.doc.createRange();
925             }
926         } else {
927             return this.doc.createRange();
928         }
929     },
930     getParentElement: function()
931     {
932         
933         this.assignDocWin();
934         var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
935         
936         var range = this.createRange(sel);
937          
938         try {
939             var p = range.commonAncestorContainer;
940             while (p.nodeType == 3) { // text node
941                 p = p.parentNode;
942             }
943             return p;
944         } catch (e) {
945             return null;
946         }
947     
948     },
949     /***
950      *
951      * Range intersection.. the hard stuff...
952      *  '-1' = before
953      *  '0' = hits..
954      *  '1' = after.
955      *         [ -- selected range --- ]
956      *   [fail]                        [fail]
957      *
958      *    basically..
959      *      if end is before start or  hits it. fail.
960      *      if start is after end or hits it fail.
961      *
962      *   if either hits (but other is outside. - then it's not 
963      *   
964      *    
965      **/
966     
967     
968     // @see http://www.thismuchiknow.co.uk/?p=64.
969     rangeIntersectsNode : function(range, node)
970     {
971         var nodeRange = node.ownerDocument.createRange();
972         try {
973             nodeRange.selectNode(node);
974         } catch (e) {
975             nodeRange.selectNodeContents(node);
976         }
977     
978         var rangeStartRange = range.cloneRange();
979         rangeStartRange.collapse(true);
980     
981         var rangeEndRange = range.cloneRange();
982         rangeEndRange.collapse(false);
983     
984         var nodeStartRange = nodeRange.cloneRange();
985         nodeStartRange.collapse(true);
986     
987         var nodeEndRange = nodeRange.cloneRange();
988         nodeEndRange.collapse(false);
989     
990         return rangeStartRange.compareBoundaryPoints(
991                  Range.START_TO_START, nodeEndRange) == -1 &&
992                rangeEndRange.compareBoundaryPoints(
993                  Range.START_TO_START, nodeStartRange) == 1;
994         
995          
996     },
997     rangeCompareNode : function(range, node)
998     {
999         var nodeRange = node.ownerDocument.createRange();
1000         try {
1001             nodeRange.selectNode(node);
1002         } catch (e) {
1003             nodeRange.selectNodeContents(node);
1004         }
1005         
1006         
1007         range.collapse(true);
1008     
1009         nodeRange.collapse(true);
1010      
1011         var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1012         var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
1013          
1014         //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1015         
1016         var nodeIsBefore   =  ss == 1;
1017         var nodeIsAfter    = ee == -1;
1018         
1019         if (nodeIsBefore && nodeIsAfter) {
1020             return 0; // outer
1021         }
1022         if (!nodeIsBefore && nodeIsAfter) {
1023             return 1; //right trailed.
1024         }
1025         
1026         if (nodeIsBefore && !nodeIsAfter) {
1027             return 2;  // left trailed.
1028         }
1029         // fully contined.
1030         return 3;
1031     },
1032 /*
1033     // private? - in a new class?
1034     cleanUpPaste :  function()
1035     {
1036         // cleans up the whole document..
1037         Roo.log('cleanuppaste');
1038         
1039         this.cleanUpChild(this.doc.body);
1040         var clean = this.cleanWordChars(this.doc.body.innerHTML);
1041         if (clean != this.doc.body.innerHTML) {
1042             this.doc.body.innerHTML = clean;
1043         }
1044         
1045     },
1046     */
1047     cleanWordChars : function(input) {// change the chars to hex code
1048         var he = Roo.HtmlEditorCore;
1049         
1050         var output = input;
1051         Roo.each(he.swapCodes, function(sw) { 
1052             var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1053             
1054             output = output.replace(swapper, sw[1]);
1055         });
1056         
1057         return output;
1058     },
1059     
1060      
1061     
1062         
1063     
1064     cleanUpChild : function (node)
1065     {
1066         
1067         new Roo.htmleditor.FilterComment({node : node});
1068         new Roo.htmleditor.FilterAttributes({
1069                 node : node,
1070                 attrib_black : this.ablack,
1071                 attrib_clean : this.aclean,
1072                 style_white : this.cwhite,
1073                 style_black : this.cblack
1074         });
1075         new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1076         new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1077          
1078         
1079     },
1080     
1081     /**
1082      * Clean up MS wordisms...
1083      * @deprecated - use filter directly
1084      */
1085     cleanWord : function(node)
1086     {
1087         new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1088         
1089     },
1090    
1091     
1092     /**
1093
1094      * @deprecated - use filters
1095      */
1096     cleanTableWidths : function(node)
1097     {
1098         new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1099         
1100  
1101     },
1102     
1103      
1104         
1105     applyBlacklists : function()
1106     {
1107         var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white  : [];
1108         var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black :  [];
1109         
1110         this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean :  Roo.HtmlEditorCore.aclean;
1111         this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack :  Roo.HtmlEditorCore.ablack;
1112         this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove :  Roo.HtmlEditorCore.tag_remove;
1113         
1114         this.white = [];
1115         this.black = [];
1116         Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1117             if (b.indexOf(tag) > -1) {
1118                 return;
1119             }
1120             this.white.push(tag);
1121             
1122         }, this);
1123         
1124         Roo.each(w, function(tag) {
1125             if (b.indexOf(tag) > -1) {
1126                 return;
1127             }
1128             if (this.white.indexOf(tag) > -1) {
1129                 return;
1130             }
1131             this.white.push(tag);
1132             
1133         }, this);
1134         
1135         
1136         Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1137             if (w.indexOf(tag) > -1) {
1138                 return;
1139             }
1140             this.black.push(tag);
1141             
1142         }, this);
1143         
1144         Roo.each(b, function(tag) {
1145             if (w.indexOf(tag) > -1) {
1146                 return;
1147             }
1148             if (this.black.indexOf(tag) > -1) {
1149                 return;
1150             }
1151             this.black.push(tag);
1152             
1153         }, this);
1154         
1155         
1156         w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite  : [];
1157         b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack :  [];
1158         
1159         this.cwhite = [];
1160         this.cblack = [];
1161         Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1162             if (b.indexOf(tag) > -1) {
1163                 return;
1164             }
1165             this.cwhite.push(tag);
1166             
1167         }, this);
1168         
1169         Roo.each(w, function(tag) {
1170             if (b.indexOf(tag) > -1) {
1171                 return;
1172             }
1173             if (this.cwhite.indexOf(tag) > -1) {
1174                 return;
1175             }
1176             this.cwhite.push(tag);
1177             
1178         }, this);
1179         
1180         
1181         Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1182             if (w.indexOf(tag) > -1) {
1183                 return;
1184             }
1185             this.cblack.push(tag);
1186             
1187         }, this);
1188         
1189         Roo.each(b, function(tag) {
1190             if (w.indexOf(tag) > -1) {
1191                 return;
1192             }
1193             if (this.cblack.indexOf(tag) > -1) {
1194                 return;
1195             }
1196             this.cblack.push(tag);
1197             
1198         }, this);
1199     },
1200     
1201     setStylesheets : function(stylesheets)
1202     {
1203         if(typeof(stylesheets) == 'string'){
1204             Roo.get(this.iframe.contentDocument.head).createChild({
1205                 tag : 'link',
1206                 rel : 'stylesheet',
1207                 type : 'text/css',
1208                 href : stylesheets
1209             });
1210             
1211             return;
1212         }
1213         var _this = this;
1214      
1215         Roo.each(stylesheets, function(s) {
1216             if(!s.length){
1217                 return;
1218             }
1219             
1220             Roo.get(_this.iframe.contentDocument.head).createChild({
1221                 tag : 'link',
1222                 rel : 'stylesheet',
1223                 type : 'text/css',
1224                 href : s
1225             });
1226         });
1227
1228         
1229     },
1230     
1231     removeStylesheets : function()
1232     {
1233         var _this = this;
1234         
1235         Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1236             s.remove();
1237         });
1238     },
1239     
1240     setStyle : function(style)
1241     {
1242         Roo.get(this.iframe.contentDocument.head).createChild({
1243             tag : 'style',
1244             type : 'text/css',
1245             html : style
1246         });
1247
1248         return;
1249     }
1250     
1251     // hide stuff that is not compatible
1252     /**
1253      * @event blur
1254      * @hide
1255      */
1256     /**
1257      * @event change
1258      * @hide
1259      */
1260     /**
1261      * @event focus
1262      * @hide
1263      */
1264     /**
1265      * @event specialkey
1266      * @hide
1267      */
1268     /**
1269      * @cfg {String} fieldClass @hide
1270      */
1271     /**
1272      * @cfg {String} focusClass @hide
1273      */
1274     /**
1275      * @cfg {String} autoCreate @hide
1276      */
1277     /**
1278      * @cfg {String} inputType @hide
1279      */
1280     /**
1281      * @cfg {String} invalidClass @hide
1282      */
1283     /**
1284      * @cfg {String} invalidText @hide
1285      */
1286     /**
1287      * @cfg {String} msgFx @hide
1288      */
1289     /**
1290      * @cfg {String} validateOnBlur @hide
1291      */
1292 });
1293
1294 Roo.HtmlEditorCore.white = [
1295         'area', 'br', 'img', 'input', 'hr', 'wbr',
1296         
1297        'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
1298        'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
1299        'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
1300        'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
1301        'table',   'ul',         'xmp', 
1302        
1303        'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
1304       'thead',   'tr', 
1305      
1306       'dir', 'menu', 'ol', 'ul', 'dl',
1307        
1308       'embed',  'object'
1309 ];
1310
1311
1312 Roo.HtmlEditorCore.black = [
1313     //    'embed',  'object', // enable - backend responsiblity to clean thiese
1314         'applet', // 
1315         'base',   'basefont', 'bgsound', 'blink',  'body', 
1316         'frame',  'frameset', 'head',    'html',   'ilayer', 
1317         'iframe', 'layer',  'link',     'meta',    'object',   
1318         'script', 'style' ,'title',  'xml' // clean later..
1319 ];
1320 Roo.HtmlEditorCore.clean = [
1321     'script', 'style', 'title', 'xml'
1322 ];
1323 Roo.HtmlEditorCore.tag_remove = [
1324     'font'
1325 ];
1326 // attributes..
1327
1328 Roo.HtmlEditorCore.ablack = [
1329     'on'
1330 ];
1331     
1332 Roo.HtmlEditorCore.aclean = [ 
1333     'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc' 
1334 ];
1335
1336 // protocols..
1337 Roo.HtmlEditorCore.pwhite= [
1338         'http',  'https',  'mailto'
1339 ];
1340
1341 // white listed style attributes.
1342 Roo.HtmlEditorCore.cwhite= [
1343       //  'text-align', /// default is to allow most things..
1344       
1345          
1346 //        'font-size'//??
1347 ];
1348
1349 // black listed style attributes.
1350 Roo.HtmlEditorCore.cblack= [
1351       //  'font-size' -- this can be set by the project 
1352 ];
1353
1354
1355 Roo.HtmlEditorCore.swapCodes   =[ 
1356     [    8211, "&#8211;" ], 
1357     [    8212, "&#8212;" ], 
1358     [    8216,  "'" ],  
1359     [    8217, "'" ],  
1360     [    8220, '"' ],  
1361     [    8221, '"' ],  
1362     [    8226, "*" ],  
1363     [    8230, "..." ]
1364 ]; 
1365
1366