Удоб­ный до­ступ к бу­фе­ру об­ме­на с Clipboard API

Редактура Никита Дубко Вадим Макеев

Возможность скопировать что-то в буфер обмена существует в браузерах достаточно давно. Но синхронный запуск команд copy или write через функцию document.execCommand() всегда был не самым лучшим API, а сейчас считается устаревшим. Его неудобство сподвигло разработчиков к написанию множества библиотек, которые помогали пользоваться копированием с помощью более удобного интерфейса. К счастью, W3C задумался над разработкой нового, более удобного способа взаимодействия с Clipboard API, и уже в декабре 2016 года был опубликован первый черновик современного API. В 2021 году эта возможность уже есть во всех браузерах, хотя и с некоторыми отличиями в поддержке.

Как это было раньшеСкопировать ссылку

Почему же всё-таки старая реализация была не самой удачной? Давайте рассмотрим на примере копирования произвольной строки. Для копирования мы должны вызвать document.execCommand('copy'). В случае команды copy функция execCommand принимает только один аргумент. Но что же будет помещено в буфер обмена? Правильный ответ — то, что в данный момент выделено на странице пользователем.

Но что делать, если нам нужно скопировать что-то произвольное, неужели просить пользователя это выделить?

Можно имитировать эту ситуацию с помощью трюка: поместить текст в поле ввода, убрать у него все стили, чтобы строка выглядела как текст, и поставить этому полю readonly. В этом случае мы можем ставить фокус на это поле, выделять в нем текст и вызывать команду копирования. Когда нам нужно скопировать текст, который не отображается на странице, все становится сложнее. Для того, чтобы наглядно увидеть, сколько всего нужно предусмотреть, я сделал небольшой пример.

const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const button = document.querySelector('button');

button.addEventListener('click', (evt) => {
    const input = document.createElement('input');
    const text = 'https://web-standards.ru/articles/clipboard-api/';
    const yPosition = window.pageYOffset || document.documentElement.scrollTop;

    // Скрываем поле за краями экрана
    // в зависимости от направления
    // текста в текущей локали
    input.style.position = 'absolute';
    input.style[isRTL ? 'right' : 'left'] = '-9999px';

    // Предотвращаем срабатывание зума на iOS
    input.style.fontSize = '16px';

    // Предотвращаем скролл к элементу
    input.style.top = `${yPosition}px`;

    // Не допускаем появления
    // клавиатуры на мобильных девайсах
    input.setAttribute('readonly', '');

    // Вставляем элемент в DOM
    document.body.appendChild(input);
    input.value = text;

    // Помещаем поле в фокус
    input.focus();

    // Выделяем текст в поле
    input.select();

    // Копируем текст в поле обмена
    document.execCommand('copy');

    // Удаляем фейковый элемент
    document.body.removeChild(input);
});

Теперь кажется должно стать понятно, почему никто не будет скучать по execCommand.

Почему работа с буфером обмена важна для сайтовСкопировать ссылку

Кажется, что такие знакомые сочетания клавиш как Ctrl C и Ctrl V, ну или Cmd C и Cmd V в macOS, знакомы каждому. Но пользователю иногда всё-таки легче кликнуть по кнопке и получить содержимое из буфера обмена. Например, если пользователь хочет скопировать ссылку, то сочетания клавиш не помогают и нужно совершить больше действий. Прямо как на этом сайте: вы можете скопировать ссылку на конкретное место в статье, обозначенное заголовком, если кликнете на иконку справа от заголовка. Попробуйте, получилось?

Если вы точно знаете, что пользователю нужно будет скопировать какие-то данные, путь к этому можно сократить с помощью простой кнопки.

Текст с заголовком, рядом с которым стоит кнопка с иконкой ссылки: два кольца цепи (links).
Применение Clipboard API для копирования ссылок.
Фрагмент кода в несколько строк, рядом с которым стоит кнопка с иконкой буфера обмена.
Удобное копирование сниппетов кода.
Длинный непрерывный фрагмент текста из случайных символов, рядом с которым стоит кнопка с иконкой буфера обмена.
Копирование важной информации, которая нужна пользователю.

Возможности современных браузеровСкопировать ссылку

Современный API позволяет работать не только с текстом, но и с картинками, а также копировать смешанный контент или исключать какие-то элементы при попытке копирования или вставки. Например, если скопирововать контент из MS Word и вставить в WYSIWIG, то мы можем отфильтровать все ненужное и привести содержимое в порядок перед тем, как поместим его в форму для редактирования.

