Pobierz zawartośćEdytowalna pozycja indeksu daszka

119

Znajduję mnóstwo dobrych, crossbrowserowych odpowiedzi na temat tego, jak USTAWIĆ pozycję indeksu kursora lub daszka w contentEditableelemencie, ale nie ma żadnych, jak GET lub znaleźć jego indeks ...

Chcę tylko poznać indeks karetki w tym div keyup.

Tak więc, kiedy użytkownik wpisuje tekst, mogę w dowolnym momencie poznać indeks jego kursora w contentEditableelemencie.

EDYCJA: Szukam INDEKSU w treści div (tekst), a nie współrzędnych kursora.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});
Bertvan
źródło
Spójrz na jego pozycję w tekście. Następnie wyszukaj ostatnie wystąpienie znaku „@” przed tą pozycją. Więc tylko logika tekstowa.
Bertvan
Nie planuję też zezwalać na inne tagi w <diV>, tylko tekst
Bertvan,
ok, tak mam zamiar potrzebują innych znaczników w obrębie <div>. Będą <a> tagi, ale nie będzie zagnieżdżania ...
Bertvan
@Bertvan: jeśli daszek znajduje się wewnątrz <a>elementu wewnątrz elementu <div>, jakie przesunięcie chcesz w takim razie? Przesunięcie w tekście wewnątrz znaku <a>?
Tim Down
Nigdy nie powinien znajdować się wewnątrz elementu <a>. Element <a> powinien być renderowany w formacie html, aby użytkownik nie mógł w rzeczywistości umieścić w nim karetki.
Bertvan

Odpowiedzi:

121

Poniższy kod zakłada:

  • W edytowalnym obszarze zawsze znajduje się jeden węzeł tekstowy <div>i nie ma innych węzłów
  • Edytowalny element div nie ma white-spacewłaściwości CSS ustawionej napre

Jeśli potrzebujesz bardziej ogólnego podejścia, które będzie działać z zawartością z zagnieżdżonymi elementami, wypróbuj tę odpowiedź:

https://stackoverflow.com/a/4812022/96100

Kod:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>

Tim Down
źródło
9
To nie zadziała, jeśli są tam jakieś inne tagi. Pytanie: jeśli daszek znajduje się wewnątrz <a>elementu wewnątrz elementu <div>, jakie przesunięcie chcesz w takim razie? Przesunięcie w tekście wewnątrz znaku <a>?
Tim Down
3
@Richard: Cóż, keyupprawdopodobnie będzie to niewłaściwe wydarzenie, ale zostało użyte w pierwotnym pytaniu. getCaretPosition()samo w sobie jest w porządku w ramach własnych ograniczeń.
Tim Down
3
Demo JSFIDDLE kończy się niepowodzeniem, jeśli naciśnę klawisz Enter i przejdę do nowej linii. Pozycja pokaże 0.
giorgio79
5
@ giorgio79: Tak, ponieważ podział wiersza generuje element <br>lub <div>, co narusza pierwsze założenie wymienione w odpowiedzi. Jeśli potrzebujesz nieco bardziej ogólnego rozwiązania, możesz wypróbować stackoverflow.com/a/4812022/96100
Tim Down
2
Czy w ogóle można to zrobić, aby zawierał numer linii?
Adjit
28

Kilka zmarszczek, których nie widzę w innych odpowiedziach:

  1. element może zawierać wiele poziomów węzłów potomnych (np. węzły potomne, które mają węzły potomne, które mają węzły potomne ...)
  2. wybór może składać się z różnych pozycji początkowych i końcowych (np. wybranych jest wiele znaków)
  3. węzeł zawierający początek / koniec Caret nie może być ani elementem, ani jego bezpośrednimi potomkami

Oto sposób na uzyskanie pozycji początkowej i końcowej jako przesunięć względem wartości textContent elementu:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}
mwag
źródło
3
To musi być wybrane jako prawidłowa odpowiedź. Działa z tagami w tekście (zaakceptowana odpowiedź nie)
hamboy75,
17

