Search code examples
jqueryhtmlcssperformanceautosize

Expanding textarea to fit height gets slow with JS. Looking into div and contenteditable instead


I was using Autosize to make textareas fit the height of the text. It was working, but it got extremely slow on a couple of hundred of textareas (which may be expected).

I looked into using a div with contenteditable instead, and after a lot of trying I found a solution that is doing what I want, but it seems a little hacky, especially in Firefox with odd double-linebreak-treatment. Is there anything I can do to refine this even further or achieve the mentioned hacks in neater ways?

In Firefox on Mac you still get 2 line-breaks on enter if you edit the text somewhere in the middle. Any workarounds for that?

// Paste fix for contenteditable
$('[contenteditable]').on('paste', function (e) {
    e.preventDefault();

    if (window.clipboardData)
    {
        content = window.clipboardData.getData('Text');        
        if (window.getSelection)
        {
            var selObj = window.getSelection();
            var selRange = selObj.getRangeAt(0);
            selRange.deleteContents();                
            selRange.insertNode(document.createTextNode(content));
        }
    }
    else if (e.originalEvent.clipboardData)
    {
        content = (e.originalEvent || e).clipboardData.getData('text/plain');
        document.execCommand('insertText', false, content);
    }        
});

// Set Current Value on Focus
$(document).on('focus', '.edit-area', function() {
    var $self = $(this);
    if( $self.is("div") )
    {
        cur_val = $self[0].innerText.trim();
    }
    else
    {
        cur_val = $self.val();
    }
});

// Blur on enter and fix line-breaks especially for FF
$(document).on('keydown', '.edit-area', function(e) {
    var $self = $(this);
    var esc = e.keyCode == 27;
    var nl = e.keyCode == 13;

    if (esc)
    {
        // Restore if ESC
        if( $self.is("div") )
        {
            $self.text(cur_val);
        }
        else
        {
            $self.val(cur_val);
        }
        $self.blur();
    }
    else if (nl)
    {
        if( $self.is("div") )
        {
            /* Tried this instead of the document.execCommand below but it leaves a space in there which I'd love to avoid
            e.preventDefault(); //Prevent default browser behavior
            if (window.getSelection) {
                var selection = window.getSelection(),
                range = selection.getRangeAt(0),
                br = document.createElement("br"),
                textNode = document.createTextNode($("<div>&nbsp;</div>").text()); //Passing " " directly will not end up being shown correctly
                range.deleteContents();//required or not?
                range.insertNode(br);
                range.collapse(false);
                range.insertNode(textNode);
                range.selectNodeContents(textNode);

                selection.removeAllRanges();
                selection.addRange(range);
                return false;
            }*/
            /* Tried this but it still gives an extra br in FF
            document.execCommand('insertHTML', false, '<br><br>'); // fix for line-breaks
            return false;*/
            // This Seems to be the best solution so far...
            if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
               var sel, node, offset, text, textBefore, textAfter, range;

               sel = window.getSelection();

               // the node that contains the caret
               node = sel.anchorNode;

               // if ENTER was pressed while the caret was inside the input field

               // prevent the browsers from inserting <div>, <p>, or <br> on their own
               e.preventDefault();

               // the caret position inside the node
               offset = sel.anchorOffset;        

               // insert a '\n' character at that position
               text = node.textContent;
               textBefore = text.slice( 0, offset );
               textAfter = text.slice( offset ) || ' ';
               node.textContent = textBefore + '\n' + textAfter;

               // position the caret after that new-line character
               range = document.createRange();
               range.setStart( node, offset + 1 );
               range.setEnd( node, offset + 1 );

               // update the selection
               sel.removeAllRanges();
               sel.addRange( range );
            }
        }
    }
});

