想做一個(gè)好用的在線(xiàn)編輯器,不管是地圖編輯器、PPT創(chuàng)作平臺(tái)還是通過(guò)拖拽快速創(chuàng)建活動(dòng)頁(yè)面的編輯器等等,必然要給用戶(hù)提供各種快捷的操作方法。如非常常用的復(fù)制粘貼功能。
舉個(gè)例子,在iPresst創(chuàng)作平臺(tái),我們的作品在好幾頁(yè)都要用到同一張圖片,總不能每次都點(diǎn)擊上傳一次圖片吧?右鍵復(fù)制粘貼或者直接按快捷鍵無(wú)疑是最符合用戶(hù)預(yù)期的操作方式,然而我們編輯器用到的元素一般比較特別,而且我們復(fù)制粘貼的時(shí)候經(jīng)常要做一些特殊處理,此時(shí)我們就需要覆蓋瀏覽器給我們提供的復(fù)制粘貼功能了。
實(shí)現(xiàn)的原理也挺簡(jiǎn)單:
方法1:監(jiān)聽(tīng)鍵盤(pán)事件
document.addEventListener('keydown', function(e){ if(e.ctrlKey) { switch(e.keyCode) { case 88: console.log('Ctrl + X, cutting'); break; case 67: console.log('Ctrl + C, copying'); break; case 86: console.log('Ctrl + V, pasting'); break; default: } } }, false);
此時(shí)我們要覆蓋掉瀏覽器的默認(rèn)右鍵菜單,不然快捷鍵和右鍵菜單的復(fù)制粘貼操作效果不一致。這并不奇怪,一般的稍復(fù)雜的編輯器都有定制自己的右鍵菜單。
document.addEventListener('contextmenu', function(e){ e.preventDefault(); console.log('show my context menu'); }, false);
方法2:直接覆蓋剪切復(fù)制粘貼事件
document.addEventListener('cut', function(e){ e.preventDefault(); console.log('Ctrl + X, cutting'); }, false); document.addEventListener('copy', function(e){ e.preventDefault(); console.log('Ctrl + C, copying'); }, false); document.addEventListener('paste', function(e){ e.preventDefault(); console.log('Ctrl + V, pasting'); }, false);
如此我們就可以定制我們編輯器的特色復(fù)制粘貼功能。
(完)
開(kāi)玩笑,如果就這樣結(jié)束那也太水了,前面那些只是鋪墊,鋪墊,咳咳。
上面的代碼只是實(shí)現(xiàn)了編輯器的內(nèi)部元素復(fù)制粘貼的閉環(huán),那來(lái)自外部的元素呢?如別的地方拷貝的一段文本,如用QQ截了一張圖,能否直接粘貼在我們的編輯器生成特有的文本元素、圖片元素?
這就是接下去要講的高級(jí)玩法,Clipboard API。
其實(shí)訪(fǎng)問(wèn)剪貼板的數(shù)據(jù)這并不新鮮,早在多年前IE就支持了,我們可以通過(guò)下面的方式訪(fǎng)問(wèn):
window.clipboardData.clearData(); window.clipboardData.setData('Text', 'abcd'); // window.clipboardData.setData('Text');
但這種接口注定淪為歷史的塵埃。為什么?不安全!如果用戶(hù)打開(kāi)一個(gè)網(wǎng)頁(yè),在他不知不覺(jué)中JavaScript就訪(fǎng)問(wèn)了系統(tǒng)剪貼板的數(shù)據(jù),然后上傳到服務(wù)器或者做各種猥瑣的操作,那用戶(hù)會(huì)泄露多少的隱私。所以在新的瀏覽器如chrome是不支持這種接口的,一般情況下js代碼是訪(fǎng)問(wèn)不到系統(tǒng)的剪貼板,我們?cè)诰W(wǎng)上看到的點(diǎn)擊復(fù)制網(wǎng)址之類(lèi)的功能,基本都是用Flash來(lái)實(shí)現(xiàn)。
那如果用戶(hù)點(diǎn)擊了瀏覽器右鍵菜單的復(fù)制粘貼或按下相應(yīng)快捷鍵,此時(shí)訪(fǎng)問(wèn)剪貼板就合理了,而瀏覽器確實(shí)是這么做的。前面提到的方法1監(jiān)聽(tīng)鍵盤(pán)事件是不行的,此時(shí)必須使用方法2,我們可以通過(guò)下面代碼獲取到剪貼板里的圖片或者文本:
document.addEventListener('paste', function(e){ var clipboard = e.clipboardData; // 有無(wú)內(nèi)容 if(!clipboard.items || !clipboard.items.length){ clear(); return; } var temp; if((temp = clipboard.items[0]) && temp.kind === 'file' && temp.type.indexOf('image') === 0){ // 獲取圖片文件 var imgFile = temp.getAsFile(); // TODO: 做愛(ài)做的事 } else if(temp = clipboard.getData('text/plain')){ // 將文本預(yù)格式化 var splitList = temp.split(//n/); temp = ''; for(var i = 0, len = splitList.length; i < len; i++){ temp += splitList[i].replace(//t/g, ' ') .replace(/ /g, ' ') + '<br>'; } // TODO: 做愛(ài)做的事 } }, false);
要注意兩個(gè)小點(diǎn),第一,我們通過(guò)上面獲取到的圖片文件是一個(gè)file對(duì)象,這跟我們從一個(gè) type=file 的上傳文件節(jié)點(diǎn)監(jiān)聽(tīng)change事件,通過(guò) e.target.files[0] 拿到的file對(duì)象是一樣的(之所以監(jiān)聽(tīng)change是為了實(shí)現(xiàn)選擇文件即時(shí)上傳的效果不用額外點(diǎn)擊上傳按鈕)。從file對(duì)象中可以獲取到圖片的base64編碼:
var reader = new FileReader(); reader.onload = function(e){ var src = e.target.result; // Todo }; reader.readAsDataURL(imgFile);
file對(duì)象中還可以獲取到文件類(lèi)型等信息,大家想更深入了解可以搜索 e.target.files 。
第二個(gè)要注意的點(diǎn)是從剪貼板獲取到的文本是系統(tǒng)格式的,如果我們不做處理直接通過(guò)類(lèi)似 innerHTML 的方法使用,會(huì)導(dǎo)致?lián)Q行丟失等顯示問(wèn)題。
Ok,大家可以在iPresst的編輯創(chuàng)作頁(yè)面體驗(yàn)效果,QQ截完圖可以直接粘貼進(jìn)來(lái)的感覺(jué)就是爽!
但是這時(shí)候有另外一個(gè)問(wèn)題,怎么保持內(nèi)部元素的復(fù)制和外部元素復(fù)制的統(tǒng)一?簡(jiǎn)單講,我在編輯器里面復(fù)制了我的特有元素,此時(shí)系統(tǒng)的剪貼板不管有什么都應(yīng)該被覆蓋,反之亦然,我在編輯器里面復(fù)制一個(gè)特有元素,然后在別的地方復(fù)制了一段文本,那此時(shí)我在編輯器里面粘貼應(yīng)該是粘貼這段文本而不是粘貼之前的特有元素。
要做到這一點(diǎn),只要處理好兩個(gè)事情:在編輯器里剪切復(fù)制的時(shí)候覆蓋剪貼板、在編輯器里粘貼時(shí)區(qū)分要粘貼的是內(nèi)部元素還是外部元素。程序員嘛,直接上代碼:
var defaultText = 'iPresst,一個(gè)性感的網(wǎng)站'; document.addEventListener('paste', function(e){ e.clipboardData.setData('text/plain', defaultText); e.clipboardData.setData('text/ipresst', 'ipresst'); eventType = 'cut'; // TODO: 獲取要剪切的內(nèi)部元素 }, false); document.addEventListener('paste', function(e){ e.clipboardData.setData('text/plain', defaultText); e.clipboardData.setData('text/ipresst', 'ipresst'); eventType = 'copy'; // TODO: 獲取要復(fù)制的內(nèi)部元素 }, false); document.addEventListener('paste', function(e){ var clipboard = e.clipboardData; // 有無(wú)內(nèi)容 if(!clipboard.items || !clipboard.items.length){ clear(); return; } // 先區(qū)分是內(nèi)部粘貼還是外部粘貼 if(clipboard.getData('text/ipresst') === 'ipresst'){ if(!eventType || !elList.length){ // TODO: 清空標(biāo)志位 return; } // 粘貼 if(eventType === 'cut') { // TODO: 剪切粘貼 } else { // TODO: 復(fù)制粘貼 } } else { var temp; // …… // 此處略去N行前面貼過(guò)的代碼 } }, false);
我們?cè)诩糍N板里面設(shè)置了我們的特色數(shù)據(jù) text/ipresst ,如果用戶(hù)在其他地方剪切復(fù)制了東西,剪貼板會(huì)被清空這個(gè)標(biāo)志位就不存在,所以可以用來(lái)區(qū)分內(nèi)部粘貼和外部粘貼。而這行代碼
e.clipboardData.setData('text/plain', defaultText);
則讓我們復(fù)制了內(nèi)部元素然后在外面如QQ聊天窗口粘貼時(shí)(顯然在聊天窗口沒(méi)法粘貼我們編輯器的內(nèi)部特有元素),貼出文本:iPresst,一個(gè)性感的網(wǎng)站。so cool!
此時(shí)我們內(nèi)部和外部的閉環(huán)就打通了。只是很遺憾地,為了保持交互邏輯的一致性,我不得不把iPresst的自定義右鍵菜單中剪切、復(fù)制、粘貼這幾項(xiàng)去掉,因?yàn)辄c(diǎn)擊事件沒(méi)法訪(fǎng)問(wèn)到剪貼板對(duì)象(只有cut/copy/paste可以訪(fǎng)問(wèn)到),也就說(shuō)沒(méi)法粘貼外部元素,和按下快捷鍵的表現(xiàn)是不一致的。這一點(diǎn)沒(méi)有更好的解決方案,當(dāng)然你放棄自定義右鍵菜單就不會(huì)有這個(gè)問(wèn)題。
或許有人會(huì)說(shuō):那我們可以點(diǎn)擊右鍵菜單的復(fù)制粘貼時(shí),通過(guò) execCommand 或者模擬鍵盤(pán)事件來(lái)觸發(fā)cut、copy、paste事件,那不就可以訪(fǎng)問(wèn)到剪貼板了?我只能說(shuō):朋友,你想多了。那樣會(huì)跟前面討論的IE的接口一樣,有安全風(fēng)險(xiǎn)的,我自測(cè)過(guò)在chrome是行不通的。在caniuse.com上面也是這樣寫(xiě):
至此,復(fù)制粘貼的高級(jí)玩法講完了,雖說(shuō)還有點(diǎn)小不滿(mǎn)意的點(diǎn),但還是一個(gè)比較推薦的實(shí)用性挺高的實(shí)踐。
(完)
(真的完了)
聯(lián)系客服