Javascript: копируем данные в буфер обмена

Один из клиентов недавно жаловался, что код его ссылки, предназначенный для обмена с другими сайтами, неудобно копировать со страницы.

Какие проблемы?! Сейчас сделаем кнопку, которая будет копировать необходимый текст в буфер обмена, и все, проблема решена!

Но оказалось все не так-то и просто...

Задача

Начнем по порядку. Что требуется от скрипта?

  • Чтобы его было просто и удобно использовать.
  • Чтобы он был кроссбраузерным.
  • Чтобы у людей с отключенным яваскриптом не маячили кнопки "Скопировать в буфер обмена".

Для того, чтобы не перемешивать javascript-код с html-кодом, я обычно использую одну из двух техник:

  • после загрузки страницы по событию "load" срабатывает функция, которая развешивает необходимые мне обработчики событий, расставляет кнопки и так далее;
  • в некоторых местах html-кода или в его конце (по обстоятельствам) расставляются подобные инструкции: <ins><script type="text/javascript"> insertButton(); </script></ins>

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

Кнопку вставлять будем так: <div id="code" class="text-to-copy"> <a href="test">Код нашей воображаемой ссылки</a>. Этот код и текст должены оказаться в буфере! </div> <ins><script type="text/javascript"> insertCopyToClipboardButton("code"); </script></ins>

Таким образом, javascript-код должен будет создать после блока с id="code" кнопку, щелкнув по которой, мы отправим в буфер текст, содержащийся в этом блоке.

Структура скрипта

Скрипт будет содержать две основные функции:

  • insertCopyToClipboardButton(wrapper_id) - вставляет кнопку и вешает на нее обработчик события click,
  • copyToClipboard(text) - собственно, функция, которая и будет копировать текст в буфер.

