vue3自定义指令之复制指令、拖拽指令、按钮长按指令

2023-11-01

1.复制指令

主要用到 document.execCommand('Copy')这个api

app.directive('copy', {
    beforeMount(el, binding) {
    el.targetContent = binding.value;
    el.addEventListener('click', () => {
      if (!el.targetContent) return console.warn('没有需要复制的目标内容');
      // 创建textarea标签
      const textarea = document.createElement('textarea');
      // 设置相关属性
      textarea.readOnly = 'readonly';
      textarea.style.position = 'fixed';
      textarea.style.top = '-99999px';
      // 把目标内容赋值给它的value属性
      textarea.value = el.targetContent;
      // 插入到页面
      document.body.appendChild(textarea);
      // 调用onselect()方法
      textarea.select();

      const success = binding.arg;
      // 把目标内容复制进剪贴板, 该API会返回一个Boolean
      const res = document.execCommand('Copy');
      res && success ? success(el.targetContent) : ''
      // 移除textarea标签
      document.body.removeChild(textarea);
    })
  },
  updated(el, binding) {
    // 实时更新最新的目标内容
    el.targetContent = binding.value;
  },
  unmounted(el) {
    el.removeEventListener('click', () => { })
  }

})

 在组件中使用:v-copy="复制的数据"

<template>
  <div v-copy="copyvalue">点击复制</div>
</template>

<script setup>
import { ref } from 'vue'
const copyvalue = ref('点击复制哈哈哈 ')
</script>

如果复制之后还有其他操作,需要个回调方法做其他事情:v-copy:[回调函数]="要复制的数据"

<template>
  <div v-copy:[success]="copyvalue">点击复制</div>
</template>

<script setup>
import { ref } from 'vue'
const copyvalue = ref('点击复制哈哈哈 ')
const success = (val) => {
  console.log('内容是', val)
}
</script>

2.拖拽指令

让一个盒子限制宽高的父级元素内移动,没有限制的时候是相对于屏幕上的可视区内移动的。

app.directive("dragable",{
  mounted(el, binding) {
    // 设置目标元素基础属性
    el.style.cursor = 'move';
    el.style.position = 'fixed';
    // 获取容器宽高
    const containerId = binding.arg || null;
    let containerWidth = window.innerWidth- getScrollWidth();
    let containerHeight = window.innerHeight;
    // 存在父级容器
    if (containerId) {
      const containerEle = document.getElementById(containerId);
      let { width, height } = containerEle.getBoundingClientRect();
      containerWidth = width;
      containerHeight = height;
      if (!['fixed', 'absolute', 'relative'].includes(getStyle(containerEle, 'position'))) {
        containerEle.style.position = 'relative';
      }
      el.style.position = 'absolute';
    }

    // 鼠标在目标元素上按下        
    el.addEventListener('mousedown', (e) => {
      let { width, height } = el.getBoundingClientRect();
      // 当前目标元素的left与top
      const left = el.offsetLeft;
      const top = el.offsetTop;
      // 保存按下的鼠标的X与Y
      const mouseX = e.clientX;
      const mouseY = e.clientY;
      // 计算边界值
      const leftLimit = left;
      const rightLimit = containerWidth - left - width;
      const topLimit = top;
      const bottomLimit = containerHeight - top - height;

      // 监听鼠标移动
      document.onmousemove = (e) => {
        // 鼠标移动的距离
        let disX = e.clientX - mouseX;
        let disY = e.clientY - mouseY;
        // 左右边界
        if (disX < 0 && disX <= -leftLimit) {
          el.style.left = (left - leftLimit) + 'px';
        } else if (disX > 0 && disX >= rightLimit) {
          el.style.left = (left + rightLimit) + 'px';
        } else {
          el.style.left = (left + disX) + 'px';
        }
        // 上下边界
        if (disY < 0 && disY <= -topLimit) {
          el.style.top = (top - topLimit) + 'px';
        } else if (disY > 0 && disY >= bottomLimit) {
          el.style.top = (top + bottomLimit) + 'px';
        } else {
          el.style.top = (top + disY) + 'px';
        }
        return false;
      }

      // 监听鼠标抬起
      document.onmouseup = () => {
        document.onmousemove = null;
        document.onmouseup = null;
      }



    });

    // 获取元素的相关CSS
    function getStyle(el, attr) {
      return el.currentStyle ? el.currentStyle[attr] : window.getComputedStyle(el, false)[attr];
    }

    // 返回滚动条的宽度, 没有则返回0
    function getScrollWidth() {
      let noScroll, scroll, oDiv = document.createElement("DIV");
      oDiv.style.cssText = "position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;";
      noScroll = document.body.appendChild(oDiv).clientWidth;
      oDiv.style.overflowY = "scroll";
      scroll = oDiv.clientWidth;
      document.body.removeChild(oDiv);
      let isExsit = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
      return isExsit ? noScroll - scroll : 0
    }
  }

})

 使用:给父级唯一个 id 值,通过 v-draggable:id属性,传递进去。或者 直接v-draggable则是以浏览器为准

  <div id="dragbox" style="width: 500px;height:500px;border:1px solid red;margin: 100px auto">
    <div v-dragable:dragbox style="width:100px;height:100px;background:green;"></div>
  </div>

