使多个 contenteditables 表现得像一个文档
Make multiple contenteditables behave like one document
我有一堆垂直排列的多行 contenteditable div,我想允许通过箭头键在它们之间进行自然导航(就好像它是一个文档)。为此,在 keydown
事件中我需要:
- 了解插入符号的当前行和行数以确定我们是否需要向上移动(第一行and ↑ key pressed) or down (last line and ↓ key)
- 知道当前字符(显示字符串中的位置)以确定我们是否需要向上移动(位置==0 并按下 ← 键) 或向下 (position==text.length and → pressed)
- 当按键被按住且未松开时,进程不应在切换元素之间停止(因此
keydown
事件,而不是 keyup
)
- 最好:事件应该停止传播(例如,如果我在最后一行的第一列并且我按下 ↓ 键,它不应该跳到该行的最后一个字符然后继续下去)
- 最好(真的很棒):在我们跳转到下一个元素后,我们不会只是
.focus()
元素,而是在与之前相同的垂直位置模拟点击,这样感觉很自然,就像在文本编辑器中一样。
我迄今为止发现的所有 scripts/libraries 要么没有完成我需要的所有事情,要么有错误。请在您的建议中包含演示,这样我就可以在不先合并到我的代码中的情况下进行测试。谢谢!
更新: 可视化解释 - 注意有超过 2 个 div,'arrow down key on the last line' 只是四个之一触发器
我已经写了一些代码,但还没有完成...也许你可以从那里开始,如果你愿意,可以尝试完成我所做的;)我将在本周继续努力,以便为您提供解决方案...这是我到目前为止所做的:
var ajaxResult = [
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates exitialis certamina cogebatur",
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus",
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quamos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates exitialis certamina cogebatur",
];
/*************************************************************
*
* LIST OF CONTENT EDITABLE DIVS MANAGEMENT
*
**************************************************************/
// Create the editable divs
window.onload = function(){
var contentEditables = createContentEditables();
document.body.appendChild(contentEditables);
}
// Remember all the content editable elements in the order they appear in the dom
var _currentEdit,
_edits = [];
function createContentEditables(){
var div;
var result = document.createDocumentFragment();
for (var i = 0, n = ajaxResult.length ; i < n ; i++){
div = createContentEditable(ajaxResult[i]);
_edits.push(div);
result.appendChild(div);
}
return result;
}
function getPreviousEdit(edit){
// Search for the edit index
var index = _edits.indexOf(edit);
if(index == 0)
return;
// Return the previous one
return _edits[index - 1];
}
function getNextEdit(edit){
// Search for the edit index
var index = _edits.indexOf(edit);
if(index == _edits.length - 1)
return;
// Return the previous one
return _edits[index + 1];
}
/*************************************************************
*
* CONTENT EDITABLE MANAGEMENT
*
**************************************************************/
// We need to define the line height of the div to be able to retrieve the number of lines
var LINE_HEIGHT = 16;
// variables to keep trace of relevant information about the div
var _lines, _caretPosition;
/*
* Create a div with contenteditable set to true with the text
* received from the server
*/
function createContentEditable(text){
var element = document.createElement('div');
element.className = 'contenteditable';
element.innerHTML = text;
element.style.lineHeight = LINE_HEIGHT + 'px';
element.setAttribute('contenteditable', true);
// Set listeners
element.addEventListener('mouseup', onEdit_mouseup);
element.addEventListener('keydown', onEdit_keydown);
element.addEventListener('focus', onEdit_focus);
return element;
}
function onEdit_keydown(domEvent){
// Update caret position
_caretPosition = getCaretPosition(domEvent.target);
switch(domEvent.keyCode){
case 37: // left arrow
if (_caretPosition.index == 0){
var previousEdit = getPreviousEdit(domEvent.target);
if(previousEdit){
console.log("go to end of previous edit");
console.log(previousEdit);
previousEdit.focus();
}
}
break;
case 38: // up arrow
if (_caretPosition.line == 1){
var previousEdit = getPreviousEdit(domEvent.target);
if(previousEdit){
console.log("go to previous edit keeping the caret offset");
console.log(previousEdit);
previousEdit.focus();
}
}
break;
case 39: // right arrow
if (_caretPosition.index == domEvent.target.innerHTML.length){
var nextEdit = getNextEdit(domEvent.target);
if(nextEdit){
console.log("go to beginning of next edit");
console.log(nextEdit);
nextEdit.focus();
}
}
break;
case 40: // down arrow
if (_caretPosition.line == getLines(domEvent.target)){
var nextEdit = getNextEdit(domEvent.target);
if(nextEdit){
console.log("go to next edit keeping the caret offset");
console.log(nextEdit);
nextEdit.focus();
}
}
break;
}
}
function onEdit_mouseup(domEvent){
// Update caret position
_caretPosition = getCaretPosition(domEvent.target);
}
function onEdit_focus(domEvent){
// Add listeners
_currentEdit = domEvent.target;
_currentEdit.addEventListener('blur', onEdit_blur);
window.addEventListener('resize', onWindow_resize);
}
function onEdit_blur(domEvent){
// Remove listeners
domEvent.target.removeEventListener('blur', onEdit_blur);
window.removeEventListener('resize', onWindow_resize);
}
function onWindow_resize(domEvent){
// Update caret position
_caretPosition = getCaretPosition(_currentEdit);
}
/*************************************************************
*
* HELPERS
*
**************************************************************/
//
//
function getCaretPosition(element){
var caretPosition = {index: 0, line: 0};
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var elemOffsetTop = element.offsetTop;
var sel;
// Get the x position of the caret
if (typeof win.getSelection != "undefined") {
sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
// Retrieve the current line
var rects = range.getClientRects();
var caretOffsetTop;
if (typeof rects[1] != "undefined"){
caretOffsetTop = rects[1].top;
}
else if (typeof rects[0] != "undefined"){
caretOffsetTop = rects[0].top;
}
else{
// Create dummy element to get y position of the caret
var dummy = document.createElement('CANVAS');
dummy.id = 'findCaretHelper';
range.insertNode(dummy);
caretOffsetTop = dummy.offsetTop;
element.removeChild(dummy);
}
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
// Remember caret position
caretPosition.index = preCaretRange.toString().length;
caretPosition.line = Math.ceil((caretOffsetTop - elemOffsetTop)/LINE_HEIGHT) + 1;
}
}
// support ie
//else if ( (sel = doc.selection) && sel.type != "Control") {
//var textRange = sel.createRange();
//var preCaretTextRange = doc.body.createTextRange();
//preCaretTextRange.moveToElementText(element);
//preCaretTextRange.setEndPoint("EndToEnd", textRange);
//caretPosition.x = preCaretTextRange.text.length;
//}
return caretPosition;
}
function getLines(element){
return element.clientHeight/LINE_HEIGHT;;
}
.contenteditable{
border: solid 1px #aaa;
margin: 10px 0;
}
我设法获取了有关当前行的信息、可编辑内容中的当前字符索引 div 以及其他一些内容...我仍然需要专注于其他可编辑内容 div为了将插入符放在正确的位置...我希望这个解决方案的开头对您有所帮助!
您可以简单地将父元素/包含元素设为 contenteditable
而不是每个段落。这将相应地自动添加/删除 p
标签。
我有一堆垂直排列的多行 contenteditable div,我想允许通过箭头键在它们之间进行自然导航(就好像它是一个文档)。为此,在 keydown
事件中我需要:
- 了解插入符号的当前行和行数以确定我们是否需要向上移动(第一行and ↑ key pressed) or down (last line and ↓ key)
- 知道当前字符(显示字符串中的位置)以确定我们是否需要向上移动(位置==0 并按下 ← 键) 或向下 (position==text.length and → pressed)
- 当按键被按住且未松开时,进程不应在切换元素之间停止(因此
keydown
事件,而不是keyup
) - 最好:事件应该停止传播(例如,如果我在最后一行的第一列并且我按下 ↓ 键,它不应该跳到该行的最后一个字符然后继续下去)
- 最好(真的很棒):在我们跳转到下一个元素后,我们不会只是
.focus()
元素,而是在与之前相同的垂直位置模拟点击,这样感觉很自然,就像在文本编辑器中一样。
我迄今为止发现的所有 scripts/libraries 要么没有完成我需要的所有事情,要么有错误。请在您的建议中包含演示,这样我就可以在不先合并到我的代码中的情况下进行测试。谢谢!
更新: 可视化解释 - 注意有超过 2 个 div,'arrow down key on the last line' 只是四个之一触发器
我已经写了一些代码,但还没有完成...也许你可以从那里开始,如果你愿意,可以尝试完成我所做的;)我将在本周继续努力,以便为您提供解决方案...这是我到目前为止所做的:
var ajaxResult = [
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates exitialis certamina cogebatur",
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus",
"Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quamos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates exitialis certamina cogebatur",
];
/*************************************************************
*
* LIST OF CONTENT EDITABLE DIVS MANAGEMENT
*
**************************************************************/
// Create the editable divs
window.onload = function(){
var contentEditables = createContentEditables();
document.body.appendChild(contentEditables);
}
// Remember all the content editable elements in the order they appear in the dom
var _currentEdit,
_edits = [];
function createContentEditables(){
var div;
var result = document.createDocumentFragment();
for (var i = 0, n = ajaxResult.length ; i < n ; i++){
div = createContentEditable(ajaxResult[i]);
_edits.push(div);
result.appendChild(div);
}
return result;
}
function getPreviousEdit(edit){
// Search for the edit index
var index = _edits.indexOf(edit);
if(index == 0)
return;
// Return the previous one
return _edits[index - 1];
}
function getNextEdit(edit){
// Search for the edit index
var index = _edits.indexOf(edit);
if(index == _edits.length - 1)
return;
// Return the previous one
return _edits[index + 1];
}
/*************************************************************
*
* CONTENT EDITABLE MANAGEMENT
*
**************************************************************/
// We need to define the line height of the div to be able to retrieve the number of lines
var LINE_HEIGHT = 16;
// variables to keep trace of relevant information about the div
var _lines, _caretPosition;
/*
* Create a div with contenteditable set to true with the text
* received from the server
*/
function createContentEditable(text){
var element = document.createElement('div');
element.className = 'contenteditable';
element.innerHTML = text;
element.style.lineHeight = LINE_HEIGHT + 'px';
element.setAttribute('contenteditable', true);
// Set listeners
element.addEventListener('mouseup', onEdit_mouseup);
element.addEventListener('keydown', onEdit_keydown);
element.addEventListener('focus', onEdit_focus);
return element;
}
function onEdit_keydown(domEvent){
// Update caret position
_caretPosition = getCaretPosition(domEvent.target);
switch(domEvent.keyCode){
case 37: // left arrow
if (_caretPosition.index == 0){
var previousEdit = getPreviousEdit(domEvent.target);
if(previousEdit){
console.log("go to end of previous edit");
console.log(previousEdit);
previousEdit.focus();
}
}
break;
case 38: // up arrow
if (_caretPosition.line == 1){
var previousEdit = getPreviousEdit(domEvent.target);
if(previousEdit){
console.log("go to previous edit keeping the caret offset");
console.log(previousEdit);
previousEdit.focus();
}
}
break;
case 39: // right arrow
if (_caretPosition.index == domEvent.target.innerHTML.length){
var nextEdit = getNextEdit(domEvent.target);
if(nextEdit){
console.log("go to beginning of next edit");
console.log(nextEdit);
nextEdit.focus();
}
}
break;
case 40: // down arrow
if (_caretPosition.line == getLines(domEvent.target)){
var nextEdit = getNextEdit(domEvent.target);
if(nextEdit){
console.log("go to next edit keeping the caret offset");
console.log(nextEdit);
nextEdit.focus();
}
}
break;
}
}
function onEdit_mouseup(domEvent){
// Update caret position
_caretPosition = getCaretPosition(domEvent.target);
}
function onEdit_focus(domEvent){
// Add listeners
_currentEdit = domEvent.target;
_currentEdit.addEventListener('blur', onEdit_blur);
window.addEventListener('resize', onWindow_resize);
}
function onEdit_blur(domEvent){
// Remove listeners
domEvent.target.removeEventListener('blur', onEdit_blur);
window.removeEventListener('resize', onWindow_resize);
}
function onWindow_resize(domEvent){
// Update caret position
_caretPosition = getCaretPosition(_currentEdit);
}
/*************************************************************
*
* HELPERS
*
**************************************************************/
//
//
function getCaretPosition(element){
var caretPosition = {index: 0, line: 0};
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var elemOffsetTop = element.offsetTop;
var sel;
// Get the x position of the caret
if (typeof win.getSelection != "undefined") {
sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
// Retrieve the current line
var rects = range.getClientRects();
var caretOffsetTop;
if (typeof rects[1] != "undefined"){
caretOffsetTop = rects[1].top;
}
else if (typeof rects[0] != "undefined"){
caretOffsetTop = rects[0].top;
}
else{
// Create dummy element to get y position of the caret
var dummy = document.createElement('CANVAS');
dummy.id = 'findCaretHelper';
range.insertNode(dummy);
caretOffsetTop = dummy.offsetTop;
element.removeChild(dummy);
}
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
// Remember caret position
caretPosition.index = preCaretRange.toString().length;
caretPosition.line = Math.ceil((caretOffsetTop - elemOffsetTop)/LINE_HEIGHT) + 1;
}
}
// support ie
//else if ( (sel = doc.selection) && sel.type != "Control") {
//var textRange = sel.createRange();
//var preCaretTextRange = doc.body.createTextRange();
//preCaretTextRange.moveToElementText(element);
//preCaretTextRange.setEndPoint("EndToEnd", textRange);
//caretPosition.x = preCaretTextRange.text.length;
//}
return caretPosition;
}
function getLines(element){
return element.clientHeight/LINE_HEIGHT;;
}
.contenteditable{
border: solid 1px #aaa;
margin: 10px 0;
}
我设法获取了有关当前行的信息、可编辑内容中的当前字符索引 div 以及其他一些内容...我仍然需要专注于其他可编辑内容 div为了将插入符放在正确的位置...我希望这个解决方案的开头对您有所帮助!
您可以简单地将父元素/包含元素设为 contenteditable
而不是每个段落。这将相应地自动添加/删除 p
标签。