最近闲来无事写了个组件,达到了这样的效果:
大概讲一下具体是怎么写的吧。文中出现的 popover 以及菜单都是描述这个组件……我不大知道如何称呼这种东西。
初始化
我使用了一个叫做 vue-sfc-rollup 的工具初始化 SFC。它是这么描述自己的:
Quickly generate redistributable Vue components with Rollup
也就是说:
使用 Rollup 快速生成可再使用的 Vue 组件
如果你也想用这个,先全局安装一下:
# 全局安装
npm install -g vue-sfc-rollup
sfc-init
或者如果你只是一次性使用就 npx
一下好了:
# 不用安装
npx vue-sfc-rollup
然后:
# cd 到目录
cd path/to/my-component-or-lib
npm install
# serve
npm run serve
# build
npm run build
# publish 吧!
使用的时候会有一些选项,如果你想按着这篇文章的步骤来实现这么一个划词菜单,那么这么选:
✔ Which version of Vue are you writing for? › Vue 2
✔ Is this a single component or a library? › Single Component
✔ What is the npm name of your component? … foobar
✔ What is the kebab-case tag name for your component? … foo-bar
✔ Will this component be written in JavaScript or TypeScript? › JavaScript
✔ Enter a location to save the component files: … ./foo-bar
选完了之后,应该就会得到这样一个目录:
➜ tut ll
total 16
-rw-r--r-- 1 admin staff 414B Jun 6 19:18 babel.config.js
drwxr-xr-x 3 admin staff 96B Jun 6 19:18 build
drwxr-xr-x 4 admin staff 128B Jun 6 19:18 dev
-rw-r--r-- 1 admin staff 1.4K Jun 6 19:18 package.json
drwxr-xr-x 5 admin staff 160B Jun 6 19:18 src
再执行 npm install
或者 yarn
,装依赖,得到 node_modules
文件夹。
代码
在 src 目录下,有这么几个文件:
➜ src ll
total 24
-rw-r--r-- 1 admin staff 721B 6 Jun 19:18 entry.esm.js
-rw-r--r-- 1 admin staff 465B 6 Jun 19:18 entry.js
-rw-r--r-- 1 admin staff 1.6K 6 Jun 19:18 foo-bar.vue
其中,entry.js
以及 entry.esm.js
这两个文件不用管,只要看 foo-bar.vue
,因为它就是我们的组件。
点进去,你会发现里面已经有了内容。但是这个内容是可以删掉的,因为它就是一个默认生成的 counter 组件。删掉之后,输入如下内容:
<template>
</template>
<script>
export default {
}
</script>
<style>
</style>
这就是我们的大纲了。现在,就要实现选中文本菜单功能了。
菜单
先写 template
里头需要的代码:
<template>
<div>
<div
v-show="showMenu"
class="popover"
>
<span class="item">
Share
</span>
<span class="item">
Highlight
</span>
<!-- 更多按钮 -->
</div>
<!-- 文字 -->
<slot />
</div>
</template>
在这里,我在 v-show
后面使用了 showMenu
,但是我们还没有创建它,所以现在在 script
里头把它写出来:
<script>
export default {
data () {
return {
x: 0,
y: 0,
showMenu: false,
selectedText: ''
}
}
}
</script>
其中:
x
以及y
是菜单的位置- 用
showMenu
决定菜单开关 selectedText
就是选中的文本
然后,移步 computed
:
computed: {
highlightableEl () {
return this.$slots.default[0].elm
}
}
highlightableEl
定义了可以出现菜单的范围。也就是说,规定了在 <VueSelectionShare>
以及 </VueSelectionShare>
里头可以使用。
然后,加入 mounted
以及 beforeDestroy
钩子:
mounted () {
window.addEventListener('mouseup', this.onMouseup)
},
beforeDestroy () {
window.removeEventListener('mouseup', this.onMouseup)
}
这些是用来监听在 onMouseup
方法中处理的 mouseup
事件的。
然后,创建 onMouseup
方法:
onMouseup () {
const selection = window.getSelection()
const selectionRange = selection.getRangeAt(0)
// startNode 是选取开始的元素
const startNode = selectionRange.startContainer.parentNode
// endNode 是选取结束的元素
const endNode = selectionRange.endContainer.parentNode
// 如果选中部分不在 <VueSelectionShare> 里头
// 或者
// 如果 startNode !== endNode
// 不显示菜单
if (!startNode.isSameNode(this.highlightableEl) || !startNode.isSameNode(endNode)) {
this.showMenu = false
return
}
// 获取选中文本的 x, y, width
const { x, y, width } = selectionRange.getBoundingClientRect()
// 如果 width === 0,隐藏菜单
if (!width) {
this.showMenu = false
return
}
// 设置菜单的位置
// 设置 selectedText 设置成选中的文本
// 显示菜单
this.x = x + (width / 2)
this.y = y + window.scrollY - 10
this.selectedText = selection.toString()
this.showMenu = true
},
这里注释都解释得很清楚了,就不多说了。
然后,更改 template
内容:
<template>
<div>
<div
v-show="showMenu"
class="popover"
:style="{
left: `${x}px`,
top: `${y}px`
}"
@mousedown.prevent=""
>
<span
class="item"
@mousedown.prevent="handleAction('share')"
>
Share
</span>
<span
class="item"
@mousedown.prevent="handleAction('highlight')"
>
Highlight
</span>
<!-- more buttons here -->
</div>
<!-- insterted text is displayed here -->
<slot />
</div>
</template>
主要改动:
- 将
x
,y
应用于菜单 - 向菜单添加了
@mousedown.prevent=""
,防止在单击菜单内部时关闭他就自己关掉了 - 在按钮上添加了
@mousedown.prevent="handleAction('share')"
来处理点击。
有些细心的人可能要问了,为什么用 mousedown
不用 click
呢?——主要是为了防止文本被取消选择。这样一来菜单就不会直接被关掉了。
最后,添加 handleAction
方法:
handleAction (action) {
this.$emit(action, this.selectedText)
}
当然不要忘了 style
:
.popover {
height: 30px;
padding: 5px 10px;
background: #333;
border-radius: 3px;
position: absolute;
top: 0;
left: 0;
transform: translate(-50%, -100%);
transition: 0.2s all;
display: flex;
justify-content: center;
align-items: center;
}
.popover:after {
content: '';
position: absolute;
left: 50%;
bottom: -5px;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #333;
}
.item {
/* hover 前颜色 */
color: #FFF;
cursor: pointer;
}
.item:hover {
/* hover 后颜色(粉色) */
color: #ff69b4;
}
.item + .item {
margin-left: 10px;
}
这个就不多作解释了。
到这里就结束了。如果有帮到你,记得给我的 repo 点个 star。