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