如何实现文本框粘贴图片、表格;浏览器唤醒本地应用

217 阅读8分钟

最近做了些需求,都是些比较实用的技术,给大家分享一下,以后再遇到类似的需求可以有个参考。

文本框粘贴图片

需求描述

这个需求是要求文本框能粘贴图片也能输入或粘贴文字,而且这个图片可以是本地的图片也可以是截图,数量也不确定。 我们日常使用的 input 文本框只能输入文本是不能粘贴图片的,虽然可以通过一些技术手段让 input 实现该功能,但我们其实有更简便的技术方案来实现它。

界面样式

我们可以借助 H5 来实现,H5 为标签带来一个新属性 contenteditable,它可以让标签实现可编辑模式,如同文本框一样。

<div contenteditable="true">请粘贴图片</div>

image.png

仅需给标签设置 contenteditable="true" 就能实现标签编辑功能。不过这样光秃秃的并不好看,我们需要给标签加点样式,让它变得跟文本框一样美观;而且因为是文本框,需要把输入的内容提交,所以还得加个提交按钮。

<style>
#editable{
    min-height: 24px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 10px;
    outline: none;
}
#editable img {
    max-width: 100%;/*防止图片过大超过文本框宽度*/
    cursor: default;
}
#submit{
    margin: 8px 0;
    padding: 6px 22px;
    background-color: #409eff;
    border: none;
    color: #fff;
    cursor: pointer;
    outline: none;
    border-radius: 4px;
}
#submit:hover {
    background-color: rgb(121.3, 187.1, 255);/* 鼠标悬浮时按钮颜色 */
}
</style>

<div id="editable" contenteditable="true">请粘贴图片</div>
<button id="submit">提交</button>

image.png

功能实现

这个需求的最核心的功能就是粘贴图片,既然是粘贴,那我们就得要监听粘贴事件,因为是在文本框内粘贴,所以我们只需要监听文本框内的粘贴事件。监听到有图片粘贴进来,我们就为文本框插入一个 img 标签并把图片渲染出来。

// 监听文本框的粘贴事件
document.getElementById('editable').addEventListener('paste', (e) => {
  let clipboardData, items, types;
  clipboardData = e.clipboardData || window.clipboardData;
  items = clipboardData.items;

  if (!clipboardData || !items) return;

  types = Array.prototype.map.call(items, (item) => {
    return item.type;
  });

  // 查找是否有图片
  if (types.indexOf('image/png') !== -1 || types.indexOf('image/jpeg') !== -1) {
    let file = clipboardData.items[0].getAsFile();

    // 使用FileReader读取图片
    let reader = new FileReader();
    reader.onload = (event) => {
      // 插入图片
      let img = document.createElement('img');
      img.src = event.target.result;
      inputArea.appendChild(img);
    };
    reader.readAsDataURL(file);

    e.preventDefault();
  }
});

这样我们就已实现了该需求最核心的图片粘贴在文本框的功能,接下来就是获取文本框的内容提交并渲染了。 虽然我们是用 div 代替的文本框,但其实获取到的内容也跟 input 文本框一样也是 string。提交给后端保存,获取时候的数据跟提交的数据是一样的,

// 提交
const onSubmit = () => {
  const value = inputArea.innerHTML.replace(/(&nbsp;)+|(&nbsp;)+$/g, '');// 去除文本框前后空格
  if(value && value!=='请粘贴图片') {
    //...
  }
}

/**
 * 字符串标签转化为标签
 * */
const stringToElement = (htmlString) => {
  let parser = new DOMParser();
  let doc = parser.parseFromString(htmlString, 'text/html');
  return doc.body.firstChild;
}
document.getElementsByClassName('content')[0].appendChild(stringToElement(response || ''));// response是我们提交给后端保存的文本框的内容,字符串形式

完整代码