// Save data on blur
$(document).on('blur', '.edit-area', function() {
    var $self = $(this);
    if( $self.is("div") )
    {
        var value = $self[0].innerText.trim();
        $self.text(value);
    }
    else
    {
        if( $self.val() instanceof Array )
        {
            var value = $self.val();
        }
        else
        {
            var value = trim($self.val());
            $self.val(value);
        }
    }
    if( String(value) == String(cur_val) ) 
    {
        return false; // Return false if value is current value
    }
    // Save to database here...
    alert("Save this to database:\n\n"+value);
});
div.edit-area[contenteditable] {
  outline: none;
  white-space: pre-wrap;
}
div.edit-area[contenteditable]:empty:before {
  content: '-';
}
div.edit-area[contenteditable]:focus:before {
  color: transparent;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable="true" class="edit-area">Editable text with
line-breaks
preserved, enter hack, paste fix, trimmed result and no html</div>


Solution

  • Works and tested in Safari, Chrome and FF for Mac.

    // Paste fix for contenteditable
    $('[contenteditable]').on('paste', function (e) {
        e.preventDefault();
    
        if (window.clipboardData)
        {
            content = window.clipboardData.getData('Text');        
            if (window.getSelection)
            {
                var selObj = window.getSelection();
                var selRange = selObj.getRangeAt(0);
                selRange.deleteContents();                
                selRange.insertNode(document.createTextNode(content));
            }
        }
        else if (e.originalEvent.clipboardData)
        {
            content = (e.originalEvent || e).clipboardData.getData('text/plain');
            document.execCommand('insertText', false, content);
        }        
    });
    
    // Set Current Value on Focus
    $(document).on('focus', '.edit-area', function() {
        var $self = $(this);
        if( $self.is("div") )
        {
            cur_val = $self[0].innerText.trim();
        }
        else
        {
            cur_val = $self.val();
        }
    });
    
    // Blur on enter and fix line-breaks especially for FF
    $(document).on('keydown', '.edit-area', function(e) {
        var $self = $(this);
        var esc = e.keyCode == 27;
        var nl = e.keyCode == 13;
    
        if (esc)
        {
            // Restore if ESC
            if( $self.is("div") )
            {
                $self.text(cur_val);
            }
            else
            {
                $self.val(cur_val);
            }
            $self.blur();
        }
        else if (nl)
        {
            if( $self.is("div") )
            {
                if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
                   var sel, node, offset, text, textBefore, textAfter, range;
    
                   sel = window.getSelection();
    
                   // the node that contains the caret
                   node = sel.anchorNode;
    
                   // if ENTER was pressed while the caret was inside the input field
    
                   // prevent the browsers from inserting <div>, <p>, or <br> on their own
                   e.preventDefault();
    
                   // the caret position inside the node
                   offset = sel.anchorOffset;        
    
                   // insert a '\n' character at that position
                   text = node.textContent;
                   textBefore = text.slice( 0, offset );
                   textAfter = text.slice( offset ) || ' ';
                   node.textContent = textBefore + '\n' + textAfter;
    
                   // position the caret after that new-line character
                   range = document.createRange();
                   range.setStart( node, offset + 1 );
                   range.setEnd( node, offset + 1 );
    
                   // update the selection
                   sel.removeAllRanges();
                   sel.addRange( range );
                }
            }
        }
    });
    
    // Save data on blur
    $(document).on('blur', '.edit-area', function() {
        var $self = $(this);
        if( $self.is("div") )
        {
            var value = $self[0].innerText.trim();
            $self.text(value);
        }
        else
        {
            if( $self.val() instanceof Array )
            {
                var value = $self.val();
            }
            else
            {
                var value = trim($self.val());
                $self.val(value);
            }
        }
        if( String(value) == String(cur_val) ) 
        {
            return false; // Return false if value is current value
        }
        // Save to database here...
        alert("Save this to database:\n\n"+value);
    });
    div.edit-area[contenteditable] {
      outline: none;
      white-space: pre-wrap;
    }
    div.edit-area[contenteditable]:empty:before {
      content: '-';
    }
    div.edit-area[contenteditable]:focus:before {
      color: transparent;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <div contenteditable="true" class="edit-area">Editable text with
    line-breaks
    preserved, enter hack, paste fix, trimmed result and no html</div>