Vue 中實現選中文本彈出彈窗的完整指南

在現代 Web 應用中,選中文本后顯示相關操作或信息是一種常見的交互模式。本文將詳細介紹如何在 Vue 中實現選中文本后彈出彈窗的功能,包括其工作原理、多種實現方式以及實際項目中的應用示例。

一、實現原理

1. 文本選中檢測機制

瀏覽器提供了 Selection API 來檢測用戶選中的文本內容。我們可以通過監聽 mouseup keyup 事件來檢測用戶是否進行了文本選擇操作。

核心 API:

  • window.getSelection() - 獲取當前選中的文本
  • selection.toString() - 獲取選中文本的字符串內容
  • selection.rangeCount - 獲取選中范圍的個數
  • selection.getRangeAt(index) - 獲取具體的選區范圍
2. 彈窗顯示邏輯

當選中文本后,我們需要:

  1. 檢測是否有文本被選中(排除空選擇)
  2. 獲取選中文本的內容和位置信息
  3. 在合適的位置顯示彈窗(通常在選中文本附近)
  4. 處理彈窗的顯示/隱藏狀態

二、基礎實現方案

方案一:使用原生 JavaScript + Vue 組合
<template><div class="text-container" @mouseup="handleTextSelect" @keyup="handleTextSelect"><p>這是一段可以選中文本的示例內容。當你選中這段文本時,將會顯示一個彈窗,展示選中文本的相關信息和操作選項。你可以嘗試選中任意文字來體驗這個功能。</p><p>Vue.js 是一個用于構建用戶界面的漸進式框架。它被設計為可以自底向上逐層應用。Vue 的核心庫只關注視圖層,不僅易于上手,還便于與第三方庫或既有項目整合。</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">復制文本</button><button @click="searchText">搜索文本</button><button @click="closePopup">關閉</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 = selectedContentthis.showPopup = truethis.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 // 預估彈窗寬度const viewportWidth = window.innerWidthconst viewportHeight = window.innerHeightlet x = rect.left + window.scrollXlet y = rect.bottom + window.scrollY + 5// 水平位置調整if (x + popupWidth > viewportWidth) {x = rect.right + window.scrollX - popupWidth}// 垂直位置調整if (y + 200 > viewportHeight + window.scrollY) {y = rect.top + window.scrollY - 200}this.popupPosition = { x, y }}},closePopup() {this.showPopup = falsethis.clearSelection()},clearSelection() {const selection = window.getSelection()selection.removeAllRanges()},copyText() {navigator.clipboard.writeText(this.selectedText).then(() => {alert('文本已復制到剪貼板')this.closePopup()}).catch(() => {// 降級方案const textArea = document.createElement('textarea')textArea.value = this.selectedTextdocument.body.appendChild(textArea)textArea.select()document.execCommand('copy')document.body.removeChild(textArea)alert('文本已復制到剪貼板')this.closePopup()})},searchText() {const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(this.selectedText)}`window.open(searchUrl, '_blank')this.closePopup()}},mounted() {// 監聽點擊其他地方關閉彈窗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>
方案解析
  1. 事件監聽:通過 @mouseup@keyup 事件監聽用戶的文本選擇操作
  2. 選擇檢測:使用 window.getSelection() 獲取用戶選中的文本
  3. 位置計算:通過 getBoundingClientRect() 獲取選中文本的位置,智能計算彈窗顯示位置
  4. 彈窗控制:使用 Vue 的響應式數據控制彈窗的顯示/隱藏
  5. 功能擴展:實現了復制文本、搜索文本等實用功能

三、進階實現方案

方案二:使用自定義指令實現

創建一個可復用的 Vue 自定義指令,讓任何元素都具備選中文本彈窗功能。

