Obtém o cursor ou a posição do texto em pixels para o elemento de input

IE me permite criar um intervalo de texto em um elemento de input, no qual eu posso chamar getBoundingClientRect() e obter a posição em pixels de um determinado caractere ou o cursor / cursor. Existe alguma maneira de obter a posição de um determinado caractere em pixels em outros navegadores?

 var input = $("#myInput")[0]; var pixelPosition = null; if (input.createTextRange) { var range = input.createTextRange(); range.moveStart("character", 6); pixelPosition = range.getBoundingClientRect(); } else { // Is there any way to create a range on an input's value? } 

Estou usando o jQuery, mas duvido que seja capaz de resolver minha situação. Espero uma solução de JavaScript pura, se houver, mas as respostas do jQuery são bem-vindas.

Demonstração
Eu escrevi uma function que se comporta como esperado. Um painel de demonstração muito detalhado pode ser encontrado aqui: Fiddle: http://jsfiddle.net/56Rep/5/
A interface na demonstração é autoexplicativa.

A funcionalidade conforme solicitado na pergunta seria implementada na minha function da seguinte maneira:
var pixelPosition = getTextBoundingRect(input, 6)

Dependências de Função
Atualizado : A function é JavaScript puro, e não depende de nenhum plugin ou framework!
A function assume que o método getBoundingClientRect existe. Intervalos de texto são usados ​​quando são suportados. Caso contrário, a funcionalidade é obtida usando minha lógica de function.

Lógica de function
O código em si contém vários comentários. Esta parte vai em um detalhe mais profundo.

  1. Um contêiner

    temporário é criado.

  2. 1 – 3 elementos são criados. Cada span contém uma parte do valor da input (offsets 0 para selectionStart , selectionStart para selectionEnd , selectionEnd para final de string, somente o segundo span é meaninngful).
  3. Diversas propriedades de estilo significativas do elemento de input são copiadas para essas tags

    e . Somente propriedades de estilo significativas são copiadas. Por exemplo, a color não é copiada, porque não afeta os deslocamentos de um caractere de forma alguma. # 1

  4. O

    é posicionado na posição exata do nó de texto (valor da input). Bordas e preenchimentos são levados em conta, para garantir que o

    temporário esteja posicionado corretamente .

  5. Uma variável é criada, que contém o valor de retorno de div.getBoundingClientRect() .
  6. O temporário

    é removido, a menos que o parâmetro debug esteja definido como true.

  7. A function retorna o object ClientRect . Para mais informações sobre este object, veja esta página . A demonstração também mostra uma lista de propriedades: top , left , right , bottom , height e width .

# 1 : getBoundingClientRect() (e algumas propriedades secundárias) é usado para determinar a posição do elemento de input. Em seguida, o preenchimento e a largura da borda são adicionados para obter a posição real de um nó de texto.

Problemas Conhecidos
O único caso de inconsistência foi encontrado quando getComputedStyle retornou um valor errado para font-family : Quando uma página não definiu uma propriedade font-family , o computedStyle retorna um valor incorreto (até mesmo o Firebug está com esse problema; ambiente: Linux, Firefox 3.6.23, fonte “Sans Serif”).

Como é visível na demo, o posicionamento é às vezes ligeiramente desligado (quase zero, sempre menor que 1 pixel).

Restrições técnicas impedem que o script obtenha o deslocamento exato de um fragment de texto quando o conteúdo foi movido, por exemplo, quando o primeiro caractere visível em um campo de input não é igual ao caractere do primeiro valor.

Código

 // @author Rob W http://stackoverflow.com/users/938089/rob-w // @name getTextBoundingRect // @param input Required HTMLElement with `value` attribute // @param selectionStart Optional number: Start offset. Default 0 // @param selectionEnd Optional number: End offset. Default selectionStart // @param debug Optional boolean. If true, the created test layer // will not be removed. function getTextBoundingRect(input, selectionStart, selectionEnd, debug) { // Basic parameter validation if(!input || !('value' in input)) return input; if(typeof selectionStart == "string") selectionStart = parseFloat(selectionStart); if(typeof selectionStart != "number" || isNaN(selectionStart)) { selectionStart = 0; } if(selectionStart < 0) selectionStart = 0; else selectionStart = Math.min(input.value.length, selectionStart); if(typeof selectionEnd == "string") selectionEnd = parseFloat(selectionEnd); if(typeof selectionEnd != "number" || isNaN(selectionEnd) || selectionEnd < selectionStart) { selectionEnd = selectionStart; } if (selectionEnd < 0) selectionEnd = 0; else selectionEnd = Math.min(input.value.length, selectionEnd); // If available (thus IE), use the createTextRange method if (typeof input.createTextRange == "function") { var range = input.createTextRange(); range.collapse(true); range.moveStart('character', selectionStart); range.moveEnd('character', selectionEnd - selectionStart); return range.getBoundingClientRect(); } // createTextRange is not supported, create a fake text range var offset = getInputOffset(), topPos = offset.top, leftPos = offset.left, width = getInputCSS('width', true), height = getInputCSS('height', true); // Styles to simulate a node in an input field var cssDefaultStyles = "white-space:pre;padding:0;margin:0;", listOfModifiers = ['direction', 'font-family', 'font-size', 'font-size-adjust', 'font-variant', 'font-weight', 'font-style', 'letter-spacing', 'line-height', 'text-align', 'text-indent', 'text-transform', 'word-wrap', 'word-spacing']; topPos += getInputCSS('padding-top', true); topPos += getInputCSS('border-top-width', true); leftPos += getInputCSS('padding-left', true); leftPos += getInputCSS('border-left-width', true); leftPos += 1; //Seems to be necessary for (var i=0; i 0) appendPart(0, selectionStart); var fakeRange = appendPart(selectionStart, selectionEnd); if(textLen > selectionEnd) appendPart(selectionEnd, textLen); // Styles to inherit the font styles of the element fakeClone.style.cssText = cssDefaultStyles; // Styles to position the text node at the desired position fakeClone.style.position = "absolute"; fakeClone.style.top = topPos + "px"; fakeClone.style.left = leftPos + "px"; fakeClone.style.width = width + "px"; fakeClone.style.height = height + "px"; document.body.appendChild(fakeClone); var returnValue = fakeRange.getBoundingClientRect(); //Get rect if (!debug) fakeClone.parentNode.removeChild(fakeClone); //Remove temp return returnValue; // Local functions for readability of the previous code function appendPart(start, end){ var span = document.createElement("span"); span.style.cssText = cssDefaultStyles; //Force styles to prevent unexpected results span.textContent = text.substring(start, end); fakeClone.appendChild(span); return span; } // Computing offset position function getInputOffset(){ var body = document.body, win = document.defaultView, docElem = document.documentElement, box = document.createElement('div'); box.style.paddingLeft = box.style.width = "1px"; body.appendChild(box); var isBoxModel = box.offsetWidth == 2; body.removeChild(box); box = input.getBoundingClientRect(); var clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, scrollTop = win.pageYOffset || isBoxModel && docElem.scrollTop || body.scrollTop, scrollLeft = win.pageXOffset || isBoxModel && docElem.scrollLeft || body.scrollLeft; return { top : box.top + scrollTop - clientTop, left: box.left + scrollLeft - clientLeft}; } function getInputCSS(prop, isnumber){ var val = document.defaultView.getComputedStyle(input, null).getPropertyValue(prop); return isnumber ? parseFloat(val) : val; } } 

Atualização de maio de 2014: A biblioteca de componentes incrivelmente leve e robusta textarea-caret-position agora também suporta , tornando todas as outras respostas obsoletas.

Uma demonstração está disponível em http://jsfiddle.net/dandv/aFPA7/

Obrigado a Rob W pela inspiração para o suporte RTL.

Acabei criando uma input falsa escondida de um intervalo posicionado de forma absoluta e estilizada de forma semelhante à input. Eu defino o texto desse intervalo para o valor da input até o caractere cuja posição desejo localizar. Eu insiro o intervalo antes da input e obtenho seu deslocamento:

 function getInputTextPosition(input, charOffset) { var pixelPosition = null; if (input.createTextRange) { var range = input.createTextRange(); range.moveStart("character", charOffset); pixelPosition = range.getBoundingClientRect(); } else { var text = input.value.substr(0, charOffset).replace(/ $/, "\xa0"); var sizer = $("#sizer").insertBefore(input).text(text); pixelPosition = sizer.offset(); pixelPosition.left += sizer.width(); if (!text) sizer.text("."); // for computing height. An empty span returns 0 pixelPosition.bottom = pixelPosition.top + sizer.height(); } return pixelPosition } 

O css para o meu span sizer:

 #sizer { position: absolute; display: inline-block; visibility: hidden; margin: 3px; /* simulate padding and border without affecting height and width */ font-family: "segoe ui", Verdana, Arial, Sans-Serif; font-size: 12px; } 

Atualização de 2016: Uma solução baseada em HTML5 mais moderna seria usar a propriedade contenteditable .

 
Block of regular text, and text of interest

Podemos agora encontrar a posição do span usando jquery offset() . E, claro, as tags podem ser inseridas antecipadamente ou dinamicamente.