3.按钮长按指令

点击一个按钮,不松开达到一定的时间才触发 ,移动端,pc端均可使用:

app.directive('longhandle', {

  beforeMount(el, binding) {
    const cb = binding.value;
    el.$duration = binding.arg || 3000; // 获取长按时长, 默认3秒执行长按事件
    if (typeof cb !== 'function') return console.warn('v-longpress指令必须接收一个回调函数');
    let timer = null;
    const add = (e) => {
      // 排除点击与右键情况, event.button: 0-左键  2-右键
      if (e.type === 'click' && e.button !== 0) return;
      e.preventDefault();
      if (timer === null) {
        timer = setTimeout(() => {
          cb();
          timer = null;
        }, el.$duration)
      }
    }
    const cancel = () => {
      if (timer !== null) {
        clearTimeout(timer);
        timer = null;
      }
    }

    // 添加计时器
    el.addEventListener('mousedown', add);
    el.addEventListener('touchstart', add);
    // 取消计时器
    el.addEventListener('click', cancel);
    el.addEventListener('mouseout', cancel);
    el.addEventListener('touchend', cancel)
    el.addEventListener('touchcancel', cancel)
  },
  updated(el, binding) {
    // 可以实时更新时长
    el.$duration = binding.arg;
  },
  unmounted(el) {
    el.removeEventListener('mousedown', () => { });
    el.removeEventListener('touchstart', () => { });
    el.removeEventListener('click', () => { });
    el.removeEventListener('mouseout', () => { });
    el.removeEventListener('touchend', () => { });
    el.removeEventListener('touchcancel', () => { });
  }
})

使用:v-longhandle:[毫秒时间]="回调函数"

<template>
   <button v-longhandle:[2000]="longpress">按钮</button>
</template>

<script setup>
const longpress = () => {
  console.log('时间')
}
</script>

一般项目中自定义指令比较多,都是放在一个文件里面,然后统一在main.js中注册

utils下的directive.js文件:

export const copy = {
  beforeMount(el, binding) {
    el.targetContent = binding.value;
    el.addEventListener('click', () => {
      if (!el.targetContent) return console.warn('没有需要复制的目标内容');
      // 创建textarea标签
      const textarea = document.createElement('textarea');
      // 设置相关属性
      textarea.readOnly = 'readonly';
      textarea.style.position = 'fixed';
      textarea.style.top = '-99999px';
      // 把目标内容赋值给它的value属性
      textarea.value = el.targetContent;
      // 插入到页面
      document.body.appendChild(textarea);
      // 调用onselect()方法
      textarea.select();

      const success = binding.arg;
      // 把目标内容复制进剪贴板, 该API会返回一个Boolean
      const res = document.execCommand('Copy');
      res && success ? success(el.targetContent) : ''
      // 移除textarea标签
      document.body.removeChild(textarea);
    })
  },
  updated(el, binding) {
    // 实时更新最新的目标内容
    el.targetContent = binding.value;
  },
  unmounted(el) {
    el.removeEventListener('click', () => { })
  }
}

