如何在 contenteditable 元素的文本中重复 select 给定的字符串
How to select a given string repeatedly within the text of a contenteditable element
我搜索了几个小时来回答这个问题,但没有找到任何合适的答案,所以我决定尝试编写自己的 JQuery 插件来完成这项工作。
这些是要求:
- Select 从已存在于 contenteditable 元素中的文本中传递给函数的任何字符串(即 'abc')。
- Selection 应该从活动插入符号位置之后的元素中该字符串的第一个实例开始,并在下次调用该函数时移动到同一字符串的下一个实例,无论是否第一个 selected 实例被编辑为其他内容(即 'abc' 被用户更改为 'xyz'。
- 字符串应该在文本节点中找到,而不是在元素节点或其属性节点中,以允许使用类名和 ID 引用进行适当的样式设置。
- 应 select 编辑整个字符串。
- 搜索应仅限于 contenteditable 元素。
例如,如果我有
<div id='main' class='main' contenteditable>
<p><span class='lead'>Date:</span>abc def abc 123 abc</p>
</div>
我在 "def" 上有插入符,然后是 运行 函数,第二个 "abc" 应该是 selected。如果我不再次移动插入符号和 运行 函数,则第三个 "abc" 应该 selected.
虽然我的插件的目标更广泛一些,但我惊讶地发现甚至 select 一个给定的字符串都没有通用的解决方案 div.
我将 post 下面是我当前 JQuery 插件的工作版本。 运行 代码片段,将插入符号放在内容可编辑的 dive 中,然后按 F9 以 运行 函数和 select 文本。如您所见,该解决方案不是很优雅,仅当光标位于特定位置时才有效。我不得不假设有一个更优雅的解决方案,理想情况下甚至可以接受 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> %fill%</p>
<p><span class='lineheader'>Item 1:</span> %fill%</p>
<p><span class='lineheader'>Item 2:</span> %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> 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)。这个库非常有用,有助于在给定元素的所有子节点中查找下一次出现的字符串。
首先,我使用条件语句来处理在 contenteditable 元素中按下 F9 键时 selection/caret 状态的以下可能性:
- 已选择搜索字符串实例
- 已经选择了其他内容,但不是选择与搜索字符串不匹配
- 未选择任何内容,但插入符号位于 contenteditable 元素中的某个位置
- 插入符号不在 contenteditable 元素中,但 contenteditable div 有焦点(即用户选择了滚动条)
如下:
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> 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> fill_me.</p>
<p><span class='lineheader'>Item 1:</span> fill_me</p>
<p><span class='lineheader'>Item 2:</span> 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> 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>
我搜索了几个小时来回答这个问题,但没有找到任何合适的答案,所以我决定尝试编写自己的 JQuery 插件来完成这项工作。
这些是要求:
- Select 从已存在于 contenteditable 元素中的文本中传递给函数的任何字符串(即 'abc')。
- Selection 应该从活动插入符号位置之后的元素中该字符串的第一个实例开始,并在下次调用该函数时移动到同一字符串的下一个实例,无论是否第一个 selected 实例被编辑为其他内容(即 'abc' 被用户更改为 'xyz'。
- 字符串应该在文本节点中找到,而不是在元素节点或其属性节点中,以允许使用类名和 ID 引用进行适当的样式设置。
- 应 select 编辑整个字符串。
- 搜索应仅限于 contenteditable 元素。
例如,如果我有
<div id='main' class='main' contenteditable>
<p><span class='lead'>Date:</span>abc def abc 123 abc</p>
</div>
我在 "def" 上有插入符,然后是 运行 函数,第二个 "abc" 应该是 selected。如果我不再次移动插入符号和 运行 函数,则第三个 "abc" 应该 selected.
虽然我的插件的目标更广泛一些,但我惊讶地发现甚至 select 一个给定的字符串都没有通用的解决方案 div.
我将 post 下面是我当前 JQuery 插件的工作版本。 运行 代码片段,将插入符号放在内容可编辑的 dive 中,然后按 F9 以 运行 函数和 select 文本。如您所见,该解决方案不是很优雅,仅当光标位于特定位置时才有效。我不得不假设有一个更优雅的解决方案,理想情况下甚至可以接受 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> %fill%</p>
<p><span class='lineheader'>Item 1:</span> %fill%</p>
<p><span class='lineheader'>Item 2:</span> %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> 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)。这个库非常有用,有助于在给定元素的所有子节点中查找下一次出现的字符串。
首先,我使用条件语句来处理在 contenteditable 元素中按下 F9 键时 selection/caret 状态的以下可能性:
- 已选择搜索字符串实例
- 已经选择了其他内容,但不是选择与搜索字符串不匹配
- 未选择任何内容,但插入符号位于 contenteditable 元素中的某个位置
- 插入符号不在 contenteditable 元素中,但 contenteditable div 有焦点(即用户选择了滚动条)
如下:
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> 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> fill_me.</p>
<p><span class='lineheader'>Item 1:</span> fill_me</p>
<p><span class='lineheader'>Item 2:</span> 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> 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>