javascript - 如何使范围偏移与多行 contenteditable div 中的 HTML 元素一起使用?

标签 javascript html range contenteditable caret

我的代码在插入符定位、内容可编辑的 div 和其中的 HTML 标签方面存在一些问题。

我正在努力实现的目标

我想要一个内容可编辑的 div,它允许通过键入某种快捷方式插入换行符和多个 HTML 标记 - 在我的例子中是双左括号“{{”。

到目前为止我取得了什么

div 允许使用单个 HTML 标记,并且仅适用于单行文本。

问题

1) 当我用回车键换行时,{{ 不再触发标签显示。我假设您必须以某种方式使脚本在创建范围时考虑换行符(节点?)。

2) 如果您已经有一个可见的 HTML 标记,则不能插入另一个。相反,您会在浏览器的控制台中收到以下错误。

未捕获的 DOMException:无法在“Range”上执行“setStart”:偏移量 56 大于节点的长度 (33)。

我注意到范围偏移变为 0(或从 HTML 标记的末尾开始),这可能是这里问题的罪魁祸首。

下面是我到目前为止的代码...

一切都在 keyup 或 mouseclick 上触发。

var tw_template_trigger = '{{';
var tw_template_tag = '<span class="tw-template-tag" contenteditable="false"><a href="#" class="tw-template-tag-remove"><i class="tw-icon tw-icon-close"></i></a>Pick a tag</span>';

$('.tw-post-template-content').on( 'keyup mouseup', function() {

    // Basically check if someone typed {{ 
    // if yes, attempt to delete those two characters
    // then paste tag HTML in that position
    if( checkIfTagIsTriggered( this ) && deleteTagTrigger( this ) ) {
        pasteTagAtCaret();
    }   

});


function pasteTagAtCaret(selectPastedContent) {

    // Then add the tag
    var sel, range;
    if (window.getSelection) {
        // IE9 and non-IE
        sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
            range = sel.getRangeAt(0);
            range.deleteContents();

            // Range.createContextualFragment() would be useful here but is
            // only relatively recently standardized and is not supported in
            // some browsers (IE9, for one)
            var el = document.createElement("div");
            el.innerHTML = tw_template_tag;
            var frag = document.createDocumentFragment(), node, lastNode;
            while ( (node = el.firstChild) ) {
                lastNode = frag.appendChild(node);
            }
            var firstNode = frag.firstChild;
            range.insertNode(frag);

            // Preserve the selection
            if (lastNode) {
                range = range.cloneRange();
                range.setStartAfter(lastNode);
                range.collapse(true);
                sel.removeAllRanges();
                sel.addRange(range);
            }
        }
    } else if ( (sel = document.selection) && sel.type != "Control") {
        // IE < 9
        var originalRange = sel.createRange();
        originalRange.collapse(true);
        sel.createRange().pasteHTML( tw_template_tag );
    }

}

function checkIfTagIsTriggered(containerEl) {

    var precedingChar = "", sel, range, precedingRange;
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.rangeCount > 0) {
            range = sel.getRangeAt(0).cloneRange();
            range.collapse(true);
            range.setStart(containerEl, 0);
            precedingChar = range.toString().slice(-2);
        }
    } else if ( (sel = document.selection) && sel.type != "Control") {
        range = sel.createRange();
        precedingRange = range.duplicate();
        precedingRange.moveToElementText(containerEl);
        precedingRange.setEndPoint("EndToStart", range);
        precedingChar = precedingRange.text.slice(-2);
    }

    if( tw_template_trigger == precedingChar )
        return true;

    return false;

}

function deleteTagTrigger(containerEl) {

    var preceding = "",
        sel,
        range,
        precedingRange;
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.rangeCount > 0) {
            range = sel.getRangeAt(0).cloneRange();
            range.collapse(true);
            range.setStart(containerEl, 0);
            preceding = range.toString();
        }
    } else if ((sel = document.selection) && sel.type != "Control") {
        range = sel.createRange();
        precedingRange = range.duplicate();
        precedingRange.moveToElementText(containerEl);
        precedingRange.setEndPoint("EndToStart", range);
        preceding = precedingRange.text;
    }

    // First Remove {{
    var words = range.toString().trim().split(' '),
    lastWord = words[words.length - 1];

    if (lastWord && lastWord == tw_template_trigger ) {

        /* Find word start and end */
        var wordStart = range.toString().lastIndexOf(lastWord);
        var wordEnd = wordStart + lastWord.length;

        range.setStart(containerEl.firstChild, wordStart);
        range.setEnd(containerEl.firstChild, wordEnd);

        range.deleteContents();
        range.insertNode(document.createTextNode(' '));
        // delete That specific word and replace if with resultValue

        return true;

    }

    return false;

}

我注意到这两行导致第二期浏览器错误

range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);

理论上,我知道问题出在哪里。我相信这两个问题都可以通过使范围创建脚本使用父节点而不是子节点以及循环遍历换行符所在的文本节点来解决。但是,此时我不知道如何实现它。

你能给我指出正确的方向吗?

编辑

我实际上已经设法上传了一个演示,其中包含到目前为止的进度,以使其更加清晰。

Demo

最佳答案

我自己解决了这个问题,并将所有功能合并为一个。整洁的!下面是最终代码。在进一步考虑后,我删除了按回车键的功能。

希望对大家有帮助

    var tw_template_trigger = '{{';
    var tw_template_tag = '<span class="tw-template-tag" contenteditable="false">Pick a tag</span>';

    $(".tw-post-template-content").keypress(function(e){ return e.which != 13; });

    $('.tw-post-template-content').on( 'keyup mouseup', function() {
        triggerTag( this ); 
    });

    function triggerTag(containerEl) {

        var sel,
            range,
            text;

        if (window.getSelection) {
            sel = window.getSelection();
            if (sel.rangeCount > 0) {
                range = sel.getRangeAt(0).cloneRange(); // clone current range into another variable for manipulation#
                range.collapse(true);
                range.setStart(containerEl, 0);
                text = range.toString();
            }
        }

        if( text && text.slice(-2) == tw_template_trigger ) {
            range.setStart( range.endContainer, range.endOffset - tw_template_trigger.length);
            range.setEnd( range.endContainer, range.endOffset );
            range.deleteContents();
            range.insertNode(document.createTextNode(' '));

            //

            var el = document.createElement("div");
            el.innerHTML = tw_template_tag;
            var frag = document.createDocumentFragment(), node, lastNode;
            while ( (node = el.firstChild) ) {
                lastNode = frag.appendChild(node);
            }
            var firstNode = frag.firstChild;
            range.insertNode(frag);

            // Preserve the selection
            if (lastNode) {
                range = range.cloneRange();
                range.setStartAfter(lastNode);
                range.collapse(true);
                sel.removeAllRanges();
                sel.addRange(range);
            }

            return true;
        }

        return false;

    }

关于javascript - 如何使范围偏移与多行 contenteditable div 中的 HTML 元素一起使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46985158/

相关文章:

javascript - 全局对象中 __proto__ 和 [[prototype]] 之间的区别?

javascript - 在 View 更改时 react JS 本地存储更新

javascript - 将php文件中的数据返回到html

arrays - Ruby Range 测试用例 SyntaxError

MySQL根据另一列创建日期范围

javascript - 如何检查传递给 Typescript 中函数的参数?

javascript - 将 html5 视频设置为特定帧

javascript - jQuery ajax 不会加载代码

css - 将列表显示为菜单,然后展开 div 以填充剩余宽度

javascript - 如何对类别中的数值输入进行分类?