例如下图展示的点击右上方的按钮复制目标的值,我们常遇到的这样的需求。今天来梳理下有哪几种方式可以实现
复制 input 或 textarea 元素中的内容
简单示例
拿 textarea 举例:
<textarea ref="textarea">look 这里是 textarea</textarea>
methods:{
onTextareaCopyClick() {
this.$refs.textarea.select()
document.execCommand('copy')
}
}
注意 textarea 是没有 value 属性的,通过简单的两行代码就可以实现复制 textarea 中的内容。
是不是很简单,但是当前代码实现有几个不足点:
- 更多场景我们复制的内容并不是 input/textarea 内容,而是一个普通的 div 或者其他元素中的内容,这些元素并没有 select 方法。
- 当我们执行 select 方法后,文本是处于选中的状态,当我们复制操作结束后需要去除选中状态。
优化
由于我们需要 textarea 这样的元素来帮我们完成复制的操作,我们可以自行创建一个,然后将需要复制的内容传给它,来完成复制的需求。
下面的代码和简单示例的一致,只是添加了一个 setSelectionRange 的api的操作,这里是设置你要复制内容的长度,如果不设置 select 方法会默认选中所有的内容,当然这里设置的也是同样的效果,可以按照需求自行定义。
const copy = (text) => {
const fakeElem = document.createElement('textarea')
fakeElem.style.border = '0'
fakeElem.style.padding = '0'
fakeElem.style.margin = '0'
fakeElem.style.position = 'absolute'
fakeElem.style.left = '-9999px'
fakeElem.value = text
document.body.appendChild(fakeElem)
fakeElem.select()
fakeElem.setSelectionRange(0, fakeElem.value.length)
document.execCommand('copy')
fakeElem.blur()
}
selection + range 复制任意元素的值
<div ref="text">点击按钮 get </div>
methods:{
onTextCopyClick() {
let selection = window.getSelection() // 1
let range = document.createRange() // 2
range.selectNode(this.$refs.text) // 3
if (selection.rangeCount > 0) { // 4
selection.removeAllRanges() // 5
}
selection.addRange(range) // 6
let isSuccess = document.execCommand('Copy') // 7
selection.removeRange(range) // 8
}
}
Every document with a browsing context has a unique selection associated with it
For instance, if the DOM changes in a way that changes the range's boundary points, or a script modifies the boundary points of the range, the same range object must continue to be associated with the selection. However, if the user changes the selection or a script calls addRange(), the selection must be associated with a new range object, as required elsewhere in this specification.
参考 w3c 的解释,英语不好我就不翻译了,我理解的就是每个浏览器中都会有各自的 selection 对象,需要配合 range 对象来使用,更多可以参考 w3c 解释
来解释下上面的代码:
- 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。
- 然后一个 Range 对象,用于设置dom的临界点。
- 将 Range 设置为包含整个 Node 及其内容。Range 的起始和结束节点的父节点与 referenceNode 的父节点相同。
- 判断 context 是否为空,如果是0那么是空,如果是1表示非空
- 非空情况清空
- 给 context 添加range
- 当 context 有值时执行 copy 操作进行复制,document.execCommand 返回是否操作成功
- 移除 context 上的 range
结合
根据我的使用场景判断,有时候执行 copy 操作时想要传入目标元素的dom,有时候想直接传入复制的内容。那么可以将上面两种方法结合实现需求。
/*
*
* @param {Object} value
* @return {Boolean}
*/
export const isNode = function(value) {
return value !== undefined &&
value instanceof HTMLElement &&
value.nodeType === 1
}
// 通过字面量创建的 string 或者 构造函数创建的
export const isString = function(value) {
return typeof value === 'string' ||
value instanceof String
}
export default class {
/**
* @param {HTMLElement|String}trigger
*/
copy(trigger) {
if (isNode(trigger)) {
this.container = trigger
this.createRange()
} else if (isString(trigger)) {
this.text = trigger
this.createTextarea()
}
}
createRange() {
const range = document.createRange()
const selection = window.getSelection()
// 选中container
range.selectNode(this.container)
// 移除所有的 range
selection.removeAllRanges()
selection.addRange(range)
return this.action()
}
createTextarea() {
this.fakeElem = document.createElement('textarea')
this.fakeElem.style.border = '0'
this.fakeElem.style.padding = '0'
this.fakeElem.style.margin = '0'
this.fakeElem.style.position = 'absolute'
this.fakeElem.style.left = '-9999px'
this.fakeElem.value = this.text
document.body.appendChild(this.fakeElem)
this.fakeElem.select()
this.fakeElem.setSelectionRange(0, this.fakeElem.value.length)
return this.action()
}
/**
* 可选参数 copy , cut
* @param {String} action
* @param {Boolean}
*/
action(action = 'copy') {
const isSuccess = document.execCommand(action)
this.removeSelect()
return isSuccess
}
removeSelect() {
document.activeElement.blur()
window.getSelection().removeAllRanges()
}
}
- 首先在 copy 中区分传入的是 dom 还是文本。
- dom和文本不同点就是如何去实现 select 这块,也就是上面讲的两个方法
- 执行 copy 操作的部分是相同的,可以将操作结果返回出去
- 最后通过 document.activeElement.blur() 将选中的dom(input,textarea) 移除焦点,通过 window.getSelection().removeAllRanges() 移除所有 selection 中的 range 和文本选中的状态。