Làm cách nào để đặt vị trí dấu mũ (con trỏ) trong phần tử có thể nội dung (div)?


188

Tôi có ví dụ HTML đơn giản này:

<div id="editable" contenteditable="true">
  text text text<br>
  text text text<br>
  text text text<br>
</div>
<button id="button">focus</button>

Tôi muốn điều đơn giản - khi tôi nhấp vào nút, tôi muốn đặt dấu mũ (con trỏ) vào vị trí cụ thể trong div có thể chỉnh sửa. Từ tìm kiếm trên web, tôi có mã này được đính kèm với nút bấm, nhưng nó không hoạt động (FF, Chrome):

var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);

Có thể đặt vị trí caret thủ công như thế này?

Câu trả lời:


257

Trong hầu hết các trình duyệt, bạn cần RangeSelectioncác đối tượng. Bạn chỉ định từng ranh giới lựa chọn là một nút và một phần bù trong nút đó. Ví dụ: để đặt dấu mũ thành ký tự thứ năm của dòng văn bản thứ hai, bạn sẽ làm như sau:

var el = document.getElementById("editable");
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el.childNodes[2], 5);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);

IE <9 hoạt động hoàn toàn khác nhau. Nếu bạn cần hỗ trợ các trình duyệt này, bạn sẽ cần mã khác.

ví dụ jsFiddle: http://jsfiddle.net/timdown/vXnCM/


2
Giải pháp của bạn hoạt động hoàn hảo. Cảm ơn rất nhiều. Có khả năng nó có thể được thực hiện để hoạt động trong "ngữ cảnh văn bản" - điều đó có nghĩa là vị trí số 5 sẽ là chữ cái thứ năm trên màn hình chứ không phải là chữ cái thứ năm trong mã?
Frodik

3
@Frodik: Bạn có thể sử dụng setSelectionRange()chức năng từ câu trả lời tôi đã viết ở đây: stackoverflow.com/questions/6240139/ . Như tôi đã lưu ý trong câu trả lời, có nhiều thứ nó sẽ không xử lý chính xác / nhất quán nhưng nó có thể đủ tốt.
Tim Down

7
làm thế nào về việc đặt dấu mũ bên trong thẻ span như thế này: << div id = "editable" contenteditable = "true"> test1 <br> test2 <br> <span> </ span> </ div>
Med Akram Z

1
@MalcolmOcean: Barf, vì IE <9 không có document.createRange(hoặc window.getSelection, nhưng nó sẽ không đi xa đến thế).
Tim Down

1
@undroid: jsfiddle hoạt động tốt với tôi trong Firefox 38.0.5 trên Mac.
Tim Down

60

Hầu hết các câu trả lời bạn tìm thấy về định vị con trỏ có thể nội dung khá đơn giản ở chỗ chúng chỉ phục vụ cho các đầu vào có văn bản vanilla đơn giản. Khi bạn sử dụng các phần tử html trong vùng chứa, văn bản được nhập sẽ được chia thành các nút và được phân phối tự do trên một cấu trúc cây.

Để đặt vị trí con trỏ, tôi có chức năng này vòng quanh tất cả các nút văn bản con trong nút được cung cấp và đặt phạm vi từ đầu nút ban đầu đến ký tự chars.count :

function createRange(node, chars, range) {
    if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
    }

    if (chars.count === 0) {
        range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.length < chars.count) {
                chars.count -= node.textContent.length;
            } else {
                range.setEnd(node, chars.count);
                chars.count = 0;
            }
        } else {
           for (var lp = 0; lp < node.childNodes.length; lp++) {
                range = createRange(node.childNodes[lp], chars, range);

                if (chars.count === 0) {
                    break;
                }
            }
        }
    } 

    return range;
};

Sau đó tôi gọi thủ tục với chức năng này:

function setCurrentCursorPosition(chars) {
    if (chars >= 0) {
        var selection = window.getSelection();

        range = createRange(document.getElementById("test").parentNode, { count: chars });

        if (range) {
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
};

Phạm vi.collapse (false) đặt con trỏ ở cuối phạm vi. Tôi đã thử nghiệm nó với các phiên bản mới nhất của Chrome, IE, Mozilla và Opera và tất cả đều hoạt động tốt.

Tái bút Nếu có ai quan tâm tôi sẽ lấy vị trí con trỏ hiện tại bằng mã này:

function isChildOf(node, parentId) {
    while (node !== null) {
        if (node.id === parentId) {
            return true;
        }
        node = node.parentNode;
    }

    return false;
};

function getCurrentCursorPosition(parentId) {
    var selection = window.getSelection(),
        charCount = -1,
        node;

    if (selection.focusNode) {
        if (isChildOf(selection.focusNode, parentId)) {
            node = selection.focusNode; 
            charCount = selection.focusOffset;

            while (node) {
                if (node.id === parentId) {
                    break;
                }

                if (node.previousSibling) {
                    node = node.previousSibling;
                    charCount += node.textContent.length;
                } else {
                     node = node.parentNode;
                     if (node === null) {
                         break
                     }
                }
           }
      }
   }

    return charCount;
};

Mã này làm ngược lại với hàm set - nó nhận được window.getSelection (). FocusNode và FocusPackset và đếm ngược tất cả các ký tự văn bản gặp phải cho đến khi nó chạm vào nút cha có id của containerId. Chức năng isChildOf chỉ kiểm tra trước khi chạy mà nút suplied thực sự là một đứa trẻ được cung cấp ParentID .

Mã sẽ hoạt động ổn định mà không thay đổi, nhưng tôi vừa lấy nó từ một plugin jQuery mà tôi đã phát triển nên đã hack một vài trong số này - hãy cho tôi biết nếu có gì không hoạt động!


1
Bạn có thể cung cấp một jsfiddle của công việc này xin vui lòng? Tôi đang đấu tranh để tìm hiểu làm thế nào điều này hoạt động vì tôi không chắc chắn những gì node.idparentIdliên quan đến mà không có ví dụ. Cảm ơn :)
Bendihossan

4
@Bendihossan - hãy thử jsfiddle.net/nrx9yvw9/5 này - vì một số lý do, div nội dung có thể chỉnh sửa trong ví dụ này được thêm vào một số ký tự và trả về vận chuyển khi bắt đầu văn bản ; t làm tương tự trên máy chủ asp.net của tôi).
Liam

@Bendihossan - các phần tử html trong div nội dung có thể được chia thành cấu trúc cây với một nút cho mỗi phần tử html. Hàm getCienCthonP vị trí có được vị trí lựa chọn hiện tại và quay trở lại cây đếm xem có bao nhiêu ký tự văn bản đơn giản. Node.id là id phần tử html, trong khi ParentId đề cập đến id phần tử html, nó sẽ dừng đếm ngược lại
Liam

Điều này thực sự tuyệt vời, tôi đã gói các phương thức vào một plugin jQuery để sử dụng, nhưng sau đó đọc lại nhận xét của bạn và bạn nói rằng bạn đã có một cái rồi. Bạn có chia sẻ nó trong một repo ở đâu đó mà tôi có thể tham khảo và cung cấp tín dụng không?
Dom Hastings

1
Nó nằm trong danh sách việc cần làm của tôi để viết một cái hoàn toàn tách biệt với mã UI của tôi - Tôi sẽ đăng nó khi tôi có một giây.
Liam

3

Nếu bạn không muốn sử dụng jQuery, bạn có thể thử phương pháp này:

public setCaretPosition() {
    const editableDiv = document.getElementById('contenteditablediv');
    const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, '');
    const selection = window.getSelection();
    selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length);
}

editableDivbạn có thể chỉnh sửa thành phần, đừng quên thiết lập idcho nó. Sau đó, bạn cần phải lấy của bạn innerHTMLtừ các yếu tố và cắt tất cả các dòng phanh. Và chỉ cần thiết lập sự sụp đổ với các đối số tiếp theo.


3
  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }

3

function set_mouse() {
  var as = document.getElementById("editable");
  el = as.childNodes[1].childNodes[0]; //goal is to get ('we') id to write (object Text) because it work only in object text
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el, 1);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);

  document.getElementById("we").innerHTML = el; // see out put of we id
}
<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd
  <p>dd</p>psss
  <p>dd</p>
  <p>dd</p>
  <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>

Nó rất khó đặt dấu mũ ở vị trí thích hợp khi bạn có phần tử trước như (p) (span), v.v ... Mục tiêu là để có được (văn bản đối tượng):

<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p>
    <p>dd</p>
    <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>
<script>

    function set_mouse() {
        var as = document.getElementById("editable");
        el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text
        var range = document.createRange();
        var sel = window.getSelection();
        range.setStart(el, 1);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);

        document.getElementById("we").innerHTML = el;// see out put of we id
    }
</script>

1
Để có thể kiểm tra câu trả lời của bạn một cách nhanh chóng, bạn có thể chỉnh sửa câu trả lời của mình thành một đoạn mã có thể chạy được không? Cảm ơn bạn trước.
Basj