Начнем с простого - со вставки кнопки. Вставлять новый элемент в html-документ будем через DOM, а значит, предварительно надо будет проверить, поддерживает ли браузер такую возможность. Подробно расписывать код этой функции не буду, остановлюсь только на некоторых особенностях. function insertCopyToClipboardButton(wrapper_id) { if (!document.getElementById || !document.getElementById(wrapper_id)) { // Браузер не поддерживает необходимые нам функции return false; } else { wrapper = document.getElementById(wrapper_id); } if (wrapper && wrapper.parentNode.insertBefore) { button = document.createElement("a"); button.setAttribute("href", "#"); button.innerHTML = "Скопировать в буфер"; button.onclick = function() { copyToClipboard(innerText(wrapper)); return false; } // emulate insertAfter: wrapper.parentNode.insertBefore(button, wrapper.nextSibling); } else { return false } }

Кнопка создается через DOM, а ее содержимое вставляется через innerHTML? Можно было и содержимое кнопки вставлять через DOM, но в данном случае удобнее innerHTML - это позволит при необходимости сделать графическую кнопку ( button.innerHTML = '<img src="clip.gif" alt="Скопировать в буфер" />'; ).

Еще одна особенность - для извлечения текста из элемента используется вспомогательная функция innerText(). Ей пришлось воспользоваться, т.к. непродолжительные пляски с бубном вокруг свойств innerHTML, innerText (IE) и textContent (FF/Mozilla) положительного результата не дали - вместо &quot; (кода) упорно копировался просто символ (").

Код функции: function innerText(node) { // @author Dethe Elza // @article "XML Matters: Beyond the DOM. Tips and tricks for a friendlier DOM" if (node.nodeType == 3 || node.nodeType == 4) { return node.data; } var i; var returnValue = []; for (i = 0; i < node.childNodes.length; i++) { returnValue.push(innerText(node.childNodes[i])); } return returnValue.join(''); }

Подробнее об этой и других полезных фишках можно почитать здесь: XML Matters: Beyond the DOM. Tips and tricks for a friendlier DOM

Копирование в буфер

Ну вот, приготовления закончились, наконец-то приступаем к основной части :)

Решение задачи, показавшейся мне сначала очень легкой, оказалось простым только для одного браузера - IE. В Internet Explorer'е копирование в буфер требует всего одной строчки кода: window.clipboardData.setData("Text", text);

Firefox и Opera не позволяют javascript-коду работать с буфером обмена по соображениям безопасности (ну-ка, кто хочет, чтобы любой сайт "читал", что у вас в буфере обмена или писал вам туда всякую фигню?) - Opera не позволяет вообще, а Firefox позволяет только, когда вы выставите в настройках (наберите about:config в строке адреса) значение true для свойства signed.applets.codebase_principal_support.

Ну что ж... попробуем озаботиться судьбой пользователей Firefox'а, которые зачем-то изменили эту настройку и позволили нашим скриптам чуть больше, чем им разрешено по умолчанию. Это уже так... не для практической пользы, а исключительно для развлечения.

Для начала нам придется спросить пользователя, даст ли он нам разрешение на доступ к API своего браузера: try { netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); } catch (e) { // do nothing }

После этого надо будет получить доступ к сервису Clipboard (буфер обмена) и создать контейнер для передачи данных между приложениями (объект Transferable): var clipboard = Components.classes["@mozilla.org/widget/clipboard;1"].getService(); if (clipboard) { clipboard = clipboard.QueryInterface(Components.interfaces.nsIClipboard); } var transferable = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(); if (transferable) { transferable = transferable.QueryInterface(Components.interfaces.nsITransferable); }

Если все прошло нормально, т.е. пользователь нам разрешает пользоваться всем этим добром, то следующим нашим шагом будет создание объекта с данными (т.е. с текстом), который мы потом засунем в контейнер (transferable):

if (clipboard && transferable) { // тип передаваемых данных transferable.addDataFlavor("text/unicode"); var textObj = new Object(); var textObj = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString); if (textObj) { textObj.data = text; // Упаковываем данные в контейнер transferable.setTransferData("text/unicode", textObj, text.length*2); var clipid=Components.interfaces.nsIClipboard; clipboard.setData(transferable,null,clipid.kGlobalClipboard); } }

Обратите внимание на строку transferable.setTransferData("text/unicode", textObj, text.length*2);

Последний аргумент функции - размер передаваемых данных в байтах. Т.к. мы при передаче указываем тип данных "text/unicode" (это настоятельно рекомендуют разработчики Мозиллы), то надо указывать длину нашей реальной строки, умножененную на 2 - так мы получим соответствие размеру строки в двухбайтовой кодировке.

Все, теперь наш текст находится в буфере, все танцуют. Все кроме тех, у кого нормально настроен Firefox, или тех, кто пользуется Оперой :)

Им мы, пожалуй, скажем, что они о своем браузере знают далеко не все :)

Результат

Ну, посмотрим, что получится, если склеить все эти куски кода вместе.

Небольшой апдейт

Эта заметка была написана довольно давно, сейчас я только случайно нашел ее текст. Исходные коды, к сожалению, не сохранились, т.к. во всех реальных проектах я пользовался более простым вариантом скрипта, который даже не пытается создать кнопку и залезть в буфер, если браузер не поддерживает метод setData у объекта window.clipboardData.

Примеры я воссоздал, но для использования на рабочих сайтах рекомендую пользоваться вариантом, о котором написано абзацем выше: JS-код, Страница с кнопкой

Комментарии (15) на “Javascript: копируем данные в буфер обмена”

  1. Ивантеевка Says:

    Отличная статейка - очень помагло!

  2. Виктор Says:

    Спасибо - полезная статейка. но эта фича - для ооочень ленивых =)

  3. Lex1 Says:

    [quote]Firefox и Opera не позволяют javascript-коду работать с буфером обмена по соображениям безопасности[/quote]
    Но флэш туда доступ имеет :) Так что если очень хочется, то см. http://webchicanery.com/code/clipbjavascript.zip

  4. Дмитрий Пленкин Says:

    Lex1, а ведь и правда. :) Спасибо за наводку! С флэш-программированием сталкиваюсь редко, так что самому бы мне это в голову не пришло.

  5. Snowcore Says:

    А как насчет Opera (без флешки?)

  6. Дмитрий Пленкин Says:

    Не знаю, честно говоря. Я не спец по Опере и не особенно с этим вопросом заморачивался, т.к. соотношение пользы к временным затратам получается не очень интересное :)

  7. Сережка Says:

    Зря вы так - ff и опера позволяют копировать в буфер, посмотрите библиотеку prototype на примере популярных сайтов, кликаешь в формочку и все отлично копируется

  8. Kiev Says:

    не работает ни первый ни второй!
    в фоксе

  9. Великолепный Says:

    Чесно говоря это бред полный. С нормальной точки зрения копание в буфере обмена плохой тон и правильно делают что запретили в нормальных браузерах это. А насчёт IE6,7 и т.д … да кто ими пользуется? Разве что скачать FF или Opera после краха винды…

  10. Дмитрий Пленкин Says:

    Великолепный, никакого бреда - все под контролем :)

    1) Плохой тон - это втихоря копаться в чужом буфере. Если на кнопку написано “Скопировать в буфер”, то все OK.

    То, что запретили в ряде браузеров - да, так для пользователя безопасней, все верно. Но “законопослушных” веб-мастеров это лишает ряда полезных фишек, что прискорбно. Но, ничего не поделаешь.

    2) Да, конечно, IE6 и 7 никто не пользуется. 50% - это погрешность в статистике Яндекса :)

  11. Мишка Says:

    хорошая статья… жаль, что не особо применишь из-за такой кроссбраузерности…

    кстати забавно… видимо фф из тех же соображений безопасности позволяет в скрипте заблокировать все клавиши, например f5 и даже alt+f4 (решается возвратом false из обработчика onMouseDown) и при перемещении мышки сдвигать окно браузера так, чтобы юзер не мог его никак закрыть иначе чем через панель задач или процессы -) при этом они делают вид безопасности, втыкая палки в колёса вот с этим буфером или, например, с запретом возможности вставки картинки с локальной машины… например задачка… отлавливаем изменение в инпуте типа файл, и вставляем картинку с путём значения в value… т.е юзер может сразу увидеть закачиваемую картинку… в ИЕ работает, в опере нет, т.к value не содержит полного пути (могли бы и сделать), а в фф не работает просто из-за таких же призрачных соображений безопасности… просто запрещено и досвидос

    в общем, параноя местами у разработчиков… при том, что многие фокусы флэш позволяет делать, в то время как js’у это запрещено… бред полнейший

    Великолепный… ну я пользуюсь 6-м ИЕ… я занимаюсь разработкой сложных интерфейсов и для меня фаворитным и наиболее адекватным является как раз ИЕ 6-й версии… а фф с прилежным старанием исполнять тупорылые стандарты частенько вызывает недоумение из-за противоречий здравому смыслу и подобных попыток вставлять палки в колёса… примеров таких не два и не три… некоторые такие палки в DOM заставляют для фф писать целые циклы на js, что естественно отражается на производительности… так что можете и дальше думать, что фф - бог, если вам так нравится -) но всё это не более чем маркетинг и реклама

  12. Альберт Says:

    Зачем одни лишь крайности? Можно ведь найти компромис: функцию не выпиливать, а просто разрешить лишь только после пользовательского подтверждения. Пусть та же строчка window.clipboardData.setData(”Text”, text); работала во всех браузерах, и выполнялась только после нажатия ОК в диалоговом окне типа confirm();

  13. barba Says:

    С флешом, кстати, все стало тоже не так безоблачно: В 10 версии flash из соображений безопасности запрещен прямой вызов System.setClipboard().

  14. Дмитрий Пленкин Says:

    Да, блин, со всех сторон прижимают =)

  15. Murz Says:

    Меня инетересует вопрос: если я задаю в firefox опцию
    signed.applets.codebase_principal_support
    в true,
    то тогда скрипт получит доступ только к записи в буфер обмена, или читать его тоже сможет?
    Не хотелось бы давать доступ скриптам на чтение моих паролей и других личных данных, которые часто попадают в буфер обмена.
    А вот возможность только записи в буфер мне жизнь особо попортить не сможет, поэтому хотелось бы не полностью дать скрипту доступ к буферу, а только на запись…

Оставить комментарий