Search code examples
javascriptjquerydrag-and-dropwysiwygcontenteditable

Remove default styles from draggable content inside of contenteditable div


I am trying to create simple WYSIWYG editor and I have faced with following problem. When user copies or drag text inside of contenteditable div. According to information from the Google I have added following code for copy/paste:

var $mainInput = $('#main-input');
$mainInput.on('paste', function(e) {
    e.preventDefault();
    var text = e.originalEvent.clipboardData.getData("text/plain");
    lastCaretIndex.insertNode(document.createTextNode(text));
});

And it works like a charm:

enter image description here

But with drag&drop it doesn't work as expected. I found similar posts on Stackoverflow:

1) This one uses jQuery UI which is irrelevant for my case

2) Event described here doesn't provide ability to customize draggable text, only target

And some others pieces, so I got following code:

$mainInput.on('dragend', function(e) {
 window.getSelection().getRangeAt(0).insertNode(document.createTextNode(strip(t)));
});

And the result:

enter image description here

Also, I tried to do this according to documentation, but this fully removes old element content. Code:

$mainInput.bind('drop', function (e) {
    event.target.textContent = e.originalEvent.dataTransfer.getData("text/plain");
    event.preventDefault();
});

Example:

enter image description here

Can somebody please help me, or give some advice how this could be handled.

Current state is available in this fiddle

================= PS =============== This is solved. Version which works for most browsers - fiddle


Solution

  • When the text is dropped, the span at the drop point is split in two and a new span is inserted in between. I haven't seen an option of the drag-and-drop to prevent that behavior. One way to avoid it is to replace the default transfer mechanism.

    In the drop event handler, you can move the text manually if you find that it will end up in a different span. The destination range can be found with createCollapsedRangeFromPoint, a function provided by Tim Down. The range can then be compared with the original selection range.

    The following code can be tested in this jsfiddle. It works in Chrome and Firefox but not in IE11. The default drag-and-drop behavior is used when the customized one is not supported.

    var $mainInput = $('#main-input');
    var draggedText;
    var dragStartContainer;
    var dragEndContainer;
    var dragStartOffset;
    var dragEndOffset;
    
    $mainInput.on('dragstart', function(e) {
        var dragRange = getSelectionRange();
        dragStartContainer = dragRange.startContainer;
        dragEndContainer = dragRange.endContainer;
        dragStartOffset = dragRange.startOffset;
        dragEndOffset = dragRange.endOffset;
        draggedText = e.originalEvent.dataTransfer.getData("text");
    });
    
    $mainInput.on('drop', function(e) {
        // Get caret at mouse cursor position
        var range = createCollapsedRangeFromPoint(e.clientX, e.clientY);
        var container = range.startContainer;
        // If text is moved to another container, do it manually
        if (container && (container !== dragStartContainer || container !== dragEndContainer)) {
            e.preventDefault();
            // Insert dragged text at caret position
            var startOffset = range.startOffset;
            var str = container.textContent;
            var strPrepend = str.substr(0, range.startOffset);
            var strAppend = str.substr(range.startOffset);
            container.textContent = strPrepend + draggedText + strAppend;
            // Delete text at original position
            var spaceCount = getSpaceCountAtStart(container);
            setSelectionRange(dragStartContainer, dragEndContainer, dragStartOffset, dragEndOffset);
            document.execCommand("delete", false);
            var spaceCountCorrection = getSpaceCountAtStart(container) - spaceCount;
            // Select text at new position
            var startRemoved = (dragEndContainer === container ? dragEndOffset - 1 : 0);
            var startOffset = Math.max(0, startOffset + spaceCountCorrection - startRemoved);
            var endOffset = startOffset + draggedText.length;
            setSelectionRange(container, container, startOffset, endOffset);
        }
    });
    
    // Get the number of spaces at start of container
    // After moving text, that number may change
    // and screw up range position calculations
    function getSpaceCountAtStart(container) {
        var innerHTML = container.parentNode.innerHTML;
        var count = 0;
        for (var i = 0; i < innerHTML.length; i++) {
            if (innerHTML[i] === ' ') {
                count += 1;
            } else {
                break;
            }
        }
        return count;
    }
    
    // Function provided by Tim Down at https://stackoverflow.com/a/28275304/1009922
    function createCollapsedRangeFromPoint(x, y) {
        var doc = document;
        var position, range = null;
        if (typeof doc.caretPositionFromPoint != "undefined") {
            position = doc.caretPositionFromPoint(x, y);
            range = doc.createRange();
            range.setStart(position.offsetNode, position.offset);
            range.collapse(true);
        } else if (typeof doc.caretRangeFromPoint != "undefined") {
            range = doc.caretRangeFromPoint(x, y);
        } else if (typeof doc.body.createTextRange != "undefined") {
            range = doc.body.createTextRange();
            range.moveToPoint(x, y);
        }
        return range;
    }
    
    function getSelectionRange() {
        var sel;
        if (window.getSelection) {
            sel = window.getSelection();
            if (sel.rangeCount) {
                return sel.getRangeAt(0);
            }
        } else if (document.selection) {
            return document.createRange();
        }
        return null;
    }
    
    function setSelectionRange(startContainer, endContainer, startOffset, endOffset) {
        var sel = window.getSelection();
        sel.removeAllRanges();
        var range = document.createRange();
        range.setStart(startContainer, startOffset);
        range.setEnd(endContainer, endOffset);
        sel.addRange(range);
    }