1

Tôi đang viết một cú pháp tô sáng (và trình soạn thảo mã cơ bản) và tôi cần biết cách tự động gõ một trích dẫn char duy nhất và di chuyển dấu mũ trở lại (giống như nhiều trình soạn thảo mã hiện nay).

Đây là một đoạn giải pháp của tôi, nhờ có nhiều sự trợ giúp từ chủ đề này, các tài liệu MDN và rất nhiều giao diện điều khiển moz đang xem ..

//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event

Đây là một yếu tố div có thể nội dung

Tôi để lại đây như một lời cảm ơn, nhận ra đã có một câu trả lời được chấp nhận.


1

Tôi đã làm điều này cho trình soạn thảo văn bản đơn giản của tôi.

Sự khác biệt so với các phương pháp khác:

  • Hiệu suất cao
  • Hoạt động với mọi không gian

sử dụng

// get current selection
const [start, end] = getSelectionOffset(container)

// change container html
container.innerHTML = newHtml

// restore selection
setSelectionOffset(container, start, end)

// use this instead innerText for get text with keep all spaces
const innerText = getInnerText(container)
const textBeforeCaret = innerText.substring(0, start)
const textAfterCaret = innerText.substring(start)

lựa chọn

/** return true if node found */
function searchNode(
    container: Node,
    startNode: Node,
    predicate: (node: Node) => boolean,
    excludeSibling?: boolean,
): boolean {
    if (predicate(startNode as Text)) {
        return true
    }

    for (let i = 0, len = startNode.childNodes.length; i < len; i++) {
        if (searchNode(startNode, startNode.childNodes[i], predicate, true)) {
            return true
        }
    }

    if (!excludeSibling) {
        let parentNode = startNode
        while (parentNode && parentNode !== container) {
            let nextSibling = parentNode.nextSibling
            while (nextSibling) {
                if (searchNode(container, nextSibling, predicate, true)) {
                    return true
                }
                nextSibling = nextSibling.nextSibling
            }
            parentNode = parentNode.parentNode
        }
    }

    return false
}

function createRange(container: Node, start: number, end: number): Range {
    let startNode
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            const dataLength = (node as Text).data.length
            if (start <= dataLength) {
                startNode = node
                return true
            }
            start -= dataLength
            end -= dataLength
            return false
        }
    })

    let endNode
    if (startNode) {
        searchNode(container, startNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                const dataLength = (node as Text).data.length
                if (end <= dataLength) {
                    endNode = node
                    return true
                }
                end -= dataLength
                return false
            }
        })
    }

    const range = document.createRange()
    if (startNode) {
        if (start < startNode.data.length) {
            range.setStart(startNode, start)
        } else {
            range.setStartAfter(startNode)
        }
    } else {
        if (start === 0) {
            range.setStart(container, 0)
        } else {
            range.setStartAfter(container)
        }
    }

    if (endNode) {
        if (end < endNode.data.length) {
            range.setEnd(endNode, end)
        } else {
            range.setEndAfter(endNode)
        }
    } else {
        if (end === 0) {
            range.setEnd(container, 0)
        } else {
            range.setEndAfter(container)
        }
    }

    return range
}

export function setSelectionOffset(node: Node, start: number, end: number) {
    const range = createRange(node, start, end)
    const selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange(range)
}

function hasChild(container: Node, node: Node): boolean {
    while (node) {
        if (node === container) {
            return true
        }
        node = node.parentNode
    }

    return false
}

function getAbsoluteOffset(container: Node, offset: number) {
    if (container.nodeType === Node.TEXT_NODE) {
        return offset
    }

    let absoluteOffset = 0
    for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) {
        const childNode = container.childNodes[i]
        searchNode(childNode, childNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                absoluteOffset += (node as Text).data.length
            }
            return false
        })
    }

    return absoluteOffset
}