// directives/textSelectionPopup.js
export default {mounted(el, binding) {let showPopup = falselet selectedText = ''let popupTimeout = nullconst showSelectionPopup = () => {if (popupTimeout) {clearTimeout(popupTimeout)}popupTimeout = setTimeout(() => {const selection = window.getSelection()const content = selection.toString().trim()if (content && content.length > 0) {selectedText = contentshowPopup = trueupdatePopupPosition(selection, el)binding.value?.onShow?.({ text: selectedText, element: el })} else {hidePopup()}}, 10)}const hidePopup = () => {showPopup = falseselectedText = ''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)}}// 監聽容器內的選擇事件el.addEventListener('mouseup', showSelectionPopup)el.addEventListener('keyup', showSelectionPopup)// 全局點擊關閉const handleClickOutside = (e) => {if (showPopup && !el.contains(e.target)) {// 檢查點擊的是否是彈窗本身(需要通過 binding 傳遞彈窗引用)hidePopup()}}// 保存清理函數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>使用自定義指令的文本選擇區域</h2><p>這個區域使用了自定義指令來實現文本選擇彈窗功能。指令封裝了所有的選擇檢測和彈窗邏輯,使得組件代碼更加簡潔。</p><p>你可以選中任意文本,系統會自動檢測并觸發相應的回調函數。這種方式更加靈活,可以在不同的組件中復用相同的邏輯。</p></div><!-- 彈窗組件(可以是全局組件) --><TextSelectionPopupv-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.textthis.popupVisible = trueconsole.log('彈窗顯示', data)},handlePopupHide() {this.popupVisible = false},handlePositionChange(position) {this.popupPosition = { x: position.x, y: position.y + 20 }},closePopup() {this.popupVisible = false},copyText() {// 復制文本邏輯console.log('復制文本:', this.selectedText)},searchText() {// 搜索文本邏輯console.log('搜索文本:', this.selectedText)}}
}
</script>
方案三:使用 Composition API 封裝

對于 Vue 3 項目,我們可以使用 Composition API 創建一個可復用的 composable 函數。

// composables/useTextSelectionPopup.js
import { ref, onMounted, onUnmounted } from 'vue'export function useTextSelectionPopup(options = {}) {const {onTextSelected = () => {},onPopupClose = () => {},popupComponent: PopupComponent = null,popupProps = {}} = optionsconst 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 = contentshowPopup.value = trueupdatePopupPosition(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 = falseselectedText.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 來封裝文本選擇彈窗功能。通過創建可復用的 composable 函數,我們可以在多個組件中輕松使用相同的功能。</p><div class="text-block"><p>Vue 3 的 Composition API 提供了更靈活的邏輯復用方式。</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">關閉</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('彈窗已關閉')}
})// 監聽全局點擊事件
const handleGlobalClick = (event) => {if (showPopup && popupRef.value && !popupRef.value.contains(event.target)) {hidePopup()}
}// 在 setup 中添加全局事件監聽
import { onMounted, onUnmounted } from 'vue'onMounted(() => {document.addEventListener('click', handleGlobalClick)
})onUnmounted(() => {document.removeEventListener('click', handleGlobalClick)
})
</script>

四、性能優化與注意事項

1. 性能優化
  • 防抖處理:使用 setTimeout 避免頻繁觸發選擇檢測
  • 事件委托:在父容器上監聽事件,減少事件監聽器數量
  • 條件渲染:只在需要時渲染彈窗組件
  • 內存管理:及時清理事件監聽器和定時器
2. 用戶體驗優化
  • 智能定位:確保彈窗不超出視窗邊界
  • 動畫效果:添加平滑的顯示/隱藏動畫
  • 無障礙支持:為彈窗添加適當的 ARIA 屬性
  • 多語言支持:根據用戶語言環境顯示相應文本
3. 兼容性考慮
  • 瀏覽器兼容:檢查 Selection API 和相關方法的兼容性
  • 移動端適配:處理觸摸設備的文本選擇事件
  • 框架版本:根據使用的 Vue 版本選擇合適的實現方案

五、總結

本文詳細介紹了在 Vue 中實現選中文本彈出彈窗的多種方法,從基礎的實現原理到進階的組件化方案。通過這些技術,你可以為用戶提供更加豐富和便捷的交互體驗。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922021.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922021.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922021.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

第4節-排序和限制-FETCH

摘要: 在本教程中&#xff0c;你將學習如何使用 PostgreSQL 的 FETCH 子句從查詢中檢索部分行。 PostgreSQL FETCH 簡介 在 PostgreSQL 中&#xff0c;OFFSET 子句的作用類似于 LIMIT 子句。FETCH 子句允許你限制查詢返回的行數。 LIMIT 子句并非 SQL 標準的一部分。不過&#…

