mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { useMainStore, useSlidesStore } from '@/store'
|
|
import type { PPTTableElement } from '@/types/slides'
|
|
import message from '@/utils/message'
|
|
|
|
interface SearchTextResult {
|
|
elType: 'text' | 'shape'
|
|
slideId: string
|
|
elId: string
|
|
}
|
|
interface SearchTableResult {
|
|
elType: 'table'
|
|
slideId: string
|
|
elId: string
|
|
cellIndex: [number, number]
|
|
}
|
|
|
|
type SearchResult = SearchTextResult | SearchTableResult
|
|
|
|
type Modifiers = 'g' | 'gi'
|
|
|
|
export default () => {
|
|
const mainStore = useMainStore()
|
|
const slidesStore = useSlidesStore()
|
|
const { handleElement } = storeToRefs(mainStore)
|
|
const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore)
|
|
|
|
const searchWord = ref('')
|
|
const replaceWord = ref('')
|
|
const searchResults = ref<SearchResult[]>([])
|
|
const searchIndex = ref(-1)
|
|
|
|
const modifiers = ref<Modifiers>('g')
|
|
|
|
const search = () => {
|
|
const textList: SearchResult[] = []
|
|
const matchRegex = new RegExp(searchWord.value, modifiers.value)
|
|
const textRegex = /(<([^>]+)>)/g
|
|
|
|
for (const slide of slides.value) {
|
|
for (const el of slide.elements) {
|
|
if (el.type === 'text') {
|
|
const text = el.content.replace(textRegex, '')
|
|
const rets = text.match(matchRegex)
|
|
rets && textList.push(...new Array(rets.length).fill({
|
|
slideId: slide.id,
|
|
elId: el.id,
|
|
elType: el.type,
|
|
}))
|
|
}
|
|
else if (el.type === 'shape' && el.text && el.text.content) {
|
|
const text = el.text.content.replace(textRegex, '')
|
|
const rets = text.match(matchRegex)
|
|
rets && textList.push(...new Array(rets.length).fill({
|
|
slideId: slide.id,
|
|
elId: el.id,
|
|
elType: el.type,
|
|
}))
|
|
}
|
|
else if (el.type === 'table') {
|
|
for (let i = 0; i < el.data.length; i++) {
|
|
const row = el.data[i]
|
|
for (let j = 0; j < row.length; j++) {
|
|
const cell = row[j]
|
|
if (!cell.text) continue
|
|
const text = cell.text.replace(textRegex, '')
|
|
const rets = text.match(matchRegex)
|
|
rets && textList.push(...new Array(rets.length).fill({
|
|
slideId: slide.id,
|
|
elId: el.id,
|
|
elType: el.type,
|
|
cellIndex: [i, j],
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (textList.length) {
|
|
searchResults.value = textList
|
|
searchIndex.value = 0
|
|
highlightCurrentSlide()
|
|
}
|
|
else {
|
|
message.warning('未查找到匹配项')
|
|
clearMarks()
|
|
}
|
|
}
|
|
|
|
const getTextNodeList = (dom: Node): Text[] => {
|
|
const nodeList = [...dom.childNodes]
|
|
const textNodes = []
|
|
while (nodeList.length) {
|
|
const node = nodeList.shift()!
|
|
if (node.nodeType === node.TEXT_NODE) {
|
|
(node as Text).wholeText && textNodes.push(node as Text)
|
|
}
|
|
else {
|
|
nodeList.unshift(...node.childNodes)
|
|
}
|
|
}
|
|
return textNodes
|
|
}
|
|
|
|
const getTextInfoList = (textNodes: Text[]) => {
|
|
let length = 0
|
|
const textList = textNodes.map(node => {
|
|
const startIdx = length, endIdx = length + node.wholeText.length
|
|
length = endIdx
|
|
return {
|
|
text: node.wholeText,
|
|
startIdx,
|
|
endIdx
|
|
}
|
|
})
|
|
return textList
|
|
}
|
|
|
|
type TextInfoList = ReturnType<typeof getTextInfoList>
|
|
|
|
const getMatchList = (content: string, keyword: string) => {
|
|
const reg = new RegExp(keyword, modifiers.value)
|
|
const matchList = []
|
|
let match = reg.exec(content)
|
|
while (match) {
|
|
matchList.push(match)
|
|
match = reg.exec(content)
|
|
}
|
|
return matchList
|
|
}
|
|
|
|
const highlight = (textNodes: Text[], textList: TextInfoList, matchList: RegExpExecArray[], index: number) => {
|
|
for (let i = matchList.length - 1; i >= 0; i--) {
|
|
const match = matchList[i]
|
|
const matchStart = match.index
|
|
const matchEnd = matchStart + match[0].length
|
|
|
|
for (let textIdx = 0; textIdx < textList.length; textIdx++) {
|
|
const { text, startIdx, endIdx } = textList[textIdx]
|
|
if (endIdx < matchStart) continue
|
|
if (startIdx >= matchEnd) break
|
|
|
|
let textNode = textNodes[textIdx]
|
|
const nodeMatchStartIdx = Math.max(0, matchStart - startIdx)
|
|
const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx
|
|
|
|
if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx)
|
|
if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)
|
|
|
|
const mark = document.createElement('mark')
|
|
mark.dataset.index = index + i + ''
|
|
mark.innerText = text.substring(nodeMatchStartIdx, nodeMatchStartIdx + nodeMatchLength)
|
|
textNode.parentNode!.replaceChild(mark, textNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
const highlightTableText = (nodes: NodeListOf<Element>, index: number) => {
|
|
for (const node of nodes) {
|
|
node.innerHTML = node.innerHTML.replace(new RegExp(searchWord.value, modifiers.value), () => {
|
|
return `<mark data-index=${index++}>${searchWord.value}</mark>`
|
|
})
|
|
}
|
|
}
|
|
|
|
const clearMarks = () => {
|
|
const markNodes = document.querySelectorAll('.editable-element mark')
|
|
for (const mark of markNodes) {
|
|
setTimeout(() => {
|
|
const parentNode = mark.parentNode!
|
|
const text = mark.textContent!
|
|
parentNode.replaceChild(document.createTextNode(text), mark)
|
|
}, 0)
|
|
}
|
|
}
|
|
|
|
const highlightCurrentSlide = () => {
|
|
clearMarks()
|
|
|
|
setTimeout(() => {
|
|
for (let i = 0; i < searchResults.value.length; i++) {
|
|
const lastTarget = searchResults.value[i - 1]
|
|
const target = searchResults.value[i]
|
|
if (target.slideId !== currentSlide.value.id) continue
|
|
if (lastTarget && lastTarget.elId === target.elId) continue
|
|
|
|
const node = document.querySelector(`#editable-element-${target.elId}`)
|
|
if (node) {
|
|
if (target.elType === 'table') {
|
|
const cells = node.querySelectorAll('.cell-text')
|
|
highlightTableText(cells, i)
|
|
}
|
|
else {
|
|
const textNodes = getTextNodeList(node)
|
|
const textList = getTextInfoList(textNodes)
|
|
const content = textList.map(({ text }) => text).join('')
|
|
const matchList = getMatchList(content, searchWord.value)
|
|
highlight(textNodes, textList, matchList, i)
|
|
}
|
|
}
|
|
}
|
|
}, 0)
|
|
}
|
|
|
|
const setActiveMark = () => {
|
|
const markNodes = document.querySelectorAll('mark[data-index]')
|
|
for (const node of markNodes) {
|
|
setTimeout(() => {
|
|
const index = (node as HTMLElement).dataset.index
|
|
if (index !== undefined && +index === searchIndex.value) {
|
|
node.classList.add('active')
|
|
}
|
|
else node.classList.remove('active')
|
|
}, 0)
|
|
}
|
|
}
|
|
|
|
const turnTarget = () => {
|
|
if (searchIndex.value === -1) return
|
|
|
|
const target = searchResults.value[searchIndex.value]
|
|
|
|
if (target.slideId === currentSlide.value.id) setTimeout(setActiveMark, 0)
|
|
else {
|
|
const index = slides.value.findIndex(slide => slide.id === target.slideId)
|
|
if (index !== -1) slidesStore.updateSlideIndex(index)
|
|
}
|
|
}
|
|
|
|
const searchNext = () => {
|
|
if (!searchWord.value) return message.warning('请先输入查找内容')
|
|
mainStore.setActiveElementIdList([])
|
|
if (searchIndex.value === -1) search()
|
|
else if (searchIndex.value < searchResults.value.length - 1) searchIndex.value += 1
|
|
else searchIndex.value = 0
|
|
turnTarget()
|
|
}
|
|
|
|
const searchPrev = () => {
|
|
if (!searchWord.value) return message.warning('请先输入查找内容')
|
|
mainStore.setActiveElementIdList([])
|
|
if (searchIndex.value === -1) search()
|
|
else if (searchIndex.value > 0) searchIndex.value -= 1
|
|
else searchIndex.value = searchResults.value.length - 1
|
|
turnTarget()
|
|
}
|
|
|
|
const replace = () => {
|
|
if (!searchWord.value) return
|
|
if (searchIndex.value === -1) {
|
|
searchNext()
|
|
return
|
|
}
|
|
|
|
const target = searchResults.value[searchIndex.value]
|
|
let targetElement = null
|
|
if (target.elType === 'table') {
|
|
const [i, j] = target.cellIndex
|
|
targetElement = document.querySelector(`#editable-element-${target.elId} .cell[data-cell-index="${i}_${j}"] .cell-text`)
|
|
}
|
|
else targetElement = document.querySelector(`#editable-element-${target.elId} .ProseMirror`)
|
|
if (!targetElement) return
|
|
|
|
const fakeElement = document.createElement('div')
|
|
fakeElement.innerHTML = targetElement.innerHTML
|
|
|
|
let replaced = false
|
|
const marks = fakeElement.querySelectorAll('mark[data-index]')
|
|
for (const mark of marks) {
|
|
const parentNode = mark.parentNode!
|
|
if (mark.classList.contains('active')) {
|
|
if (replaced) parentNode.removeChild(mark)
|
|
else {
|
|
parentNode.replaceChild(document.createTextNode(replaceWord.value), mark)
|
|
replaced = true
|
|
}
|
|
}
|
|
else {
|
|
const text = mark.textContent!
|
|
parentNode.replaceChild(document.createTextNode(text), mark)
|
|
}
|
|
}
|
|
|
|
if (target.elType === 'text') {
|
|
const props = { content: fakeElement.innerHTML }
|
|
slidesStore.updateElement({ id: target.elId, props })
|
|
}
|
|
else if (target.elType === 'shape') {
|
|
const el = currentSlide.value.elements.find(item => item.id === target.elId)
|
|
if (el && el.type === 'shape' && el.text) {
|
|
const props = { text: { ...el.text, content: fakeElement.innerHTML } }
|
|
slidesStore.updateElement({ id: target.elId, props })
|
|
}
|
|
}
|
|
else if (target.elType === 'table') {
|
|
const el = currentSlide.value.elements.find(item => item.id === target.elId)
|
|
if (el && el.type === 'table') {
|
|
const data = el.data.map((row, i) => {
|
|
if (i === target.cellIndex[0]) {
|
|
return row.map((cell, j) => {
|
|
if (j === target.cellIndex[1]) {
|
|
return {
|
|
...cell,
|
|
text: fakeElement.innerHTML,
|
|
}
|
|
}
|
|
return cell
|
|
})
|
|
}
|
|
return row
|
|
})
|
|
const props = { data }
|
|
slidesStore.updateElement({ id: target.elId, props })
|
|
}
|
|
}
|
|
|
|
searchResults.value.splice(searchIndex.value, 1)
|
|
if (searchResults.value.length) {
|
|
if (searchIndex.value > searchResults.value.length - 1) {
|
|
searchIndex.value = 0
|
|
}
|
|
nextTick(() => {
|
|
highlightCurrentSlide()
|
|
turnTarget()
|
|
})
|
|
}
|
|
else searchIndex.value = -1
|
|
}
|
|
|
|
const replaceAll = () => {
|
|
if (!searchWord.value) return
|
|
if (searchIndex.value === -1) {
|
|
searchNext()
|
|
return
|
|
}
|
|
|
|
for (let i = 0; i < searchResults.value.length; i++) {
|
|
const lastTarget = searchResults.value[i - 1]
|
|
const target = searchResults.value[i]
|
|
if (lastTarget && lastTarget.elId === target.elId) continue
|
|
|
|
const targetSlide = slides.value.find(item => item.id === target.slideId)
|
|
if (!targetSlide) continue
|
|
const targetElement = targetSlide.elements.find(item => item.id === target.elId)
|
|
if (!targetElement) continue
|
|
|
|
const fakeElement = document.createElement('div')
|
|
if (targetElement.type === 'text') fakeElement.innerHTML = targetElement.content
|
|
else if (targetElement.type === 'shape') fakeElement.innerHTML = targetElement.text?.content || ''
|
|
|
|
if (target.elType === 'table') {
|
|
const data = (targetElement as PPTTableElement).data.map(row => {
|
|
return row.map(cell => {
|
|
if (!cell.text) return cell
|
|
return {
|
|
...cell,
|
|
text: cell.text.replace(new RegExp(searchWord.value, 'g'), replaceWord.value),
|
|
}
|
|
})
|
|
})
|
|
const props = { data }
|
|
slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
|
|
}
|
|
else {
|
|
const textNodes = getTextNodeList(fakeElement)
|
|
const textList = getTextInfoList(textNodes)
|
|
const content = textList.map(({ text }) => text).join('')
|
|
const matchList = getMatchList(content, searchWord.value)
|
|
highlight(textNodes, textList, matchList, i)
|
|
|
|
const marks = fakeElement.querySelectorAll('mark[data-index]')
|
|
let lastMarkIndex = -1
|
|
for (const mark of marks) {
|
|
const markIndex = +(mark as HTMLElement).dataset.index!
|
|
const parentNode = mark.parentNode!
|
|
if (markIndex === lastMarkIndex) parentNode.removeChild(mark)
|
|
else {
|
|
parentNode.replaceChild(document.createTextNode(replaceWord.value), mark)
|
|
lastMarkIndex = markIndex
|
|
}
|
|
}
|
|
|
|
if (target.elType === 'text') {
|
|
const props = { content: fakeElement.innerHTML }
|
|
slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
|
|
}
|
|
else if (target.elType === 'shape') {
|
|
const el = currentSlide.value.elements.find(item => item.id === target.elId)
|
|
if (el && el.type === 'shape' && el.text) {
|
|
const props = { text: { ...el.text, content: fakeElement.innerHTML } }
|
|
slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
searchResults.value = []
|
|
searchIndex.value = -1
|
|
}
|
|
|
|
const reset = () => {
|
|
searchIndex.value = -1
|
|
searchResults.value = []
|
|
|
|
if (!searchWord.value) clearMarks()
|
|
}
|
|
|
|
watch(searchWord, reset)
|
|
|
|
watch(slideIndex, () => {
|
|
nextTick(() => {
|
|
highlightCurrentSlide()
|
|
setTimeout(setActiveMark, 0)
|
|
})
|
|
})
|
|
|
|
watch(handleElement, () => {
|
|
if (handleElement.value) {
|
|
searchIndex.value = -1
|
|
searchResults.value = []
|
|
clearMarks()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(clearMarks)
|
|
|
|
const toggleModifiers = () => {
|
|
modifiers.value = modifiers.value === 'g' ? 'gi' : 'g'
|
|
reset()
|
|
}
|
|
|
|
return {
|
|
searchWord,
|
|
replaceWord,
|
|
searchResults,
|
|
searchIndex,
|
|
modifiers,
|
|
searchNext,
|
|
searchPrev,
|
|
replace,
|
|
replaceAll,
|
|
toggleModifiers,
|
|
}
|
|
} |