roojs-ui.js
[roojs1] / Roo / form / HtmlEditor.js
index dc859f7..1371905 100644 (file)
@@ -23,7 +23,8 @@
  * @class Ext.form.HtmlEditor
  * @extends Ext.form.Field
  * Provides a lightweight HTML Editor component.
- * WARNING - THIS CURRENTlY ONLY WORKS ON FIREFOX - USE FCKeditor for a cross platform version
+ *
+ * This has been tested on Fireforx / Chrome.. IE may not be so great..
  * 
  * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
  * supported by this editor.</b><br/><br/>
@@ -57,6 +58,13 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
      * @cfg {Number} width (in pixels)
      */   
     width: 500,
+    
+    /**
+     * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
+     * 
+     */
+    stylesheets: false,
+    
     // id of frame..
     frameId: false,
     
@@ -168,7 +176,40 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
      * want to change the initialization markup of the iframe (e.g. to add stylesheets).
      */
     getDocMarkup : function(){
-        return '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style></head><body></body></html>';
+        // body styles..
+        var st = '';
+        if (this.stylesheets === false) {
+            
+            Roo.get(document.head).select('style').each(function(node) {
+                st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
+            });
+            
+            Roo.get(document.head).select('link').each(function(node) { 
+                st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
+            });
+            
+        } else if (!this.stylesheets.length) {
+                // simple..
+                st = '<style type="text/css">' +
+                    'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
+                   '</style>';
+        } else {
+            Roo.each(this.stylesheets, function(s) {
+                st += '<link rel="stylesheet" type="text/css" href="' + s +'" />'
+            });
+            
+        }
+        
+        st +=  '<style type="text/css">' +
+            'IMG { cursor: pointer } ' +
+        '</style>';
+
+        
+        return '<html><head>' + st  +
+            //<style type="text/css">' +
+            //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
+            //'</style>' +
+            ' </head><body class="roo-htmleditor-body"></body></html>';
     },
 
     // private
@@ -205,10 +246,8 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
         }
 
         this.frameId = Roo.id();
-        this.createToolbar(this);
-        
-        
         
+        this.createToolbar(this);
         
       
         
@@ -218,7 +257,8 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
             name: this.frameId,
             frameBorder : 'no',
             'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL  :  "javascript:false"
-        });
+        }, this.el
+        );
         
        // console.log(iframe);
         //this.wrap.dom.appendChild(iframe);
@@ -258,7 +298,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
             this.setSize(this.wrap.getSize());
         }
         if (this.resizeEl) {
-            this.resizeEl.resizeTo.defer(100, this.resizeEl,this.width,this.height);
+            this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
             // should trigger onReize..
         }
     },
