En este capítulo cubriremos la selección en el documento, así como la selección en campos de formulario, como <input>
.
JavaScript puede acceder una selección existente, seleccionar/deseleccionar nodos DOM tanto en su totalidad como parcialmente, eliminar la parte seleccionada del documento, envolverla en una etiqueta, etc.
Puedes encontrar algunas recetas para tareas comunes al final del artículo, en la sección “Resumen”. Pero será mucho más beneficiosa la lectura de todo el capítulo.
Los objetos subyacentes Range
y Selection
son fáciles de captar y no necesitarás recetas para que hagan lo que deseas.
Range
El concepto básico de selección Range, es básicamente un par de “puntos límite”: inicio y fin del rango.
Un objeto rango se crea sin parámetros:
let range = new Range();
Entonces podemos establecer los límites de selección usando range.setStart(node, offset)
y range.setEnd(node, offset)
.
En adelante usaremos objetos Range
para selección, pero primero creemos algunos de ellos.
Seleccionando el texto parcialmente
Lo interesante es que el primer argumento node
en ambos métodos puede ser tanto un nodo de texto o un nodo de elemento, y el significado del segundo argumento depende de ello.
Si node
es un nodo de texto, offset
debe ser la posición en su texto.
Por ejemplo, dado el elemento <p>Hello</p>
, podemos crear el rango conteniendo las letras “ll”:
<p id="p">Hello</p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.firstChild, 4);
// toString de un rango devuelve su contenido como un texto
console.log(range); // ll
</script>
Aquí tomamos el primer hijo de <p>
(que es el nodo de texto) y especificamos la posición del texto dentro de él:
Seleccionando nodos de elemento
Alternativamente, si node
es un nodo de elemento, offset
debe ser el número de hijo.
Esto es práctico para hacer rangos que contienen nodos como un todo, no detenerse en algún lugar dentro de su texto.
Por ejemplo, tenemos un fragmento de documento más complejo:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
Aquí está su estructura DOM usando ambos, nodos de texto y nodos de elemento:
Hagamos un rango para "Example: <i>italic</i>"
.
Como podemos ver, esta frase consiste de exactamente dos hijos de <p>
con índices 0
y 1
:
- El punto de inicio tiene
<p>
como nodo padrenode
, y0
como offset.
Así que podemos establecerlo como range.setStart(p, 0)
.
-
El punto final también tiene
<p>
como nodo padre, but2
como offset (especifica el rango “hasta”, pero no incluyendo,offset
).Entonces podemos establecerlo como
range.setEnd(p, 2)
.
Aquí la demo. Si la ejeutas, puedes ver el texto siendo seleccionado::
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.setStart(p, 0);
range.setEnd(p, 2);
// toString de un rango devuelve su contenido como texto (sin etiquetas)
alert(range); // Ejemplo: italic
// aplicar este rango para la selección de documentos (explicado más adelante)
document.getSelection().addRange(range);
</script>
Aquí hay un banco de pruebas más flexible donde puedes establecer números de principio y fin y explorar otras variantes:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
From <input id="start" type="number" value=1> – To <input id="end" type="number" value=4>
<button id="button">Click to select</button>
<script>
button.onclick = () => {
let range = new Range();
range.setStart(p, start.value);
range.setEnd(p, end.value);
// aplicar la selección, explicado más adelante
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);
};
</script>
Ej. seleccionando de 1
a 4
da como rango <i>italic</i> and <b>bold</b>
.
No tenemos que usar el mismo nodo en setStart
y setEnd
. Un rango puede abarcar muchos nodos no relacionados. Solo es importante que el final sea posterior al comienzo.
Seleccionar partes de nodos de texto
Seleccionemos el texto parcialmente, así:
Eso también es posible, solo necesitamos establecer el inicio y el final como un desplazamiento relativo en los nodos de texto.
Necesitamos crear un rango, que:
- comienza desde la posición 2 en
<p>
primer hijo (tomando todas menos dos primeras letras de “Example:”) - termina en la posición 3 de
<b>
primer hijo (tomando las primeras tres letras de “bold”, pero no más):
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
alert(range); // amplio: italic and bol
// use este rango para la selección (explicado más adelante)
window.getSelection().addRange(range);
</script>
Como puedes ver, es fácil hacer un rango con lo que quieras.
Si queremos tomar los nodos como un todo, podemos pasar los elementos en setStart/setEnd
. Si no, podemos trabajar en el nivel de texto.
Propiedades de Range
El objeto rango que creamos arriba tiene las siguientes propiedades:
startContainer
,startOffset
– nodo y desplazamiento del inicio,- en el ejemplo anterior: primer nodo de texto dentro de
<p>
y2
.
- en el ejemplo anterior: primer nodo de texto dentro de
endContainer
,endOffset
– nodo y desplazamiento del final,- en el ejemplo anterior: primer nodo de texto dentro de
<b>
y3
.
- en el ejemplo anterior: primer nodo de texto dentro de
collapsed
– booleano,true
si el rango comienza y termina en el mismo punto (por lo que no hay contenido dentro del rango),- en el ejemplo anterior:
false
- en el ejemplo anterior:
commonAncestorContainer
– el ancestro común más cercano de todos los nodos dentro del rango,- en el ejemplo anterior:
<p>
- en el ejemplo anterior:
Métodos de selección de rango
Hay muchos métodos convenientes para manipular rangos.
Ya hemos visto setStart
y setEnd
, aquí hay otros métodos similares.
Establecer inicio de rango:
setStart(node, offset)
establecer inicio en: posiciónoffset
ennode
setStartBefore(node)
establecer inicio en: justo antesnode
setStartAfter(node)
establecer inicio en: justo despuésnode
Establecer fin de rango (métodos similares):
setEnd(node, offset)
establecer final en: posiciónoffset
ennode
setEndBefore(node)
establecer final en: justo antesnode
setEndAfter(node)
establecer final en: justo despuésnode
Técnicamente, setStart/setEnd
puede hacer cualquier cosa, pero más métodos brindan más conveniencia.
En todos estos métodos node
puede ser un nodo de texto o de elemento: para nodos de texto offset
salta esa cantidad de caracteres, mientras que para los nodos de elementos es la cantidad de nodos secundarios.**
Más métodos aún para crear rangos:
selectNode(node)
establecer rango para seleccionar elnode
selectNodeContents(node)
establecer rango para seleccionar todo el contenido denode
collapse(toStart)
sitoStart=true
establece final=comienzo, de otra manera comienzo=final, colapsando así el rangocloneRange()
crea un nuevo rango con el mismo inicio/final
Métodos para edición en el rango:
Una vez creado el rango, podemos manipular su contenido usando estos métodos:
deleteContents()
– eliminar el contenido de rango del documentoextractContents()
– eliminar el contenido de rango del documento y lo retorna como DocumentFragmentcloneContents()
– clonar el contenido del rango y lo retorna como DocumentFragmentinsertNode(node)
– insertanode
en el documento al comienzo del rangosurroundContents(node)
– envuelvenode
alrededor del contenido del rango. Para que esto funcione, el rango debe contener etiquetas de apertura y cierre para todos los elementos dentro de él, sin rangos parciales como<i>abc
.
Con estos métodos podemos hacer básicamente cualquier cosa con los nodos seleccionados.
Aquí está el banco de pruebas para verlos en acción:
Haga clic en los botones para ejecutar métodos en la selección, "resetExample" para restablecerla.
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
<p id="result"></p>
<script>
let range = new Range();
// Cada método demostrado se representa aquí:
let methods = {
deleteContents() {
range.deleteContents()
},
extractContents() {
let content = range.extractContents();
result.innerHTML = "";
result.append("extracted: ", content);
},
cloneContents() {
let content = range.cloneContents();
result.innerHTML = "";
result.append("cloned: ", content);
},
insertNode() {
let newNode = document.createElement('u');
newNode.innerHTML = "NEW NODE";
range.insertNode(newNode);
},
surroundContents() {
let newNode = document.createElement('u');
try {
range.surroundContents(newNode);
} catch(e) { console.log(e) }
},
resetExample() {
p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`;
result.innerHTML = "";
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
};
for(let method in methods) {
document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`);
}
methods.resetExample();
</script>
También existen métodos para comparar rangos, pero rara vez se utilizan. Cuando los necesite, consulte el spec o manual MDN.
Selection
Range
es un objeto genérico para gestionar rangos de selección. Pero crearlos no significa que podamos ver la selección en la pantalla.
Podemos crear objetos Range
, pasarlos; no seleccionan nada visualmente por sí mismos.
La selección de documento está representada por el objeto Selection
, que se puede obtener como window.getSelection()
o document.getSelection()
Una selección puede incluir cero o más rangos. Al menos, la especificación Selection API lo dice. Sin embargo, en la práctica, solo Firefox permite seleccionar múltiples rangos en el documento usando Ctrl+click (Cmd+click para Mac).
Aquí hay una captura de pantalla de una selección con 3 rangos en Firefox:
Otros navegadores admiten un rango máximo de 1. Como veremos, algunos de los métodos de Selection
implica que puede haber muchos rangos, pero nuevamente, en todos los navegadores excepto Firefox, hay un máximo de 1.
Aquí hay una pequeña demo que muestra la selección actual (selecciona algo y haz clic) como texto:
Propiedades de Selection
Como dijimos antes, una selección en teoría tiene múltiples rangos. Podemos obtener estos objetos rango usando el método:
getRangeAt(i)
– obtiene el rango “i” comenzando desde0
. En todos los navegadores excepto Firefox, solo0
es usado.
También existen propiedades que a menudo brindan conveniencia.
Similar a Range, una selección tiene un inicio, llamado “ancla(anchor)”, y un final, llamado “foco(focus)”.
Las principales propiedades de selection son:
anchorNode
– el nodo donde comienza la selección,anchorOffset
– el desplazamiento enanchorNode
donde comienza la selección,focusNode
– el nodo donde termina la selección,focusOffset
– el desplazamiento enfocusNode
donde termina la selección,isCollapsed
–true
si la selección no selecciona nada (rango vacío), o no existe.rangeCount
– recuento de rangos en la selección, máximo “1” en todos los navegadores excepto Firefox.
Hay una diferencia importante entre anchor/focus (ancla/foco) de una selección comparado al inicio/fin de un Range
.
Sabemos que los objetos Range
siempre tienen el inicio antes que el final.
En las selecciones, no siempre es así.
Seleccionar algo con el ratón puede hacerse en ambas direcciones: tanto de izquierda a derecha como de derecha a izquierda.
Cuando el botón es presionado, cuando se mueve hacia adelante en el documento, entonces su final (foco) estará después del inicio (ancla).
Ej. si el usuario comienza a seleccionar con el mouse y pasa de “Example” a “italic”:
…Pero la selección puede hacerse hacia atrás: comenzando por “italic” terminando en “Example”, su foco estará antes del ancla:
Eventos Selection
Hay eventos para realizar un seguimiento de la selección:
elem.onselectstart
– cuando una selección comienza enelem
, ej. el usuario comienza a mover el mouse con el botón presionado.- Evitar la acción predeterminada hace que la selección no se inicie.
document.onselectionchange
– siempre que cambie una selección.- Tenga en cuenta: este controlador solo se puede configurar en
document
.
- Tenga en cuenta: este controlador solo se puede configurar en
Demostración de seguimiento de selección
Aquí hay una pequeña demostración que muestra los límites de selección de forma dinámica a medida que cambia:
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
let {anchorNode, anchorOffset, focusNode, focusOffset} = selection;
// anchorNode y focusNode usualmente son nodos de texto
from.value = `${anchorNode?.data}, offset ${anchorOffset}`;
to.value = `${focusNode?.data}, offset ${focusOffset}`;
};
</script>
Demostración de copia de selección
Hay dos enfoques para la copia de contenido seleccionado:
- Podemos usar
document.getSelection().toString()
para obtenerlo como texto. - O copiar el DOM entero; por ejemplo, si necesitamos mantener el formato, podemos obtener los rangos correspondientes con
getRangeAt(...)
. Un objetoRange
, a su vez, tiene el métodocloneContents()
que clona su contenido y devuelve un objetoDocumentFragment
, que podemos insertar en algún otro lugar.
Una demostración de cómo obtener la selección como texto y como nodos DOM:
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
cloned.innerHTML = astext.innerHTML = "";
// Clonar nodos DOM de rangos (admitimos selección múltiple aquí)
for (let i = 0; i < selection.rangeCount; i++) {
cloned.append(selection.getRangeAt(i).cloneContents());
}
// Obtener como texto
astext.innerHTML += selection;
};
</script>
Métodos de selección
Podemos trabajar con métodos de selección para agregar y eliminar rangos:
getRangeAt(i)
– obtener el rango i-ésimo, comenzando desde “0”. En todos los navegadores, excepto Firefox, solo se utiliza0
.addRange(rango)
– agrega unrango
a la selección. Todos los navegadores excepto Firefox ignoran la llamada, si la selección ya tiene un rango asociado.removeRange(rango)
--eliminarango
de la selección.removeAllRanges()
--elimina todos los rangos.empty()
– alias pararemoveAllRanges
.
Además, existen métodos convenientes para manipular el rango de selección directamente, sin llamadas intermedias a Range
:
collapse(node, offset)
– Reemplazar el rango seleccionado con uno nuevo que comienza y termina en elnode
dado, en posiciónoffset
.setPosition(node, offset)
– alias paracollapse
.collapseToStart()
– colapsar (reemplazar con un rango vacío) al inicio de la selección,collapseToEnd()
– colapso hasta el final de la selección,extend(node, offset)
– mover el foco de la selección alnode
dado, posiciónoffset
,setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)
– reemplazar el rango de selección con el inicio dadoanchorNode/anchorOffset
y finalfocusNode/focusOffset
. Se selecciona todo el contenido entre ellos.selectAllChildren(node)
– seleccionar todos los hijos delnode
.deleteFromDocument()
– eliminar el contenido seleccionado del documento.containsNode(node, allowPartialContainment = false)
– comprueba si la selección contienenode
(parcialmente si el segundo argumento estrue
)
Entonces, para muchas tareas podemos llamar a los métodos de Selection
, y no es necesario acceder al objeto Range
subyacente.
Por ejemplo, seleccionando todo el contenido del párrafo <p>
:
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
// seleccione desde el 0 hijo de <p> hasta el último hijo
document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>
Lo mismo usando rangos:
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.selectNodeContents(p); // o selectNode(p) para seleccionar el tag <p> también
document.getSelection().removeAllRanges(); // borrar la selección existente si la hubiera
document.getSelection().addRange(range);
</script>
Si la selección ya existe, vacíelo primero con removeAllRanges()
. Y luego agregue rangos. De lo contrario, todos los navegadores excepto Firefox ignoran los nuevos rangos.
La excepción son algunos métodos de selección, que reemplazan la selección existente, como setBaseAndExtent
.
Selección en controles de formulario
Elementos de formulario, como input
y textarea
proporciona API especial para la selección, sin objetos Selection
o Range
. Como un valor de entrada es un texto puro, no HTML, no hay necesidad de tales objetos, todo es mucho más simple.
Propiedades:
input.selectionStart
– posición de inicio de selección (escribible),input.selectionEnd
– posición del final de la selección (escribible),input.selectionDirection
– dirección de selección, una de: “adelante” “hacia atrás” o “ninguno” (si, por ejemplo, se selecciona con un doble clic del mouse),
Eventos:
input.onselect
– se activa cuando se selecciona algo.
Métodos:
-
input.select()
– selecciona todo en el control de texto (puede sertextarea
en vez deinput
), -
input.setSelectionRange(start, end, [direction])
– cambiar la selección para abarcar desde la posiciónstart
hastaend
, en la dirección indicada (opcional). -
input.setRangeText(replacement, [start], [end], [selectionMode])
– reemplace un rango de texto con el nuevo texto.Los argumentos opcionales
start
yend
, si se proporcionan, establecen el inicio y el final del rango; de lo contrario, se utiliza la selección del usuario.El último argumento,
selectionMode
, determina cómo se establecerá la selección después de que se haya reemplazado el texto. Los posibles valores son:"select"
– se seleccionará el texto recién insertado."start"
– el rango de selección se colapsa justo antes del texto insertado (el cursor estará inmediatamente antes)."end"
– el rango de selección se colapsa justo después del texto insertado (el cursor estará justo después)."preserve"
– intenta preservar la selección. Este es el predeterminado.
Ahora veamos estos métodos en acción.
Ejemplo: Seguimiento de selección
Por ejemplo, este código usa el evento onselect
para rastrear la selección:
<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
area.onselect = function() {
from.value = area.selectionStart;
to.value = area.selectionEnd;
};
</script>
Tenga en cuenta:
onselect
se activa cuando se selecciona algo, pero no cuando se elimina la selección.- El evento
document.onselectionchange
no debería activarse para las selecciones dentro de un control de formulario, según el spec, ya que no está relacionado con la selección y los rangos del “documento”. Algunos navegadores lo generan, pero no debemos confiar en él.
Ejemplo: cursor en movimiento
Podemos cambiar selectionStart
y selectionEnd
, que establece la selección.
Un caso límite importante es cuando selectionStart
y selectionEnd
son iguales entre sí. Entonces es exactamente la posición del cursor. O, para reformular, cuando no se selecciona nada, la selección se contrae en la posición del cursor.
Entonces, al establecer selectionStart
y selectionEnd
en el mismo valor, movemos el cursor.
Por ejemplo:
<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>
<script>
area.onfocus = () => {
// zero delay setTimeout to run after browser "focus" action finishes
setTimeout(() => {
// we can set any selection
// if start=end, the cursor is exactly at that place
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
Ejemplo: modificar la selección
Para modificar el contenido de la selección, podemos utilizar el método input.setRangeText()
Por supuesto, podemos leer selectionStart/End
y, con el conocimiento de la selección, cambiar la subcadena correspondiente de value
, pero setRangeText
es más poderoso y a menudo más conveniente.
Ese es un método algo complejo. En su forma más simple de un argumento, reemplaza el rango seleccionado por el usuario y elimina la selección.
Por ejemplo, aquí la selección de usuario estará envuelta por *...*
:
<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>
<script>
button.onclick = () => {
if (input.selectionStart == input.selectionEnd) {
return; // nada fue seleccionado
}
let selected = input.value.slice(input.selectionStart, input.selectionEnd);
input.setRangeText(`*${selected}*`);
};
</script>
Con más argumentos, podemos establecer un rango start
y end
.
En este ejemplo, encontramos THIS
en el texto de entrada, lo reemplazamos y mantenemos el reemplazo seleccionado:
<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>
<script>
button.onclick = () => {
let pos = input.value.indexOf("THIS");
if (pos >= 0) {
input.setRangeText("*THIS*", pos, pos + 4, "select");
input.focus(); // focus to make selection visible
}
};
</script>
Ejemplo: insertar en el cursor
Si no se selecciona nada, o usamos el mismo comienzo
y final
en setRangeText
, entonces el nuevo texto se acaba de insertar, no se elimina nada.
También podemos insertar algo “en el cursor” usando setRangeText
.
Aquí hay un botón que se inserta "HELLO"
en la posición del cursor y lo coloca inmediatamente después. Si la selección no está vacía, entonces se reemplaza (podemos detectarla comparando selectionStart!=selectionEnd
y hacer otra cosa en su lugar):
<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>
<script>
button.onclick = () => {
input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
input.focus();
};
</script>
Haciendo no seleccionable
Para hacer algo no seleccionable, hay tres formas:
-
Usar propiedad CSS
user-select: none
.<style> #elem { user-select: none; } </style> <div>Selectable <div id="elem">Unselectable</div> Selectable</div>
Esto no permite que la selección comience en
elem
. Pero el usuario puede iniciar la selección en otro lugar e incluirelem
en ella.Entonces,
elem
se convertirá en parte dedocument.getSelection )
, por lo que la selección realmente ocurre, pero su contenido generalmente se ignora al copiar y pegar. -
Evita la acción predeterminada en los eventos
onselectstart
omousedown
.<div>Selectable <div id="elem">Unselectable</div> Selectable</div> <script> elem.onselectstart = () => false; </script>
Esto evita que la selección se inicie en
elem
, pero el visitante puede iniciarla en otro elemento y luego extenderla aelem
.
Eso es conveniente cuando hay otro controlador de eventos en la misma acción que activa la selección (por ejemplo, mousedown
). Así que deshabilitamos la selección para evitar conflictos, permitiendo que se copien los contenidos de elem
.
- También podemos borrar la selección post-factum después de que suceda con
document.getSelection().Empty ()
. Eso se usa con poca frecuencia, ya que provoca un parpadeo no deseado cuando la selección aparece o desaparece.
Referencias
Resumen
Cubrimos dos API diferentes para las selecciones:
- Para el documento: objetos
Selection
yRange
. - Para
input
,textarea
: métodos y propiedades adicionales.
La segunda API es muy simple, ya que funciona con texto.
Las recetas más utilizadas probablemente sean:
- Obteniendo la selección:
let selection = document.getSelection(); let cloned = /* elemento para clonar los nodos seleccionados para */; // luego aplica los métodos Range a selection.getRangeAt (0) // o, como aquí, a todos los rangos para admitir selección múltiple for (let i = 0; i < selection.rangeCount; i++) { cloned.append(selection.getRangeAt(i).cloneContents()); }
- Configuración de la selección:
let selection = document.getSelection(); // directamente: selection.setBaseAndExtent(...from...to...); // o podemos crear un rango y: selection.removeAllRanges(); selection.addRange(range);
Y finalmente, sobre el cursor. La posición del cursor en elementos editables, como <textarea>
está siempre al principio o al final de la selección. Podemos usarlo para obtener la posición del cursor o para mover el cursor configurando elem.selectionStart
y elem.selectionEnd
.