export function getSelectionOffset(container: Node): [number, number] {
    let start = 0
    let end = 0

    const selection = window.getSelection()
    for (let i = 0, len = selection.rangeCount; i < len; i++) {
        const range = selection.getRangeAt(i)
        if (range.intersectsNode(container)) {
            const startNode = range.startContainer
            searchNode(container, container, node => {
                if (startNode === node) {
                    start += getAbsoluteOffset(node, range.startOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                start += dataLength
                end += dataLength

                return false
            })

            const endNode = range.endContainer
            searchNode(container, startNode, node => {
                if (endNode === node) {
                    end += getAbsoluteOffset(node, range.endOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                end += dataLength

                return false
            })

            break
        }
    }

    return [start, end]
}

export function getInnerText(container: Node) {
    const buffer = []
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            buffer.push((node as Text).data)
        }
        return false
    })
    return buffer.join('')
}

0

Tôi nghĩ không đơn giản để đặt dấu mũ ở một vị trí nào đó trong phần tử có thể điều chỉnh được. Tôi đã viết mã riêng của mình cho việc này. Nó bỏ qua cây nút có bao nhiêu ký tự còn lại và đặt dấu mũ trong phần tử cần thiết. Tôi đã không kiểm tra mã này nhiều.

//Set offset in current contenteditable field (for start by default or for with forEnd=true)
function setCurSelectionOffset(offset, forEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return;

    const firstRange = sel.getRangeAt(0);

    if (offset > 0) {
        bypassChildNodes(document.activeElement, offset);
    }else{
        if (forEnd)
            firstRange.setEnd(document.activeElement, 0);
        else
            firstRange.setStart(document.activeElement, 0);
    }



    //Bypass in depth
    function bypassChildNodes(el, leftOffset) {
        const childNodes = el.childNodes;

        for (let i = 0; i < childNodes.length && leftOffset; i++) {
            const childNode = childNodes[i];

            if (childNode.nodeType === 3) {
                const curLen = childNode.textContent.length;

                if (curLen >= leftOffset) {
                    if (forEnd)
                        firstRange.setEnd(childNode, leftOffset);
                    else
                        firstRange.setStart(childNode, leftOffset);
                    return 0;
                }else{
                    leftOffset -= curLen;
                }
            }else
            if (childNode.nodeType === 1) {
                leftOffset = bypassChildNodes(childNode, leftOffset);
            }
        }

        return leftOffset;
    }
}

Tôi cũng đã viết mã để có được vị trí caret hiện tại (không kiểm tra):

//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true)
function getCurSelectionOffset(calcEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return 0;

    const firstRange     = sel.getRangeAt(0),
          startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer,
          startOffset    = calcEnd ? firstRange.endOffset    : firstRange.startOffset;
    let needStop = false;

    return bypassChildNodes(document.activeElement);



    //Bypass in depth
    function bypassChildNodes(el) {
        const childNodes = el.childNodes;
        let ans = 0;

        if (el === startContainer) {
            if (startContainer.nodeType === 3) {
                ans = startOffset;
            }else
            if (startContainer.nodeType === 1) {
                for (let i = 0; i < startOffset; i++) {
                    const childNode = childNodes[i];

                    ans += childNode.nodeType === 3 ? childNode.textContent.length :
                           childNode.nodeType === 1 ? childNode.innerText.length :
                           0;
                }
            }

            needStop = true;
        }else{
            for (let i = 0; i < childNodes.length && !needStop; i++) {
                const childNode = childNodes[i];
                ans += bypassChildNodes(childNode);
            }
        }

        return ans;
    }
}

Bạn cũng cần lưu ý về phạm vi.start Offerset và phạm vi Range.startContainer và Range.endContainer có thể tham chiếu đến bất kỳ nút phần tử nào ở bất kỳ cấp độ nào trong cây (tất nhiên chúng cũng có thể tham chiếu đến các nút văn bản).


0

Dựa trên câu trả lời của Tim Down, nhưng nó kiểm tra hàng văn bản "tốt" được biết đến cuối cùng. Nó đặt con trỏ ở cuối.

Hơn nữa, tôi cũng có thể kiểm tra đệ quy / lặp lại đứa con cuối cùng của mỗi đứa con cuối cùng liên tiếp để tìm nút văn bản "tốt" cuối cùng tuyệt đối trong DOM.

function onClickHandler() {
  setCaret(document.getElementById("editable"));
}

function setCaret(el) {
  let range = document.createRange(),
      sel = window.getSelection(),
      lastKnownIndex = -1;
  for (let i = 0; i < el.childNodes.length; i++) {
    if (isTextNodeAndContentNoEmpty(el.childNodes[i])) {
      lastKnownIndex = i;
    }
  }
  if (lastKnownIndex === -1) {
    throw new Error('Could not find valid text content');
  }
  let row = el.childNodes[lastKnownIndex],
      col = row.textContent.length;
  range.setStart(row, col);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
  el.focus();
}

function isTextNodeAndContentNoEmpty(node) {
  return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0
}
<div id="editable" contenteditable="true">
  text text text<br>text text text<br>text text text<br>
</div>
<button id="button" onclick="onClickHandler()">focus</button>

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.