@@ -266,7 +306,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
     // private
     onResize : function(w, h)
     {
-        Roo.log('resize: ' +w + ',' + h );
+        //Roo.log('resize: ' +w + ',' + h );
         Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
         if(this.el && this.iframe){
             if(typeof w == 'number'){
@@ -279,13 +319,16 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 for (var i =0; i < this.toolbars.length;i++) {
                     // fixme - ask toolbars for heights?
                     tbh += this.toolbars[i].tb.el.getHeight();
+                    if (this.toolbars[i].footer) {
+                        tbh += this.toolbars[i].footer.el.getHeight();
+                    }
                 }
                 
                 
                 
                 
                 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
-                ah -= 10; // knock a few pixes off for look..
+                ah -= 5; // knock a few pixes off for look..
                 this.el.setHeight(this.adjustWidth('textarea', ah));
                 this.iframe.style.height = ah + 'px';
                 if(this.doc){
@@ -390,7 +433,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
     syncValue : function(){
         if(this.initialized){
             var bd = (this.doc.body || this.doc.documentElement);
-            this.cleanUpPaste();
+            //this.cleanUpPaste(); -- this is done else where and causes havoc..
             var html = bd.innerHTML;
             if(Roo.isSafari){
                 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
@@ -400,6 +443,10 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 }
             }
             html = this.cleanHtml(html);
+            // fix up the special chars..
+            html = html.replace(/([\x80-\uffff])/g, function (a, b) {
+                return "&#"+b.charCodeAt()+";" 
+            });
             if(this.fireEvent('beforesync', this, html) !== false){
                 this.el.dom.value = html;
                 this.fireEvent('sync', this, html);
@@ -479,7 +526,8 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
         dbody.bgProperties = 'fixed'; // ie
         Roo.DomHelper.applyStyles(dbody, ss);
         Roo.EventManager.on(this.doc, {
-            'mousedown': this.onEditorEvent,
+            //'mousedown': this.onEditorEvent,
+            'mouseup': this.onEditorEvent,
             'dblclick': this.onEditorEvent,
             'click': this.onEditorEvent,
             'keyup': this.onEditorEvent,
@@ -567,7 +615,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
     onEditorEvent : function(e){
         this.fireEvent('editorevent', this, e);
       //  this.updateToolbar();
-        this.syncValue();
+        this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
     },
 
     insertTag : function(tg)
@@ -619,17 +667,23 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
         this.doc.execCommand(cmd, false, value === undefined ? null : value);
         this.syncValue();
     },
-
    
     /**
      * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
      * to insert tRoo.
-     * @param {String} text
+     * @param {String} text | dom node.. 
      */
-    insertAtCursor : function(text){
+    insertAtCursor : function(text)
+    {
+        
+        
+        
         if(!this.activated){
             return;
         }
+        /*
         if(Roo.isIE){
             this.win.focus();
             var r = this.doc.selection.createRange();
@@ -638,10 +692,35 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 r.pasteHTML(text);
                 this.syncValue();
                 this.deferFocus();
+            
             }
-        }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
+            return;
+        }
+        */
+        if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
             this.win.focus();
-            this.execCmd('InsertHTML', text);
+            
+            
+            // from jquery ui (MIT licenced)
+            var range, node;
+            var win = this.win;
+            
+            if (win.getSelection && win.getSelection().getRangeAt) {
+                range = win.getSelection().getRangeAt(0);
+                node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
+                range.insertNode(node);
+            } else if (win.document.selection && win.document.selection.createRange) {
+                // no firefox support
+                var txt = typeof(text) == 'string' ? text : text.outerHTML;
+                win.document.selection.createRange().pasteHTML(txt);
+            } else {
+                // no firefox support
+                var txt = typeof(text) == 'string' ? text : text.outerHTML;
+                this.execCmd('InsertHTML', txt);
+            } 
+            
+            this.syncValue();
+            
             this.deferFocus();
         }
     },
@@ -655,16 +734,19 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 switch(c){
                     case 'b':
                         cmd = 'bold';
-                    break;
+                        break;
                     case 'i':
                         cmd = 'italic';
-                    break;
+                        break;
+                    
                     case 'u':
                         cmd = 'underline';
+                        break;
+                    
                     case 'v':
                         this.cleanUpPaste.defer(100, this);
                         return;
-                    break;
+                        
                 }
                 if(cmd){
                     this.win.focus();
@@ -782,7 +864,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
         
         
          
-        var range = this.createRange(this.getSelection());
+        var range = this.createRange(this.getSelection()).cloneRange();
         
         if (Roo.isIE) {
             var parent = range.parentElement();
@@ -800,12 +882,14 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
             return parent;
         }
         
-        
-        var ar = range.endContainer.childNodes;
-        if (!ar.length) {
-            ar = range.commonAncestorContainer.childNodes;
-            //alert(ar.length);
+        // is ancestor a text element.
+        var ac =  range.commonAncestorContainer;
+        if (ac.nodeType == 3) {
+            ac = ac.parentNode;
         }
+        
+        var ar = ac.childNodes;
+         
         var nodes = [];
         var other_nodes = [];
         var has_other_nodes = false;
@@ -825,6 +909,7 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 other_nodes.push(ar[i]);
                 continue;
             }
+            // outer..
             if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0))  {
                 continue;
             }
@@ -875,40 +960,84 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
         }
     
     },
+    /***
+     *
+     * Range intersection.. the hard stuff...
+     *  '-1' = before
+     *  '0' = hits..
+     *  '1' = after.
+     *         [ -- selected range --- ]
+     *   [fail]                        [fail]
+     *
+     *    basically..
+     *      if end is before start or  hits it. fail.
+     *      if start is after end or hits it fail.
+     *
+     *   if either hits (but other is outside. - then it's not 
+     *   
+     *    
+     **/
     
     
-    
-    // BC Hacks - cause I cant work out what i was trying to do..
+    // @see http://www.thismuchiknow.co.uk/?p=64.
     rangeIntersectsNode : function(range, node)
     {
         var nodeRange = node.ownerDocument.createRange();
         try {
             nodeRange.selectNode(node);
-        }
-        catch (e) {
+        } catch (e) {
             nodeRange.selectNodeContents(node);
         }
-
-        return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
-                 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
+    
+        var rangeStartRange = range.cloneRange();
+        rangeStartRange.collapse(true);
+    
+        var rangeEndRange = range.cloneRange();
+        rangeEndRange.collapse(false);
+    
+        var nodeStartRange = nodeRange.cloneRange();
+        nodeStartRange.collapse(true);
+    
+        var nodeEndRange = nodeRange.cloneRange();
+        nodeEndRange.collapse(false);
+    
+        return rangeStartRange.compareBoundaryPoints(
+                 Range.START_TO_START, nodeEndRange) == -1 &&
+               rangeEndRange.compareBoundaryPoints(
+                 Range.START_TO_START, nodeStartRange) == 1;
+        
+         
     },
-    rangeCompareNode : function(range, node) {
+    rangeCompareNode : function(range, node)
+    {
         var nodeRange = node.ownerDocument.createRange();
         try {
             nodeRange.selectNode(node);
         } catch (e) {
             nodeRange.selectNodeContents(node);
         }
-        var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
-        var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
-
-        if (nodeIsBefore && !nodeIsAfter)
-            return 0;
-        if (!nodeIsBefore && nodeIsAfter)
-            return 1;
+        
+        
+        range.collapse(true);
+    
+        nodeRange.collapse(true);
+     
+        var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
+        var ee = range.compareBoundaryPoints(  Range.END_TO_END, nodeRange);
+         
+        //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
+        
+        var nodeIsBefore   =  ss == 1;
+        var nodeIsAfter    = ee == -1;
+        
         if (nodeIsBefore && nodeIsAfter)
-            return 2;
-
+            return 0; // outer
+        if (!nodeIsBefore && nodeIsAfter)
+            return 1; //right trailed.
+        
+        if (nodeIsBefore && !nodeIsAfter)
+            return 2;  // left trailed.
+        // fully contined.
         return 3;
     },
 
@@ -916,11 +1045,28 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
     cleanUpPaste :  function()
     {
         // cleans up the whole document..
-      //  console.log('cleanuppaste');
+         Roo.log('cleanuppaste');
         this.cleanUpChildren(this.doc.body);
+        var clean = this.cleanWordChars(this.doc.body.innerHTML);
+        if (clean != this.doc.body.innerHTML) {
+            this.doc.body.innerHTML = clean;
+        }
         
+    },
+    
+    cleanWordChars : function(input) {
+        var he = Roo.form.HtmlEditor;
+    
+        var output = input;
+        Roo.each(he.swapCodes, function(sw) { 
         
+            var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
+            output = output.replace(swapper, sw[1]);
+        });
+        return output;
     },
+    
+    
     cleanUpChildren : function (n)
     {
         if (!n.childNodes.length) {
@@ -953,6 +1099,27 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
             return;
             
         }
+        
+        var remove_keep_children= Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1;
+        
+        // remove <a name=....> as rendering on yahoo mailer is bored with this.
+        
+        if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
+            remove_keep_children = true;
+        }
+        
+        if (remove_keep_children) {
+            this.cleanUpChildren(node);
+            // inserts everything just before this node...
+            while (node.childNodes.length) {
+                var cn = node.childNodes[0];
+                node.removeChild(cn);
+                node.parentNode.insertBefore(cn, node);
+            }
+            node.parentNode.removeChild(node);
+            return;
+        }
+        
         if (!node.attributes || !node.attributes.length) {
             this.cleanUpChildren(node);
             return;
@@ -984,15 +1151,17 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
             Roo.each(parts, function(p) {
                 p = p.replace(/\s+/g,'');
                 if (!p.length) {
-                    return;
+                    return true;
                 }
                 var l = p.split(':').shift().replace(/\s+/g,'');
                 
+                // only allow 'c whitelisted system attributes'
                 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
                     Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
                     node.removeAttribute(n);
                     return false;
                 }
+                return true;
             });
             
             
@@ -1014,10 +1183,17 @@ Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
                 cleanStyle(a.name,a.value);
             }
             /// clean up MS crap..
+            // tecnically this should be a list of valid class'es..
+            
+            
             if (a.name == 'class') {
                 if (a.value.match(/^Mso/)) {
                     node.className = '';
                 }
+                
+                if (a.value.match(/body/)) {
+                    node.className = '';
+                }
             }
             
             // style cleanup!?
@@ -1104,7 +1280,9 @@ Roo.form.HtmlEditor.black = [
 Roo.form.HtmlEditor.clean = [
     'script', 'style', 'title', 'xml'
 ];
-
+Roo.form.HtmlEditor.remove = [
+    'font'
+];
 // attributes..
 
 Roo.form.HtmlEditor.ablack = [
@@ -1120,8 +1298,22 @@ Roo.form.HtmlEditor.pwhite= [
         'http',  'https',  'mailto'
 ];
 
+// white listed style attributes.
 Roo.form.HtmlEditor.cwhite= [
         'text-align',
         'font-size'
 ];
 
+
+Roo.form.HtmlEditor.swapCodes   =[ 
+    [    8211, "--" ], 
+    [    8212, "--" ], 
+    [    8216,  "'" ],  
+    [    8217, "'" ],  
+    [    8220, '"' ],  
+    [    8221, '"' ],  
+    [    8226, "*" ],  
+    [    8230, "..." ]
+]; 
+
+    
\ No newline at end of file