Автодополнение в стиле тегов и перемещение каретки/курсора в элементах, доступных для редактирования

Я работаю над плагином jQuery, который позволит вам делать теги стиля @username, как это делает Facebook в поле ввода обновления статуса.

Моя проблема в том, что даже после нескольких часов исследований и экспериментов кажется ДЕЙСТВИТЕЛЬНО сложным просто переместить каретку. Мне удалось вставить тег <a> с чьим-то именем, но размещение курсора после него кажется ракетостроением, особенно если предполагается, что он работает во всех браузерах.

И я еще даже не рассматривал возможность замены набранного текста @username тегом, вместо того, чтобы просто вводить его, как я делаю прямо сейчас... лол

Здесь, в Stack Overflow, есть масса вопросов о работе с contenteditable, и я думаю, что прочитал их все, но на самом деле они не охватывают должным образом то, что мне нужно. Так что любая дополнительная информация, которую любой может предоставить, была бы здорово :)


person jimeh    schedule 09.05.2010    source источник
comment
Вы когда-нибудь находили дальнейшее объяснение? Я разместил аналогичный вопрос на stackoverflow.com/ вопросы/3764273/ и stackoverflow .com/questions/3972014/ но не повезло...   -  person Bertvan    schedule 19.10.2010
comment
Я, безусловно, могу сочувствовать отсутствию помощи на contenteditable! Мне пришлось много работать над собой в последнее время.   -  person Nico Burns    schedule 20.10.2010


Ответы (3)


Вы можете использовать мою библиотеку Rangy, которая с некоторым успехом пытается нормализовать реализации диапазона и выбора браузера. Если вам удалось вставить <a>, как вы говорите, и вы получили его в переменной с именем aElement, вы можете сделать следующее:

var range = rangy.createRange();
range.setStartAfter(aElement);
range.collapse(true);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
person Tim Down    schedule 20.10.2010
comment
Ваш вопрос меня заинтересовал, и я пишу общий код для подобных вещей. Я отправлю обратно в ближайшее время. - person Tim Down; 26.10.2010

Меня это заинтересовало, поэтому я написал отправную точку для полного решения. Далее используется моя библиотека Rangy с ее модуль сохранения/восстановления выбора для сохранения и восстановления выбора и нормализации кросс-браузерных проблем. Он окружает весь совпадающий текст (@независимо от того, что в данном случае) элементом ссылки и размещает выделение там, где оно было ранее. Это срабатывает после того, как в течение одной секунды не было никаких действий с клавиатурой. Он должен быть вполне многоразовым.

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>
person Tim Down    schedule 26.10.2010
comment
Замечательно. Я пытаюсь расширить это, используя rangy, чтобы выделять URL-адреса по мере ввода. Однако, похоже, есть ряд проблем: как только ссылка обнаружена, ее нельзя отредактировать, и установка для contentEditable значения true внутри тега A также нарушает работу. Есть ли лучший подход для этого? - person Matt Roberts; 25.07.2014
comment
@MattRoberts: я не думаю, что есть принципиально лучший подход, просто лучшая и более тщательная реализация. На самом деле это было задумано как отправная точка. - person Tim Down; 28.07.2014
comment
Спасибо, Тим. Мне интересно, подходит ли ваш новый модуль выделения для такого рода задач? - person Matt Roberts; 29.07.2014

Как вы сказали, вы уже можете вставить тег в каретку, я начну оттуда. Первое, что нужно сделать, это присвоить тегу идентификатор при его вставке. Затем у вас должно получиться что-то вроде этого:

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

Вот функция, которая должна поместить курсор сразу после тега.

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: протестирован и исправлен скрипт. Он определенно работает в IE6, chrome 8, firefox 4 и Opera 11. У меня нет других браузеров для тестирования, но он не использует какие-либо недавно измененные функции, поэтому он должен работать во всем, что поддерживает contenteditable.

Эта кнопка удобна для тестирования: <input type='button' onclick='setCursorAfterA()' value='Place Cursor After &lt;a/&gt; tag' >

Нико

person Nico Burns    schedule 19.10.2010