Vue實(shí)現(xiàn)選中文本彈出彈窗功能的完多種方法
一、實(shí)現(xiàn)原理
1. 文本選中檢測(cè)機(jī)制
瀏覽器提供了 Selection API 來(lái)檢測(cè)用戶選中的文本內(nèi)容。我們可以通過(guò)監(jiān)聽(tīng) mouseup 和 keyup 事件來(lái)檢測(cè)用戶是否進(jìn)行了文本選擇操作。
核心 API:
window.getSelection()- 獲取當(dāng)前選中的文本selection.toString()- 獲取選中文本的字符串內(nèi)容selection.rangeCount- 獲取選中范圍的個(gè)數(shù)selection.getRangeAt(index)- 獲取具體的選區(qū)范圍
2. 彈窗顯示邏輯
當(dāng)選中文本后,我們需要:
- 檢測(cè)是否有文本被選中(排除空選擇)
- 獲取選中文本的內(nèi)容和位置信息
- 在合適的位置顯示彈窗(通常在選中文本附近)
- 處理彈窗的顯示/隱藏狀態(tài)
二、基礎(chǔ)實(shí)現(xiàn)方案
方案一:使用原生 JavaScript + Vue 組合
<template>
<div class="text-container" @mouseup="handleTextSelect" @keyup="handleTextSelect">
<p>
這是一段可以選中文本的示例內(nèi)容。當(dāng)你選中這段文本時(shí),
將會(huì)顯示一個(gè)彈窗,展示選中文本的相關(guān)信息和操作選項(xiàng)。
你可以嘗試選中任意文字來(lái)體驗(yàn)這個(gè)功能。
</p>
<p>
Vue.js 是一個(gè)用于構(gòu)建用戶界面的漸進(jìn)式框架。它被設(shè)計(jì)為可以自底向上逐層應(yīng)用。
Vue 的核心庫(kù)只關(guān)注視圖層,不僅易于上手,還便于與第三方庫(kù)或既有項(xiàng)目整合。
</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()
// 計(jì)算彈窗位置,避免超出視窗
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(() => {
// 降級(jí)方案
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)聽(tīng)點(diǎ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)聽(tīng):通過(guò)
@mouseup和@keyup事件監(jiān)聽(tīng)用戶的文本選擇操作 - 選擇檢測(cè):使用
window.getSelection()獲取用戶選中的文本 - 位置計(jì)算:通過(guò)
getBoundingClientRect()獲取選中文本的位置,智能計(jì)算彈窗顯示位置 - 彈窗控制:使用 Vue 的響應(yīng)式數(shù)據(jù)控制彈窗的顯示/隱藏
- 功能擴(kuò)展:實(shí)現(xiàn)了復(fù)制文本、搜索文本等實(shí)用功能
三、進(jìn)階實(shí)現(xiàn)方案
方案二:使用自定義指令實(shí)現(xiàn)
創(chuàng)建一個(gè)可復(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)聽(tīng)容器內(nèi)的選擇事件
el.addEventListener('mouseup', showSelectionPopup)
el.addEventListener('keyup', showSelectionPopup)
// 全局點(diǎn)擊關(guān)閉
const handleClickOutside = (e) => {
if (showPopup && !el.contains(e.target)) {
// 檢查點(diǎn)擊的是否是彈窗本身(需要通過(guò) 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 中注冊(cè)指令:
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>
這個(gè)區(qū)域使用了自定義指令來(lái)實(shí)現(xiàn)文本選擇彈窗功能。
指令封裝了所有的選擇檢測(cè)和彈窗邏輯,使得組件代碼更加簡(jiǎn)潔。
</p>
<p>
你可以選中任意文本,系統(tǒng)會(huì)自動(dòng)檢測(cè)并觸發(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 封裝
對(duì)于 Vue 3 項(xiàng)目,我們可以使用 Composition API 創(chuàng)建一個(gè)可復(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>
這個(gè)示例展示了如何使用 Vue 3 的 Composition API 來(lái)封裝文本選擇彈窗功能。
通過(guò)創(chuàng)建可復(fù)用的 composable 函數(shù),我們可以在多個(gè)組件中輕松使用相同的功能。
</p>
<div class="text-block">
<p>Vue 3 的 Composition API 提供了更靈活的邏輯復(fù)用方式。</p>
<p>你可以選中這些文字來(lái)測(cè)試文本選擇彈窗功能。</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)聽(tīng)全局點(diǎn)擊事件
const handleGlobalClick = (event) => {
if (showPopup && popupRef.value && !popupRef.value.contains(event.target)) {
hidePopup()
}
}
// 在 setup 中添加全局事件監(jiān)聽(tīng)
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
document.addEventListener('click', handleGlobalClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick)
})
</script>四、性能優(yōu)化與注意事項(xiàng)
1. 性能優(yōu)化
- 防抖處理:使用
setTimeout避免頻繁觸發(fā)選擇檢測(cè) - 事件委托:在父容器上監(jiān)聽(tīng)事件,減少事件監(jiān)聽(tīng)器數(shù)量
- 條件渲染:只在需要時(shí)渲染彈窗組件
- 內(nèi)存管理:及時(shí)清理事件監(jiān)聽(tīng)器和定時(shí)器
2. 用戶體驗(yàn)優(yōu)化
- 智能定位:確保彈窗不超出視窗邊界
- 動(dòng)畫效果:添加平滑的顯示/隱藏動(dòng)畫
- 無(wú)障礙支持:為彈窗添加適當(dāng)?shù)?ARIA 屬性
- 多語(yǔ)言支持:根據(jù)用戶語(yǔ)言環(huán)境顯示相應(yīng)文本
3. 兼容性考慮
- 瀏覽器兼容:檢查
SelectionAPI 和相關(guān)方法的兼容性 - 移動(dòng)端適配:處理觸摸設(shè)備的文本選擇事件
- 框架版本:根據(jù)使用的 Vue 版本選擇合適的實(shí)現(xiàn)方案
五、總結(jié)
以上就是Vue實(shí)現(xiàn)選中文本彈出彈窗功能的完整指南的詳細(xì)內(nèi)容,更多關(guān)于Vue選中文本彈出彈窗的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vuex實(shí)現(xiàn)購(gòu)物車的增加減少移除
這篇文章主要為大家詳細(xì)介紹了vuex實(shí)現(xiàn)購(gòu)物車的增加減少移除,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
vue的狀態(tài)庫(kù)管理實(shí)現(xiàn)示例
Vuex 是 Vue.js 官方推薦的狀態(tài)管理庫(kù)之一,本文主要介紹了vue的狀態(tài)庫(kù)管理實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-04-04
vue+axios+mock.js環(huán)境搭建的方法步驟
本篇文章主要介紹了vue+axios+mock.js環(huán)境搭建的方法步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Vue項(xiàng)目如何根據(jù)不同運(yùn)行環(huán)境打包項(xiàng)目
這篇文章主要介紹了Vue項(xiàng)目如何根據(jù)不同運(yùn)行環(huán)境打包項(xiàng)目問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
Vue如何優(yōu)雅處理Token過(guò)期并自動(dòng)續(xù)期
干了6年前端,和Token斗智斗勇了不知道多少回,本文小編就跟大家聊聊如何優(yōu)雅處理Token過(guò)期,甚至讓它自動(dòng)續(xù)期,讓用戶無(wú)感知,有需要的小伙伴可以了解下2025-07-07
vue中的stylus及stylus-loader版本問(wèn)題
這篇文章主要介紹了vue中的stylus及stylus-loader版本問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08

