在 contentEditable < div > 上设置光标位置

我正在寻找一个明确的跨浏览器解决方案,当 contentEditable = ‘ on’< div > 重新获得焦点时,将光标/插入符号位置设置为最后一个已知位置。内容可编辑 div 的默认功能似乎是在每次单击时将插入符号/光标移动到 div 中文本的开头,这是不可取的。

我相信我必须在一个变量中存储当前光标的位置,当它们离开 div 的焦点时,然后重新设置它,当它们再次有焦点时,但我还没有能够放在一起,或找到一个工作的代码样本。

如果有人有任何想法,工作代码片段或示例,我很乐意看到他们。

我现在还没有任何代码,但是我有这样的代码:

<script type="text/javascript">
// jQuery
$(document).ready(function() {
$('#area').focus(function() { .. }  // focus I would imagine I need.
}
</script>
<div id="area" contentEditable="true"></div>

附言。我已经尝试了这个资源,但它似乎不适用于 < div > 。也许只有文本区(如何将光标移动到可内容实体的末尾)

131865 次浏览

这与基于标准的浏览器兼容,但在 IE 中可能会失败。我把它作为一个起点。IE 不支持 DOM Range。

var editable = document.getElementById('editable'),
selection, range;


// Populates selection and range variables
var captureSelection = function(e) {
// Don't capture selection outside editable region
var isOrContainsAnchor = false,
isOrContainsFocus = false,
sel = window.getSelection(),
parentAnchor = sel.anchorNode,
parentFocus = sel.focusNode;


while(parentAnchor && parentAnchor != document.documentElement) {
if(parentAnchor == editable) {
isOrContainsAnchor = true;
}
parentAnchor = parentAnchor.parentNode;
}


while(parentFocus && parentFocus != document.documentElement) {
if(parentFocus == editable) {
isOrContainsFocus = true;
}
parentFocus = parentFocus.parentNode;
}


if(!isOrContainsAnchor || !isOrContainsFocus) {
return;
}


selection = window.getSelection();


// Get range (standards)
if(selection.getRangeAt !== undefined) {
range = selection.getRangeAt(0);


// Get range (Safari 2)
} else if(
document.createRange &&
selection.anchorNode &&
selection.anchorOffset &&
selection.focusNode &&
selection.focusOffset
) {
range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
} else {
// Failure here, not handled by the rest of the script.
// Probably IE or some older browser
}
};


// Recalculate selection while typing
editable.onkeyup = captureSelection;


// Recalculate selection after clicking/drag-selecting
editable.onmousedown = function(e) {
editable.className = editable.className + ' selecting';
};
document.onmouseup = function(e) {
if(editable.className.match(/\sselecting(\s|$)/)) {
editable.className = editable.className.replace(/ selecting(\s|$)/, '');
captureSelection();
}
};


editable.onblur = function(e) {
var cursorStart = document.createElement('span'),
collapsed = !!range.collapsed;


cursorStart.id = 'cursorStart';
cursorStart.appendChild(document.createTextNode('—'));


// Insert beginning cursor marker
range.insertNode(cursorStart);


// Insert end cursor marker if any text is selected
if(!collapsed) {
var cursorEnd = document.createElement('span');
cursorEnd.id = 'cursorEnd';
range.collapse();
range.insertNode(cursorEnd);
}
};


// Add callbacks to afterFocus to be called after cursor is replaced
// if you like, this would be useful for styling buttons and so on
var afterFocus = [];
editable.onfocus = function(e) {
// Slight delay will avoid the initial selection
// (at start or of contents depending on browser) being mistaken
setTimeout(function() {
var cursorStart = document.getElementById('cursorStart'),
cursorEnd = document.getElementById('cursorEnd');


// Don't do anything if user is creating a new selection
if(editable.className.match(/\sselecting(\s|$)/)) {
if(cursorStart) {
cursorStart.parentNode.removeChild(cursorStart);
}
if(cursorEnd) {
cursorEnd.parentNode.removeChild(cursorEnd);
}
} else if(cursorStart) {
captureSelection();
var range = document.createRange();


if(cursorEnd) {
range.setStartAfter(cursorStart);
range.setEndBefore(cursorEnd);


// Delete cursor markers
cursorStart.parentNode.removeChild(cursorStart);
cursorEnd.parentNode.removeChild(cursorEnd);


// Select range
selection.removeAllRanges();
selection.addRange(range);
} else {
range.selectNode(cursorStart);


// Select range
selection.removeAllRanges();
selection.addRange(range);


// Delete cursor marker
document.execCommand('delete', false, null);
}
}


// Call callbacks here
for(var i = 0; i < afterFocus.length; i++) {
afterFocus[i]();
}
afterFocus = [];


// Register selection again
captureSelection();
}, 10);
};

更新

我已经写了一个跨浏览器的范围和选择库,称为 兰吉,其中包含了一个改进版本的代码,我张贴在下面。对于这个特定的问题,您可以使用 选择保存和恢复模块,但是如果您在项目中没有使用任何其他选择,并且不需要大量的库,那么我会倾向于使用类似于 @ Nico Burns 的回答的代码。

前一个答案

您可以使用 IERange (http://code.google.com/p/ierange/)将 IE 的 TextRange 转换为类似 DOM Range 的东西,并将其与类似于无眼皮的起始点的东西结合使用。就个人而言,我只会使用 IERange 的算法进行 Range <-> TextRange 转换,而不会使用整个过程。IE 的选择对象没有 focus Node 和 anchorNode 属性,但是您应该能够只使用从选择中获得的 Range/TextRange。

我可能会整理一些东西来做这个,如果我这样做,将回到这里。

编辑:

我已经创建了一个脚本的演示来实现这一点。除了 Opera 9中的一个 bug,我还没有时间去研究它。它支持的浏览器包括 IE 5.5,6和7,Chrome 2,Firefox 2,3和3.5,以及 Safari 4。

Http://www.timdown.co.uk/code/selections/

请注意,在浏览器中可以向后进行选择,以便焦点节点位于选择的开始位置,点击鼠标右键或左键将把插入符号移动到相对于选择开始位置的位置。我认为在恢复一个选区时不可能复制这个过程,所以焦点节点总是在选区的末尾。

我很快就会把这些完整地写出来。

这个解决方案适用于所有主流浏览器:

saveSelection()附加到 div 的 onmouseuponkeyup事件,并将所选内容保存到变量 savedRange

restoreSelection()附加到 div 的 onfocus事件并重新选择保存在 savedRange中的选项。

除非您希望在用户单击 div 时恢复所选内容(这有点不直观,因为通常您希望光标指向您单击的位置,但是为了完整性而包含了代码) ,否则这种方法是完美的

为了实现这一点,onclickonmousedown事件被函数 cancelEvent()取消,cancelEvent()是一个跨浏览器函数来取消事件。cancelEvent()函数还运行 restoreSelection()函数,因为当点击事件被取消时,div 不会收到焦点,因此除非运行这个函数,否则根本不会选择任何内容。

变量 isInFocus存储它是否处于焦点,并被更改为“ false”onblur和“ true”onfocus。这样,只有当 div 不在焦点中时,才能取消单击事件(否则根本无法更改所选内容)。

如果您希望在 div 通过单击获得焦点时改变选择,而不是恢复选择 onclick(并且只有当使用 document.getElementById("area").focus();或类似的编程方式将焦点赋予元素时,才会删除 onclickonmousedown事件。在这些情况下,也可以安全地删除 onblur事件以及 onDivBlur()cancelEvent()功能。

如果你想快速测试这段代码,这段代码应该可以直接放到 html 页面的主体中:

<div id="area" style="width:300px;height:300px;" onblur="onDivBlur();" onmousedown="return cancelEvent(event);" onclick="return cancelEvent(event);" contentEditable="true" onmouseup="saveSelection();" onkeyup="saveSelection();" onfocus="restoreSelection();"></div>
<script type="text/javascript">
var savedRange,isInFocus;
function saveSelection()
{
if(window.getSelection)//non IE Browsers
{
savedRange = window.getSelection().getRangeAt(0);
}
else if(document.selection)//IE
{
savedRange = document.selection.createRange();
}
}


function restoreSelection()
{
isInFocus = true;
document.getElementById("area").focus();
if (savedRange != null) {
if (window.getSelection)//non IE and there is already a selection
{
var s = window.getSelection();
if (s.rangeCount > 0)
s.removeAllRanges();
s.addRange(savedRange);
}
else if (document.createRange)//non IE and no selection
{
window.getSelection().addRange(savedRange);
}
else if (document.selection)//IE
{
savedRange.select();
}
}
}
//this part onwards is only needed if you want to restore selection onclick
var isInFocus = false;
function onDivBlur()
{
isInFocus = false;
}


function cancelEvent(e)
{
if (isInFocus == false && savedRange != null) {
if (e && e.preventDefault) {
//alert("FF");
e.stopPropagation(); // DOM style (return false doesn't always work in FF)
e.preventDefault();
}
else {
window.event.cancelBubble = true;//IE stopPropagation
}
restoreSelection();
return false; // false = IE style
}
}
</script>

在 Firefox 中,可以在子节点(o_div.childNodes[0])中保存 div 的文本

var range = document.createRange();


range.setStart(o_div.childNodes[0],last_caret_pos);
range.setEnd(o_div.childNodes[0],last_caret_pos);
range.collapse(false);


var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);

我遇到了一个相关的情况,我特别需要将光标位置设置为可内容的 div 的 END。我不想使用像 Rangy 这样成熟的库,而且许多解决方案都太重量级了。

最后,我用这个简单的 jQuery 函数将 carat 位置设置为 contenteditable div 的末尾:

$.fn.focusEnd = function() {
$(this).focus();
var tmp = $('<span />').appendTo($(this)),
node = tmp.get(0),
range = null,
sel = null;


if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
} else if (window.getSelection) {
range = document.createRange();
range.selectNode(node);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
tmp.remove();
return this;
}

原理很简单: 将 span 添加到可编辑文件的末尾,选择它,然后删除 span-在 div 的末尾留下一个光标。您可以对此解决方案进行调整,以便在任何需要的地方插入 span,从而将光标放在特定的位置。

用法很简单:

$('#editable').focusEnd();

就是这样!

我采用 Nico Burns 的回答,并使用 jQuery 编写了它:

  • 每个 div contentEditable="true"
  • 短一点

你需要 jQuery 1.6或更高版本:

savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
var s = window.getSelection();
var t = $('div[contenteditable="true"]').index(this);
if (typeof(savedRanges[t]) === "undefined"){
savedRanges[t]= new Range();
} else if(s.rangeCount > 0) {
s.removeAllRanges();
s.addRange(savedRanges[t]);
}
}).bind("mouseup keyup",function(){
var t = $('div[contenteditable="true"]').index(this);
savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
if(!$(this).is(":focus")){
e.stopPropagation();
e.preventDefault();
$(this).focus();
}
});

savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
var s = window.getSelection();
var t = $('div[contenteditable="true"]').index(this);
if (typeof(savedRanges[t]) === "undefined"){
savedRanges[t]= new Range();
} else if(s.rangeCount > 0) {
s.removeAllRanges();
s.addRange(savedRanges[t]);
}
}).bind("mouseup keyup",function(){
var t = $('div[contenteditable="true"]').index(this);
savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
if(!$(this).is(":focus")){
e.stopPropagation();
e.preventDefault();
$(this).focus();
}
});
div[contenteditable] {
padding: 1em;
font-family: Arial;
outline: 1px solid rgba(0,0,0,0.5);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contentEditable="true"></div>
<div contentEditable="true"></div>
<div contentEditable="true"></div>

在玩了一会儿之后,我修改了上面的回答,把它做成了一个 jQuery 插件,这样你就可以做下面这些事情之一:

var html = "The quick brown fox";
$div.html(html);


// Select at the text "quick":
$div.setContentEditableSelection(4, 5);


// Select at the beginning of the contenteditable div:
$div.setContentEditableSelection(0);


// Select at the end of the contenteditable div:
$div.setContentEditableSelection(html.length);

请原谅这篇长长的代码文章,但它可能对某些人有所帮助:

$.fn.setContentEditableSelection = function(position, length) {
if (typeof(length) == "undefined") {
length = 0;
}


return this.each(function() {
var $this = $(this);
var editable = this;
var selection;
var range;


var html = $this.html();
html = html.substring(0, position) +
'<a id="cursorStart"></a>' +
html.substring(position, position + length) +
'<a id="cursorEnd"></a>' +
html.substring(position + length, html.length);
console.log(html);
$this.html(html);


// Populates selection and range variables
var captureSelection = function(e) {
// Don't capture selection outside editable region
var isOrContainsAnchor = false,
isOrContainsFocus = false,
sel = window.getSelection(),
parentAnchor = sel.anchorNode,
parentFocus = sel.focusNode;


while (parentAnchor && parentAnchor != document.documentElement) {
if (parentAnchor == editable) {
isOrContainsAnchor = true;
}
parentAnchor = parentAnchor.parentNode;
}


while (parentFocus && parentFocus != document.documentElement) {
if (parentFocus == editable) {
isOrContainsFocus = true;
}
parentFocus = parentFocus.parentNode;
}


if (!isOrContainsAnchor || !isOrContainsFocus) {
return;
}


selection = window.getSelection();


// Get range (standards)
if (selection.getRangeAt !== undefined) {
range = selection.getRangeAt(0);


// Get range (Safari 2)
} else if (
document.createRange &&
selection.anchorNode &&
selection.anchorOffset &&
selection.focusNode &&
selection.focusOffset
) {
range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
} else {
// Failure here, not handled by the rest of the script.
// Probably IE or some older browser
}
};


// Slight delay will avoid the initial selection
// (at start or of contents depending on browser) being mistaken
setTimeout(function() {
var cursorStart = document.getElementById('cursorStart');
var cursorEnd = document.getElementById('cursorEnd');


// Don't do anything if user is creating a new selection
if (editable.className.match(/\sselecting(\s|$)/)) {
if (cursorStart) {
cursorStart.parentNode.removeChild(cursorStart);
}
if (cursorEnd) {
cursorEnd.parentNode.removeChild(cursorEnd);
}
} else if (cursorStart) {
captureSelection();
range = document.createRange();


if (cursorEnd) {
range.setStartAfter(cursorStart);
range.setEndBefore(cursorEnd);


// Delete cursor markers
cursorStart.parentNode.removeChild(cursorStart);
cursorEnd.parentNode.removeChild(cursorEnd);


// Select range
selection.removeAllRanges();
selection.addRange(range);
} else {
range.selectNode(cursorStart);


// Select range
selection.removeAllRanges();
selection.addRange(range);


// Delete cursor marker
document.execCommand('delete', false, null);
}
}


// Register selection again
captureSelection();
}, 10);
});
};

您可以利用现代浏览器支持的 SelectNodeContent

var el = document.getElementById('idOfYoursContentEditable');
var selection = window.getSelection();
var range = document.createRange();
selection.removeAllRanges();
range.selectNodeContents(el);
range.collapse(false);
selection.addRange(range);
el.focus();