這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 頁面效果 具體實現 新增 1、監聽滑鼠抬起事件,通過window.getSelection()方法獲取滑鼠用戶選擇的文本範圍或游標的當前位置。 2、通過 選中的文字長度是否大於0或window.getSelection().isColla ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
頁面效果
具體實現
新增
- 1、監聽滑鼠抬起事件,通過
window.getSelection()
方法獲取滑鼠用戶選擇的文本範圍或游標的當前位置。 - 2、通過
選中的文字長度是否大於0
或window.getSelection().isCollapsed
(返回一個布爾值用於描述選區的起始點和終止點是否位於一個位置,即是否框選了)來判斷是否展示標簽選擇的彈窗。 - 3、標簽選擇的彈窗採用
子絕父相
的定位方式,通過滑鼠抬起的位置確認彈窗的top
與left
值。
const TAG_WIDTH = 280 //自定義最大範圍,以保證不超過內容的最大寬度 const tagInfo = ref({ visible: false, top: 0, left: 0, }) const el = document.getElementById('text-container') //滑鼠抬起 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 } //清空重選/取消數據 resetEditTag()
const selectedText = reactive({ start: 0, end: 0, content: '', }) //獲取選取的文字數據 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) { //從左到右標註 selectedText.start = nodeValueSatrtIndex + anchorOffset selectedText.end = nodeValueSatrtIndex + focusOffset } else { //從右到左 selectedText.start = nodeValueSatrtIndex + focusOffset selectedText.end = nodeValueSatrtIndex + anchorOffset } }
javascript操作游標和選區詳情可參考文檔:blog.51cto.com/u_14524391/…
- 4、選中標簽後,採用markjs的
markRanges()
方式去創建一個選中的元素併為其添加樣式和綁定事件。 - 5、定義一個響應式的文字列表,專門記錄標記的內容,添加完元素後可追加一條已標記的數據。
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, }) }
刪除
點擊已進行標記的文字————>重選/取消彈窗顯示————>點擊取消
如何判斷點擊的文字是否已標記,通過在創建的標記元素中綁定點擊事件,觸發則表示已標記。
- 在點擊事件中記錄該標記的相關內容,如顏色,文字,起始位置,以及唯一標識id(新建時給元素添加一個id屬性,點擊時即可通過
e.target.id
獲取)
import { nanoid } from 'nanoid' //選擇標簽後 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, } } }, } )
- 點擊取消後,獲取在此前記錄的id,根據id查詢相關的標記元素
- 使用
markjs.unmark()
方法即可刪除此元素。 - 綁定的響應式數據,可使用
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, } }
重選
和取消的步驟一樣,只不過在點擊重選後,先彈出標簽彈窗,選擇標簽後,需要先刪除選中的元素,然後再新增一個標記元素。由於在標簽選擇,在標簽選擇中判斷一下是否是重選,是重選的話就需刪除後再創建元素,不是的話就代表是新增,直接新增標記元素(綜上所述)。
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) { //如若重選,則刪除後再新增標簽 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, }) }
清空標記
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( '這是標註的內容有業績還是我我很快就很快就開完如突然好幾個地方各級很大功夫資料庫二極體捍衛國家和我回家很晚十九世紀俄國激活工具和丈母娘環境和顛覆國家的高房價奧蘇愛哦因為i乙太網圖的還是覺得好看啊空間函數調用加快速度還是饑渴的發貨可是磕碰日俄和那那麼會就開始開會的資料庫和也會覺得講故事的而黃金九二額呵呵三角函數的吧合乎實際的和儘快核實當升科技看交互的介面和送二ui為人開朗少女都被你們進貨金額麥當娜錶面上的' ) 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: '標簽一', tag_color: `#DE050CFF`, tag_id: 'tag_id1', }, { tag_name: '標簽二', tag_color: `#6ADE05FF`, tag_id: 'tag_id2', }, { tag_name: '標簽三', tag_color: `#DE058BFF`, tag_id: 'tag_id3', }, { tag_name: '標簽四', tag_color: `#9205DEFF`, tag_id: 'tag_id4', }, { tag_name: '標簽五', 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('標註的數據', 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) { //如若重選,則刪除後再新增標簽 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, }) } /** * 獲取選取的文字數據 */ 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) { //從左到右標註 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 = () => { //模擬後端返回的數據 const res = [ { start: 2, //必備 end: 6, tag_color: '#DE050CFF', tag_id: 'tag_id1', tag_name: '標簽一', mark_content: '標註的內容', mark_id: 'mark_id1', }, { start: 39, end: 41, tag_color: '#6ADE05FF', tag_id: 'tag_id2', tag_name: '標簽二', mark_content: '二極體', mark_id: 'mark_id2', }, { start: 58, end: 61, tag_color: '#DE058BFF', tag_id: 'tag_id3', tag_name: '標簽三', 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') //滑鼠抬起 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 } //清空重選/取消數據 resetEditTag() }) //從後端獲取標註數據,進行初始化標註 drawMark() }) </script> <template> <header> <n-button type="primary" :disabled="selectedTextList.length == 0 ? true : false" ghost @click="handleAllDelete" > 清空標記 </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> <!-- 標簽選擇 --> <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>