İçeriğe geçirilebilir öğede (div) düzeltme işareti (imleç) konumu nasıl ayarlanır?


191

Örnek olarak bu basit HTML var:

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

Basit bir şey istiyorum - düğmeyi tıkladığımda, düzenlenebilir div içindeki imleci (imleci) belirli bir yere yerleştirmek istiyorum. Web üzerinden arama, düğme tıklama bağlı bu JS var, ama çalışmıyor (FF, Chrome):

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

El ile düzeltme konumu bu şekilde ayarlanabilir mi?

Yanıtlar:


261

Çoğu tarayıcıda Rangeve Selectionnesnelerine ihtiyacınız vardır . Seçim sınırlarının her birini bir düğüm ve o düğüm içindeki bir uzaklık olarak belirtirsiniz. Örneğin, imleci metnin ikinci satırının beşinci karakterine ayarlamak için aşağıdakileri yaparsınız:

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 tamamen farklı çalışır. Bu tarayıcıları desteklemeniz gerekiyorsa, farklı bir koda ihtiyacınız olacaktır.

jsFiddle örneği: http://jsfiddle.net/timdown/vXnCM/


2
Çözümünüz mükemmel çalışıyor. Çok teşekkürler. "Metin bağlamında" çalışmak için bir şans var mı - yani konum # 5'in bir koddaki beşinci harf değil, ekranda beşinci harf olacağı anlamına mı geliyor?
Frodik

3
@Frodik: setSelectionRange()İşlevi burada yazdığım cevaptan kullanabilirsiniz : stackoverflow.com/questions/6240139/… . Cevabımda belirttiğim gibi, doğru / tutarlı bir şekilde ele almayacağı çeşitli şeyler var, ancak yeterince iyi olabilir.
Tim Down

7
şöyle bir yayılma etiketi içinde düzeltme işareti ayarlamaya ne dersiniz: << div id = "editable" contenteditable = "true"> test1 <br> test2 <br> <span> </span> </div>
Med Akram Z

1
@MalcolmOcean: Barf, çünkü IE <9'da hiç yok document.createRange(veya window.getSelectionbu kadar ileri gitmeyecek).
Tim Down

1
@undroid: jsfiddle Mac'te Firefox 38.0.5 sürümünde benim için iyi çalışıyor.
Tim Down

62

İçeriği düzeltilebilir imleç konumlandırmasında bulduğunuz çoğu yanıt oldukça basittir, çünkü yalnızca düz vanilya metinli girdilere hitap ederler. Kapsayıcı içindeki html öğelerini kullandığınızda, girilen metin düğümlere bölünür ve bir ağaç yapısı boyunca serbestçe dağıtılır.

İmleç konumunu ayarlamak için, verilen düğüm içindeki tüm alt metin düğümlerinin çevresinde dolaşan ve ilk düğümün başlangıcından chars.count karakterine bir aralık belirleyen bu işleve sahibim :

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;
};

Sonra bu işlevi ile rutin çağırmak:

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);
        }
    }
};

Range.collapse (false), imleci aralığın sonuna ayarlar. Chrome, IE, Mozilla ve Opera'nın en son sürümleriyle test ettim ve hepsi iyi çalışıyor.

PS. Herkes ilgileniyorsa ben bu kodu kullanarak geçerli imleç konumunu almak:

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;
};

Kod set fonksiyonunun tersini yapar - geçerli window.getSelection (). FocusNode ve focusOffset öğesini alır ve containerId kimliğine sahip bir üst düğüme çarpana kadar karşılaşılan tüm metin karakterlerini geriye doğru sayar. Suplied düğüm sağlanan bir çocuk aslında olduğunu çalıştırmadan önce isChildOf fonksiyonu sadece denetler parentId .

Kod değişmeden düz çalışması gerekir, ama ben sadece geliştirdiğim bir jQuery eklentisi almış bu yüzden bu bir çift hack var - bir şey işe yaramazsa bana bildirin!


1
Lütfen bu çalışma hakkında bir fikir verir misiniz? Ben I gibi bu eserler emin neyi değilim nasıl anlamaya mücadele ediyorum node.idve parentIdbir örnek olmadan ilgilidir. Teşekkürler :)
Bendihossan

4
@Bendihossan - bu jsfiddle.net/nrx9yvw9/5'i deneyin - bir nedenden dolayı bu örnekteki içerik düzenlenebilir div bazı karakterler ekliyor ve metnin başlangıcında bir satır başı ekliyor (hatta jsfiddle kendisi yaptığı gibi yapıyor olabilir) ; aynı şeyi asp.net sunucumda da yapmam).
Liam

@Bendihossan - içerikli div içindeki html öğeleri, her html öğesi için bir düğümü olan bir ağaç yapısına bölünür. GetCurrentCursorPosition geçerli seçim konumunu alır ve kaç düz metin karakterinin olduğunu sayarak ağaca geri gider. Node.id, html öğesi kimliğidir, parentId ise html öğesi kimliğine başvururken geri saymayı durdurmalıdır
Liam

1
UI kodumdan tamamen ayrı bir tane yazmak benim yapılacaklar listemde - Bir saniyem olduğunda göndereceğim.
Liam

1
Farklı çözümlerinizi hızlı bir şekilde test edebilmek için cevabınızı çalıştırılabilir kod snippet'lerinde düzenleyebilir misiniz? Şimdiden teşekkür ederim.
Basj