export const longhandle = {
  beforeMount(el, binding) {
    const cb = binding.value;
    el.$duration = binding.arg || 3000; // 获取长按时长, 默认3秒执行长按事件
    if (typeof cb !== 'function') return console.warn('v-longpress指令必须接收一个回调函数');
    let timer = null;
    const add = (e) => {
      // 排除点击与右键情况, event.button: 0-左键  2-右键
      if (e.type === 'click' && e.button !== 0) return;
      e.preventDefault();
      if (timer === null) {
        timer = setTimeout(() => {
          cb();
          timer = null;
        }, el.$duration)
      }
    }
    const cancel = () => {
      if (timer !== null) {
        clearTimeout(timer);
        timer = null;
      }
    }

    // 添加计时器
    el.addEventListener('mousedown', add);
    el.addEventListener('touchstart', add);
    // 取消计时器
    el.addEventListener('click', cancel);
    el.addEventListener('mouseout', cancel);
    el.addEventListener('touchend', cancel)
    el.addEventListener('touchcancel', cancel)
  },
  updated(el, binding) {
    // 可以实时更新时长
    el.$duration = binding.arg;
  },
  unmounted(el) {
    el.removeEventListener('mousedown', () => { });
    el.removeEventListener('touchstart', () => { });
    el.removeEventListener('click', () => { });
    el.removeEventListener('mouseout', () => { });
    el.removeEventListener('touchend', () => { });
    el.removeEventListener('touchcancel', () => { });
  }
}

export const dragable = {
  mounted(el, binding) {
    // 设置目标元素基础属性
    el.style.cursor = 'move';
    el.style.position = 'fixed';
    // 获取容器宽高
    const containerId = binding.arg || null;
    let containerWidth = window.innerWidth- getScrollWidth();
    let containerHeight = window.innerHeight;
    // 存在父级容器
    if (containerId) {
      const containerEle = document.getElementById(containerId);
      let { width, height } = containerEle.getBoundingClientRect();
      containerWidth = width;
      containerHeight = height;
      if (!['fixed', 'absolute', 'relative'].includes(getStyle(containerEle, 'position'))) {
        containerEle.style.position = 'relative';
      }
      el.style.position = 'absolute';
    }

    // 鼠标在目标元素上按下        
    el.addEventListener('mousedown', (e) => {
      let { width, height } = el.getBoundingClientRect();
      // 当前目标元素的left与top
      const left = el.offsetLeft;
      const top = el.offsetTop;
      // 保存按下的鼠标的X与Y
      const mouseX = e.clientX;
      const mouseY = e.clientY;
      // 计算边界值
      const leftLimit = left;
      const rightLimit = containerWidth - left - width;
      const topLimit = top;
      const bottomLimit = containerHeight - top - height;

      // 监听鼠标移动
      document.onmousemove = (e) => {
        // 鼠标移动的距离
        let disX = e.clientX - mouseX;
        let disY = e.clientY - mouseY;
        // 左右边界
        if (disX < 0 && disX <= -leftLimit) {
          el.style.left = (left - leftLimit) + 'px';
        } else if (disX > 0 && disX >= rightLimit) {
          el.style.left = (left + rightLimit) + 'px';
        } else {
          el.style.left = (left + disX) + 'px';
        }
        // 上下边界
        if (disY < 0 && disY <= -topLimit) {
          el.style.top = (top - topLimit) + 'px';
        } else if (disY > 0 && disY >= bottomLimit) {
          el.style.top = (top + bottomLimit) + 'px';
        } else {
          el.style.top = (top + disY) + 'px';
        }
        return false;
      }

      // 监听鼠标抬起
      document.onmouseup = () => {
        document.onmousemove = null;
        document.onmouseup = null;
      }



    });

    // 获取元素的相关CSS
    function getStyle(el, attr) {
      return el.currentStyle ? el.currentStyle[attr] : window.getComputedStyle(el, false)[attr];
    }

    // 返回滚动条的宽度, 没有则返回0
    function getScrollWidth() {
      let noScroll, scroll, oDiv = document.createElement("DIV");
      oDiv.style.cssText = "position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;";
      noScroll = document.body.appendChild(oDiv).clientWidth;
      oDiv.style.overflowY = "scroll";
      scroll = oDiv.clientWidth;
      document.body.removeChild(oDiv);
      let isExsit = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
      return isExsit ? noScroll - scroll : 0
    }
  }
}

 main.js中注册

