如何在可满足内容的元素的文本中重复选择给定字符串

How to select a given string repeatedly within the text of a contenteditable element

本文关键字:选择 字符串 元素 可满足 文本      更新时间:2023-09-26

我已经做了几个小时的搜索来回答这个问题,但我没有找到任何合适的答案,所以我决定尝试编写自己的JQuery插件来完成这项工作。

要求如下:

  • 选择任意字符串(即'abc'),它从已经存在于contentitable元素中的文本中传递给函数。
  • 选择应该从活动插入符号位置后元素中该字符串的第一个实例开始,并在下次调用该函数时移动到同一字符串的下一个实例,无论第一个选择的实例是否被编辑为其他内容(即:'abc'被用户更改为'xyz'。
  • 字符串应该在文本节点中找到,而不是在元素节点或其属性节点中找到,以便使用类名和带有id的引用进行适当的样式化。
  • 应该选择整个字符串
  • 搜索应该仅限于可满足内容的元素。

例如,如果我有

<div id='main' class='main' contenteditable>
    <p><span class='lead'>Date:</span>abc def abc 123 abc</p>
</div>

和我在"def"上有插入符号,然后运行函数,第二个"abc"应该被选中。如果我不移动插入符号并再次运行该函数,则应该选中第三个"abc"。

尽管我的插件的目标更广泛一些,但我惊讶地发现,甚至在一个可满足内容的div中选择给定的字符串都没有通用的解决方案。

我将在下面发布我当前的JQuery插件的工作版本。运行代码片段,将插入符号放入可内容编辑的潜水中,然后按F9运行函数并选择文本。正如您所看到的,该解决方案不是很优雅,并且仅当光标位于特定位置时才有效。我必须假设有一个更优雅的解决方案,理想情况下甚至可以接受RegEx而不是字符串。我尝试用递归实现这个功能,但是我找不到一种方法在找到文本字符串后可靠地退出递归循环。有没有更好的办法?我欢迎你的想法。

document.getElementById('main').onkeydown = function(e) {
    switch (e.keyCode) {
        case 120: {
            // F9 to select next %fill%
            $('#main').selecttarget('%fill%');
        }
    }
};
//JQuery plugin
jQuery.fn.selecttarget = function(target) {
    // get collection of all elements of the object passed (this)
    // de-jQuery into its DOM object by using this[0]
    var items = this[0].getElementsByTagName("*");
    // get the node at which the cursor currently resides
    var startNode = null;
    if (window.getSelection) {
        startNode = window.getSelection().getRangeAt(0).commonAncestorContainer;
        startNode = (startNode.nodeType===1) ? startNode : startNode.parentNode;
    }
    // get the startNodes ancestry and iterate up through it until the passed element is found
    // once the passed element is found, we can step back down a level to find the ancestor
    // that is the immediate child of the passed element
    var parentEls = $(startNode).parents();
    var level = 0;
    for (var i = 0; i < parentEls.length; i++) {
        if (parentEls[i] == this[0]) {
            level = i - 1;
            break;
        }
    }
    // the above method works somewhat well except if the caret is on the first span, then
    // it misses the first %fill%
    // get the index of the node at which the cursor currently resides amongst its siblings
    var index = Array.prototype.indexOf.call(items, parentEls[level]);
    // iterate through each child of the passed element looking for one that contains the
    // text we're looking for in target
    for (var i = index; i < items.length; i++) {
        var position = items[i].innerText.indexOf(target);
        if (position >= 0) {
            // if an appropriate element is found, iterate through
            // its child nodes to look for a text node with the
            // text we're looking for in target
            for (var j = 0; j < items[i].childNodes.length; j++) {
                var node = items[i].childNodes[j];
                if (node.nodeType == 3) {
                    position = node.data.indexOf(target);
                    if (position >= 0) {
                        // if a text node with the appropriate text
                        // is found, create a range and set its boundaries
                        var range = document.createRange()
                        range.setStart(node, position);
                        range.setEnd(node, position + target.length);
                        // create a selection based on the range
                        var sel = window.getSelection();
                        sel.removeAllRanges();
                        sel.addRange(range);
                        // exit everything
                        return true;
                    }
                }
            }
        }
    }
};
div.main {
    text-align: left;
    width: 90%;
    height: 500px;
    padding: 5px;
    overflow: scroll;
    border: solid thin cornflowerblue;
}
span.lineheader {
    font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='main' class='main' contenteditable>
    <p><span class='lineheader'>Date:</span>&nbsp;%fill%</p>
    <p><span class='lineheader'>Item 1:</span>&nbsp;%fill%</p>
    <p><span class='lineheader'>Item 2:</span>&nbsp;%fill%</p>
    <p><span class='lineheader'>List 2:</span>
        <ul>
            <li>%fill%</li>
            <li>%fill%</li>
            <li>%fill%</li>
        </ul>
    </p>
    <p><span class='lineheader'>Item 2:</span>&nbsp;Sed convallis massa augue. Vivamus a enim vitae eros mattis dignissim ac non velit. Donec porta %fill% tellus in justo viverra rhoncus. Nullam et sapien dapibus, eleifend elit ac, commodo est. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum sagittis quis massa vitae bibendum. Maecenas placerat mi eget arcu aliquam, id %fill% accumsan nisi dictum.
Vestibulum consequat porttitor nisl eu ultrices. Donec non blandit tortor. Aliquam erat volutpat. Suspendisse id ante et felis porttitor convallis. Sed.</p>
</div>

经过更多的时间研究,我偶然发现了Tim Down的Rangy JavaScript范围和选择库(https://github.com/timdown/rangy)。事实证明,这个库非常有用,它有助于在给定元素的所有子节点中查找字符串的下一次出现。

首先,我使用条件语句来处理在可满足元素中按F9键时选择/插入符号状态的下列可能性:

  1. 搜索字符串的实例已经被选中
  2. 已经选择了其他内容,但没有选择与搜索字符串不匹配
  3. 未选择任何内容,但插入符号位于可内容元素
  4. 中的某个位置。
  5. 插入符号不在contentitable元素中,但是contentitablediv有焦点(即用户选择了滚动条)

:

document.getElementById('main').onkeydown = function (e) {
    switch (e.keyCode) {
        case 120: {
            // F9 to select next fill_me
            var sel = window.getSelection();
            // this should refer to document.getElementById('main')
            var target = 'fill_me';
            if (sel.toString().search(target) > -1) {
                // target is already selected; go to end of selection
                findOne(target, this, sel.focusNode, sel.focusOffset);
            }
            else if (sel.toString().length > 0) {
                // something is already selected but it's not target
                // so go to start of selection
                findOne(target, this, sel.anchorNode, sel.anchorOffset);
            }
            else if (sel.rangeCount) {
                // nothing is selected, start search at cursor
                findOne(target, this, sel.anchorNode, sel.getRangeAt(0).endOffset);
            }
            else {
                // caret is not in the contenteditable element, but the contenteditable div has focus
                findOne(target, this, null, null);
            }
            break;
        }
    }
};

在上面的每个结果中,使用Rangy的findText函数调用findOne函数来查找搜索字符串的适当实例:

function findOne(target, within, startNode, startPos) {
    if (rangy.supported) {
        var range = rangy.createRange();
        var searchScopeRange = rangy.createRange();
        var caseSensitive = true;
        // assign the search scope range to the contents of
        // the element passed as a parameter, within
        searchScopeRange.selectNodeContents(within);
        if (startNode != null && startPos != null) {
            // set the start of the search scope range if
            // passed parameters are not null; otherwise
            // its start is the beginning of  the range
            searchScopeRange.setStart(startNode, startPos);
        }
        var options = {
            caseSensitive: true,
            wholeWordsOnly: true,
            withinRange: searchScopeRange
        };
        range.selectNodeContents(within);
        if (target !== "") {
            target = new RegExp(target, caseSensitive ? "g" : "gi");
            // Find first match
            range.findText(target, options)
            // translate Rangy nodes & offsets to use for selection
            selectRange(range.startContainer, range.endContainer,
                        range.startOffset, range.endOffset);
        }
    }
    function selectRange(startNode, endNode, startPos, endPos) {
        // this function takes parameters and selects an appropriate
        // within the DOM
        var range = document.createRange()
        range.setStart(startNode, startPos);
        range.setEnd(endNode, endPos);
        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
}

整个工作项目包含在下面的代码片段中。感谢Tim Down出色的工作!

rangy.init();
document.getElementById('main').focus();
document.getElementById('main').onkeydown = function(e) {
  switch (e.keyCode) {
    case 120:
      {
        // F9 to select next %fill%
        //$('#main').selecttarget('%fill%');var node;
        var sel = window.getSelection();
        var target = 'fill_me';
        
        // this should refer to document.getElementById('main')
        if (sel.toString().search(target) > -1) {
          // target is already selected; go to end of selection
          findOne(target, this, sel.focusNode, sel.focusOffset);
        } else if (sel.toString().length > 0) {
          // something is already selected but it's not target
          // so go to start of selection
          findOne(target, this, sel.anchorNode, sel.anchorOffset);
        } else if (sel.rangeCount) {
          // nothing is selected, start search at cursor
          findOne(target, this, sel.anchorNode, sel.getRangeAt(0).endOffset);
        } else {
          // this should never really happen, but it's a catch-all
          findOne(target, this, null, null);
        }
        break;
      }
  }
};
function findOne(target, within, startNode, startPos) {
  if (rangy.supported) {
    var range = rangy.createRange();
    var caseSensitive = true;
    var searchScopeRange = rangy.createRange();
    // assign the search scope range to the contents of
    // the element passed as a parameter, within
    searchScopeRange.selectNodeContents(within);
    if (startNode != null && startPos != null) {
      // set the start of the search scope range if
      // passed parameters are not null; otherwise
      // its start is the beginning of  the range
      searchScopeRange.setStart(startNode, startPos);
    }
    var options = {
      caseSensitive: true,
      wholeWordsOnly: true,
      withinRange: searchScopeRange
    };
    if (target !== "") {
      target = new RegExp(target, caseSensitive ? "g" : "gi");
      // Find first match
      range.findText(target, options)
      // translate Rangy nodes & offsets to use for selection
      selectRange(range.startContainer, range.endContainer,
        range.startOffset, range.endOffset);
    }
  }
  function selectRange(startNode, endNode, startPos, endPos) {
    // this function takes parameters and selects an appropriate
    // within the DOM
    var range = document.createRange()
    range.setStart(startNode, startPos);
    range.setEnd(endNode, endPos);
    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
  }
}
div.main {
  text-align: left;
  width: 90%;
  height: 500px;
  padding: 5px;
  overflow: scroll;
  border: solid thin cornflowerblue;
}
span.lineheader {
  font-weight: bold;
}
<script type="text/javascript" src="http://rangy.googlecode.com/svn/trunk/currentrelease/rangy-core.js"></script>
<script type="text/javascript" src="http://rangy.googlecode.com/svn/trunk/currentrelease/rangy-textrange.js"></script>
<p><span class='lineheader'>Instructions:</span>&nbsp;Press F9 to skip between instances of <i>'fill_me'</i> in the contenteditable div below</p>
<div id='main' tabindex='0' class='main' contenteditable>
  <p><span class='lineheader'>Date:</span>&nbsp;fill_me.</p>
  <p><span class='lineheader'>Item 1:</span>&nbsp;fill_me</p>
  <p><span class='lineheader'>Item 2:</span>&nbsp;fill_me</p>
  <p><span class='lineheader'>List 1:</span>
    <ul>
      <li>fill_me</li>
      <li>fill_me</li>
      <li>fill_me</li>
    </ul>
  </p>
  <p><span class='lineheader'>Paragraph 1:</span>&nbsp;Sed convallis massa augue. Vivamus a enim vitae eros mattis dignissim ac non velit. Donec porta fill_me tellus in justo viverra rhoncus. Nullam et sapien dapibus, eleifend elit ac, commodo est. Vestibulum
    ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum sagittis quis massa vitae bibendum. Maecenas placerat mi eget arcu aliquam, id fill_me accumsan nisi dictum. Vestibulum consequat porttitor nisl eu ultrices. Donec
    non blandit tortor fill_me. Aliquam erat volutpat. fill_me id ante et felis porttitor convallis. Sed.</p>
</div>