Пошук уроків, статей та іншого контенту
Працюватимете з Clipboard API та Geolocation API, враховуючи дозволи й обмеження браузера.
Буфер обміну та геолокація належать до потужних Web API, які дають вебзастосунку доступ до даних за межами його DOM:
Clipboard API — читання та запис тексту або інших форматів у системний буфер обміну.
Geolocation API — отримання приблизного або точного розташування пристрою.
Обидва API мають спільні обмеження:
зазвичай працюють лише в захищеному контексті — через https:// або на localhost;
можуть вимагати дозвіл користувача;
доступ можуть обмежувати політики браузера, налаштування ОС або вбудовування в iframe;
користувач може змінити дозвіл у будь-який момент;
результат не можна вважати гарантовано доступним.
Тому правильна робота з цими API включає не лише виклик методу, а й обробку помилок, перевірку підтримки та зрозуміле пояснення користувачу.
Сучасний Clipboard API доступний через об’єкт navigator.clipboard.
Основні методи:
navigator.clipboard.writeText(text);
navigator.clipboard.readText();
navigator.clipboard.write(data);
navigator.clipboard.read();Методи повертають Promise, тому використовуються з await або .then().
Найпростіший приклад:
async function copyText(text) {
if (!navigator.clipboard) {
throw new Error("Clipboard API недоступний");
}
await navigator.clipboard.writeText(text);
}Виклик:
await copyText("Текст для копіювання");На практиці запис краще виконувати в обробнику дії користувача, наприклад кліку:
const copyButton = document.querySelector("#copy-button");
const message = document.querySelector("#copy-message");
copyButton.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("Скопійований текст");
message.textContent = "Текст скопійовано";
} catch (error) {
message.textContent = "Не вдалося скопіювати текст";
console.error(error);
}
});Деякі браузери можуть відхилити операцію, якщо вона не пов’язана з нещодавньою дією користувача. Наприклад, такий код може не спрацювати:
setTimeout(() => {
navigator.clipboard.writeText("Текст");
}, 5000);Тому викликайте writeText() безпосередньо з обробника click, keydown або іншої дії користувача.
Для читання використовується readText():
async function pasteText() {
if (!navigator.clipboard) {
throw new Error("Clipboard API недоступний");
}
const text = await navigator.clipboard.readText();
return text;
}Приклад із кнопкою:
const pasteButton = document.querySelector("#paste-button");
const output = document.querySelector("#paste-output");
pasteButton.addEventListener("click", async () => {
try {
const text = await navigator.clipboard.readText();
output.value = text;
} catch (error) {
if (error.name === "NotAllowedError") {
output.value = "Браузер не дозволив читання буфера обміну";
} else {
output.value = "Помилка читання буфера обміну";
}
console.error(error);
}
});Читання буфера обміну є чутливішою операцією, ніж запис. Сторінка потенційно може отримати пароль, токен або інші приватні дані, які користувач раніше скопіював. Тому браузери часто вимагають:
явну дію користувача;
дозвіл на читання;
фокус сторінки;
виконання в захищеному контексті.
Метод writeText() працює лише з текстом. Для запису HTML або зображень використовується ClipboardItem.
async function copyHtml(html, plainText) {
const item = new ClipboardItem({
"text/html": new Blob([html], { type: "text/html" }),
"text/plain": new Blob([plainText], { type: "text/plain" })
});
await navigator.clipboard.write([item]);
}Використання:
await copyHtml(
"<strong>Важливий текст</strong>",
"Важливий текст"
);Наявність MIME-типу ще не гарантує підтримку конкретного формату браузером. За потреби можна перевірити його:
if (typeof ClipboardItem !== "undefined") {
// Браузер підтримує ClipboardItem на рівні JavaScript API
}Для окремого типу можна використовувати:
if (ClipboardItem.supports("text/html")) {
// Формат text/html заявлений як підтримуваний
}Навіть після перевірки операцію потрібно обгорнути в try...catch, оскільки реальна доступність залежить від політик і дозволів.
Permissions API може повідомити поточний стан дозволу:
async function getClipboardPermission() {
if (!navigator.permissions) {
return "unknown";
}
try {
const permission = await navigator.permissions.query({
name: "clipboard-read"
});
return permission.state;
} catch {
return "unknown";
}
}Можливі стани:
"granted" — дозвіл надано;
"denied" — доступ заборонено;
"prompt" — браузер може запитати дозвіл;
"unknown" — API або конкретна назва дозволу не підтримується.
Підтримка назв дозволів відрізняється між браузерами. Не слід будувати критичну логіку лише на результаті permissions.query(). Остаточним тестом залишається сам виклик Clipboard API.
iframeДля сторінки, завантаженої в iframe, доступ може бути обмежений Permissions Policy. Батьківський документ може явно дозволити операції:
<iframe
src="https://example.test/editor"
allow="clipboard-read; clipboard-write">
</iframe>Це не скасовує інших вимог: захищеного контексту, дозволу користувача та політик браузера.
Геолокація доступна через navigator.geolocation.
Основні методи:
navigator.geolocation.getCurrentPosition(success, error, options);
navigator.geolocation.watchPosition(success, error, options);
navigator.geolocation.clearWatch(watchId);API може використовувати різні джерела:
GPS;
Wi-Fi;
дані мобільної мережі;
IP-адресу;
інформацію про доступні мережі.
Точність і час отримання результату не гарантуються.
navigator.geolocation.getCurrentPosition(
(position) => {
console.log(position.coords.latitude);
console.log(position.coords.longitude);
},
(error) => {
console.error(error.code, error.message);
}
);Об’єкт position має приблизно таку структуру:
{
coords: {
latitude: 50.4501,
longitude: 30.5234,
accuracy: 25,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null
},
timestamp: 1710000000000
}Найчастіше використовують:
latitude — широта;
longitude — довгота;
accuracy — приблизна точність у метрах;
timestamp — час отримання координат.
altitude, altitudeAccuracy, heading і speed можуть мати значення null.
Третій аргумент дозволяє керувати отриманням позиції:
const options = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
};
navigator.geolocation.getCurrentPosition(
handlePosition,
handleError,
options
);enableHighAccuracyЯкщо true, браузер намагається отримати точніші координати. Це може:
збільшити час очікування;
споживати більше заряду;
не дати кращого результату, якщо GPS недоступний.
Не встановлюйте true без потреби. Для визначення міста або приблизного регіону часто достатньо значення false.
timeoutМаксимальний час очікування позиції в мілісекундах.
{ timeout: 10000 }Після завершення цього часу буде викликано обробник помилки з кодом TIMEOUT.
maximumAgeМаксимальний допустимий вік кешованої позиції:
{ maximumAge: 60000 }Браузер може повернути позицію, отриману не більше хвилини тому. Значення 0 означає, що кешовані дані не слід використовувати.
Помилка геолокації має властивість code. Стандартні значення:
1 — PERMISSION_DENIED;
2 — POSITION_UNAVAILABLE;
3 — TIMEOUT.
Приклад обробника:
function handleGeolocationError(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
console.error("Користувач заборонив доступ до геолокації");
break;
case error.POSITION_UNAVAILABLE:
console.error("Інформація про позицію недоступна");
break;
case error.TIMEOUT:
console.error("Час очікування геолокації завершився");
break;
default:
console.error("Невідома помилка геолокації");
}
}Не покладайтеся лише на error.message. Текст повідомлення може відрізнятися між браузерами та локалями. Для програмної логіки використовуйте error.code.
watchPosition() викликає обробник щоразу, коли браузер отримує оновлену позицію:
const watchId = navigator.geolocation.watchPosition(
(position) => {
console.log(position.coords.latitude);
console.log(position.coords.longitude);
},
handleGeolocationError,
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 5000
}
);Коли стеження більше не потрібне, його необхідно зупинити:
navigator.geolocation.clearWatch(watchId);Це особливо важливо для односторінкових застосунків. Якщо не очистити спостерігач під час знищення компонента або зміни маршруту, він продовжить працювати та споживати ресурси.
Дозвіл запитується браузером, а не JavaScript-кодом напряму. Застосунок може лише ініціювати операцію, яка потребує дозволу.
Для геолокації запит краще запускати після поясненої дії користувача:
button.addEventListener("click", () => {
navigator.geolocation.getCurrentPosition(
showPosition,
showError
);
});Не варто запитувати геолокацію одразу після завантаження сторінки без зрозумілого контексту. Це зменшує довіру користувача й часто призводить до відмови.
Стан дозволу може змінитися:
користувач заблокував доступ у налаштуваннях;
користувач дозволив доступ лише один раз;
вкладка втратила фокус;
змінилися налаштування операційної системи;
сторінку відкрито в іншому контексті або через iframe.
Перевірка геолокаційного дозволу:
async function getGeolocationPermission() {
if (!navigator.permissions) {
return "unknown";
}
try {
const permission = await navigator.permissions.query({
name: "geolocation"
});
return permission.state;
} catch {
return "unknown";
}
}Можна також реагувати на зміну дозволу:
async function observeGeolocationPermission() {
if (!navigator.permissions) {
return;
}
try {
const permission = await navigator.permissions.query({
name: "geolocation"
});
permission.addEventListener("change", () => {
console.log("Новий стан дозволу:", permission.state);
});
} catch {
// Певні браузери не підтримують перевірку цього дозволу
}
}Подія change не замінює обробку помилок самого getCurrentPosition() або watchPosition().
Нижче наведено невеликий застосунок, який:
копіює текст у буфер обміну;
читає текст із буфера обміну;
отримує поточну геопозицію;
запускає та зупиняє відстеження;
показує різні типи помилок.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clipboard та Geolocation</title>
</head>
<body>
<h1>Доступ до системних API</h1>
<section>
<h2>Буфер обміну</h2>
<textarea id="clipboard-text" rows="4" cols="50">
Текст для перевірки Clipboard API
</textarea>
<div>
<button id="copy-button" type="button">Скопіювати</button>
<button id="paste-button" type="button">Вставити</button>
</div>
<p id="clipboard-status" role="status"></p>
</section>
<section>
<h2>Геолокація</h2>
<button id="location-button" type="button">
Отримати поточну позицію
</button>
<button id="watch-button" type="button">
Почати відстеження
</button>
<button id="stop-watch-button" type="button" disabled>
Зупинити відстеження
</button>
<pre id="location-output"></pre>
</section>
<script>
const clipboardText = document.querySelector("#clipboard-text");
const clipboardStatus = document.querySelector("#clipboard-status");
const locationButton = document.querySelector("#location-button");
const watchButton = document.querySelector("#watch-button");
const stopWatchButton = document.querySelector("#stop-watch-button");
const locationOutput = document.querySelector("#location-output");
let watchId = null;
function setClipboardStatus(text) {
clipboardStatus.textContent = text;
}
document.querySelector("#copy-button").addEventListener("click", async () => {
if (!navigator.clipboard) {
setClipboardStatus("Clipboard API недоступний у цьому контексті");
return;
}
try {
await navigator.clipboard.writeText(clipboardText.value);
setClipboardStatus("Текст скопійовано");
} catch (error) {
setClipboardStatus("Копіювання заборонено або не вдалося");
console.error("Помилка запису в буфер обміну:", error);
}
});
document.querySelector("#paste-button").addEventListener("click", async () => {
if (!navigator.clipboard) {
setClipboardStatus("Clipboard API недоступний у цьому контексті");
return;
}
try {
clipboardText.value = await navigator.clipboard.readText();
setClipboardStatus("Текст прочитано з буфера обміну");
} catch (error) {
if (error.name === "NotAllowedError") {
setClipboardStatus("Браузер не дозволив читання буфера обміну");
} else {
setClipboardStatus("Не вдалося прочитати буфер обміну");
}
console.error("Помилка читання з буфера обміну:", error);
}
});
function renderPosition(position) {
const { latitude, longitude, accuracy } = position.coords;
locationOutput.textContent = [
`Широта: ${latitude}`,
`Довгота: ${longitude}`,
`Точність: приблизно ${Math.round(accuracy)} м`,
`Час: ${new Date(position.timestamp).toLocaleString("uk-UA")}`
].join("\n");
}
function renderGeolocationError(error) {
const messages = {
[error.PERMISSION_DENIED]:
"Доступ до геолокації заборонено користувачем або браузером.",
[error.POSITION_UNAVAILABLE]:
"Поточну позицію не вдалося визначити.",
[error.TIMEOUT]:
"Час очікування позиції завершився."
};
locationOutput.textContent =
messages[error.code] || "Сталася невідома помилка геолокації.";
console.error("Помилка геолокації:", error);
}
const geolocationOptions = {
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 60000
};
locationButton.addEventListener("click", () => {
if (!navigator.geolocation) {
locationOutput.textContent =
"Geolocation API не підтримується цим браузером.";
return;
}
locationOutput.textContent = "Отримання позиції...";
navigator.geolocation.getCurrentPosition(
renderPosition,
renderGeolocationError,
geolocationOptions
);
});
watchButton.addEventListener("click", () => {
if (!navigator.geolocation) {
locationOutput.textContent =
"Geolocation API не підтримується цим браузером.";
return;
}
if (watchId !== null) {
return;
}
locationOutput.textContent = "Відстеження позиції запущено.";
watchId = navigator.geolocation.watchPosition(
renderPosition,
renderGeolocationError,
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 5000
}
);
watchButton.disabled = true;
stopWatchButton.disabled = false;
});
stopWatchButton.addEventListener("click", () => {
if (watchId === null) {
return;
}
navigator.geolocation.clearWatch(watchId);
watchId = null;
locationOutput.textContent = "Відстеження позиції зупинено.";
watchButton.disabled = false;
stopWatchButton.disabled = true;
});
</script>
</body>
</html>Для перевірки відкрийте файл через localhost або розгорніть його на HTTPS. Відкриття через file:// може спричинити відмінності в роботі API залежно від браузера.
Вміст буфера обміну може містити конфіденційні дані. Тому:
не читайте буфер автоматично під час завантаження сторінки;
не надсилайте прочитаний текст на сервер без явної згоди;
не вставляйте HTML із буфера через innerHTML без санітизації;
для звичайного тексту використовуйте textContent або властивість value.
Небезпечний приклад:
result.innerHTML = await navigator.clipboard.readText();Якщо вміст обробляється як HTML, це може створити ризик XSS. Для відображення звичайного тексту безпечніше:
result.textContent = await navigator.clipboard.readText();Координати є персональними та чутливими даними. Застосунок повинен:
пояснити, навіщо потрібна геолокація;
запитувати її лише в потрібний момент;
не зберігати координати довше, ніж необхідно;
не передавати їх на сервер без обґрунтованої потреби;
обмежувати точність, якщо точні координати не потрібні;
зупиняти watchPosition(), коли користувач залишає функцію.
Показ координат на екрані ще не означає, що їх можна безпечно передати сторонньому сервісу.
У складніших застосунках зручно перетворювати помилки браузерних API на власні повідомлення домену:
function getGeolocationErrorMessage(error) {
switch (error.code) {
case 1:
return "Користувач заборонив доступ до геолокації.";
case 2:
return "Позиція тимчасово недоступна.";
case 3:
return "Не вдалося отримати позицію вчасно.";
default:
return "Невідома помилка.";
}
}
function getClipboardErrorMessage(error) {
if (error?.name === "NotAllowedError") {
return "Операція потребує дозволу або дії користувача.";
}
if (error?.name === "NotFoundError") {
return "У буфері обміну немає потрібного формату.";
}
return "Операція з буфером обміну не виконана.";
}Такий підхід не прив’язує інтерфейс до текстів, які повертає конкретний браузер.
navigator.clipboard може бути undefined, а геолокація може завершитися помилкою, якщо сторінка працює не через HTTPS.
Перевіряйте підтримку:
const clipboardSupported = Boolean(navigator.clipboard);
const geolocationSupported = Boolean(navigator.geolocation);PromiseЦей код створює необроблену відмову:
navigator.clipboard.writeText("Текст");Використовуйте await у try...catch або обробляйте .catch():
navigator.clipboard
.writeText("Текст")
.catch((error) => {
console.error(error);
});Автоматичне читання буфера після завантаження сторінки часто блокується та створює неприйнятний UX. Запускайте його натисканням кнопки.
enableHighAccuracyВисока точність може бути непотрібною для функції, якій достатньо приблизного регіону. Вона також може збільшити енергоспоживання та час очікування.
accuracyКоординати без оцінки точності можуть бути хибно інтерпретовані. Позиція з точністю 5 метрів і позиція з точністю 5 кілометрів — це різні результати.
if (position.coords.accuracy > 1000) {
console.warn("Координати надто неточні для цієї операції");
}watchPosition() повертає ідентифікатор. Зберігайте його та викликайте clearWatch().
Навіть якщо Permissions API повернув "granted", фактичний виклик може завершитися помилкою через зміну налаштувань, втрату контексту або обмеження документа. Перевірка дозволу — це підказка, а не гарантія.
Не припускайте, що всі поля coords заповнені. Наприклад, altitude або speed можуть бути null.
Clipboard API працює асинхронно через navigator.clipboard.
Для тексту використовуються writeText() і readText().
Для HTML та інших форматів застосовується ClipboardItem.
Операції з буфером обміну можуть вимагати дії користувача, дозволу та HTTPS.
Geolocation API надає позицію через getCurrentPosition() або відстежує її через watchPosition().
Завжди обробляйте коди PERMISSION_DENIED, POSITION_UNAVAILABLE і TIMEOUT.
enableHighAccuracy, timeout і maximumAge впливають на якість, швидкість та енергоспоживання.
watchPosition() потрібно завершувати через clearWatch().
Permissions API допомагає дізнатися стан дозволу, але не замінює обробку реального виклику.
Буфер обміну та геолокація потребують обережного ставлення до конфіденційності користувача.