完成上一步后,该需求最核心的功能就都实现了,但还是有一些地方需要优化,比如文本框的清空、图片的大小问题、图片预览等;这里我就不再赘述,直接贴出完整代码供大家参考。 注意:下方代码把文本框内容是通过 localStorage 直接存储在浏览器本地没有调用接口的,若有需要调用接口存储请自行修改相应代码。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
  <title>Demo</title>
  <style>
    body {
        margin: 0;
        padding: 0;
    }
    .content{
        padding: 10px;
    }
    .content img {
        max-width: 300px;
        cursor: pointer;
    }
    #editable{
        min-height: 24px;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 10px;
        outline: none;
    }
    #editable img {
        max-width: 100%;/*防止图片过大超过文本框宽度*/
        cursor: default;
    }
    #submit{
        margin: 8px 0;
        padding: 6px 22px;
        background-color: #409eff;
        border: none;
        color: #fff;
        cursor: pointer;
        outline: none;
        border-radius: 4px;
    }
    #submit:hover {
        background-color: rgb(121.3, 187.1, 255);/* 鼠标悬浮时按钮颜色 */
    }
    .delete{
        margin: 8px 0;
        padding: 6px 22px;
        background-color: #f56c6c;
        border: none;
        color: #fff;
        cursor: pointer;
        outline: none;
        border-radius: 4px;
    }
    .delete:hover {
        background-color: rgb(248, 152.1, 152.1);/* 鼠标悬浮时按钮颜色 */
    }
    .mask{
        display: none;
        position: fixed;
        z-index: 1;
        width: 100%;
        height: 100%;
        background-color: rgba(0,0,0,.5);
    }
    .mask img{
        position: absolute;
        left:0;
        right:0;
        top:0;
        bottom:0;
        width: auto;
        max-width: 100%;
        margin: auto;
    }
    .mask span{
        position: absolute;
        top: 40px;
        right: 40px;
        z-index: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 44px;
        height: 44px;
        font-size: 24px;
        color: #fff;
        background-color: #606266FF;
        border-color: #fff;
        border-radius: 50%;
        opacity: .8;
        cursor: pointer;
        box-sizing: border-box;
        -webkit-user-select: none;
        user-select: none;
    }
    .mask i{
        display: block;
        width: 24px;
        height: 24px;
        font-size: 24px;
    }
  </style>
</head>

<body>
<div class="mask">
  <span onclick="document.getElementsByClassName('mask')[0].style.display='none';">
    <i>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"></path></svg>
    </i>
  </span>
  <img src=""/>
</div>
<div class="content">
  <div id="editable" contenteditable="true" onfocus="onFocus()">
    请粘贴图片
  </div>
  <button id="submit" onclick="onSubmit()">提交</button>
  <button class="delete" onclick="onDelete()">删除</button>
  <hr>
</div>

<script>
  const content = document.getElementsByClassName('content')[0];
  const inputArea = document.getElementById('editable');
  inputArea.addEventListener('paste', (e) => {
    let clipboardData, items, types;
    clipboardData = e.clipboardData || window.clipboardData;
    items = clipboardData.items;

    if (!clipboardData || !items) return;

    types = Array.prototype.map.call(items, (item) => {
      return item.type;
    });

    // 查找是否有图片
    if (types.indexOf('image/png') !== -1 || types.indexOf('image/jpeg') !== -1) {
      let file = clipboardData.items[0].getAsFile();

      // 使用FileReader读取图片
      let reader = new FileReader();
      reader.onload = (event) => {
        // 插入图片
        let img = document.createElement('img');
        img.src = event.target.result;
        inputArea.appendChild(img);
      };
      reader.readAsDataURL(file);

      e.preventDefault();
    }
  });

  const onFocus = () => {
    inputArea.innerHTML.includes('请粘贴图片') && (inputArea.innerHTML ='');
  }
  const onSubmit = () => {
    const value = inputArea.innerHTML.replace(/(&nbsp;)+|(&nbsp;)+$/g, '');//去除文本框前后空格
    if(value && value!=='请粘贴图片') {
      localStorage.setItem('rawDemo', `<div class="editable-raw">${value}</div>`);
      location.reload();
    }
  }
  const onDelete = () => {
    localStorage.setItem('rawDemo','');
    location.reload();
  }

  /**
   * 字符串标签转化为标签
   * */
  const stringToElement = (htmlString) => {
    let parser = new DOMParser();
    let doc = parser.parseFromString(htmlString, 'text/html');
    return doc.body.firstChild;
  }
  let str = localStorage.getItem('rawDemo') || '';
  content.appendChild(stringToElement(str));

  const editableRaw = document.getElementsByClassName('editable-raw')[0];
  editableRaw.addEventListener('click', function (e) {
    e = e || window.event;
    let target = e.target || e.srcElement;
    if (target.nodeName === 'IMG') {
      document.getElementsByClassName('mask')[0].style.display = 'block';
      document.querySelector('.mask img').src = target.src
    }
    });
