Представь: ты делаешь текстовый редактор или инструмент аннотирования документов. Пользователь выделяет фрагмент текста — нужно прочитать его, запомнить позицию, добавить подсветку. Или нужна кнопка «копировать код» в документации — одно нажатие, и код в буфере. Для всего этого — Selection и Range API.
Range описывает произвольный фрагмент документа с началом и концом. Selection — это то, что пользователь выделил мышью. Clipboard API позволяет работать с буфером обмена. Вместе они дают полный контроль над текстовым выделением в браузере.
selectionchange уведомляет об изменении выделенияRange описывает фрагмент документа: у него есть начало (start) и конец (end). Range можно создать, изменить и использовать для операций с текстом.
// Создать Range
const range = document.createRange()
// Установить границы: (узел, смещение)
range.setStart(textNode, 5) // начало: 5-й символ текстового узла
range.setEnd(textNode, 15) // конец: 15-й символ
// Полезные свойства
console.log(range.collapsed) // true если start === end (нет выделения)
console.log(range.toString()) // текст внутри Range
// Выбрать весь содержимое элемента
range.selectNodeContents(element)
// Получить размеры и позицию Range в viewport
const rect = range.getBoundingClientRect()
console.log(rect.top, rect.left, rect.width, rect.height)const selection = window.getSelection()
// Текст выделенного фрагмента
console.log(selection.toString()) // 'выделенный текст'
// Количество диапазонов (обычно 0 или 1)
console.log(selection.rangeCount) // 0 или 1
// Получить первый Range из выделения
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
console.log(range.toString())
}
// Снять выделение программно
selection.removeAllRanges()
// Добавить Range к выделению
selection.addRange(range)function selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element) // выделить весь текст элемента
selection.removeAllRanges() // сбросить старое выделение
selection.addRange(range) // применить новое
}
// Выделить часть текста
function selectRange(element, start, end) {
const selection = window.getSelection()
const range = document.createRange()
range.setStart(element.firstChild, start)
range.setEnd(element.firstChild, end)
selection.removeAllRanges()
selection.addRange(range)
}// Копировать текст в буфер (возвращает Promise)
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
console.log('Скопировано!')
} catch (err) {
console.error('Ошибка копирования:', err)
}
}
// Прочитать из буфера (требует разрешения пользователя)
async function pasteText() {
const text = await navigator.clipboard.readText()
return text
}
// Старый способ (без async): работает в большинстве браузеров
document.execCommand('copy') // копирует текущее выделениеfunction addCopyButton(codeBlock) {
const btn = document.createElement('button')
btn.textContent = 'Копировать'
btn.addEventListener('click', async () => {
const code = codeBlock.textContent
await navigator.clipboard.writeText(code)
btn.textContent = 'Скопировано!'
setTimeout(() => { btn.textContent = 'Копировать' }, 2000)
})
codeBlock.parentElement.appendChild(btn)
}Поскольку в sandbox нет реального DOM, мы симулируем Range через строковую логику — она полностью отражает принцип работы Range:
// Range в реальном DOM: (node, charOffset) → (node, charOffset)
// В строковой симуляции: (string, startIndex) → (string, endIndex)
function createStringRange(text, start, end) {
return {
text,
start,
end,
get collapsed() { return this.start === this.end },
toString() { return this.text.slice(this.start, this.end) },
get length() { return this.end - this.start },
}
}1. Читать selection.toString() до проверки rangeCount
// ПЛОХО — может вернуть '' если ничего не выделено, непонятно почему
const text = window.getSelection().toString()
if (text) { ... } // работает, но не объясняет почему пусто
// ХОРОШО — явная проверка
const selection = window.getSelection()
if (selection.rangeCount === 0) return // нет выделения
const text = selection.toString()
if (!text) return // collapsed (курсор без выделения)2. Не вызывать selection.removeAllRanges() перед addRange
// ПЛОХО — предыдущее выделение может остаться (в Firefox допускается несколько Range)
function selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection.addRange(range) // добавляет к существующему!
}
// ХОРОШО — сначала сбросить, потом установить
function selectText(element) {
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.selectNodeContents(element)
selection.addRange(range)
}3. Использовать document.execCommand('copy') — устаревший API
// ПЛОХО — execCommand устарел и будет удалён
document.execCommand('copy') // Deprecated!
// ХОРОШО — используй Clipboard API
async function copyText(text) {
await navigator.clipboard.writeText(text)
// требует HTTPS и разрешения пользователя
}navigator.clipboard.writeText(codeBlock.textContent)Range — выделение, вставка, форматированиеRange для подсветки совпадений, ты можешь реализовать то же через window.find() или Range APIСимуляция Range и Selection через строки: выделение, копирование, форматирование фрагментов текста
// Симуляция Range/Selection API через строки
// В браузере Range работает с текстовыми узлами DOM
// --- StringRange: имитация Range ---
function createRange(text, start, end) {
if (start > end) [start, end] = [end, start]
return {
text,
start: Math.max(0, start),
end: Math.min(text.length, end),
get collapsed() { return this.start === this.end },
toString() { return this.text.slice(this.start, this.end) },
get length() { return this.end - this.start },
cloneRange() { return createRange(this.text, this.start, this.end) },
}
}
// --- StringSelection: имитация window.getSelection() ---
function createSelection() {
let ranges = []
return {
get rangeCount() { return ranges.length },
getRangeAt(i) { return ranges[i] },
addRange(r) { ranges = [r] },
removeAllRanges() { ranges = [] },
toString() { return ranges.map(r => r.toString()).join('') },
}
}
// --- Демо 1: Работа с Range ---
console.log('=== Range: выделение фрагментов ===')
const articleText = 'JavaScript — мультипарадигменный язык программирования'
const range1 = createRange(articleText, 0, 10)
const range2 = createRange(articleText, 15, 34)
const rangeAll = createRange(articleText, 0, articleText.length)
console.log('range1:', range1.toString()) // 'JavaScript'
console.log('range2:', range2.toString()) // 'мультипарадигменный'
console.log('rangeAll:', rangeAll.toString()) // вся строка
console.log('collapsed:', createRange(articleText, 5, 5).collapsed) // true
console.log('length:', range2.length) // 19
// --- Демо 2: Selection ---
console.log('\n=== Selection: текущее выделение ===')
const selection = createSelection()
console.log('rangeCount до:', selection.rangeCount) // 0
console.log('toString до:', selection.toString()) // ''
selection.addRange(range1)
console.log('rangeCount после:', selection.rangeCount) // 1
console.log('Выделено:', selection.toString()) // 'JavaScript'
selection.removeAllRanges()
selection.addRange(range2)
console.log('Новое выделение:', selection.toString()) // 'мультипарадигменный'
// --- Демо 3: Имитация Clipboard API ---
console.log('\n=== Clipboard API симуляция ===')
let mockClipboard = ''
const clipboardAPI = {
async writeText(text) {
mockClipboard = text
return Promise.resolve()
},
async readText() {
return Promise.resolve(mockClipboard)
},
}
async function copySelectedText(selection) {
const text = selection.toString()
if (!text) { console.log('Ничего не выделено'); return }
await clipboardAPI.writeText(text)
console.log(`Скопировано в буфер: "${text}"`)
}
selection.removeAllRanges()
selection.addRange(createRange(articleText, 0, 10))
copySelectedText(selection).then(() => clipboardAPI.readText()).then(t => console.log('Из буфера:', t))
// --- Демо 4: Поиск и выделение ---
console.log('\n=== Поиск текста и создание Range ===')
function findInText(text, query) {
const results = []
let idx = text.toLowerCase().indexOf(query.toLowerCase())
while (idx !== -1) {
results.push(createRange(text, idx, idx + query.length))
idx = text.toLowerCase().indexOf(query.toLowerCase(), idx + 1)
}
return results
}
const content = 'Найти все вхождения слова "найти" в тексте. Найти и выделить.'
const found = findInText(content, 'найти')
console.log(`Найдено вхождений: ${found.length}`)
found.forEach((r, i) => {
console.log(` [${i + 1}] позиция ${r.start}-${r.end}: "${r.toString()}"`)
})Представь: ты делаешь текстовый редактор или инструмент аннотирования документов. Пользователь выделяет фрагмент текста — нужно прочитать его, запомнить позицию, добавить подсветку. Или нужна кнопка «копировать код» в документации — одно нажатие, и код в буфере. Для всего этого — Selection и Range API.
Range описывает произвольный фрагмент документа с началом и концом. Selection — это то, что пользователь выделил мышью. Clipboard API позволяет работать с буфером обмена. Вместе они дают полный контроль над текстовым выделением в браузере.
selectionchange уведомляет об изменении выделенияRange описывает фрагмент документа: у него есть начало (start) и конец (end). Range можно создать, изменить и использовать для операций с текстом.
// Создать Range
const range = document.createRange()
// Установить границы: (узел, смещение)
range.setStart(textNode, 5) // начало: 5-й символ текстового узла
range.setEnd(textNode, 15) // конец: 15-й символ
// Полезные свойства
console.log(range.collapsed) // true если start === end (нет выделения)
console.log(range.toString()) // текст внутри Range
// Выбрать весь содержимое элемента
range.selectNodeContents(element)
// Получить размеры и позицию Range в viewport
const rect = range.getBoundingClientRect()
console.log(rect.top, rect.left, rect.width, rect.height)const selection = window.getSelection()
// Текст выделенного фрагмента
console.log(selection.toString()) // 'выделенный текст'
// Количество диапазонов (обычно 0 или 1)
console.log(selection.rangeCount) // 0 или 1
// Получить первый Range из выделения
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
console.log(range.toString())
}
// Снять выделение программно
selection.removeAllRanges()
// Добавить Range к выделению
selection.addRange(range)function selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element) // выделить весь текст элемента
selection.removeAllRanges() // сбросить старое выделение
selection.addRange(range) // применить новое
}
// Выделить часть текста
function selectRange(element, start, end) {
const selection = window.getSelection()
const range = document.createRange()
range.setStart(element.firstChild, start)
range.setEnd(element.firstChild, end)
selection.removeAllRanges()
selection.addRange(range)
}// Копировать текст в буфер (возвращает Promise)
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
console.log('Скопировано!')
} catch (err) {
console.error('Ошибка копирования:', err)
}
}
// Прочитать из буфера (требует разрешения пользователя)
async function pasteText() {
const text = await navigator.clipboard.readText()
return text
}
// Старый способ (без async): работает в большинстве браузеров
document.execCommand('copy') // копирует текущее выделениеfunction addCopyButton(codeBlock) {
const btn = document.createElement('button')
btn.textContent = 'Копировать'
btn.addEventListener('click', async () => {
const code = codeBlock.textContent
await navigator.clipboard.writeText(code)
btn.textContent = 'Скопировано!'
setTimeout(() => { btn.textContent = 'Копировать' }, 2000)
})
codeBlock.parentElement.appendChild(btn)
}Поскольку в sandbox нет реального DOM, мы симулируем Range через строковую логику — она полностью отражает принцип работы Range:
// Range в реальном DOM: (node, charOffset) → (node, charOffset)
// В строковой симуляции: (string, startIndex) → (string, endIndex)
function createStringRange(text, start, end) {
return {
text,
start,
end,
get collapsed() { return this.start === this.end },
toString() { return this.text.slice(this.start, this.end) },
get length() { return this.end - this.start },
}
}1. Читать selection.toString() до проверки rangeCount
// ПЛОХО — может вернуть '' если ничего не выделено, непонятно почему
const text = window.getSelection().toString()
if (text) { ... } // работает, но не объясняет почему пусто
// ХОРОШО — явная проверка
const selection = window.getSelection()
if (selection.rangeCount === 0) return // нет выделения
const text = selection.toString()
if (!text) return // collapsed (курсор без выделения)2. Не вызывать selection.removeAllRanges() перед addRange
// ПЛОХО — предыдущее выделение может остаться (в Firefox допускается несколько Range)
function selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection.addRange(range) // добавляет к существующему!
}
// ХОРОШО — сначала сбросить, потом установить
function selectText(element) {
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.selectNodeContents(element)
selection.addRange(range)
}3. Использовать document.execCommand('copy') — устаревший API
// ПЛОХО — execCommand устарел и будет удалён
document.execCommand('copy') // Deprecated!
// ХОРОШО — используй Clipboard API
async function copyText(text) {
await navigator.clipboard.writeText(text)
// требует HTTPS и разрешения пользователя
}navigator.clipboard.writeText(codeBlock.textContent)Range — выделение, вставка, форматированиеRange для подсветки совпадений, ты можешь реализовать то же через window.find() или Range APIСимуляция Range и Selection через строки: выделение, копирование, форматирование фрагментов текста
// Симуляция Range/Selection API через строки
// В браузере Range работает с текстовыми узлами DOM
// --- StringRange: имитация Range ---
function createRange(text, start, end) {
if (start > end) [start, end] = [end, start]
return {
text,
start: Math.max(0, start),
end: Math.min(text.length, end),
get collapsed() { return this.start === this.end },
toString() { return this.text.slice(this.start, this.end) },
get length() { return this.end - this.start },
cloneRange() { return createRange(this.text, this.start, this.end) },
}
}
// --- StringSelection: имитация window.getSelection() ---
function createSelection() {
let ranges = []
return {
get rangeCount() { return ranges.length },
getRangeAt(i) { return ranges[i] },
addRange(r) { ranges = [r] },
removeAllRanges() { ranges = [] },
toString() { return ranges.map(r => r.toString()).join('') },
}
}
// --- Демо 1: Работа с Range ---
console.log('=== Range: выделение фрагментов ===')
const articleText = 'JavaScript — мультипарадигменный язык программирования'
const range1 = createRange(articleText, 0, 10)
const range2 = createRange(articleText, 15, 34)
const rangeAll = createRange(articleText, 0, articleText.length)
console.log('range1:', range1.toString()) // 'JavaScript'
console.log('range2:', range2.toString()) // 'мультипарадигменный'
console.log('rangeAll:', rangeAll.toString()) // вся строка
console.log('collapsed:', createRange(articleText, 5, 5).collapsed) // true
console.log('length:', range2.length) // 19
// --- Демо 2: Selection ---
console.log('\n=== Selection: текущее выделение ===')
const selection = createSelection()
console.log('rangeCount до:', selection.rangeCount) // 0
console.log('toString до:', selection.toString()) // ''
selection.addRange(range1)
console.log('rangeCount после:', selection.rangeCount) // 1
console.log('Выделено:', selection.toString()) // 'JavaScript'
selection.removeAllRanges()
selection.addRange(range2)
console.log('Новое выделение:', selection.toString()) // 'мультипарадигменный'
// --- Демо 3: Имитация Clipboard API ---
console.log('\n=== Clipboard API симуляция ===')
let mockClipboard = ''
const clipboardAPI = {
async writeText(text) {
mockClipboard = text
return Promise.resolve()
},
async readText() {
return Promise.resolve(mockClipboard)
},
}
async function copySelectedText(selection) {
const text = selection.toString()
if (!text) { console.log('Ничего не выделено'); return }
await clipboardAPI.writeText(text)
console.log(`Скопировано в буфер: "${text}"`)
}
selection.removeAllRanges()
selection.addRange(createRange(articleText, 0, 10))
copySelectedText(selection).then(() => clipboardAPI.readText()).then(t => console.log('Из буфера:', t))
// --- Демо 4: Поиск и выделение ---
console.log('\n=== Поиск текста и создание Range ===')
function findInText(text, query) {
const results = []
let idx = text.toLowerCase().indexOf(query.toLowerCase())
while (idx !== -1) {
results.push(createRange(text, idx, idx + query.length))
idx = text.toLowerCase().indexOf(query.toLowerCase(), idx + 1)
}
return results
}
const content = 'Найти все вхождения слова "найти" в тексте. Найти и выделить.'
const found = findInText(content, 'найти')
console.log(`Найдено вхождений: ${found.length}`)
found.forEach((r, i) => {
console.log(` [${i + 1}] позиция ${r.start}-${r.end}: "${r.toString()}"`)
})Реализуй функцию highlightMatches(text, query) которая находит все вхождения query в text и возвращает массив объектов { before, match, after } — фрагменты текста вокруг каждого совпадения. Регистр игнорировать. Также реализуй surround(text, start, end, tag) которая оборачивает фрагмент текста от start до end в HTML-тег, возвращая строку.
highlightMatches: before = text.slice(0, idx), match = text.slice(idx, idx + query.length), after = text.slice(idx + query.length), следующий поиск от idx + 1. surround: before = text.slice(0, start), content = text.slice(start, end), after = text.slice(end).