import { createApp } from 'vue'
import App from './App.vue'
import * as dictive from './utils/directive'

const app = createApp(App)

//注册
let dictValue= Object.values(dictive)
Object.keys(dictive).forEach((item,i)=>{
    app.directive(item,dictValue[i])
})

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue3自定义指令之复制指令、拖拽指令、按钮长按指令 的相关文章

  • angularjs:如何向资源对象添加缓存?

    在 http 中添加缓存非常简单 通过传递cache true http docs angularjs org api ng http https docs angularjs org api ng service 24http有缓存选项
  • 如何使用 selenium 和 Mocha 获取 xPath() 选择的锚标记的文本

    我已经成功选择了 a 标签 我想显示锚标记的文本 但无法这样做 我正在使用 selenium mocha javascript 和 phantomJS 这是我的脚本 详细 var assert require assert var test
  • 网站 YouTube 嵌入视频不断播放

    我正在使用 youtube 提供的 iframe 在我的网站上嵌入视频 我还使用了一个 css 弹出窗口 这是我从这个页面学到的http www pat burt com web development how to do a css po
  • 如何在ASP.NET Webform中使用Jquery表单插件?

    我遇到了这个插件 http malsup com jquery form getting started http malsup com jquery form getting started 我想知道如何在 ASP NET WebForm
  • 非 DOM 对象上的 jQuery 自定义事件

    我最近阅读了一些代码 其功能如下 bob name Bob Smith rank 7 bob bind nameChanged function bob trigger nameChanged 这似乎有效 但我在 jQuery 文档或源代码
  • jquery 验证错误位置

    这看起来很简单 但我无法弄清楚 我正在使用 jquery 验证插件 我验证所有文件 但我想要的是在输入文本行中显示验证消息警报 例如在电子邮件输入中 请填写电子邮件地址 但现在它出现在所有字段下 在我的html中
  • 如何使用 jest 模拟第三方库

    我正在开发一个node js应用程序使用nestjs我有一堂课叫LoggerService如下 export class LoggerService private logger Rollbar constructor this logge
  • jquery window.open 在 ajax 成功中被阻止

    尝试在我的 ajax 成功调用中打开一个新的浏览器窗口 但是 它被阻止为弹出窗口 我做了一些搜索 发现用户事件需要绑定到 window open 才能避免这种情况发生 我还找到了这个解决方案 您可以在 ajax 之前打开一个空白窗口 然后在
  • Node.js - console.log 不显示数组中的项目,而是显示 [Object]

    我在注销对象内数组的内容时遇到问题 实际的物体看起来像这样 var stuff accepted item1 item2 rejected response Foo envelope from The sender to new item1
  • 仅一页 JavaScript 应用程序

    您是否尝试过单页 Web 应用程序 即浏览器仅从服务器 获取 一页 其余部分由客户端 JavaScript 代码处理 此类 应用程序页面 的一个很好的例子是 Gmail 对于更简单的应用程序 例如博客和 CMS 使用这种方法有哪些优点和缺点
  • 如果链接包含特定文本,jQuery 将类添加到 href

    我的网站上的列表中有一些动态填充的链接 这些链接链接到文件 是否可以使用 jQuery 查看文件名是否以 pdf 结尾 并在 href 或类似的链接文本以 mp3 结尾时添加一个类 例如 我的列表中有以下链接 文件1 pdf 歌曲1 mp3
  • 是否有任何非轮询方式来检测 DOM 元素的大小或位置何时发生变化?

    很长一段时间以来 我一直在寻找一种方法来检测 DOM 元素的大小或位置何时发生变化 这可能是因为窗口调整了大小 或者因为向该元素添加了新的子元素 或者因为在该元素周围添加了新元素 或者因为 CSS 规则已更改 或者因为用户更改了浏览器的字体
  • Typeahead.js substringMatcher 函数说明

    我只是在做一些研究Typeahead js这是一个非常酷的图书馆 感谢文档 我已经成功地获得了一个基本的示例 该文档也非常好 但是我试图弄清楚以下代码块实际上在做什么 var substringMatcher function strs r
  • 淡出和循环一组 div 的最佳方式

    假设我有以下 div div class a You are funny div div class b You are smart div div class c You are cool div 最好的展示方式是什么div a持续 5
  • 为什么“tbody”不设置表格的背景颜色?

    我在用 tbody 作为 CSS 选择器来设置background color在一个表中 我这样做是因为我有多个 tbody 表内的部分 它们具有不同的背景颜色 我的问题是 当使用border radius在细胞上 细胞不尊重backgro
  • 查询为空 Node Js Sequelize

    我正在尝试更新 Node js 应用程序中的数据 我和邮递员测试过 我的开发步骤是 从数据库 MySQL 获取ID为10的数据进行更新 gt gt 未处理的拒绝SequelizeDatabaseError 查询为空 我认识到 我使用了错误的
  • 如何在jquery中获取保存时间和当前时间的差异?

    我想在 javascript 或 jquery 中获取保存时间和当前时间之间的时差 我节省的时间看起来像Sun Oct 24 15 55 56 GMT 05 30 2010 java中的日期格式代码如下 String newDate 201
  • Nodejs mysql 获取正确的时间戳格式

    我在用着mysqljs https github com mysqljs mysql得到结果后sql我变得不同TimeStamp格式如下 created at Sat Jul 16 2016 23 52 54 GMT 0430 IRDT 但
  • 如何在 gulp.src 中使用基本正则表达式?

    我正在尝试选择两个文件gulp src highcharts js and highcharts src js 当然 我知道我可以使用数组表达式显式添加这两个表达式 但出于学习目的 我尝试为它们编写一个表达式 我读过可以使用简单的正则表达式
  • 单击列表时使用 bootstrap Dropdown 防止下拉菜单消失

    我正在使用使用引导下拉菜单 http twitter github com bootstrap javascript html dropdowns生成下拉菜单 我想防止点击菜单时菜单消失 我已经实现了以下代码 但它不起作用 知道如何修复它吗