洛谷 P2680 [NOIP 2015 提高組] 運輸計劃(二分答案 + 樹上差分)

題目鏈接題目概括與評價 很經典&#xff0c;突破口藏的很深&#xff0c;求最小值這里&#xff0c;是問題切入點&#xff0c;想到用二分答案&#xff0c;然后思考怎么寫 f_check 函數。二分答案樹上差分。代碼 #include <iostream> #include <vector> #include <…

接力鄧承浩,姜海榮能講好深藍汽車新故事嗎?

出品 | 何璽排版 | 葉媛深藍汽車迎來新話事人。9月5日&#xff0c;新央企長安汽車旗下品牌深藍汽車傳出新的人事調整。多家業內媒體報道稱&#xff0c;榮耀前中國區CMO姜海榮已正式加入長安汽車&#xff0c;并出任旗下深藍汽車CEO一職。原CEO鄧承浩則升任深藍汽車董事長&#x…

esp32-c3寫一個收集附近 WiFi 和藍牙信號通過

下面給你一個基于 ESP-IDF(v5.x) 的完整示例&#xff1a;在 ESP32-C3 上同時掃描附近 Wi-Fi 與藍牙&#xff08;BLE&#xff09;廣播&#xff0c;把結果以 JSON 結構統一輸出到串口&#xff0c;并且可可選通過 MQTT 上報到服務器&#xff08;打開一個宏即可&#xff09;。日志默…

文心大模型 X1.1:百度交出的“新深度思考”答卷

文心大模型 X1.1&#xff1a;百度交出的“新深度思考”答卷 2025年9月9日&#xff0c;WAVE SUMMIT 2025深度學習開發者大會在北京正式召開&#xff0c;由深度學習技術及應用國家工程研究中心主辦&#xff0c;百度飛槳與文心大模型聯合承辦。大會上&#xff0c;百度正式發布了基…

開始 ComfyUI 的 AI 繪圖之旅-Flux.1圖生圖(八)

文章標題一、Flux Kontext Dev1.關于 FLUX.1 Kontext Dev1.1 版本說明1.2 工作流說明1.3 模型下載2.Flux.1 Kontext Dev 工作流2.1 工作流及輸入圖片下載2.2 按步驟完成工作流的運行3.Flux Kontext 提示詞技巧3.1 基礎修改3.2 風格轉換3.3 角色一致性3.4 文本編輯4.常見問題解決…

Java 生成微信小程序二維碼

1. java 二維碼生成工具類import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import com.pdatao.api.controller.file.FileController; import com.pdatao.api.error.CommunityException; import org.apache.commons.io.IOUtils; import org.springframe…

智慧健康觸手可及:AI健康小屋——未來健康管理的全能守護者

AI健康小屋&#xff0c;這座融合人工智能、物聯網與醫療科技的“健康堡壘”&#xff0c;正悄然重構健康管理生態。它以科技為引擎&#xff0c;將專業醫療資源下沉至社區、企業、家庭&#xff0c;通過智能檢測、精準分析、個性化干預&#xff0c;實現從疾病治療到主動預防的健康…

[工作表控件19] 驗證規則實戰:如何用正則表達式規范業務輸入?

在企業應用中,數據準確性至關重要。工作表控件通過“驗證規則”能力,支持在文本字段和附件字段中使用正則表達式(RegEx)進行格式校驗。它能幫助開發者輕松實現郵箱、身份證號、車牌號、URL 等格式的高效驗證,大幅提升數據質量與表單使用體驗。 一、官方功能介紹與基礎能力…

uniapp分包實現

關于分包優化的說明 在對應平臺的配置下添加"optimization":{"subPackages":true}開啟分包優化 目前只支持mp-weixin、mp-qq、mp-baidu、mp-toutiao、mp-kuaishou的分包優化 分包優化具體邏輯&#xff1a; 靜態文件&#xff1a;分包下支持 static 等靜態…

ctfshow_web14------(PHP+switch case 穿透+SQL注入+文件讀取)

