解决方案已经发布成了npm包,需要的话直接下载就好了(目前还暂时不支持自定义样式,后续可以集成)
https://www.npmjs.com/package/copy-and-paste-kiko
需要源码的话可以访问我的仓库
https://gitee.com/yangdan1028/copy-and-paste
问题描述
公司目前的协作管理平台用到的是飞书,然后公司内部的一些业务系统也是用的是在飞书里面嵌套网页的形式。但是在飞书里面嵌套网页,由于飞书禁用了右键,因此不能够用右键来进行复制粘贴,可是老板又必须要使用右键来复制粘贴(让他cv他不干)。所以只能去询问了飞书的开发人员有没有相应的解决方案,得到的回应是让我们cv…
没办法,飞书没有这个功能,那么我们只能手动写一个复制粘贴的功能。
解决思路
首先在开始写代码前,先捋一下要完成这个功能需要实现的功能点。
- 首先需要在项目初始化的时候监听鼠标右键。
- 在鼠标右键的位置展示复制和粘贴的弹窗。
- 右键的时候获取当前页面存不存在拖蓝的文本,存在拖蓝的时候需要展示复制按钮。
- 如果是在文本输入框里右键需要获取当前的光标的位置(在指定的位置粘贴),并且如果是在输入框里面右键需要展示粘贴按钮。
- 点击复制的时候需要复制拖蓝的文本。
- 点击粘贴的时候,如果文本框里存在拖蓝的文本,需要删除后再在光标的位置输入文本。
- 粘贴的时候,不能直接给文本框赋值,必须要模拟键盘的输入事件,因为在vue或react等项目里,直接给文本框赋值不能给输入框的双向绑定对象绑定上值,此时需要模拟键盘的输入事件。
解决方案
知道了问题的解决步骤后,就一步一步的根据步骤来解决问题。
监听鼠标右键
window.oncontextmenu=()=>{console.log('鼠标点击了右键')}
展示复制和粘贴的弹窗
要在鼠标右键的位置展示弹窗,首先就需要获取当前鼠标右键的位置,我们可以根据事件对象里面的pageX和pageY拿到鼠标当前右击的位置
window.oncontextmenu=(e)=>{
console.log(e.pageX) //右击鼠标的x坐标
console.log(e.pageY) //右击鼠标的y坐标
}
拿到位置后,我们需要在这个位置展示复制粘贴的弹窗,步骤是创建一个div,然后给这个div添加样式,然后apend到指定(鼠标右击)的位置上面去。
window.oncontextmenu = (e) => {
let div = document.createElement('div') //创建复制粘贴大盒子
// 给复制粘贴盒子配置样式
div.style.transform = 'scale(0)'
div.className = 'sniPaste'
div.id = 'sniPaste'
div.style.position = 'fixed'
div.style.width = '190px'
div.style.backgroundColor = '#fff'
div.style.left = e.pageX + 3 + 'px'
div.style.top = e.pageY + 3 + 'px'
div.style.borderRadius = '4px'
div.style.boxShadow = ' 5px 5px 10px #bebebe'
div.style.zIndex = '99999999'
div.style.transformOrigin = '0 0'
div.style.transition = 'all 0.2s'
div.style.overflow = 'hidden'
div.style.border = '1px solid #d9dbdf'
div.style.borderTop = 'none'
div.style.height = '50px'
document.body.appendChild(div)
setTimeout(() => {
div.style.transform = 'scale(1)'
}, 20)
}
然后我们右击页面看一下效果,可以看到页面上浏览器自带的右击弹窗出现了,并且覆盖住了我们的弹窗,因此为了便于测试我们先把这个弹窗给禁用掉,只保留我们自己的弹窗
我们可以用阻止默认行为的方式禁用掉弹窗
e.preventDefault();
e.stopPropagation();
再来看一下效果,可以看到我们自己的弹窗已经出现了,并且是可以固定出现在鼠标右键的位置。
但是发现在多次点击右键的时候会出现多个弹窗,因此在点右键哈唤出弹窗前,需要先把之前的弹窗删除。
let sniPaste = document.getElementById("sniPaste"); //获取复制粘贴大盒子
if (sniPaste) {
sniPaste.remove(); //先移除一次
}
再次测试发现始终只会出现一个弹窗了
添加复制按钮
能够展示弹窗后接下来就是向这个弹窗里添加复制和粘贴按钮,首先先添加复制按钮,并且在鼠标移入和移除按钮的时候加上一个样式反馈。
let copy = document.createElement("div");
copy.style.borderTop = "1px solid #d9dbdf";
copy.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">复制</span><span style="color:gray"></span></div>`;
copy.style.fontSize = "14px";
copy.style.padding = "10px 15px";
copy.style.cursor = "pointer";
copy.addEventListener("mouseover", () => {
copy.style.backgroundColor = "rgb(232, 235, 234)";
});
copy.addEventListener("mouseout", () => {
copy.style.backgroundColor = "#fff";
});
div.appendChild(copy);
加上后,效果如下所示
但是在实际的使用过程中,右键能不能复制,应该是根据当前是否存在选中的文本(以下称为拖蓝)来决定的。那么如何获取当前页面是否存在选中的文本呢?
获取当前的拖蓝的文本
可以根据window.getSelection()获取当前拖蓝的文本
//获取拖蓝的文本
let text = window.getSelection().toString()? window.getSelection().toString(): ""
获取到了拖蓝的文本后,再次点击右键唤出复制粘贴的弹出框的时候需要先增加一个判断(当前是否存在拖蓝的文本)
//获取拖蓝的文本
let text = window.getSelection().toString()? window.getSelection().toString(): ""
//存在拖蓝的情况下展示copy按钮
if (text) {
let copy = document.createElement("div");
copy.style.borderTop = "1px solid #d9dbdf";
copy.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">复制</span><span style="color:gray"></span></div>`;
copy.style.fontSize = "14px";
copy.style.padding = "10px 15px";
copy.style.cursor = "pointer";
copy.addEventListener("mouseover", () => {
copy.style.backgroundColor = "rgb(232, 235, 234)";
});
copy.addEventListener("mouseout", () => {
copy.style.backgroundColor = "#fff";
});
div.appendChild(copy);
}
然后再来试一下,可以看到,虽然没有拖蓝的情况下,复制按钮不展示了,但是按钮的容器还是会展示。
因此我们需要在盒子的展示条件上也加上判断
if (text) {
document.body.appendChild(div);
}
再次测试发现已经能够实现需求了。
完成复制按钮的功能
接下来完成复制按钮的功能,首先给复制按钮注册点击事件,可以根据 document.execCommand(“Copy”) 指令来实现复制功能。再复制成功后,需要将创建的input以及容纳按钮的盒子移除掉。
copy.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
const input = document.createElement("input");
document.body.appendChild(input);
input.value = text;
input.select(); // 选中文本
document.execCommand("Copy");
if (input) {
input.remove();
}
if (div) {
div.remove();
}
});
然后来测试一下,复制一下文本,然后在控制台粘贴一下,可以看到已经将文本复制下来了,目前为止,复制按钮的功能就完成了。接下来完成粘贴按钮的功能
添加粘贴按钮
let paste = document.createElement("div");
paste.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">粘贴</span><span style="color:gray"></span></div>`;
paste.style.fontSize = "14px";
paste.style.padding = "10px 15px";
paste.style.cursor = "pointer";
paste.style.borderTop = "1px solid #d9dbdf";
paste.addEventListener("mouseover", () => {
paste.style.backgroundColor = "rgb(232, 235, 234)";
});
paste.addEventListener("mouseout", () => {
paste.style.backgroundColor = "";
});
然后我们来看一下效果👇🏻
可以看到粘贴的按钮已经能够正常显示了,但是粘贴的展示条件有问题,我们都知道,粘贴是需要在文本框里,并且输入框是处于聚焦的状态才能展示粘贴的。在其他地方是不能展示粘贴按钮的,那么怎么判断当前的操作元素是文本框且文本框处于了聚集的状态呢。由于我们监听到了鼠标的右键,鼠标的右键又能拿到事件对象e,因此我们可以根据 e.target.selectionStart获取光标的位置,如果这个值返回的不是 undefined,那么当前右键的元素一定是输入框,且输入框是处于聚焦的状态的👇🏻
l
et startPos = e.target.selectionStart; //获取光标的位置
if (startPos !== undefined) {
let paste = document.createElement("div");
paste.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">粘贴</span><span style="color:gray"></span></div>`;
paste.style.fontSize = "14px";
paste.style.padding = "10px 15px";
paste.style.cursor = "pointer";
paste.style.borderTop = "1px solid #d9dbdf";
paste.addEventListener("mouseover", () => {
paste.style.backgroundColor = "rgb(232, 235, 234)";
});
paste.addEventListener("mouseout", () => {
paste.style.backgroundColor = "";
});
}
//容器的展示条件也要增加一个粘贴按钮的
if (text||startPos !== undefined ) {
document.body.appendChild(div)
}
测试一下,可以看到,只要输入框,且输入框聚焦时才展示粘贴按钮
完成粘贴按钮的功能
粘贴按钮要比复制复杂很多,首先要完成粘贴的话,我们需要读取剪切板的内容,然后需要在输入框光标的位置插入剪贴板的内容(存在拖蓝的情况下需要先删除拖蓝的文本,再插入),最后粘贴的时候,不能直接给文本框赋值,必须要模拟键盘的输入事件,因为在vue或react等项目里,直接给文本框赋值不能给输入框的双向绑定对象绑定上值,此时需要模拟键盘的输入事件。接下来就一步步完成按钮的粘贴功能。
读取剪切板的值
要读取剪切板的值,可以使用 navigator.clipboard 来读取
navigator.clipboard.readText().then((res) => {console.log(res)})
//res就是剪切板里的文本
但是这个地方有个坑就是navigator.clipboard,需要读取剪切板的权限,并且只能在https协议下,才能使用,因此如果本地的测试服务是http协议的话,是不能使用的,所以如果需要在本地测试,则需要在本地启动https协议的服务,怎么启动https协议的服务后面单独写一篇来讲解,这里的话不做过多的赘述,总之子啊https协议下,是可以通过navigator来读取剪切板内容的。
再加上这篇文章是主要讲解怎么在飞书里面如何实现复制粘贴,而飞书刚好提供了获取剪切板内容api,因此我们用飞书的方式来实现。👇🏻
值得注意的是调用飞书的tt系api首先需要鉴权,如何鉴权可以参考飞书开发者文档,后面也会单独写一篇文章讲解。
if ((window as any).h5sdk) {
; (window as any).h5sdk.ready(() => {
; (window as any).tt.getClipboardData({
success(res) {
// 粘贴分为两种情况,直接在选择的地方插入。第二种是拖蓝后,首先需要将拖蓝的文本删除掉,才能插入
// 粘贴的时候存在拖蓝的文本
let str = ''
if (text) {
str =
value.slice(0, startPos) +
res?.data +
value.substr(startPos + text?.length)
} else {
str =
value.slice(0, startPos) +
res?.data +
value.substr(startPos)
}
console.log(str)//拿到粘贴后输入框的值
},
fail(res) {
alert(`${JSON.stringify(res)}`)
},
})
})
}
模拟键盘的输入事件
通过一下方法,就可以模拟键盘的输入事件,达到触发双向绑定对象的值改变的目的。其中形参dom就是要触发的文本实例,st就是要键入的字符串,然后我们在获取到的粘贴后输入框的值时,直接调用这个方法。
// 模拟键盘输入事件
const inputValue = function (dom, st) {
let evt = new InputEvent('input', {
inputType: 'insertText',
data: st,
dataTransfer: null,
isComposing: false,
})
dom.value = st
dom.dispatchEvent(evt)
}
if ((window as any).h5sdk) {
; (window as any).h5sdk.ready(() => {
; (window as any).tt.getClipboardData({
success(res) {
// 粘贴分为两种情况,直接在选择的地方插入。第二种是拖蓝后,首先需要将拖蓝的文本删除掉,才能插入
// 粘贴的时候存在拖蓝的文本
let str = ''
if (text) {
str =
value.slice(0, startPos) +
res?.data +
value.substr(startPos + text?.length)
} else {
str =
value.slice(0, startPos) +
res?.data +
value.substr(startPos)
}
console.log(str)//拿到粘贴后输入框的值
inputValue(e.target, str)//<---------------------------------------
},
fail(res) {
alert(`${JSON.stringify(res)}`)
},
})
})
}
测试一下👇🏻,是可以实现效果的。
但是有一点就是点击左键空白区域的时候,弹窗消失,因此我在创建弹窗的同时,再次创建一个遮罩层浮在弹窗下面,点击空白区域其实是点击到了遮罩层,点击到遮罩层的时候让弹窗和遮罩层移除掉就好了。(在所有移除弹窗的位置都要同时移除掉遮罩层和弹窗)
// 创建遮罩层盒子 点击的时候
let mask = document.createElement("div"); //创建复制粘贴大盒子
mask.style.position = "fixed";
mask.style.left = "0";
mask.style.right = "0";
mask.style.top = "0";
mask.style.bottom = "0";
mask.style.zIndex = "9999";
mask.addEventListener("click", () => {
if (div) {
div.remove();
}
if (mask) {
mask.remove();
}
});
目前为止,实现的思路基本就是这样,基本实现了老板说的右键复制粘贴的功能。下面我会贴出完整的代码
完整代码
为了兼容更多的人,因此把获取剪切板的内容从飞书改成了navigator的方式
// 模拟键盘输入事件
const inputValue = function (dom, st) {
let evt = new InputEvent("input", {
inputType: "insertText",
data: st,
dataTransfer: null,
isComposing: false,
});
dom.value = st;
dom.dispatchEvent(evt);
};
function type() {
let agent = navigator.userAgent.toLowerCase();
let isMac = /macintosh|mac os x/i.test(navigator.userAgent);
if (agent.indexOf("win32") >= 0 || agent.indexOf("wow32") >= 0) {
return "windows";
}
if (agent.indexOf("win64") >= 0 || agent.indexOf("wow64") >= 0) {
return "windows";
}
if (isMac) {
return "mac";
}
}
export default function () {
window.oncontextmenu = (e) => {
// 获取当前的系统是mac还是windows
let system = type();
let noticeC = system === "mac" ? "⌘+C" : "ctrl+C";
let noticeP = system === "mac" ? "⌘+V" : "ctrl+V";
e.stopPropagation();
e.preventDefault();
let startPos = e.target.selectionStart; //获取光标的位置
let value = e.target.value ? e.target.value : ""; //输入框的值
let text = window.getSelection().toString()
? window.getSelection().toString()
: ""; //获取拖蓝的文本
let sniPaste = document.getElementById("sniPaste"); //获取复制粘贴大盒子
if (sniPaste) {
sniPaste.remove(); //先移除一次
}
let div = document.createElement("div"); //创建复制粘贴大盒子
// 给复制粘贴盒子配置样式
div.style.transform = "scale(0)";
div.className = "sniPaste";
div.id = "sniPaste";
div.style.position = "fixed";
div.style.width = "190px";
div.style.backgroundColor = "#fff";
div.style.left = e.pageX + 3 + "px";
div.style.top = e.pageY + 3 + "px";
div.style.borderRadius = "4px";
div.style.boxShadow = " 5px 5px 10px #bebebe";
div.style.zIndex = "99999999";
div.style.transformOrigin = "0 0";
div.style.transition = "all 0.2s";
div.style.overflow = "hidden";
div.style.border = "1px solid #d9dbdf";
div.style.borderTop = "none";
// 创建遮罩层盒子 点击的时候
let mask = document.createElement("div"); //创建复制粘贴大盒子
mask.style.position = "fixed";
mask.style.left = "0";
mask.style.right = "0";
mask.style.top = "0";
mask.style.bottom = "0";
mask.style.zIndex = "9999";
mask.addEventListener("click", () => {
if (div) {
div.remove();
}
if (mask) {
mask.remove();
}
});
// 阻止大盒子冒泡以及默认时间
div.addEventListener("click", () => {
e.stopPropagation();
e.preventDefault();
});
// 只有存在文本拖蓝的情况下才展示复制按钮
if (text) {
let copy = document.createElement("div");
copy.style.borderTop = "1px solid #d9dbdf";
copy.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">复制</span><span style="color:gray">${noticeC}</span></div>`;
copy.style.fontSize = "14px";
copy.style.padding = "10px 15px";
copy.style.cursor = "pointer";
copy.addEventListener("mouseover", () => {
copy.style.backgroundColor = "rgb(232, 235, 234)";
});
copy.addEventListener("mouseout", () => {
copy.style.backgroundColor = "#fff";
});
copy.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
const input = document.createElement("input");
document.body.appendChild(input);
input.value = text;
input.select(); // 选中文本
document.execCommand("Copy");
if (input) {
input.remove();
}
if (div) {
div.remove();
}
if (mask) {
mask.remove();
}
});
div.appendChild(copy);
}
// 只在input元素上展示粘贴按钮
if (startPos !== undefined) {
let paste = document.createElement("div");
paste.innerHTML = `<div class="my_copy_box" style="display:flex;justify-content: space-between;align-items: center"><span style="display:flex;align-items: center">粘贴</span><span style="color:gray">${noticeP}</span></div>`;
paste.style.fontSize = "14px";
paste.style.padding = "10px 15px";
paste.style.cursor = "pointer";
paste.style.borderTop = "1px solid #d9dbdf";
paste.addEventListener("mouseover", () => {
paste.style.backgroundColor = "rgb(232, 235, 234)";
});
paste.addEventListener("mouseout", () => {
paste.style.backgroundColor = "";
});
paste.addEventListener("click", async () => {
e.stopPropagation();
e.preventDefault();
navigator.clipboard.readText().then((res) => {
// 粘贴分为两种情况,直接在选择的地方插入。第二种是拖蓝后,首先需要将拖蓝的文本删除掉,才能插入
// 粘贴的时候存在拖蓝的文本
let str = "";
if (text) {
str =
value.slice(0, startPos) +
res +
value.substr(startPos + text.length);
} else {
str = value.slice(0, startPos) + res + value.substr(startPos);
}
// 模拟键盘输入
inputValue(e.target, str);
if (div) {
div.remove();
}
if (mask) {
mask.remove();
}
});
});
div.appendChild(paste);
}
if (startPos !== undefined || text) {
document.body.appendChild(div);
document.body.appendChild(mask);
setTimeout(() => {
div.style.transform = "scale(1)";
}, 20);
}
};
}
有什么问题或者想和我交流的话,欢迎加我的vx一起学习: YoungDan_Y