随机推荐

  • python中的特殊运算符

    运算符 描述 相当于python中的关键字 or 简述 usr bin env python coding UTF 8 Time 2019 9 16 15 10 Email spirit az foxmail com File tst py
  • Unity 回合制战斗系统(中级篇)

    项目文件找出来了 老版本的脚本有报错 我在新版2019 4 21f1c1下解决了报错 战斗场景可以正常跑的 需要的同学点下面地址下载 关注就行啦不用积分 祝大家都早日学成 项目包下载 上一篇文章里实现了较为初级的回合制战斗系统 仅限与1v1
  • LabVIEW开放神经网络交互工具包【ONNX】,大幅降低人工智能开发门槛,实现飞速推理

    文章目录 前言 一 工具包内容 二 工具包下载链接 三 工具包安装步骤 四 实现物体识别 五 实现图像分割 六 自然场景下的文字识别 七 人体关键点检测 总结 前言 前面给大家介绍了自己开发的LabVIEW ai视觉工具包 后来发现有一些o
  • SCI三区论文大修笔记(已录用)

    本人5月份往Journal of Process Control期刊投了一篇论文 是基于深度学习图像序列预测的 前几天收到一审结果 大修 两个审稿人给了几篇参考文献 此贴专门用来做笔记方便自己查阅 论文1 Video salient obj
  • Shell 输入输出重定向

    1 普通重定向 命令 说明 command gt file 将输出重定向到 file command lt file 将输入重定向到 file command gt gt file 将输出以追加的方式重定向到 file n gt file
  • 共享经济与颠覆,产生的反向是什么?理念与文化

    这几年共享经济 一个字 火 身边做这个的人也很多 火的原因是 给用户带来便捷 gt 投资者不需要较大资金就可以参与 gt 收益较稳定 众筹的理念从此剥离出 从以上 分析 产品的便捷性是启动共享经济最主要的起动机 然后更多带来管理上的难题 无
  • C++SVD分解求伪逆 (Eigen库)(附C++代码)

    SVD求解矩阵伪逆过程 首先对矩阵A进行SVD分解得到U D V三个矩阵 其中D为列矩阵 是从上到下 由大到小排列的A矩阵的奇异值 若D矩阵中元素个数为n则原矩阵有n个奇异值 构建大小为V cols U cols 的S矩阵 其中S矩阵的前n
  • 24 openEuler管理进程-调度启动进程

    文章目录 24 openEuler管理进程 调度启动进程 24 1 定时运行一批程序 at 24 1 1 at命令 24 1 2 设置时间 24 1 3 执行权限 24 2 周期性运行一批程序 cron 24 2 1 运行机制 24 2 2
  • 解决智慧树考试酷无法复制粘粘的问题

    相信用过智慧树和考试酷的大学生在做章节测试和考试等都会遇到无法复制粘粘的困惑 这篇博客总结了几个步骤 希望能帮助到大家 复制 首先在我们答题的页面 按住F12 有的电脑是Fn F12 点击下图圈出来的位置 选择我们要复制的地方 我们将鼠标移
  • 基础算法题——家庭作业(并查集的标记法、贪心)

    家庭作业 题目描述 输入格式 第一行一个整数n 表示作业的数量 接下来 n行 每行包括两个整数 第一个整数表示作业的完成期限 第二个数表示该作业的学分 输出格式 输出一个整数表示可以获得的最大学分 保证答案不超过 C C 的 int 范围
  • 《系统集成项目管理工程师》必背81-100题

    软考系统集成项目管理工程师交流裙941853339 附带资料 81 简述风险管理过程 1 规划风险管理 2 识别风险 3 实施定性风险分析 评估并分析风险发生概率和影响 对风险进行优先排序 4 实施定量风险分析 就已识别风险对项目整体目标的
  • cpu算力计算

    CPU的算力与CPU的核心的个数 核心的频率 核心单时钟周期的能力三个因素有关系 常用双精度浮点运算能力衡量CPU的科学计算的能力 就是处理64bit小数点浮动数据的能力 支持AVX2的处理器在1个核心1个时钟周期可以执行16次浮点运算 也
  • python selenium解决报错SessionNotCreatedException session not created: No matching capabilitie与问题解析

    解决方法 查看导包是否正确 如果是webdriver Chrome 那么导包的一切都要是chrome的 如果是webdriver Firefox 那么导入的包也都源于firefox的包 出现这个问题是因为由于selenium很多类都很相似
  • 怎么在springboot实现简单的登录操作关于拦截器中cookie,token,redis的使用,以及使用全局变量ThreadLocal

    1 什么情况下需要登录操作 首先抛出一个问题 什么情况下才需要登录操作 其实登录操作在很多的管理系统 后台系统中都会涉及到的一个看似简单 但是又特别重要的操作 2 登录是简单的验证数据库账号密码 这么简单吗 在之前我总觉得登录应该是一个很简
  • 树莓派之Linux内核源码(基础)

    目录 一 树莓派等芯片带操作系统的启动过程 二 树莓派Linux源码目录树分析 1 linux内核是开源的 支持多架构多平台代码 2 Linux内核源代码目录树结构 三 树莓派Linux源码配置 1 第一种配置方式 使用源码里自带的conf
  • csdn笔记

    一阶系统的单位阶跃响应 unit Step Response 时间常数 Time constant 1 对于一阶系统 线性时不变 来说 传递函数的一般形式 2 时间常数 是系统特性 可用来识别系统 1 a 稳定时间Tss 4 4 a 3 系
  • pandas写入数据到Excel

    pandas将dataframe写入Excel 大数据写入 split count df shape 0 1000000 1 for i in range split count writer pd ExcelWriter xlsx for
  • 第十九篇 ResNet——论文翻译

    文章目录 摘要 1 引言 2 相关工作 3 深度残差学习 3 1 残差学习 3 2 快捷恒等映射 3 3 网络架构 3 4 实现 4 实验 4 1 ImageNet 分类 4 2 CIFAR 10 和分析 4 3 PASCAL 和 MS C
  • kvm中支持SCSI硬盘接口

    在KVM中安装ubuntu14 硬盘接口采用SCSI 可以正常安装 但是安装centos7却不行 原因是Centos7中没有SCSI驱动 在ubuntu中用lsmod命令查看 可以发现ubuntu中加载了sym53c8xx驱动 就是因为缺少
  • vue3自定义指令之复制指令、拖拽指令、按钮长按指令

    1 复制指令 主要用到 document execCommand Copy 这个api app directive copy beforeMount el binding el targetContent binding value el