</script>
</body>
</html>

image.png

文本框粘贴表格

需求描述

上面讲了文本框粘贴图片,接下来我们讲个类似的需求,文本框粘贴 Excel 表格并渲染。该需求要求文本框能粘贴 Excel 复制的表格并渲染出来。

大家可能会因为上面的粘贴图片需求陷入固定思维,继续用 div 标签来实现。但在上面我们也说过其实 input 也是可以实现,只是实现图片的粘贴会有点麻烦,才用的 div 可编辑方式,这次需求我们用 input 标签就可以轻松实现。

界面样式

我们给页面一个 input 元素,并为它添加写样式,使其变得美观。

<style>
input{
    display: block;
    box-sizing: border-box;
    width: 100%;
    min-height: 24px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 10px;
    outline: none;
    resize: none;
}
</style>

<input id="paste-area" placeholder="在这里粘贴表格数据" />
<button class="delete">删除</button>
<div id="data-table"></div>

因为这次的需求是直接粘贴表格内容后自动提交渲染出来即可,这里就不需要提交按钮了,只需要监听到粘贴事件,自动提交上传就可以了。

功能实现

该功能的核心代码也是通过对文本框的粘贴事件的监听实现的,

document.getElementById('paste-area').addEventListener('paste', (event) => {
  const clipboardData = event.clipboardData || window.clipboardData;// 获取复制到的数据
  document.getElementById('data-table').innerHTML = clipboardData.getData('text/html');// 获取复制到的数据的标签及样式并渲染出来

  // 清空文本框
  setTimeout(() => {
    pasteArea.value = '';
  }, 50)
});

这段代码就是该功能的核心,将复制到的 Excel 内容粘贴渲染出来,而且甚至你粘贴的 Excel 里有图片也会带出来(实现粘贴图片的另一种思路)。

clipboardData.getData('text/html') 实际就是一段 html 代码,它还能把 Excel 里的标签、链接、样式都带出来,所以我们拿到后直接渲染就可以了。

完整代码

同样的,这里我也贴出完整代码供大家参考。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
  <title>Demo</title>
  <style>
      body {
          margin: 0;
          padding: 0;
      }
      .content{
          padding: 10px;
      }
      input{
          display: block;
          box-sizing: border-box;
          width: 100%;
          min-height: 24px;
          padding: 10px;
          border: 1px solid #ddd;
          border-radius: 10px;
          outline: none;
          resize: none;
      }
      .delete{
          margin: 8px 0;
          padding: 6px 22px;
          background-color: #f56c6c;
          border: none;
          color: #fff;
          cursor: pointer;
          outline: none;
          border-radius: 4px;
      }
      .delete:hover {
          background-color: rgb(248, 152.1, 152.1);/* 鼠标悬浮时按钮颜色 */
      }
  </style>
</head>

<body>
<div class="content">
  <input id="paste-area" placeholder="在这里粘贴表格数据" />
  <button class="delete" onclick="onDelete()">删除</button>
  <div id="data-table"></div>
</div>

<script>
  let pasteArea = document.getElementById('paste-area');
  let table = document.getElementById('data-table');

  pasteArea.addEventListener('paste', (event) => {
    const clipboardData = event.clipboardData || window.clipboardData;// 获取复制到的数据
    document.getElementById('data-table').innerHTML = clipboardData.getData('text/html');// 获取复制到的数据的标签及样式并渲染出来
    localStorage.setItem('rawTable',table.innerHTML);

    // 清空文本框
    setTimeout(() => {
      pasteArea.value = '';
    }, 50)
  });

  const onDelete = () => {
    localStorage.setItem('rawTable','');
    location.reload();
  }
  table.innerHTML = localStorage.getItem('rawTable');
</script>
</body>
</html>

image.png

浏览器唤醒本地应用

大家平常在网上冲浪时可能会发现有些网站的某些按钮可以调起我们电脑已下载的应用,比较典型的是我们在网上找到某部电影的资源想下载到本地离线观看,这时候点击下载可能就会唤醒本地已有的相应的应用开启下载。

image.png

需求描述

