實習工作中,遇到一個需求,需要完成點選複製的功能,當文字過長的時候,讓使用者手拖再ctrl+c這種方式體驗就不是很好了,如果可以點選一下直接複製就是一種不錯的優化使用者體驗的方式。
經過查閱檔案,網路上完成這個功能大多使用兩大類方法
第一種是以document.execCommand() 方法為主,無論是手寫還是使用clipboard.js外掛都是依賴的這個方法,但是在MDN 檔案中已經顯示過時了。
第二種是用了navigator.clipboard的方法,避免了過時問題,但是在複製圖片的時候會有一定的瀏覽器相容性問題
這個方法其實就是在模擬使用者選擇元素然後右鍵複製的動作。儘管MDN已經顯示這個方法過時了,但是僅針對copy這個指令,大部分主流瀏覽器都可以支援,所以這個方法仍然可以作為一種實現問題的方案。
根據MDN檔案學習本方法的傳參和返回值
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
這個方法可以傳3個引數,並且會返回一個布林值
先從返回值開始,返回值相對比較簡單,如果返回的值是false就表示瀏覽器不支援使用這個操作,反之瀏覽器支援該操作就返回true。
雖然這個返回值看似可以用來提前判斷瀏覽器相容性,但是檔案中不推薦在呼叫一個命令前,嘗試使用返回值去校驗瀏覽器的相容性
引數一共可以傳3個,但是使用複製命令的時候只需要傳第一個引數就可以。這裡簡單介紹一下3個引數
以本文主要講的複製命令為例子:document.execCommand('copy')
前文講到,MDN不推薦在呼叫一個命令前,嘗試使用返回值去校驗瀏覽器的相容性,那麼就需要用另外的方法去檢測瀏覽器是否支援某個指令,瀏覽器為我們提供了一個方法叫document.queryCommandSupported(),使用這個方法可以檢測瀏覽器是否支援某個指令,這個方法比較簡單,只有1個引數,引數就傳指令字串,方法的返回值是一個布林值表示當前瀏覽器是否支援這個指令。
舉例如下:
if(document.queryCommandSupported && document.queryCommandSupported('copy')){
//先檢測是否支援document.queryCommandSupported和copy指令
//如果都支援直接執行指令
document.execCommand('copy')
}
MDN檔案中提到,document.queryCommandSupported也被棄用了,但是為了相容性依然保留可用,當我們使用document.execCommand的時候仍然可以用document.queryCommandSupported來檢測是否支援。同時,它的瀏覽器相容性也是比較好的,大部分主流瀏覽器都支援。
複製文字這個操作對比複製圖片是相對比較簡單的,一共包含2大步
一是選中要複製的元素
二是執行復制指令。
執行復制指令在前面的基本語法裡已經講到了,直接呼叫document.execCommand('copy')就可以了。剩下要做的便是先選中元素了。下面便介紹一下和選中元素相關的selection api
MDN檔案上寫道:Selection
物件表示使用者選擇的文字範圍或插入符號的當前位置。它代表頁面中的文字選區,可能橫跨多個元素。文字選區由使用者拖拽滑鼠經過文字而產生。如果要獲取用於檢查或修改的 Selection
物件,可以呼叫 window.getSelection()
方法。
這看起來就十分的官方和抽象,簡單的來說Selection 物件所對應的是使用者所選擇的 ranges
(區域),俗稱 拖藍。上圖中的拖藍就是selection物件中的一個區域。
通過getRangeAt方法可以獲取到具體的選中區域
let selection = window.getSelection() //獲取selection物件
let range = selection.getRangeAt(0) //獲取第一個選中的區域
除了獲取選區中的區域之外,我們還可以通過 document.createRange()建立一個新的區域,然後將該區域新增到選區中
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
</body>
<script>
let selection = window.getSelection() //獲取selection物件
const hello = document.querySelector('#hello')
if(selection.rangeCount > 0){
//如果有已經選中的區域,直接全部去除
selection.removeAllRanges()
}
let range = document.createRange() //建立range
range.selectNode(hello) //range選中hello
selection.addRange(range) //加入到選區中
</script>
效果如下,當程式碼執行後,你好這個元素被直接選中
加入區域的api包括range.selectNode和range.selectNodeContents。其中selectNode表示選中整個節點而selectNodeContents表示選中節點中的內容,針對文字的複製需要選中節點的內容,而圖片的複製需要選中節點本身。
用法如下
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<button class="btn">點選複製</button>
</body>
<script>
const yes = document.querySelector('#yes')
const selection = window.getSelection()
const range= document.createRange()
range.selectNode(yes)
range.selectNode(yes)
</script>
通過以上的selection api可以完成 建立selection物件-->選中節點內容-->新增到區域-->執行一下copy指令就可以完成複製文字了
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<button class="btn">點選複製</button>
</body>
<script>
const btn = document.querySelector('.btn')
const hello = document.querySelector('#hello')
btn.addEventListener('click', () => {
let range = document.createRange() //建立range
range.selectNodeContents(hello) //range選中hello
let selection = window.getSelection() //獲取selection物件
if (selection.rangeCount > 0) {
//如果有已經選中的區域,直接全部去除
selection.removeAllRanges()
}
selection.addRange(range) //加入到選區中
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
//先檢測是否支援document.queryCommandSupported和copy指令
//如果都支援直接執行指令
document.execCommand('copy')
//去除選中區域,取消拖藍效果
selection.removeAllRanges()
}
})
</script>
複製影象的操作是和複製文字基本相同的,只是需要在加入區域時選中整個節點,也就是把selectNodeContents方法換成selectNode
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<img src="./test.png" alt="">
<button class="btn">點選複製</button>
</body>
<script>
const btn = document.querySelector('.btn')
const hello = document.querySelector('#hello')
const img = document.querySelector('img')
btn.addEventListener('click', () => {
let range = document.createRange() //建立range
range.selectNode(img) //range選中影象節點
let selection = window.getSelection() //獲取selection物件
if (selection.rangeCount > 0) {
//如果有已經選中的區域,直接全部去除
selection.removeAllRanges()
}
selection.addRange(range) //加入到選區中
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
//先檢測是否支援document.queryCommandSupported和copy指令
//如果都支援直接執行指令
document.execCommand('copy')
//去除選中區域,取消拖藍效果
selection.removeAllRanges()
}
})
</script>
clipboard.js是一個第三方庫,也是使用了前文所講到的document.execCommand('copy')來實現的點選複製,使用方便,但是隻能用於文字的複製。
使用npm安裝
npm install clipboard --save
安裝後在html檔案內引入
<script src="dist/clipboard.min.js"></script>
或者使用CDN引入(這裡只寫了一種CDN引入方式,可以選擇多種不同CDN方,具體請看https://github.com/zenorocha/clipboard.js/wiki/CDN-Providers)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js"></script>
使用import的方式引入
import Clipboard from "clipboard";
直接建立一個ClipboardJS物件,傳的引數可以是選擇器字串或者是DOM元素或者是DOM元素列表
new ClipboardJS('.btn') // import方式為 new Clipboard('.btn')
初始化完後,可以到要繫結的對應元素下新增data-clipboard-target屬性,屬性值是要複製的元素的選擇器,這裡要複製的元素是 ‘是的’ 那個div,所以屬性值就寫#yes。不進行其他設定時,我們點選按鈕,觸發點選事件後,就可以完成複製文字 ‘是的’ 了。
<body>
<div id="hello" >你好</div>
<div id="yes">是的</div>
<button class="btn" data-clipboard-target="#yes">點選複製</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js"></script>
<script>
new ClipboardJS('.btn') // import方式為 new Clipboard('.btn')
</script>
點選後,是的這個元素被選中(拖藍),使用ctrl+v可以完成文字的複製,效果已經達到。
此時有2個問題需要優化
這時就要用到一個新的屬性data-clipboard-text,屬性值就是希望動態複製的內容。對ClipboardJS繫結的元素設定這個屬性就可以動態複製自己設定的內容,此時就不需要再設定data-clipboard-target屬性了(如果同時寫2個屬性,data-clipboard-text優先)。以下程式碼是一個寫死的簡單展示,真實使用的時候屬性值要用js設定成需要複製的值。
<body>
<div id="hello" >你好</div>
<div id="yes">是的</div>
<button class="btn" data-clipboard-text="動態設定的內容">點選複製</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js"></script>
<script>
new ClipboardJS('.btn') // import方式為 new Clipboard('.btn')
</script>
上圖顯示點選之後,複製內容成功,這樣沒有選中元素的效果,不會拖藍,互動效果更好的同時又能動態設定內容
data-clipboard-action屬性可以決定執行的操作,這個屬性有2個可選值copy或者是cut,預設是copy也就是複製,前面的所有程式碼中都沒有出現這個屬性,是直接使用的預設值copy。cut剪下,只能在input和textarea標籤中使用,顯然之前的div標籤是無法使用的。使用方法仍是對ClipboardJS繫結的元素設定這個屬性。
<button class="btn" data-clipboard-text="動態設定的內容" data-clipboard-action="copy">點選複製</button>
事件處理可以讓使用者設定複製或剪下成功或者失敗的回撥,事件名分別是success和error。可以通過on在ClipboardJS範例物件身上繫結success和error事件處理的回撥。以下範例寫了最簡單alert列印成功和失敗
const clipboard = new ClipboardJS('.btn') // import方式為 new Clipboard('.btn')
clipboard.on('success',function(){
alert('複製成功')
})
clipboard.on('error',function(){
alert('複製失敗')
})
如果不想改HTML,加入過多的屬性,可以直接使用純JS寫法來初始化ClipboardJS物件建構函式中傳入第二個引數,第二個引數為物件,如下的範例中僅用完成js就完成了動態設定複製內容。設定設定物件的text方法,返回值就是要複製的內容
new ClipboardJS('.btn',{
text: function(){
return '動態複製的內容'
}
})
設定設定物件的target方法,返回值就是要複製的元素
new ClipboardJS('.btn',{
target: function (){
return document.querySelector('#hello')
}
})
經過測試,當html中設定屬性的同時,又在建構函式里加入設定項,以js建構函式設定項為準(優先順序高)
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<button class="btn" data-clipboard-target="#hello">點選複製</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js"></script>
<script>
new ClipboardJS('.btn',{
target(){
return document.querySelector('#yes')
}
})
</script>
如果使用的是單頁應用程式,可能希望更精確地管理DOM的生命週期。可以使用destroy方法銷燬物件
var clipboard = new ClipboardJS('.btn');
clipboard.destroy();
看了之前的api,想了解一下這個所謂的簡單的複製庫是如何實現的,於是開啟了原始碼開始分析一下
原始碼地址 https://github.com/zenorocha/clipboard.js
建構函式裡面傳2個引數,第一個trigger即觸發點選的元素物件,第二個options設定項。從最簡單的例子來看,只需要傳一個trigger引數就可以實現功能,那就先不管options,直接看與trigger有關的listenClick方法。
listenClick方法呼叫了一個第三方庫的listen方法系結了click事件和對應的回撥函數this.onClick,在onclick方法中,呼叫了ClipboardActionDefault方法,並且傳了對應的幾個設定項引數,action container,target,text,這些值都是this.xxx方法,這幾個方法又是在哪定義的呢?
找了一下類內部,定義這些方法的地方是在前文建構函式裡的this.resolveOptions方法裡
resolveOptions方法裡的defaultAction,defaultText等等方法都是類似的,都是呼叫了一個getAttributeValue方法去獲取html模板上的屬性值
getAttributeValue方法如下,比較簡單
上面跳了這麼多方法雖然不難,但是也有點繞,主要還是在幹一件事,那就是通過定義來準備好ClipboardActionDefault這個方法的引數。這時候就要看一下ClipboardActionDefault這個方法在幹什麼。
簡單來看,這個方法主要分4個if判斷,前2個if就是一些條件的判斷,判斷action只能是複製或者剪下,還有就是判斷要複製的目標節點的節點型別和readonly問題等等,此處不展開去研究,有興趣者可以點選本部分開始處的原始碼連結下載。
後2個if判斷中的內容如下,分別用於判斷是否有text值和target值,這2個值也是通過本庫的核心屬性data-clipboard-text和data-clipboard-target在html中獲取的(或者在js設定項裡獲取)。判斷完後就呼叫了ClipboardActionCopy或者ClipboardActionCut方法去實現複製或者剪下功能。
這個方法就開始進行文字的複製了,首先判斷要複製的目標是普通的字串(通過data-clipboard-text設定)還是節點(通過data-clipboard-target設定),如果是文字或者不是普通的輸入元素,直接呼叫fakeCopyAction方法執行復制操作。
fakeCopyAction先建立了虛擬元素,然後把這個元素插入dom裡,最後執行選中+複製操作
建立虛擬元素的方法也比較簡單,先通過原生方法createElement建立了一個textarea元素,然後把它隱藏。建立這種輸入類元素的好處就是可以直接去修改它的value,最後一步操作就是把文字text賦值給textarea
建立完虛擬元素就要處理選中問題了,這裡呼叫了select方法,方法內部根據3種元素型別設定了不同的處理對策,select元素只要focus後賦值就好。輸入元素可以呼叫原生的select方法來選中元素,而普通元素就需要使用之前講到的selection api去獲取range和新增range了
function select(element) {
var selectedText;
if (element.nodeName === 'SELECT') {
//針對select元素的處理
element.focus();
selectedText = element.value;
}
else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
//選中輸入元素
var isReadOnly = element.hasAttribute('readonly');
if (!isReadOnly) {
element.setAttribute('readonly', '');
}
element.select();
element.setSelectionRange(0, element.value.length);
if (!isReadOnly) {
element.removeAttribute('readonly');
}
selectedText = element.value;
}
else {
//普通元素選中
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
最後的command('copy')也就是對執行復制指令這個方法的簡單封裝,做了一下相容性的處理。
前面的document.execCommand和第三方庫clipboard.js都非常的好用,但是他們可能面臨被棄用的風險,那麼該怎麼解決複製貼上這個問題呢? H5新推出的clipboard api是 處理複製貼上相關的api,可以很好的解決這個問題。用promise的方式把資料寫入剪貼簿,避免了頁面的卡頓。
使用Clipboard api時我們不需要手動建立Clipboard物件,而是通過navigator.clipboard來獲取
列印出Clipboard物件後可以看出,這個物件有4個方法,分為兩大類,write和read類。其中與複製相關的是write類表示把資料寫入剪貼簿,和貼上相關的是read類表示從剪貼簿裡面讀取資料
Clipboard物件中的writeText方法可以用於複製文字,也是非常簡單易用的一個方法。
引數:傳一個字串引數,即要複製的內容
返回值: 一個promise物件,如果成功複製則是成功的promise,如果寫入剪貼簿失敗(複製失敗)則是失敗的promise
範例如下:先建立了一個clipboard物件,然後直接呼叫writeText方法複製文字123
navigator.clipboard.writeText('123')
根據之前的html結構,使用Clipboard api完成文字的複製
預設情況下,會為當前的啟用的頁面自動授予剪貼簿的寫入許可權。出於安全方面考慮,這裡我們還是先主動向使用者請求剪貼簿的寫入許可權,如果被授權,就可以呼叫上面的方法直接完成複製了。
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<img src="./test.png" alt="">
<button class="btn">點選複製</button>
</body>
<script>
const btn = document.querySelector('.btn')
const hello = document.querySelector('#hello')
const img = document.querySelector('img')
btn.addEventListener('click', async () => {
const { state } = await navigator.permissions.query({
// 出於安全方面考慮,這裡我們還是主動向使用者請求剪貼簿的寫入許可權
name: "clipboard-write",
});
if (state == 'granted') {
navigator.clipboard.writeText(hello.innerHTML)
}
})
</script>
write方法除了支援文字資料之外,還支援將影象資料寫入到剪貼簿,呼叫該方法後會返回一個 Promise 物件。
以下是簡單的使用案例,先通過 Blob API 建立 Blob 物件,然後使用該 Blob 物件來構造 ClipboardItem 物件,最後再通過 write 方法把資料寫入到剪貼簿,複製了文字(當前頁面的地址)
<button onclick="copyPageUrl()">拷貝當前頁面地址</button>
<script>
async function copyPageUrl() {
const text = new Blob([location.href], {type: 'text/plain'});
try {
await navigator.clipboard.write(
new ClipboardItem({
"text/plain": text,
}),
);
console.log("頁面地址已經被拷貝到剪貼簿中");
} catch (err) {
console.error("頁面地址拷貝失敗: ", err);
}
}
</script>
瞭解了write的基本用法,那使用Clipboard物件複製圖片也有辦法了,只要先把影象變成Blob物件,然後構造 ClipboardItem 物件,最後再呼叫write方法就好。
可是,如何把一個img標籤裡的圖片轉換成Blob物件呢?
首先從Blob物件的建構函式開始,MDN檔案寫了Blob建構函式所需要的引數
var aBlob = new Blob( array, options );
array 是一個由ArrayBuffer, ArrayBufferView, Blob, DOMString 等物件構成的 Array ,或者其他類似物件的混合體,它將會被放進 Blob。DOMStrings 會被編碼為 UTF-8。
options 是一個可選的BlobPropertyBag字典,它可能會指定如下兩個屬性:
根據檔案中顯示,我們需要先準備一個對應的陣列,然後才能構造Blob物件,也就是要把圖片轉成二進位制資料。
分步驟實現把圖片轉換成Blob
下方程式碼完成了基本的功能實現:
微信輸入框顯示,可以完成複製
這個方法在瀏覽器相容性上仍存在一些問題,比如火狐可能就不支援ClipboardItem物件,此時只能用前文寫的document.execCommand方法了。
<body>
<div id="hello">你好</div>
<div id="yes">是的</div>
<img style="width: 400px; height: 200px" src="./test.png" alt="">
<button class="btn">點選複製</button>
</body>
<script>
const btn = document.querySelector('.btn')
const hello = document.querySelector('#hello')
const img = document.querySelector('img')
btn.addEventListener('click', async () => {
const {
state
} = await navigator.permissions.query({
// 出於安全方面考慮,這裡我們還是主動向使用者請求剪貼簿的寫入許可權
name: "clipboard-write",
});
if (state == 'granted') {
//建立canvas物件
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const image = new Image()
image.src = img.src
image.onload = async () => {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0, image.width, image.height); // img圖片轉成canvas
const imageDataUrl = canvas.toDataURL() //通過canvas獲取base64編碼
const binary = atob(imageDataUrl.split(',')[1]); // base64編碼轉二進位制資料
const array = [];
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
//二進位制資料轉Blob物件
const blob = new Blob([new Uint8Array(array)], {
type: 'image/png'
});
// 判斷瀏覽器是否有ClipboardItem物件,有些瀏覽器不支援本方法
if (typeof ClipboardItem !== 'undefined') {
//把blob資料寫入剪貼簿
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
])
}
}
}
})
</script>