Vue實現(xiàn)選中文本彈出彈窗功能的完多種方法
一、實現(xiàn)原理
1. 文本選中檢測機制
瀏覽器提供了 Selection API 來檢測用戶選中的文本內(nèi)容。我們可以通過監(jiān)聽 mouseup 和 keyup 事件來檢測用戶是否進(jìn)行了文本選擇操作。
核心 API:
window.getSelection()- 獲取當(dāng)前選中的文本selection.toString()- 獲取選中文本的字符串內(nèi)容selection.rangeCount- 獲取選中范圍的個數(shù)selection.getRangeAt(index)- 獲取具體的選區(qū)范圍
2. 彈窗顯示邏輯
當(dāng)選中文本后,我們需要:
- 檢測是否有文本被選中(排除空選擇)
- 獲取選中文本的內(nèi)容和位置信息
- 在合適的位置顯示彈窗(通常在選中文本附近)
- 處理彈窗的顯示/隱藏狀態(tài)
二、基礎(chǔ)實現(xiàn)方案
方案一:使用原生 JavaScript + Vue 組合
<template>
<div class="text-container" @mouseup="handleTextSelect" @keyup="handleTextSelect">
<p>
這是一段可以選中文本的示例內(nèi)容。當(dāng)你選中這段文本時,
將會顯示一個彈窗,展示選中文本的相關(guān)信息和操作選項。
你可以嘗試選中任意文字來體驗這個功能。
</p>
<p>
Vue.js 是一個用于構(gòu)建用戶界面的漸進(jìn)式框架。它被設(shè)計為可以自底向上逐層應(yīng)用。
Vue 的核心庫只關(guān)注視圖層,不僅易于上手,還便于與第三方庫或既有項目整合。
</p>
<!-- 選中文本彈窗 -->
<div
v-if="showPopup"
class="text-popup"
:style="{ left: popupPosition.x + 'px', top: popupPosition.y + 'px' }"
ref="popup"
>
<div class="popup-content">
<h4>選中文本</h4>
<p class="selected-text">{{ selectedText }}</p>
<div class="popup-actions">
<button @click="copyText">復(fù)制文本</button>
<button @click="searchText">搜索文本</button>
<button @click="closePopup">關(guān)閉</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TextSelectionPopup',
data() {
return {
selectedText: '',
showPopup: false,
popupPosition: { x: 0, y: 0 },
selectionTimeout: null
}
},
methods: {
handleTextSelect() {
// 使用 setTimeout 確保選擇操作完成后再獲取選中文本
if (this.selectionTimeout) {
clearTimeout(this.selectionTimeout)
}
this.selectionTimeout = setTimeout(() => {
const selection = window.getSelection()
const selectedContent = selection.toString().trim()
if (selectedContent && selectedContent.length > 0) {
this.selectedText = selectedContent
this.showPopup = true
this.updatePopupPosition(selection)
} else {
this.showPopup = false
}
}, 10)
},
updatePopupPosition(selection) {
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const rect = range.getBoundingClientRect()
// 計算彈窗位置,避免超出視窗
const popupWidth = 250 // 預(yù)估彈窗寬度
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let x = rect.left + window.scrollX
let y = rect.bottom + window.scrollY + 5
// 水平位置調(diào)整
if (x + popupWidth > viewportWidth) {
x = rect.right + window.scrollX - popupWidth
}
// 垂直位置調(diào)整
if (y + 200 > viewportHeight + window.scrollY) {
y = rect.top + window.scrollY - 200
}
this.popupPosition = { x, y }
}
},
closePopup() {
this.showPopup = false
this.clearSelection()
},
clearSelection() {
const selection = window.getSelection()
selection.removeAllRanges()
},
copyText() {
navigator.clipboard.writeText(this.selectedText).then(() => {
alert('文本已復(fù)制到剪貼板')
this.closePopup()
}).catch(() => {
// 降級方案
const textArea = document.createElement('textarea')
textArea.value = this.selectedText
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
alert('文本已復(fù)制到剪貼板')
this.closePopup()
})
},
searchText() {
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(this.selectedText)}`
window.open(searchUrl, '_blank')
this.closePopup()
}
},
mounted() {
// 監(jiān)聽點擊其他地方關(guān)閉彈窗
document.addEventListener('click', (e) => {
if (this.showPopup && !this.$refs.popup?.contains(e.target)) {
this.closePopup()
}
})
},
beforeUnmount() {
if (this.selectionTimeout) {
clearTimeout(this.selectionTimeout)
}
document.removeEventListener('click', this.closePopup)
}
}
</script>
<style scoped>
.text-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
font-size: 16px;
}
.text-popup {
position: fixed;
z-index: 1000;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
max-width: 300px;
animation: popupShow 0.2s ease-out;
}
@keyframes popupShow {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.popup-content {
padding: 12px;
}
.popup-content h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.selected-text {
margin: 8px 0;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
font-size: 13px;
word-break: break-word;
color: #333;
}
.popup-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.popup-actions button {
flex: 1;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.popup-actions button:hover {
background: #f0f0f0;
border-color: #999;
}
.popup-actions button:first-child {
background: #007bff;
color: white;
border-color: #007bff;
}
.popup-actions button:first-child:hover {
background: #0056b3;
border-color: #0056b3;
}
</style>方案解析
- 事件監(jiān)聽:通過
@mouseup和@keyup事件監(jiān)聽用戶的文本選擇操作 - 選擇檢測:使用
window.getSelection()獲取用戶選中的文本 - 位置計算:通過
getBoundingClientRect()獲取選中文本的位置,智能計算彈窗顯示位置 - 彈窗控制:使用 Vue 的響應(yīng)式數(shù)據(jù)控制彈窗的顯示/隱藏
- 功能擴(kuò)展:實現(xiàn)了復(fù)制文本、搜索文本等實用功能
三、進(jìn)階實現(xiàn)方案
方案二:使用自定義指令實現(xiàn)
創(chuàng)建一個可復(fù)用的 Vue 自定義指令,讓任何元素都具備選中文本彈窗功能。
// directives/textSelectionPopup.js
export default {
mounted(el, binding) {
let showPopup = false
let selectedText = ''
let popupTimeout = null
const showSelectionPopup = () => {
if (popupTimeout) {
clearTimeout(popupTimeout)
}
popupTimeout = setTimeout(() => {
const selection = window.getSelection()
const content = selection.toString().trim()
if (content && content.length > 0) {
selectedText = content
showPopup = true
updatePopupPosition(selection, el)
binding.value?.onShow?.({ text: selectedText, element: el })
} else {
hidePopup()
}
}, 10)
}
const hidePopup = () => {
showPopup = false
selectedText = ''
binding.value?.onHide?.()
}
const updatePopupPosition = (selection, containerEl) => {
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const rect = range.getBoundingClientRect()
const containerRect = containerEl.getBoundingClientRect()
// 這里可以 emit 位置信息給父組件
const popupData = {
x: rect.left,
y: rect.bottom + 5,
width: rect.width,
height: rect.height,
text: selectedText
}
binding.value?.onPositionChange?.(popupData)
}
}
// 監(jiān)聽容器內(nèi)的選擇事件
el.addEventListener('mouseup', showSelectionPopup)
el.addEventListener('keyup', showSelectionPopup)
// 全局點擊關(guān)閉
const handleClickOutside = (e) => {
if (showPopup && !el.contains(e.target)) {
// 檢查點擊的是否是彈窗本身(需要通過 binding 傳遞彈窗引用)
hidePopup()
}
}
// 保存清理函數(shù)
el._textSelectionPopup = {
showSelectionPopup,
hidePopup,
handleClickOutside,
cleanup: () => {
el.removeEventListener('mouseup', showSelectionPopup)
el.removeEventListener('keyup', showSelectionPopup)
document.removeEventListener('click', handleClickOutside)
if (popupTimeout) {
clearTimeout(popupTimeout)
}
}
}
document.addEventListener('click', handleClickOutside)
},
unmounted(el) {
if (el._textSelectionPopup) {
el._textSelectionPopup.cleanup()
}
}
}在 main.js 中注冊指令:
import { createApp } from 'vue'
import App from './App.vue'
import textSelectionPopup from './directives/textSelectionPopup'
const app = createApp(App)
app.directive('text-selection-popup', textSelectionPopup)
app.mount('#app')使用示例:
<template>
<div
v-text-selection-popup="{
onShow: handlePopupShow,
onHide: handlePopupHide,
onPositionChange: handlePositionChange
}"
class="content-area"
>
<h2>使用自定義指令的文本選擇區(qū)域</h2>
<p>
這個區(qū)域使用了自定義指令來實現(xiàn)文本選擇彈窗功能。
指令封裝了所有的選擇檢測和彈窗邏輯,使得組件代碼更加簡潔。
</p>
<p>
你可以選中任意文本,系統(tǒng)會自動檢測并觸發(fā)相應(yīng)的回調(diào)函數(shù)。
這種方式更加靈活,可以在不同的組件中復(fù)用相同的邏輯。
</p>
</div>
<!-- 彈窗組件(可以是全局組件) -->
<TextSelectionPopup
v-if="popupVisible"
:text="selectedText"
:position="popupPosition"
@close="closePopup"
@copy="copyText"
@search="searchText"
/>
</template>
<script>
import TextSelectionPopup from './components/TextSelectionPopup.vue'
export default {
components: {
TextSelectionPopup
},
data() {
return {
popupVisible: false,
selectedText: '',
popupPosition: { x: 0, y: 0 }
}
},
methods: {
handlePopupShow(data) {
this.selectedText = data.text
this.popupVisible = true
console.log('彈窗顯示', data)
},
handlePopupHide() {
this.popupVisible = false
},
handlePositionChange(position) {
this.popupPosition = { x: position.x, y: position.y + 20 }
},
closePopup() {
this.popupVisible = false
},
copyText() {
// 復(fù)制文本邏輯
console.log('復(fù)制文本:', this.selectedText)
},
searchText() {
// 搜索文本邏輯
console.log('搜索文本:', this.selectedText)
}
}
}
</script>方案三:使用 Composition API 封裝
對于 Vue 3 項目,我們可以使用 Composition API 創(chuàng)建一個可復(fù)用的 composable 函數(shù)。
// composables/useTextSelectionPopup.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useTextSelectionPopup(options = {}) {
const {
onTextSelected = () => {},
onPopupClose = () => {},
popupComponent: PopupComponent = null,
popupProps = {}
} = options
const selectedText = ref('')
const showPopup = ref(false)
const popupPosition = ref({ x: 0, y: 0 })
const selectionTimeout = ref(null)
const handleTextSelect = () => {
if (selectionTimeout.value) {
clearTimeout(selectionTimeout.value)
}
selectionTimeout.value = setTimeout(() => {
const selection = window.getSelection()
const content = selection.toString().trim()
if (content && content.length > 0) {
selectedText.value = content
showPopup.value = true
updatePopupPosition(selection)
onTextSelected({ text: content, element: document.activeElement })
} else {
hidePopup()
}
}, 10)
}
const updatePopupPosition = (selection) => {
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const rect = range.getBoundingClientRect()
popupPosition.value = {
x: rect.left,
y: rect.bottom + 5
}
}
}
const hidePopup = () => {
showPopup.value = false
selectedText.value = ''
onPopupClose()
}
const clearSelection = () => {
const selection = window.getSelection()
selection.removeAllRanges()
}
const handleClickOutside = (event, popupRef) => {
if (showPopup.value && popupRef && !popupRef.contains(event.target)) {
hidePopup()
}
}
onMounted(() => {
document.addEventListener('mouseup', handleTextSelect)
document.addEventListener('keyup', handleTextSelect)
})
onUnmounted(() => {
if (selectionTimeout.value) {
clearTimeout(selectionTimeout.value)
}
document.removeEventListener('mouseup', handleTextSelect)
document.removeEventListener('keyup', handleTextSelect)
})
return {
selectedText,
showPopup,
popupPosition,
hidePopup,
clearSelection,
handleClickOutside,
handleTextSelect
}
}使用 Composition API 的組件示例:
<template>
<div class="content-area">
<h2>使用 Composition API 的文本選擇</h2>
<p>
這個示例展示了如何使用 Vue 3 的 Composition API 來封裝文本選擇彈窗功能。
通過創(chuàng)建可復(fù)用的 composable 函數(shù),我們可以在多個組件中輕松使用相同的功能。
</p>
<div class="text-block">
<p>Vue 3 的 Composition API 提供了更靈活的邏輯復(fù)用方式。</p>
<p>你可以選中這些文字來測試文本選擇彈窗功能。</p>
</div>
<!-- 如果有彈窗組件 -->
<Teleport to="body">
<div
v-if="showPopup"
class="global-popup"
:style="{ left: popupPosition.x + 'px', top: popupPosition.y + 'px' }"
ref="popupRef"
>
<div class="popup-content">
<h4>選中的文本</h4>
<p>{{ selectedText }}</p>
<button @click="hidePopup">關(guān)閉</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTextSelectionPopup } from '@/composables/useTextSelectionPopup'
const popupRef = ref(null)
const {
selectedText,
showPopup,
popupPosition,
hidePopup,
handleTextSelect
} = useTextSelectionPopup({
onTextSelected: ({ text }) => {
console.log('文本已選擇:', text)
},
onPopupClose: () => {
console.log('彈窗已關(guān)閉')
}
})
// 監(jiān)聽全局點擊事件
const handleGlobalClick = (event) => {
if (showPopup && popupRef.value && !popupRef.value.contains(event.target)) {
hidePopup()
}
}
// 在 setup 中添加全局事件監(jiān)聽
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
document.addEventListener('click', handleGlobalClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick)
})
</script>四、性能優(yōu)化與注意事項
1. 性能優(yōu)化
- 防抖處理:使用
setTimeout避免頻繁觸發(fā)選擇檢測 - 事件委托:在父容器上監(jiān)聽事件,減少事件監(jiān)聽器數(shù)量
- 條件渲染:只在需要時渲染彈窗組件
- 內(nèi)存管理:及時清理事件監(jiān)聽器和定時器
2. 用戶體驗優(yōu)化
- 智能定位:確保彈窗不超出視窗邊界
- 動畫效果:添加平滑的顯示/隱藏動畫
- 無障礙支持:為彈窗添加適當(dāng)?shù)?ARIA 屬性
- 多語言支持:根據(jù)用戶語言環(huán)境顯示相應(yīng)文本
3. 兼容性考慮
- 瀏覽器兼容:檢查
SelectionAPI 和相關(guān)方法的兼容性 - 移動端適配:處理觸摸設(shè)備的文本選擇事件
- 框架版本:根據(jù)使用的 Vue 版本選擇合適的實現(xiàn)方案
五、總結(jié)
以上就是Vue實現(xiàn)選中文本彈出彈窗功能的完整指南的詳細(xì)內(nèi)容,更多關(guān)于Vue選中文本彈出彈窗的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+axios+mock.js環(huán)境搭建的方法步驟
本篇文章主要介紹了vue+axios+mock.js環(huán)境搭建的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
Vue如何優(yōu)雅處理Token過期并自動續(xù)期
干了6年前端,和Token斗智斗勇了不知道多少回,本文小編就跟大家聊聊如何優(yōu)雅處理Token過期,甚至讓它自動續(xù)期,讓用戶無感知,有需要的小伙伴可以了解下2025-07-07

