vue3??mark.js?實(shí)現(xiàn)文字標(biāo)注功能(案例代碼)
頁面效果
具體實(shí)現(xiàn)
新增
1、監(jiān)聽鼠標(biāo)抬起事件,通過 window.getSelection()
方法獲取鼠標(biāo)用戶選擇的文本范圍或光標(biāo)的當(dāng)前位置。
2、通過 選中的文字長(zhǎng)度是否大于0
或 window.getSelection().isCollapsed
(返回一個(gè)布爾值用于描述選區(qū)的起始點(diǎn)和終止點(diǎn)是否位于一個(gè)位置,即是否框選了)來判斷是否展示標(biāo)簽選擇的彈窗。
3、標(biāo)簽選擇的彈窗采用 子絕父相
的定位方式,通過鼠標(biāo)抬起的位置確認(rèn)彈窗的 top
與 left
值。
const TAG_WIDTH = 280 //自定義最大范圍,以保證不超過內(nèi)容的最大寬度 const tagInfo = ref({ visible: false, top: 0, left: 0, }) const el = document.getElementById('text-container') //鼠標(biāo)抬起 el?.addEventListener('mouseup', (e) => { const text = window?.getSelection()?.toString() || '' if (text.length > 0) { const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 tagInfo.value = { visible: true, top: e.offsetY + 40, left: left, } getSelectedTextData() } else { tagInfo.value.visible = false } //清空重選/取消數(shù)據(jù) resetEditTag()
const selectedText = reactive({ start: 0, end: 0, content: '', }) //獲取選取的文字?jǐn)?shù)據(jù) const getSelectedTextData = () => { const select = window?.getSelection() as any console.log('selectselectselectselect', select) const nodeValue = select.focusNode?.nodeValue const anchorOffset = select.anchorOffset const focusOffset = select.focusOffset const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue) selectedText.content = select.toString() if (anchorOffset < focusOffset) { //從左到右標(biāo)注 selectedText.start = nodeValueSatrtIndex + anchorOffset selectedText.end = nodeValueSatrtIndex + focusOffset } else { //從右到左 selectedText.start = nodeValueSatrtIndex + focusOffset selectedText.end = nodeValueSatrtIndex + anchorOffset } }
javascript操作光標(biāo)和選區(qū)詳情可參考文檔:http://www.dbjr.com.cn/article/94012.htm
- 4、選中標(biāo)簽后,采用markjs的
markRanges()
方式去創(chuàng)建一個(gè)選中的元素并為其添加樣式和綁定事件。 - 5、定義一個(gè)響應(yīng)式的文字列表,專門記錄標(biāo)記的內(nèi)容,添加完元素后可追加一條已標(biāo)記的數(shù)據(jù)。
import Mark from 'mark.js' import {ref} from 'vue import { nanoid } from 'nanoid' const selectedTextList = ref([]) const handleSelectLabel = (t) => { const marker = new Mark(document.getElementById('text-container')) const { tag_color, tag_name, tag_id } = t const markId = nanoid(10) marker.markRanges( [ { start: selectedText.start, //必填 length: selectedText.content.length, //必填 }, ], { className: 'text-selected', element: 'span', each: (element: any) => { //為元素添加樣式和屬性 element.setAttribute('id', markId) element.style.borderBottom = `2px solid ${t.tag_color}` //添加下劃線 element.style.color = t.tag_color //綁定事件 element.onclick = function (e: any) { // } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: selectedText.start, end: selectedText.end, mark_content:selectedText.content, mark_id: markId, }) }
刪除
點(diǎn)擊已進(jìn)行標(biāo)記的文字————>重選/取消彈窗顯示————>點(diǎn)擊取消
如何判斷點(diǎn)擊的文字是否已標(biāo)記,通過在創(chuàng)建的標(biāo)記元素中綁定點(diǎn)擊事件,觸發(fā)則表示已標(biāo)記。
在點(diǎn)擊事件中記錄該標(biāo)記的相關(guān)內(nèi)容,如顏色,文字,起始位置,以及唯一標(biāo)識(shí)id(新建時(shí)給元素添加一個(gè)id屬性,點(diǎn)擊時(shí)即可通過 e.target.id
獲取)
import { nanoid } from 'nanoid' //選擇標(biāo)簽后 const markId = nanoid(10) marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: 'text-selected', element: 'span', each: (element: any) => { element.setAttribute('id', markId) //綁定事件 element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || '', tag_id: tag_id || '', start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } )
3.點(diǎn)擊取消后,獲取在此前記錄的id,根據(jù)id查詢相關(guān)的標(biāo)記元素
- 使用
markjs.unmark()
方法即可刪除此元素。 - 綁定的響應(yīng)式數(shù)據(jù),可使用
findIndex
和splice()
刪除編輯彈窗隱藏
const handleCancel = () => { if (!editTag.value.mark_id) return const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) tagInfo.value = { visible: false, top: 0, left: 0, } resetEditTag() } const resetEditTag = () => { editTag.value = { visible: false, top: 0, left: 0, mark_id: '', content: '', tag_id: '', start: 0, end: 0, } }
重選
和取消的步驟一樣,只不過在點(diǎn)擊重選后,先彈出標(biāo)簽彈窗,選擇標(biāo)簽后,需要先刪除選中的元素,然后再新增一個(gè)標(biāo)記元素。由于在標(biāo)簽選擇,在標(biāo)簽選擇中判斷一下是否是重選,是重選的話就需刪除后再創(chuàng)建元素,不是的話就代表是新增,直接新增標(biāo)記元素(綜上所述)。
const handleSelectLabel = (t: TTag) => { tagInfo.value.visible = false const { tag_color, tag_name, tag_id } = t const marker = new Mark(document.getElementById('text-container')) const markId = nanoid(10) const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id) ? 1 : 0 // 1:重選 0:新增 if (isReset) { //如若重選,則刪除后再新增標(biāo)簽 const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) } marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: 'text-selected', element: 'span', each: (element: any) => { element.setAttribute('id', markId) element.style.borderBottom = `2px solid ${t.tag_color}` element.style.color = t.tag_color element.style.userSelect = 'none' element.style.paddingBottom = '6px' element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || '', tag_id: tag_id || '', start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: isReset ? editTag.value.start : selectedText.start, end: isReset ? editTag.value.end : selectedText.end, mark_content: isReset ? editTag.value.content : selectedText.content, mark_id: markId, }) }
清空標(biāo)記
const handleAllDelete = () => { selectedTextList.value = [] const marker = new Mark(document.getElementById('text-container')) marker.unmark() }
完整代碼
<script setup lang="ts"> import { ref, onMounted, reactive } from 'vue' import Mark from 'mark.js' import { nanoid } from 'nanoid' type TTag = { tag_name: string tag_id: string tag_color: string } type TSelectText = { tag_id: string tag_name: string tag_color: string start: number end: number mark_content: string mark_id: string } const TAG_WIDTH = 280 const selectedTextList = ref<TSelectText[]>([]) const selectedText = reactive({ start: 0, end: 0, content: '', }) const markContent = ref( '這是標(biāo)注的內(nèi)容有業(yè)績(jī)還是我我很快就很快就開完如突然好幾個(gè)地方各級(jí)很大功夫數(shù)據(jù)庫(kù)二極管捍衛(wèi)國(guó)家和我回家很晚十九世紀(jì)俄國(guó)激活工具和丈母娘環(huán)境和顛覆國(guó)家的高房?jī)r(jià)奧蘇愛哦因?yàn)閕以太網(wǎng)圖的還是覺得好看啊空間函數(shù)調(diào)用加快速度還是饑渴的發(fā)貨可是磕碰日俄和那那么會(huì)就開始開會(huì)的數(shù)據(jù)庫(kù)和也會(huì)覺得講故事的而黃金九二額呵呵三角函數(shù)的吧合乎實(shí)際的和盡快核實(shí)當(dāng)升科技看交互的接口和送二ui為人開朗少女都被你們進(jìn)貨金額麥當(dāng)娜表面上的' ) const tagInfo = ref({ visible: false, top: 0, left: 0, }) const editTag = ref({ visible: false, top: 0, left: 0, mark_id: '', content: '', tag_id: '', start: 0, end: 0, }) const tagList: TTag[] = [ { tag_name: '標(biāo)簽一', tag_color: `#DE050CFF`, tag_id: 'tag_id1', }, { tag_name: '標(biāo)簽二', tag_color: `#6ADE05FF`, tag_id: 'tag_id2', }, { tag_name: '標(biāo)簽三', tag_color: `#DE058BFF`, tag_id: 'tag_id3', }, { tag_name: '標(biāo)簽四', tag_color: `#9205DEFF`, tag_id: 'tag_id4', }, { tag_name: '標(biāo)簽五', tag_color: `#DE5F05FF`, tag_id: 'tag_id5', }, ] const handleAllDelete = () => { selectedTextList.value = [] const marker = new Mark(document.getElementById('text-container')) marker.unmark() } const handleCancel = () => { if (!editTag.value.mark_id) return const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) tagInfo.value = { visible: false, top: 0, left: 0, } resetEditTag() } const handleReset = () => { editTag.value.visible = false tagInfo.value.visible = true } const handleSave = () => { console.log('標(biāo)注的數(shù)據(jù)', selectedTextList.value) } const handleSelectLabel = (t: TTag) => { const { tag_color, tag_name, tag_id } = t tagInfo.value.visible = false const marker = new Mark(document.getElementById('text-container')) const markId = nanoid(10) const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id) ? 1 : 0 // 1:重選 0:新增 if (isReset) { //如若重選,則刪除后再新增標(biāo)簽 const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) } marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: 'text-selected', element: 'span', each: (element: any) => { element.setAttribute('id', markId) element.style.borderBottom = `2px solid ${t.tag_color}` element.style.color = t.tag_color element.style.userSelect = 'none' element.style.paddingBottom = '6px' element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || '', tag_id: tag_id || '', start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: isReset ? editTag.value.start : selectedText.start, end: isReset ? editTag.value.end : selectedText.end, mark_content: isReset ? editTag.value.content : selectedText.content, mark_id: markId, }) } /** * 獲取選取的文字?jǐn)?shù)據(jù) */ const getSelectedTextData = () => { const select = window?.getSelection() as any const nodeValue = select.focusNode?.nodeValue const anchorOffset = select.anchorOffset const focusOffset = select.focusOffset const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue) selectedText.content = select.toString() if (anchorOffset < focusOffset) { //從左到右標(biāo)注 selectedText.start = nodeValueSatrtIndex + anchorOffset selectedText.end = nodeValueSatrtIndex + focusOffset } else { //從右到左 selectedText.start = nodeValueSatrtIndex + focusOffset selectedText.end = nodeValueSatrtIndex + anchorOffset } } const resetEditTag = () => { editTag.value = { visible: false, top: 0, left: 0, mark_id: '', content: '', tag_id: '', start: 0, end: 0, } } const drawMark = () => { //模擬后端返回的數(shù)據(jù) const res = [ { start: 2, //必備 end: 6, tag_color: '#DE050CFF', tag_id: 'tag_id1', tag_name: '標(biāo)簽一', mark_content: '標(biāo)注的內(nèi)容', mark_id: 'mark_id1', }, { start: 39, end: 41, tag_color: '#6ADE05FF', tag_id: 'tag_id2', tag_name: '標(biāo)簽二', mark_content: '二極管', mark_id: 'mark_id2', }, { start: 58, end: 61, tag_color: '#DE058BFF', tag_id: 'tag_id3', tag_name: '標(biāo)簽三', mark_content: '激活工具', mark_id: 'mark_id3', }, ] selectedTextList.value = res?.map((t) => ({ tag_id: t.tag_id, tag_name: t.tag_name, tag_color: t.tag_color, start: t.start, end: t.end, mark_content: t.mark_content, mark_id: t.mark_id, })) const markList = selectedTextList.value?.map((j) => ({ ...j, start: j.start, //必備 length: j.end - j.start + 1, //必備 })) || [] const marker = new Mark(document.getElementById('text-container')) markList?.forEach?.(function (m: any) { marker.markRanges([m], { element: 'span', className: 'text-selected', each: (element: any) => { element.setAttribute('id', m.mark_id) element.style.borderBottom = `2px solid ${m.tag_color}` element.style.color = m.tag_color element.style.userSelect = 'none' element.style.paddingBottom = '6px' element.onclick = function (e: any) { console.log('cccccc', m) const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: m.mark_id, content: m.mark_content, tag_id: m.tag_id, start: m.start, end: m.end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, }) }) } //頁面初始化 onMounted(() => { const el = document.getElementById('text-container') //鼠標(biāo)抬起 el?.addEventListener('mouseup', (e) => { const text = window?.getSelection()?.toString() || '' if (text.length > 0) { const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300 tagInfo.value = { visible: true, top: e.offsetY + 40, left: left, } getSelectedTextData() } else { tagInfo.value.visible = false } //清空重選/取消數(shù)據(jù) resetEditTag() }) //從后端獲取標(biāo)注數(shù)據(jù),進(jìn)行初始化標(biāo)注 drawMark() }) </script> <template> <header> <n-button type="primary" :disabled="selectedTextList.length == 0 ? true : false" ghost @click="handleAllDelete" > 清空標(biāo)記 </n-button> <n-button type="primary" :disabled="selectedTextList.length == 0 ? true : false" @click="handleSave" > 保存 </n-button> </header> <main> <div id="text-container" class="text"> {{ markContent }} </div> <!-- 標(biāo)簽選擇 --> <div v-if="tagInfo.visible && tagList.length > 0" :class="['tag-box p-4 ']" :style="{ top: tagInfo.top + 'px', left: tagInfo.left + 'px' }" > <div v-for="i in tagList" :key="i.tag_id" class="tag-name" @click="handleSelectLabel(i)"> <n-space> <p>{{ i.tag_name }}</p> <n-button v-if="i.tag_id == editTag.tag_id" text type="primary">√</n-button> </n-space> <div :class="['w-4 h-4']" :style="{ background: i.tag_color, }" ></div> </div> </div> <!-- 重選/取消 --> <div v-if="editTag.visible" class="edit-tag" :style="{ top: editTag.top + 'px', left: editTag.left + 'px' }" > <div class="py-1 bg-gray-100 text-center" @click="handleCancel">取 消</div> <div class="py-1 bg-gray-100 mt-2 text-center" @click="handleReset">重 選</div> </div> </main> </template> <style lang="less" scoped> header { display: flex; justify-content: space-between; align-items: center; padding: 0 24px; height: 80px; border-bottom: 1px solid #e5e7eb; user-select: none; background: #fff; } main { background: #fff; margin: 24px; height: 80vh; padding: 24px; overflow-y: auto; position: relative; box-shadow: 0 3px 8px 0 rgb(0 0 0 / 13%); .text { color: #333; font-weight: 500; font-size: 16px; line-height: 50px; } .tag-box { position: absolute; z-index: 10; width: 280px; max-height: 40vh; overflow-y: auto; background: #fff; border-radius: 4px; box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%), 0 3px 6px -2px rgb(0 0 0 / 20%); user-select: none; .tag-name { width: 100%; background: rgba(243, 244, 246, var(--tw-bg-opacity)); font-size: 14px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; margin-top: 8px; } .tag-name:nth-of-type(1) { margin-top: 0; } } .edit-tag { position: absolute; z-index: 20; padding: 16px; cursor: pointer; width: 100px; background: #fff; border-radius: 4px; box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%), 0 3px 6px -2px rgb(0 0 0 / 20%); user-select: none; } ::selection { background: rgb(51 51 51 / 20%); } } </style>
結(jié)束語
目前功能實(shí)現(xiàn)比較簡(jiǎn)單,還有很多發(fā)揮的空間,先小小的記錄一下,最后~,預(yù)祝大家,雙節(jié)快樂??!
到此這篇關(guān)于vue3 mark.js 實(shí)現(xiàn)文字標(biāo)注功能的文章就介紹到這了,更多相關(guān)vue3 mark.js文字標(biāo)注內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue上傳組件vue Simple Uploader的用法示例
本篇文章主要介紹了Vue上傳組件vue Simple Uploader的用法示例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08vuex直接修改state、commit和dispatch修改state的用法及區(qū)別說明
這篇文章主要介紹了vuex直接修改state、commit和dispatch修改state的用法及區(qū)別說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08詳細(xì)講一講vue3下會(huì)造成響應(yīng)式丟失的情況
vue3開發(fā)過程中,綁定的響應(yīng)式數(shù)據(jù)失去了響應(yīng)式,如何解決問題呢,下面這篇文章主要給大家介紹了關(guān)于vue3下會(huì)造成響應(yīng)式丟失的情況,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06vue項(xiàng)目打包時(shí)自動(dòng)更新版本號(hào)的實(shí)現(xiàn)方法
本文主要介紹了vue項(xiàng)目打包時(shí)自動(dòng)更新版本號(hào)的實(shí)現(xiàn)方法,通過在根目錄下創(chuàng)建autoVersion.js腳本文件,頁面獲取版本號(hào)時(shí)直接使用,修改package.json配置,感興趣的可以了解一下2025-02-02Vue.Draggable實(shí)現(xiàn)交換位置
這篇文章主要為大家詳細(xì)介紹了Vue.Draggable實(shí)現(xiàn)交換位置,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04vue實(shí)現(xiàn)點(diǎn)擊按鈕“查看詳情”彈窗展示詳情列表操作
這篇文章主要介紹了vue實(shí)現(xiàn)點(diǎn)擊按鈕“查看詳情”彈窗展示詳情列表操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09