API de Selección y Rango en JavaScript
Aprende a usar las interfaces Selection y Range de JavaScript para leer, crear y manipular selecciones de texto en el DOM.
Cuando un usuario arrastra el cursor sobre texto en una página, el navegador registra lo que fue seleccionado. JavaScript expone esa información — y te permite crear selecciones por tu cuenta — a través de dos interfaces del DOM relacionadas:
- Un
Rangees un par de puntos límite (un inicio y un fin) dentro del documento. Describe qué parte del documento se quiere indicar, hasta un desplazamiento de carácter específico dentro de un nodo de texto. Un rango puede existir puramente en memoria sin que el usuario sea consciente de ello. - Una
Selectiones lo que el usuario tiene actualmente resaltado. Básicamente es un envoltorio alrededor de uno o más rangos, vinculado al resaltado en pantalla y al cursor de texto (caret).
Estas APIs son necesarias cuando se quiere construir un editor de texto enriquecido, una funcionalidad de "resaltar y comentar", un buscador y reemplazador personalizado, o cualquier cosa que lea, mueva o aplique estilos al texto seleccionado mediante programación. Este capítulo cubre la construcción de rangos, la lectura y modificación de la selección del usuario, y cómo combinar ambos para resaltar e insertar contenido.
Esto se basa en cómo está estructurado el DOM. Si los tipos de nodo y los desplazamientos son nuevos para ti, lee primero Propiedades del nodo: tipo, etiqueta y contenidos y Modificar el documento.
Comprender la interfaz Selection
La interfaz Selection representa el texto que el usuario ha resaltado, o la posición actual del caret cuando no hay nada resaltado. Se accede a ella con el método global window.getSelection() (generalmente solo getSelection()). Internamente, una selección contiene cero o más objetos Range; en la práctica, la mayoría de los navegadores solo mantienen un único rango, por lo que getRangeAt(0) es la forma habitual de leerlo.
La forma más rápida de ver qué está seleccionado es toString(), que devuelve el texto seleccionado como un string simple:
// After the user highlights something on the page:
const selectedText = window.getSelection().toString();
console.log(selectedText); // whatever the user highlightedPropiedades y métodos útiles de Selection
rangeCount: El número de rangos en la selección —0cuando no hay nada seleccionado. Comprueba esto siempre antes de llamar agetRangeAt.toString(): Devuelve el texto seleccionado como un string.getRangeAt(index): Devuelve elRangeen el índice dado (usa0para la selección actual).addRange(range): Agrega unRangea la selección, resaltándolo en pantalla.removeAllRanges(): Elimina la selección por completo.removeRange(range): Elimina un rango específico. La mayoría de los navegadores solo mantienen un único rango, por lo queremoveAllRanges()es la opción práctica.collapse(node, offset): Colapsa la selección a un punto único (un caret vacío) dentro denode.
Un patrón común es leer la selección actual, modificarla y luego escribir una nueva selección — removeAllRanges() seguido de addRange().
Ejemplo práctico: resaltar texto
Para resaltar lo que el usuario seleccionó, toma su rango, extrae los nodos seleccionados del documento, envuélvelos en un <span> con estilo y vuelve a colocar ese span donde estaba el contenido.
extractContents()elimina el contenido seleccionado del DOM y lo devuelve como un fragmento de documento, dejando el rango vacío (colapsado) en ese punto — que es exactamente donde después se inserta el span.
<div id="text">Select some of this text and press the button.</div>
<button onclick="highlightText()">Highlight</button>
<script>
function highlightText() {
const selection = window.getSelection();
if (!selection.rangeCount) return false;
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.style.backgroundColor = 'yellow';
const fragment = range.extractContents();
span.appendChild(fragment);
range.insertNode(span);
}
</script>Explorar la interfaz Range
Un Range marca un fragmento del documento con dos puntos límite — un inicio y un fin — cada uno definido por un nodo y un desplazamiento. El significado del desplazamiento depende del nodo:
- Dentro de un nodo de texto, el desplazamiento es un índice de carácter (p. ej., el desplazamiento
5está entre el 5.º y el 6.º carácter). - Dentro de un nodo de elemento, el desplazamiento cuenta los nodos hijos (p. ej., el desplazamiento
0está antes del primer hijo).
Crea un rango vacío con document.createRange(), luego posiciona sus límites. Puedes encontrar los nodos a los que apuntar con getElementById / querySelector.
Establecer los límites
const p = document.querySelector('p');
const textNode = p.firstChild; // the text inside <p>
const range = document.createRange();
range.setStart(textNode, 0); // start at the first character
range.setEnd(textNode, 5); // end before the 6th character
console.log(range.toString()); // first 5 characters of the paragraphDos atajos cubren los casos más comunes para que no tengas que calcular desplazamientos:
selectNode(node)— el rango abarca el nodo y sus etiquetas circundantes.selectNodeContents(node)— el rango abarca solo lo que está dentro del nodo.
Métodos útiles de Range
setStart(node, offset)/setEnd(node, offset): Posiciona los límites de inicio y fin.selectNode(node)/selectNodeContents(node): Establece ambos límites alrededor de un nodo o su contenido.toString(): El texto dentro del rango.cloneContents(): Devuelve una copia del contenido del rango como fragmento de documento, sin tocar el documento original.extractContents(): Mueve el contenido fuera del documento hacia un fragmento y lo devuelve.deleteContents(): Elimina el contenido del rango y no devuelve nada.cloneRange(): Devuelve una copia del propio objeto de rango (no de su contenido).insertNode(node): Inserta un nodo al inicio del rango.surroundContents(node): Envuelve el contenido del rango dentro denode— útil para resaltar, pero lanza una excepción si el rango cruza parcialmente un elemento que no es texto.
Recuerda la diferencia:
cloneContents()copia,extractContents()mueve el contenido fuera y lo devuelve,deleteContents()lo elimina y descarta.
Ejemplo práctico: extraer texto
Este ejemplo lee todo lo que hay dentro de un elemento con un rango y transforma el texto sin modificar el DOM original:
<div id="content">This is some sample text for extraction.</div>
<button onclick="extractText()">Extract and Manipulate</button>
<script>
function extractText() {
const range = document.createRange();
const content = document.getElementById('content');
range.selectNodeContents(content);
const extractedText = range.toString();
const manipulatedText = extractedText.replace('sample', 'example'); // Manipulating text
alert(manipulatedText);
}
</script>En el ejemplo anterior, el script reemplaza la palabra "sample" por "example" en el texto extraído antes de mostrarlo en un cuadro de alerta. Se trata de una manipulación básica, pero demuestra cómo se puede empezar a trabajar con el texto una vez extraído.
Operaciones avanzadas de texto
Más allá de la manipulación básica de texto, las interfaces Selection y Range permiten operaciones más complejas como insertar nodos directamente en el documento.
Ejemplo: insertar texto
Este ejemplo usa un div con contenteditable: el usuario hace clic para colocar el caret, y el botón inserta 'Hello World' en ese punto exacto. Observa cómo deleteContents() primero elimina cualquier texto seleccionado, luego se inserta el nuevo nodo de texto y se vuelve a seleccionar para que el caret quede después de él.
<div id="editable" contenteditable="true" style="border: 1px solid #ccc; padding: 10px; min-height: 50px;">
Click here and set the cursor position.
</div>
<button onclick="insertText()">Insert 'Hello World'</button>
<script>
function insertText() {
const editableDiv = document.getElementById('editable');
const sel = window.getSelection();
// Check if the selection is within the editable div
if (!sel.rangeCount || !editableDiv.contains(sel.getRangeAt(0).commonAncestorContainer)) return;
const range = sel.getRangeAt(0);
range.deleteContents(); // Clears any selected text
const textNode = document.createTextNode('Hello World');
range.insertNode(textNode);
sel.removeAllRanges(); // Clear the previous selection
sel.addRange(range); // Re-select the new text node
}
</script>Resumen
- Un
Rangees dos puntos límite (nodo + desplazamiento) que describen un fragmento del documento; se crea condocument.createRange()y se posiciona consetStart/setEndo los atajosselectNode/selectNodeContents. - Una
Selection, obtenida conwindow.getSelection(), envuelve el resaltado en pantalla del usuario. Se lee contoString()ygetRangeAt(0); se reescribe conremoveAllRanges()+addRange(). - Para el contenido:
cloneContents()copia,extractContents()mueve el contenido fuera,deleteContents()lo descarta, einsertNode/surroundContentsvuelven a colocar nodos.
Juntas, estas interfaces permiten resaltar, extraer e insertar contenido con precisión — la base de los editores de texto enriquecido y las herramientas de anotación.
Continúa: Modificar el documento · Propiedades del nodo: tipo, etiqueta y contenidos · Búsqueda: getElement* y querySelector