点击复制

484 阅读2分钟

例如下图展示的点击右上方的按钮复制目标的值,我们常遇到的这样的需求。今天来梳理下有哪几种方式可以实现

复制 input 或 textarea 元素中的内容

简单示例

拿 textarea 举例:

<textarea ref="textarea">look 这里是 textarea</textarea>

methods:{
    onTextareaCopyClick() {
      this.$refs.textarea.select()
      document.execCommand('copy')
    }
}

注意 textarea 是没有 value 属性的,通过简单的两行代码就可以实现复制 textarea 中的内容。

是不是很简单,但是当前代码实现有几个不足点:

  1. 更多场景我们复制的内容并不是 input/textarea 内容,而是一个普通的 div 或者其他元素中的内容,这些元素并没有 select 方法。
  2. 当我们执行 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 解释

来解释下上面的代码:

  1. 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。
  2. 然后一个 Range 对象,用于设置dom的临界点。
  3. 将 Range 设置为包含整个 Node 及其内容。Range 的起始和结束节点的父节点与 referenceNode 的父节点相同。
  4. 判断 context 是否为空,如果是0那么是空,如果是1表示非空
  5. 非空情况清空
  6. 给 context 添加range
  7. 当 context 有值时执行 copy 操作进行复制,document.execCommand 返回是否操作成功
  8. 移除 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()
  }
}

  1. 首先在 copy 中区分传入的是 dom 还是文本。
  2. dom和文本不同点就是如何去实现 select 这块,也就是上面讲的两个方法
  3. 执行 copy 操作的部分是相同的,可以将操作结果返回出去
  4. 最后通过 document.activeElement.blur() 将选中的dom(input,textarea) 移除焦点,通过 window.getSelection().removeAllRanges() 移除所有 selection 中的 range 和文本选中的状态。