Есть несколько способов работы с Clipboard API, один из самых главных — это API для чтения и записи буфера обмена. Методы в window.navigator.clipboard дают прямой доступ к чтению или записи данных в буфер обмена. Также есть и другие возможности, которые мы рассмотрим дальше.

Запись в буфер обменаСкопировать ссылку

Для сохранения данных в буфер можно использовать универсальный метод window.navigator.clipboard.write() или специальный window.navigator.clipboard.writeText(), если мы собираемся помещать в буфер только текст. Оба метода асинхронные и возвращают Promise.

Рассмотрим простой пример с копированием ссылки:

<span class="tooltip">
    <button class="tooltip__button" data-href="#section-1"></button>
    <span class="tooltip__label" role="tooltip" id="copy-section-1">
        Скопировать ссылку
    </span>
</span>
document.querySelector('.tooltip').addEventListener('click', () => {
    const tooltip = this.nextSibling;
    const hash = this.getAttribute('data-href');

    navigator.clipboard
        .writeText(`${window.location.href}${hash}`)
        .then(() => {
            // Успех!
        })
        .catch(() => {
            // Неудача :(
        });
});

Полный пример кода на GitHub.

Метод window.navigator.clipboard.writeText возвращает Promise, что позволяет обрабатывать исключения, если они возникнут. Одним из таких случаев может быть запрет на запись в буфер. Ниже мы рассмотрим, как запросить необходимые права для чтения и записи в буфер.

Если мы используем функцию window.navigator.clipboard.write, то к копируемым данным можно добавить, например, картинку:

const blob = await fetch('picture.png').then((req) =>
    req.blob();
);
const clipboardItem = new ClipboardItem({
    [blob.type]: blob
});

window.navigator.clipboard
    .write([clipboardItem])
    .then(() => console.log('Картинка скопирована!'))
    .catch((err) => console.error(err));

Также можно изменять данные перед записью в буфер обмена, когда пользователь пытается скопировать контент на странице, например, добавлять дополнительную информацию:

document.addEventListener('copy', (evt) => {
    const source = `Источник: ${window.location.href}`;

    // Нужно заблокировать стандартное поведение
    evt.preventDefault();

    // И поместить дополнительные данные в Clipboard
    evt.clipboardData.setData(
        'text',
        `${document.getSelection()}\n\n${source}`
    );
});

Чтение из буфера обменаСкопировать ссылку

По аналогии с записью, мы также можем читать данные из буфера обмена. Для этого есть аналогичные методы read и readText:

window.navigator.clipboard
    .readText()
    .then((data) => console.log('Скопировано', data))
    .catch((err) => console.error('Не удалось скопировать', err));

Важная особенность чтения из буфера в том, что оно работает не напрямую. Например, в Google Chrome во время попытки прочитать данные из буфера пользователя уведомят о попытке чтения и предложат разрешить или запретить действие. А Safari, например, покажет контекстное меню с пунктом «Paste».

Попап под адресной строкой Google Chrome, который запрашивает доступ к копированию и предлагает: заблокировать или разрешить.
Попап запрашивает разрешение на чтение буфера обмена в Google Chrome при попытке вставки.
Контекстное меню «Вставить» поверх кнопки «Вставить из буфера обмена» в Safari.
Контекстное меню в Safari при попытке вставки из буфера обмена.

Также можно запросить разрешение на чтение буфера заранее с помощью Permissions API — хотя стоит заметить, что не все браузеры его поддерживают.

window.navigator.permissions.query({ name: 'clipboard-read' })
    .then((result) => {
        if (result.state == 'granted' || result.state == 'prompt') {
            // Можно записывать данные в буфер
        }
    });

Для запроса на запись в буфер используется аналогичная конструкция, но с аргументом { name: 'clipboard-write' }.

Другой вариант чтения данных из буфера — реагировать на вставку данных на сайте. Такое событие можно слушать как на всём document, так и, например, в поле ввода <textarea>. С помощью этого метода можно перехватить и обработать событие.

document.querySelector('textarea').addEventListener('paste', (evt) => {
    if (evt.clipboardData.files.lenght === 0) {
        return;
    }

    const files = Array.from(evt.clipboardData.files);

    evt.preventDefault();

    uploadFiles(files)
        .then(() => console.log('Загружено!'))
        .catch((err) => console.error('Произошла ошибка', err));
});

ЗаключениеСкопировать ссылку

Правильное использование современных API может значительно повысить удобство ваших интерфейсов. Clipboard API — один из таких случаев, когда вы можете упростить решение задач даже для самых неопытных пользователей.