Tag-like autocompletion and caret/cursor movement in contenteditable elements

前端 未结 3 1636
旧时难觅i
旧时难觅i 2020-12-03 06:06

I\'m working on a jQuery plugin that will allow you to do @username style tags, like Facebook does in their status update input box.

My problem is, that

相关标签:
3条回答
  • 2020-12-03 06:26

    I got interested in this, so I've written the starting point for a full solution. The following uses my Rangy library with its selection save/restore module to save and restore the selection and normalize cross browser issues. It surrounds all matching text (@whatever in this case) with a link element and positions the selection where it had been previously. This is triggered after there has been no keyboard activity for one second. It should be quite reusable.

    function createLink(matchedTextNode) {
        var el = document.createElement("a");
        el.style.backgroundColor = "yellow";
        el.style.padding = "2px";
        el.contentEditable = false;
        var matchedName = matchedTextNode.data.slice(1); // Remove the leading @
        el.href = "http://www.example.com/?name=" + matchedName;
        matchedTextNode.data = matchedName;
        el.appendChild(matchedTextNode);
        return el;
    }
    
    function shouldLinkifyContents(el) {
        return el.tagName != "A";
    }
    
    function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
        var child = el.lastChild;
        while (child) {
            if (child.nodeType == 1 && shouldSurroundFunc(el)) {
                surroundInElement(child, regex, surrounderCreateFunc, shouldSurroundFunc);
            } else if (child.nodeType == 3) {
                surroundMatchingText(child, regex, surrounderCreateFunc);
            }
            child = child.previousSibling;
        }
    }
    
    function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
        var parent = textNode.parentNode;
        var result, surroundingNode, matchedTextNode, matchLength, matchedText;
        while ( textNode && (result = regex.exec(textNode.data)) ) {
            matchedTextNode = textNode.splitText(result.index);
            matchedText = result[0];
            matchLength = matchedText.length;
            textNode = (matchedTextNode.length > matchLength) ?
                matchedTextNode.splitText(matchLength) : null;
            surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
            parent.insertBefore(surroundingNode, matchedTextNode);
            parent.removeChild(matchedTextNode);
        }
    }
    
    function updateLinks() {
        var el = document.getElementById("editable");
        var savedSelection = rangy.saveSelection();
        surroundInElement(el, /@\w+/, createLink, shouldLinkifyContents);
        rangy.restoreSelection(savedSelection);
    }
    
    var keyTimer = null, keyDelay = 1000;
    
    function keyUpLinkifyHandler() {
        if (keyTimer) {
            window.clearTimeout(keyTimer);
        }
        keyTimer = window.setTimeout(function() {
            updateLinks();
            keyTimer = null;
        }, keyDelay);
    }
    

    HTML:

    <p contenteditable="true" id="editable" onkeyup="keyUpLinkifyHandler()">
        Some editable content for @someone or other
    </p>
    
    0 讨论(0)
  • 2020-12-03 06:41

    As you say you can already insert an tag at the caret, I'm going to start from there. The first thing to do is to give your tag an id when you insert it. You should then have something like this:

    <div contenteditable='true' id='status'>I went shopping with <a href='#' id='atagid'>Jane</a></div>

    Here is a function that should place the cursor just after the tag.

    function setCursorAfterA()
    {
        var atag = document.getElementById("atagid");
        var parentdiv = document.getElementById("status");
        var range,selection;
        if(window.getSelection) //FF,Chrome,Opera,Safari,IE9+
        {
            parentdiv.appendChild(document.createTextNode(""));//FF wont allow cursor to be placed directly between <a> tag and the end of the div, so a space is added at the end (this can be trimmed later)
            range = document.createRange();//create range object (like an invisible selection)
            range.setEndAfter(atag);//set end of range selection to just after the <a> tag
            range.setStartAfter(atag);//set start of range selection to just after the <a> tag
            selection = window.getSelection();//get selection object (list of current selections/ranges)
            selection.removeAllRanges();//remove any current selections (FF can have more than one)
            parentdiv.focus();//Focuses contenteditable div (necessary for opera)
            selection.addRange(range);//add our range object to the selection list (make our range visible)
        }
        else if(document.selection)//IE 8 and lower
        { 
            range = document.body.createRange();//create a "Text Range" object (like an invisible selection)
            range.moveToElementText(atag);//select the contents of the a tag (i.e. "Jane")
            range.collapse(false);//collapse selection to end of range (between "e" and "</a>").
            while(range.parentElement() == atag)//while ranges cursor is still inside <a> tag
            {
                 range.move("character",1);//move cursor 1 character to the right
            }
            range.move("character",-1);//move cursor 1 character to the left
            range.select()//move the actual cursor to the position of the ranges cursor
        }
        /*OPTIONAL: 
        atag.id = ""; //remove id from a tag
        */
    }
    

    EDIT: Tested and fixed script. It definitely works in IE6, chrome 8, firefox 4, and opera 11. Don't have other browsers on hand to test, but it doesn't use any functions that have changed recently so it should work in anything that supports contenteditable.

    This button is handy for testing: <input type='button' onclick='setCursorAfterA()' value='Place Cursor After &lt;a/&gt; tag' >

    Nico

    0 讨论(0)
  • 2020-12-03 06:46

    You could use my Rangy library, which attempts with some success to normalize browser range and selection implementations. If you've managed to insert the <a> as you say and you've got it in a variable called aElement, you can do the following:

    var range = rangy.createRange();
    range.setStartAfter(aElement);
    range.collapse(true);
    var sel = rangy.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
    
    0 讨论(0)
提交回复
热议问题