这次的需求就是这样的需求,页面上有个按钮,点击该按钮唤醒 wps。

实现思路

报错

既然网上有这种例子,那说明该功能是可以实现的,经过一番查找,发现 URI Scheme 技术可以帮我们实现。

window.location.href = 'myapp://';

于是兴高采烈把 myapp 替换为 wps,点击后发现没反应,原来是控制台报错了: Failed to launch 'wps://' because the scheme does not have a registered handler.

疑问

wps 没注册处理程序?可我们明明已经有下载 wps 而且自己点击也能打开,是这个方法不对吗?于是分别尝试了微信、vscode、zoom,发现 weixin、vscode 能分别唤醒相应的应用,但 zoom 失败了,报了同样的错。

带着疑问一番查找才发现,这种方式唤醒应用是需要找到应用在注册表里注册的名字的。比如湃睿开发了款客户端,但在注册表里叫 pisx 而不是 pairui,那我们就只能用 pisx 去唤醒而不能用 pairui。

按着这个思路打开注册表的 HKEY_CLASSES_ROOT,找到了 zoom,原来它注册的名字是 zoommtg,难怪 zoom 打不开。试了下能打开的 weixin 和 vscode 也确实在注册表找到了相应的 name。

image.pngzoom在注册表的名称

解决

不过我们最终要打开的是 wps,在注册表搜 wps 有很多这种,看起来也不像能唤醒 wps 的样子。

image.png

陷入疑惑的时候忽然想起查资料时有提到唤醒 qq,正常的 qq 能唤醒它,tencent 腾讯公司的英文名也能唤醒它,那 wps 会不会也跟公司名挂钩呢?就试探性在注册表搜了下 k,然后一眼就看到了带着 wps 的这个。虽然下面也有带着 wps 的,但这个是 web start,很明显为浏览器准备的,把名称输入进去后点击,果然可以。

image.png能启动 wps 的名称

image.png

进阶实现

我们已经可以唤醒 wps 了,但我们正常使用一般都是打开某个表格、文档之类,也就是页面上下载下来后自动打开它,这该如何实现呢?

自动打开下载的文件是一个较为复杂的任务,因为大多数浏览器出于安全考虑禁止自动执行此类操作。尽管如此,我们可以尝试一些方法来实现这一功能,但必须注意,这些方法可能不会在所有浏览器上都有效,并且用户体验可能受到影响。

这里我们用 window.open 来实现打开的功能。

const downloadAndAutoOpen = (url, filename) => {
  // 创建隐藏的a标签用于下载
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.style.display = 'none';
  document.body.appendChild(a);

  // 模拟点击下载
  a.click();
  document.body.removeChild(a);

  // 尝试自动打开文件,注意这通常会被浏览器阻止
  setTimeout(() => {
    window.open(url);
  }, 1000); // 延迟时间可以根据文件大小和网络速度调整
}

完整代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
  <title>Demo</title>
  <style>
      body {
          margin: 0;
          padding: 0;
      }
      .content{
          padding: 10px;
      }
      .btn{
          margin: 8px 0;
          padding: 6px 22px;
          background-color: #409eff;
          border: none;
          color: #fff;
          cursor: pointer;
          outline: none;
          border-radius: 4px;
      }
      .btn:hover {
          background-color: rgb(121.3, 187.1, 255);/* 鼠标悬浮时按钮颜色 */
      }
  </style>
</head>

<body>
<div class="content">
  <button class="btn" id="launchButton">启动客户端应用程序</button>
</div>

<script>
  document.getElementById('launchButton').addEventListener('click', ()=> {
    // window.location.href = 'ksowebstartupwps://';
    downloadAndAutoOpen('http://gwc.pisx.com:81/group1/M00/03/42/rBQAZmdSx6-AXthkAAAhph3vYiA08.xlsx','ces')
  });

  const downloadAndAutoOpen = (url, filename) => {
    // 创建隐藏的a标签用于下载
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.style.display = 'none';
    document.body.appendChild(a);

    // 模拟点击下载
    a.click();
    document.body.removeChild(a);

    // 尝试自动打开文件,注意这通常会被浏览器阻止
    setTimeout(() => {
      window.open(url);
    }, 1000); // 延迟时间可以根据文件大小和网络速度调整
  }
</script>
</body>
</html>