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