Search code examples
javascriptjqueryhtmlselectionrangy

How can I best make an inline span draggable within a paragraph of text?


I have a paragraph of text in which the user may place a "pin" to mark a position. Once a pin has been placed, I would like to allow the user to move its position by dragging it to a new location in the paragraph. This is simple to do with block elements, but I have yet to see a good way to do it with inline elements. How might I accomplish this?

I have already implemented it using window.selection as a way to find the cursor's location in the paragraph, but it is not as smooth as I would like.

As a note, I am using the Rangy library to wrap the native Range and Selection functionality, but it works the same way as the native functions do.

Here is the code:

$(document).on("mousedown", '.pin', function () {
    //define what a pin is
    var el = document.createElement("span");
    el.className = "pin";
    el.id = "test";
    //make it contain an empty space so we can color it
    el.appendChild(document.createTextNode("d"));
    $(document).on("mousemove", function () {
        //get the current selection
        var selection = rangy.getSelection();
        //collapse the selection to either the front
        //or back, since we do not want the user to see it.
        if (selection.isBackwards()) {
            selection.collapseToStart();
        } else {
            selection.collapseToEnd();
        }
        //remove the old pin
        $('.pin').remove();
        //place the new pin at the current selection
        selection.getAllRanges()[0].insertNode(el);
    });
    //remove the handler when the user has stopped dragging it
    $(document).on("mouseup", function () {
        $(document).off("mousemove");
    });
});

And here is a working demo: http://jsfiddle.net/j1LLmr5b/22/ .

As you can see, it works(usually), but the user can see the selection being made. Have any ideas on how to move the span without showing the selection highlight? I will also accept an alternate method that does not use the selection at all. The goal is to allow movement of the span as cleanly as possible.


Solution

  • You can do this using ranges instead using code similar to this answer. Unfortunately the code is a bit longer than ideal because IE hasn't yet implemented document.caretPositionFromPoint(). However, the old proprietary TextRange object, still present in IE 11, comes to the rescue.

    Here's a demo:

    http://jsfiddle.net/j1LLmr5b/26/

    Here's the relevant code:

    var range, textRange, x = e.clientX, y = e.clientY;
    
    //remove the old pin
    $('.pin').remove();
    
    // Try the standards-based way first
    if (document.caretPositionFromPoint) {
        var pos = document.caretPositionFromPoint(x, y);
        range = document.createRange();
        range.setStart(pos.offsetNode, pos.offset);
        range.collapse();
    }
    // Next, the WebKit way
    else if (document.caretRangeFromPoint) {
        range = document.caretRangeFromPoint(x, y);
    }
    // Finally, the IE way
    else if (document.body.createTextRange) {
        textRange = document.body.createTextRange();
        textRange.moveToPoint(x, y);
        var spanId = "temp_" + ("" + Math.random()).slice(2);
        textRange.pasteHTML('<span id="' + spanId + '">&nbsp;</span>');
        var span = document.getElementById(spanId);
        //place the new pin
        span.parentNode.replaceChild(el, span);
    }
    if (range) {
        //place the new pin
        range.insertNode(el);
    }