• JavaScript复制文本探究


    JS复制文本基本分为两步-First: 选中需要复制的节点,及选区;Second: 执行document.execCommand('copy')命令复制

    对于选区,属于HTMLInputElement<textarea><input>元素支持element.select()方法

    <div id="test1">
      <input type="text" placeholder="你能复制我的人,但不能复制我的心" /><button>
        点击复制文字
      </button>
    </div>
    
    // 复制输入框文字
    const target1 = document.querySelector('#test1>input')
    const btn1 = document.querySelector('#test1>button')
    btn1.addEventListener(
      'click',
      () => {
        target1.select() //选中输入框中文字,这个方法执行后没有返回值
        document.execCommand('copy') //复制成功则返回值为true
        target1.blur() // 解除文字选中状态
      },
      false
    )
    


    其他元素不支持select()方法,但是浏览器也提供了相关选区的API,主要借助document.createRange()document.getSelection()这两个方法同时也存在于window对象上

    <div id="test2">
      <span>你能复制我的人,但不能带走我的心</span>
      <button>点击复制文字</button>
    </div>
    
    // 复制任意节点文字
    const target2 = document.querySelector('#test2>span')
    const btn2 = document.querySelector('#test2>button')
    btn2.addEventListener(
      'click',
      () => {
        const range = document.createRange()
        const selection = document.getSelection()
        range.selectNodeContents(target2)
        selection.removeAllRanges()
        selection.addRange(range)
        document.execCommand('copy')
        // 清除选中状态
        selection.removeAllRanges()
      },
      false
    )
    

    如果我们想自定义复制的内容,该怎么办呢?我们可以临时创建一个节点,把需要复制的内容作为节点内容,然后使用上面提到的方法进行复制,如下面的两种方法都是可行的。

    <div id="test3">
      <button>点击复制变量</button>
    </div>
    
    // 复制自定义文字 方法一
    const target3 = '你能复制我的人,但不能带走我的心eee'
    const btn3 = document.querySelector('#test3>button')
    btn3.addEventListener(
      'click',
      () => {
        const range = document.createRange()
        const selection = document.getSelection()
        const fake = document.createElement('span')
        // 重置样式
        fake.style.all = 'unset'
        fake.textContent = target3
        // fixed定位防止选中节点时页面滚动到页面底部
        fake.style.position = 'fixed'
        fake.style.top = 0
        // 让这个临时元素不可见,注意不能使用display: none和visibility: hidden,会导致元素选不中
        fake.style.clip = 'rect(0, 0, 0, 0)'
        // 保存空格和换行
        fake.style.whiteSpace = 'pre'
        document.body.appendChild(fake)
        range.selectNodeContents(fake)
        // 清除已有选区
        selection.removeAllRanges()
        selection.addRange(range)
        document.execCommand('copy')
        document.body.removeChild(fake)
      },
      false
    )
    
    

    上述代码中的注释部分都是非常重要的,网上很多教程的实现方式都有缺陷,本文实现的方法是从copy-to-clipboard抄袭的,基本考虑到了很多异常情况.不过我个人觉得使用textarea元素作为中转节点是更好的选择,起兼容性更好,结果可控。

    <div id="test4">
      <button>点击复制变量</button>
    </div>
    
    // 复制自定义文字 方法二
    const target4 = '你能复制我的人,但不能带走我的心'
    const btn4 = document.querySelector('#test4>button')
    btn4.addEventListener(
      'click',
      () => {
        const fake = document.createElement('textarea')
        // 防止弹出键盘
        fake.setAttribute('readonly', 'readonly')
        fake.value = target4
        fake.style.position = 'fixed'
        fake.style.top = 0
        fake.style.clip = 'rect(0, 0, 0, 0)'
        document.body.appendChild(fake)
        fake.select()
        document.execCommand('copy')
        document.body.removeChild(fake)
      },
      false
    )
    

    为啥不使用input元素呢,因为textarea能保持换行等.

    上述方法在React组件中也是可以使用的,不过在使用过程中很多人发现一个问题document.execCommand ('copy') don't work in React,这个问题一开始我也遇到了,有人回答是因为document.execCommand ('copy')不能在React的合成事件中调用,只能直接在原生事件中调用,其实这是不对的,react-copy-to-clipboard这个组件不也是在合成事件中调用这个命令吗?经过不断尝试,我发现问题出现在一个小的细节上,我们一开始通过const selection = document.getSelection()获取选区操作对象,默认我们添加选区前,选区信息应该为空,可以使用下面的语句打印信息

       console.log('选区个数', selection.rangeCount, '选区对象', selection)
    


    需要注意的是selection对象是不能深克隆的,在控制台上如果展开对象打印结果显示的结果是selection最终的结果(如果后面修改过selection的话)而不是打印语句时的结果

    这个结果在上面几个原生js的例子中都适用.但是在React的事件回调中,通过ocument.getSelection()获取的默认选区却不为空,如果把我们创建的节点直接塞进去,再执行document.getSelection()是不会复制成功的.

    解决方法很简单,在添加选区前清除选区,不过我发现如果使用textarea作为中转节点则不存在这个问题。

    if (selection.rangeCount > 0) {
        selection.removeAllRanges()
    }
    
    import React, { Component } from 'react'
    class App extends Component {
      render() {
        return (
          <div className="App">
            <h1
              onClick={e => {
                const range = document.createRange()
                const selection = document.getSelection()
                const mark = document.createElement('span')
                mark.textContent = 'text to copy'
                // reset user styles for span element
                mark.style.all = 'unset'
                // prevents scrolling to the end of the page
                mark.style.position = 'fixed'
                mark.style.top = 0
                mark.style.clip = 'rect(0, 0, 0, 0)'
                // used to preserve spaces and line breaks
                mark.style.whiteSpace = 'pre'
                // do not inherit user-select (it may be `none`)
                mark.style.webkitUserSelect = 'text'
                mark.style.MozUserSelect = 'text'
                mark.style.msUserSelect = 'text'
                mark.style.userSelect = 'text'
                mark.addEventListener('copy', function(e) {
                  e.stopPropagation()
                })
    
                document.body.appendChild(mark)
    
                // The following line is very important
                if (selection.rangeCount > 0) {
                  selection.removeAllRanges()
                }
    
                range.selectNodeContents(mark)
                selection.addRange(range)
                document.execCommand('copy')
                document.body.removeChild(mark)
              }}
            >
              Click to Copy Text
            </h1>
          </div>
        )
      }
    }
    
    export default App
    
    import React, { Component } from 'react'
    class App extends Component {
      render() {
        return (
          <div className="App">
            <h1
              onClick={e => {
                const mark = document.createElement('textarea')
                 //防止移动端键盘弹出          
                mark.setAttribute('readonly', 'readonly')
                mark.value = 'copy me'
                mark.style.position = 'fixed'
                mark.style.top = 0
                mark.style.clip = 'rect(0, 0, 0, 0)'
                document.body.appendChild(mark)
                mark.select()
                document.execCommand('copy')
                document.body.removeChild(mark)
    
              }}
            >
              Click to Copy Text
            </h1>
          </div>
        )
      }
    }
    
    export default App
    

    最后再补充一点,使用上面的方法复制图片是否可行呢,

        <div id="test">
          <img
            src="https://www.zxing.top/media/15546249493670/15547312147294.jpg"
            alt=""
          />
          <button>点击复制文x字</button>
        </div>
    
    const target = document.querySelector('#test>img')
    const btn = document.querySelector('#test>button')
    btn.addEventListener(
      'click',
      () => {
        const range = document.createRange()
        const selection = document.getSelection()
        selection.removeAllRanges()
        // range.selectNodeContents(target)
        range.selectNode(target)
        selection.addRange(range)
        document.execCommand('copy')
        selection.removeAllRanges()
      },
      false
    )
    

    注意上面的代码修改了一处,range.selectNode(target),上面的代码我测试了一下,可以复制图片,但是复制的是某种html对象,走一些编辑器上是可以粘贴的,但不保障兼容性,我测试了Mac上的Page应用,印象笔记和手机上的wps是可以直接粘贴的。

  • 相关阅读:
    vue项目根据不同环境动态配置接口
    微信内置浏览器手机按返回键,给出提示,是否要退出
    Mac更新系统后提示xcrun error
    curl请求本地域名问题
    PHP把PNG图片转化为JPG时透明背景变黑色
    安卓微信浏览器中window.location.href失效的问题
    上拉加载触底事件最简单写法
    记一次virtualbox和夜神模拟器冲突的问题
    技术总结
    input type file onchange上传文件的过程中,同一个文件二次上传无效的问题。
  • 原文地址:https://www.cnblogs.com/star91/p/javascript-fu-zhi-wen-ben-tan-jiu.html
Copyright © 2020-2023  润新知