題目&#xff1a;解釋&#xff1a;$c intval($_GET[c]); //獲取整數值 6sleep($c);//延遲執行當前腳本若干秒。提示一下哈沒有break會接著執行下面的但是像是44444&#xff0c;555555,sleep的時間太久我們用3進入here_1s_your_f1ag.php是一個查詢頁面&#xff0c;sql注入查看源…

linux x86_64中打包qt

下載安裝 地址: Releases linuxdeploy/linuxdeploy mv linuxdeploy-x86_64.AppImage linuxdeployqtchmod 777 linuxdeployqtsudo mv linuxdeployqt /usr/local/bin/linuxdeployqt --version報錯 Applmage默認依賴FUSE&#xff0c;需要掛載自身為虛擬文件系統才能運行, ubuntu…

華為昇騰CANN開發實戰:算子自定義與模型壓縮技術指南

點擊 “AladdinEdu&#xff0c;同學們用得起的【H卡】算力平臺”&#xff0c;注冊即送-H卡級別算力&#xff0c;80G大顯存&#xff0c;按量計費&#xff0c;靈活彈性&#xff0c;頂級配置&#xff0c;學生更享專屬優惠。 摘要 隨著人工智能技術的飛速發展&#xff0c;越來越多…

Vue3源碼reactivity響應式篇之reactive響應式對象的track與trigger

概覽 在BaseReactiveHandler類的get方法中&#xff0c;有如下代碼塊if (!isReadonly2){track(target, "get", key);}&#xff0c;這表示通過reactive、shallowReactive創建的響應式對象&#xff0c;非只讀的&#xff0c;當讀取代理對象proxyTarget的某個屬性key時&am…

VRRP 多節點工作原理

VRRP 多節點工作原理 基本概念 VRRP 的設計初衷是給一組節點提供一個 虛擬路由器&#xff0c;對外只表現出一個 VIP。協議規定&#xff1a;同一個 VRRP 實例 下始終只有 一個 Master 持有 VIP&#xff0c;其它全部是 Backup。 Master → 持有 VIP&#xff0c;負責轉發流量到Mas…

Gradio全解11——Streaming:流式傳輸的視頻應用(9)——使用FastRTC+Gemini創建沉浸式音頻+視頻的藝術評論家

Gradio全解11——Streaming&#xff1a;流式傳輸的視頻應用&#xff08;9&#xff09;——使用FastRTCGemini創建沉浸式音頻視頻的藝術評論家11.9 使用FastRTCGemini創建實時沉浸式音頻視頻的藝術評論家11.9.1 準備工作及音頻圖像編碼器1. 項目說明及準備工作2. 音頻和圖像編碼…

Django入門筆記

Python知識點&#xff1a;函數、面向對象。前端開發&#xff1a;HTML、CSS、JavaScript、jQuery、BootStrap。MySQL數據庫。Python的Web框架&#xff1a;Flask&#xff0c;自身短小精悍 第三方組件。Django&#xff0c;內部已集成了很多組件 第三方組件。【主要】1.安裝djang…

當Claude Code失靈,Qwen Code能否成為你的救星?

當Claude Code失靈&#xff0c;Qwen Code能否成為你的救星&#xff1f; 一、開頭&#xff1a;點明困境&#xff0c;引出主角 作為一個大模型博主&#xff0c;日常工作中我經常會使用各種 AI 工具來提高效率&#xff0c;Claude Code 就是我之前非常依賴的一款代碼生成助手 。它…

Go語言快速入門教程(JAVA轉go)——1 概述

優勢 第一個理由&#xff1a;對初學者足夠友善&#xff0c;能夠快速上手。 業界都公認&#xff1a;Go 是一種非常簡單的語言。Go 的設計者們在發布 Go 1.0 版本和兼容性規范后&#xff0c;似乎就把主要精力放在精心打磨 Go 的實現、改進語言周邊工具鏈&#xff0c;還有提升 Go …

【Rust多進程】征服CPU的藝術:Rust多進程實戰指南

?? 歡迎大家來到景天科技苑?? &#x1f388;&#x1f388; 養成好習慣&#xff0c;先贊后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者簡介&#xff1a;景天科技苑 &#x1f3c6;《頭銜》&#xff1a;大廠架構師&#xff0c;華為云開發者社區專家博主&#xff0c;…