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([]) const searchIndex = ref(-1) const modifiers = ref('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 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, index: number) => { for (const node of nodes) { node.innerHTML = node.innerHTML.replace(new RegExp(searchWord.value, modifiers.value), () => { return `${searchWord.value}` }) } } 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, } }