3

JQuery kullanmak istemiyorsanız bu yaklaşımı deneyebilirsiniz:

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);
}

editableDivdüzenlenebilir öğe, bunun için bir ayarlamayı unutmayın id. Sonra innerHTMLelemanından almanız ve tüm fren hatlarını kesmeniz gerekir. Ve sadece sonraki argümanlarla daraltmayı ayarlayın.


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>

(P) (span) vb. İlerletme elemanlarına sahip olduğunuzda, çok zor ayarlanmış bir caret doğru konumdadır. Amaç (nesne metni):

<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
Yanıtınızı hızlı bir şekilde test edebilmek için cevabınızı çalıştırılabilir bir kod snippet'inde düzenleyebilir misiniz? Şimdiden teşekkür ederim.
Basj

1

Bir sözdizimi vurgulayıcı (ve temel kod editörü) yazıyorum ve nasıl tek bir teklif char otomatik yazmak ve caret geri taşımak için bilmek gerekiyordu (günümüzde kod editörleri bir sürü gibi).

Bu iş parçacığı, MDN belgeleri ve çok sayıda moz konsolu izlemesi sayesinde çok sayıda çözümüm var.

//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

Bu, tartışılabilir bir div öğesinde

Bunu bir teşekkür olarak bırakıyorum, zaten kabul edilmiş bir cevap olduğunu fark ediyorum.


1

Bunu basit metin düzenleyicim için yaptım.

Diğer yöntemlerden farklılıklar:

  • Yüksek performans
  • Tüm alanlarla çalışır

kullanım

// 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)

selection.ts

/** 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('')
}

1

@ Liam'ın cevabını tekrar gözden geçirdim. Statik yöntemlerle bir sınıfa koydum, işlevlerinin #id yerine bir öğe almasını sağladım.

Bu kod, imleci birlikte yapabileceğiniz zengin bir metin kutusuna sabitlemek için özellikle iyidir <div contenteditable="true">. Aşağıdaki kod gelmeden önce birkaç gün bu sıkışmış.

edit: Onun cevabı ve bu cevap enter vurmak içeren bir hata var. Enter, karakter olarak sayılmadığından, enter tuşuna bastıktan sonra imleç konumu bozulur. Kodu düzeltebilirsem cevabımı güncelleyeceğim.

edit2: Kaydet Kendinizi baş ağrısı bir çok ve emin olun <div contenteditable=true>IS display: inline-block. Bu <div>, <br>enter tuşuna basmak yerine Chrome ile ilgili bazı hataları giderir .

Nasıl kullanılır

let richText = document.getElementById('rich-text');
let offset = Cursor.getCurrentCursorPosition(richText);
// do stuff to the innerHTML, such as adding/removing <span> tags
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus();

kod

// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
    static getCurrentCursorPosition(parentElement) {
        var selection = window.getSelection(),
            charCount = -1,
            node;
        
        if (selection.focusNode) {
            if (Cursor._isChildOf(selection.focusNode, parentElement)) {
                node = selection.focusNode; 
                charCount = selection.focusOffset;
                
                while (node) {
                    if (node === parentElement) {
                        break;
                    }

                    if (node.previousSibling) {
                        node = node.previousSibling;
                        charCount += node.textContent.length;
                    } else {
                        node = node.parentNode;
                        if (node === null) {
                            break;
                        }
                    }
                }
            }
        }
        
        return charCount;
    }
    
    static setCurrentCursorPosition(chars, element) {
        if (chars >= 0) {
            var selection = window.getSelection();
            
            let range = Cursor._createRange(element, { count: chars });

            if (range) {
                range.collapse(false);
                selection.removeAllRanges();
                selection.addRange(range);
            }
        }
    }
    
    static _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 = Cursor._createRange(node.childNodes[lp], chars, range);

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

        return range;
    }
    
    static _isChildOf(node, parentElement) {
        while (node !== null) {
            if (node === parentElement) {
                return true;
            }
            node = node.parentNode;
        }

        return false;
    }
}

0

Bence kabul edilebilir öğenin içinde belirli bir noktaya şapka koymak kolay değil. Bunun için kendi kodumu yazdım. Kaç karakter kaldığını gösteren düğüm ağacını atlar ve gerekli öğede düzeltme işareti ayarlar. Bu kodu fazla test etmedim.

//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;
    }
}

Ayrıca geçerli caret pozisyon almak için kod yazdı (test etmedi):

//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;
    }
}

Ayrıca range.startOffset ve range.endOffset öğelerinin metin düğümleri için karakter uzaklığı (nodeType === 3) ve öğe düğümleri için alt düğüm uzaklığı (nodeType === 1) içermesi gerekir. range.startContainer ve range.endContainer, ağaçtaki herhangi bir düzeydeki herhangi bir öğe düğümüne başvurabilir (elbette metin düğümlerine de başvurabilirler).


0

Tim Down'ın cevabına dayanarak, bilinen son "iyi" metin satırını kontrol eder. İmleci en sonuna yerleştirir.

Ayrıca, DOM'daki mutlak son "iyi" metin düğümünü bulmak için her ardışık son çocuğun son çocuğunu özyinelemeli / yinelemeli olarak kontrol edebilirim.

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>

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.