$("#editable").on('keydown keyup mousedown mouseup',function(e){
		   
       if($(window.getSelection().anchorNode).is($(this))){
    	  $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>

Eisa Qasemi
źródło
3
To niestety przestaje działać, gdy tylko naciśniesz enter i uruchomisz inną linię (znowu zaczyna się od 0 - prawdopodobnie licząc od CR / LF).
Ian
Nie działa poprawnie, jeśli masz jakieś pogrubione i / lub kursywa.
user2824371
14

Spróbuj tego:

Caret.js Uzyskaj pozycję daszka i przesunięcie z pola tekstowego

https://github.com/ichord/Caret.js

demo: http://ichord.github.com/Caret.js

JY Han
źródło
To jest słodkie. Potrzebowałem tego zachowania, aby ustawić daszek na koniec a contenteditable lipo kliknięciu przycisku w celu zmiany nazwy litreści.
akinuri
@AndroidDev Nie jestem autorem Caret.js, ale czy rozważałeś, że uzyskanie pozycji daszka dla wszystkich głównych przeglądarek jest bardziej skomplikowane niż kilka linijek? Czy znasz lub stworzyłeś niewyczerpaną alternatywę, którą możesz się z nami podzielić?
adelriosantiago
8

Trochę późno na imprezę, ale na wypadek, gdyby ktoś inny walczył. Żadne z wyszukiwań Google, które znalazłem w ciągu ostatnich dwóch dni, nie przyniosło niczego, co działa, ale wymyśliłem zwięzłe i eleganckie rozwiązanie, które zawsze będzie działać niezależnie od liczby zagnieżdżonych tagów:

function cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Zaznacza całą drogę z powrotem do początku akapitu, a następnie zlicza długość ciągu, aby uzyskać bieżącą pozycję, a następnie cofa zaznaczenie, aby przywrócić kursor do bieżącej pozycji. Jeśli chcesz to zrobić dla całego dokumentu (więcej niż jednego akapitu), zmień paragraphboundaryna documentboundarylub jakąkolwiek szczegółowość dla swojego przypadku. Sprawdź interfejs API, aby uzyskać więcej informacji . Twoje zdrowie! :)

Przezwisko
źródło
1
Jeśli mam <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Zawsze umieszczam kursor przed itagiem lub jakikolwiek podrzędny element html wewnątrz div, pozycja kursora zaczyna się od 0. Czy istnieje sposób na uniknięcie tej liczby ponownych uruchomień?
vam
Dziwny. Nie widzę tego zachowania w Chrome. Jakiej przeglądarki używasz?
Soubriquet
2
Wygląda na to, że selection.modify może, ale nie musi, być obsługiwane we wszystkich przeglądarkach. developer.mozilla.org/en-US/docs/Web/API/Selection
Chris Sullivan
7
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}
Nishad Up
źródło
ten faktycznie działał dla mnie, wypróbowałem wszystkie powyższe, nie działały.
iStudLion
dziękuję, ale zwraca również {x: 0, y: 0} w nowym wierszu.
hichamkazan
zwraca to pozycję piksela, a nie przesunięcie znaku
4esn0k
dzięki, szukałem pobierania pozycji piksela z daszka i działa dobrze.
Sameesh
6

window.getSelection - vs - document.selection

Ten działa dla mnie:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    var range = window.getSelection().getRangeAt(0);
    var preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(element);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    caretOffset = preCaretRange.toString().length;
  } 

  else if (document.selection && document.selection.type != "Control") {
    var textRange = document.selection.createRange();
    var preCaretTextRange = document.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }

  return caretOffset;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Linia wywołująca zależy od typu zdarzenia, w przypadku kluczowych zdarzeń użyj tego:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

dla zdarzenia myszy użyj tego:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

w tych dwóch przypadkach zwracam uwagę na linie przerwania, dodając indeks docelowy

Jonathan R.
źródło
4
//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

Uwaga: sam obiekt zakresu może być przechowywany w zmiennej i można go ponownie wybrać w dowolnym momencie, chyba że zmieni się zawartość edytowalnego elementu div.

Odniesienie do IE 8 i starszych: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Odniesienie do standardów (wszystkich innych) przeglądarek: https://developer.mozilla.org/en/DOM/range (to dokumentacja Mozilla, ale kod działa również w Chrome, Safari, Opera i IE9)

Nico Burns
źródło
1
Dzięki, ale jak dokładnie mogę uzyskać „indeks” pozycji kursora w treści div?
Bertvan,
OK, wygląda na to, że wywołanie .baseOffset na .getSelection () załatwia sprawę. Więc to, wraz z twoją odpowiedzią, odpowiada na moje pytanie. Dzięki!
Bertvan,
2
Niestety .baseOffset działa tylko w webkicie (tak mi się wydaje). Daje również tylko przesunięcie od bezpośredniego rodzica daszka (jeśli masz tag <b> wewnątrz <div>, to da on przesunięcie od początku <b>, a nie początku <div> . Zakresy oparte na standardach mogą używać range.endOffset range.startOffset range.endContainer i range.startContainer, aby uzyskać przesunięcie od węzła nadrzędnego zaznaczenia i samego węzła (obejmuje to węzły tekstowe). IE zapewnia range.offsetLeft, który jest przesunięty od lewej strony w pikselach , a więc bezużyteczny
Nico Burns
Najlepiej jest po prostu zachować własny obiekt range i użyć window.getSelection (). Addrange (range); <- standardy i zakres.select (); <- IE do zmiany pozycji kursora w tym samym miejscu. range.insertNode (nodetoinsert); <- standardy i zakres.pasteHTML (htmlcode); <- IE, aby wstawić tekst lub html pod kursorem.
Nico Burns
RangeObiekt zwrócony przez większość przeglądarek oraz TextRangeobiekt zwrócony przez IE są bardzo różne rzeczy, więc nie jestem pewien, że to odpowiedź rozwiązuje wiele.
Tim Down
3

Ponieważ zajęło mi to wieczność, aby dowiedzieć się, korzystając z nowego interfejsu API window.getSelection, które zamierzam udostępnić dla potomności. Zwróć uwagę, że MDN sugeruje szersze wsparcie dla window.getSelection, jednak Twój przebieg może się różnić.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Oto jsfiddle, który uruchamia się po włączeniu klucza . Należy jednak pamiętać, że szybkie naciśnięcia klawiszy kierunkowych, a także szybkie usuwanie wydaje się być pomijaniem zdarzeń.

Chris Sullivan
źródło
Pracuje dla mnie! Dziękuję bardzo.
dmodo
Z tym zaznaczeniem tekstu nie jest już możliwe, ponieważ jest zwinięty. Możliwy scenariusz: potrzeba oceny na każdym wydarzeniu
keyUp
0

Prosty sposób, który iteruje przez wszystkie elementy chidren edytowalnego elementu div, aż dotrze do endContainer. Następnie dodaję przesunięcie kontenera końcowego i mamy indeks znaków. Powinien działać z dowolną liczbą zagnieżdżeń. używa rekurencji.

Uwaga: do obsługi np. Wymagane jest wypełnienie poliamidoweElement.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }
alockwood05
źródło