頁面效果 具體實現 新增 1、監聽滑鼠抬起事件,通過window.getSelection()方法獲取滑鼠用戶選擇的文本範圍或游標的當前位置。 2、通過 選中的文字長度是否大於0或window.getSelection().isCollapsed (返回一個布爾值用於描述選區的起始點和終止點是否位於 ...
頁面效果
具體實現
新增
- 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操作游標和選區詳情可參考文檔:https://blog.51cto.com/u_14524391/3712814
- 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>
結束語
目前功能實現比較簡單,還有很多發揮的空間,先小小的記錄一下,最後~,預祝大家,雙節快樂!!