diff --git a/src/components/page/components/table/word.vue b/src/components/page/components/table/word.vue index 9a62071..9454e7c 100644 --- a/src/components/page/components/table/word.vue +++ b/src/components/page/components/table/word.vue @@ -1208,10 +1208,16 @@ export default { hasChange: false, hasInit: false, selectedIds: [], + isMouseSelecting: false, + _selectionSyncToCheckboxesTimer: null, + _onDocumentSelectionChange: null, + _onDocumentMouseUp: null, + _onManuscriptMouseDown: null, displayList: [], currentTypeText: '', tinymceId: this.id || 'vue-tinymce-' + +new Date() + }; }, // this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(newVal)); @@ -1274,51 +1280,60 @@ export default { }); this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true }); - document.addEventListener('selectionchange', () => { - if(this.isPreview)return; - const selection = window.getSelection(); - if (selection.rangeCount === 0) return; + // document.addEventListener('selectionchange', () => { + // if(this.isPreview)return; + // const selection = window.getSelection(); + // if (selection.rangeCount === 0) return; - const range = selection.getRangeAt(0); - // 依然保留 trim() 后的文本判断,用来决定是否显示气泡 - const plainText = selection.toString().trim(); + // const range = selection.getRangeAt(0); + // // 依然保留 trim() 后的文本判断,用来决定是否显示气泡 + // const plainText = selection.toString().trim(); - if (plainText !== '' && selection.rangeCount > 0) { - // --- 1. 获取包含标签的 HTML 内容 --- - const fragment = range.cloneContents(); - const tempDiv = document.createElement('div'); - tempDiv.appendChild(fragment); - // 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 ) - const htmlLabel = tempDiv.innerHTML; - const allPMainElements = this.getInvolvedPMain(range); - const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))]; - if (allIds.length > 0) { - this.updateBubblePosition(range); - const rootItem = this.wordList.find((item) => item.am_id == allIds[0]); + // if (plainText !== '' && selection.rangeCount > 0) { + // // --- 1. 获取包含标签的 HTML 内容 --- + // const fragment = range.cloneContents(); + // const tempDiv = document.createElement('div'); + // tempDiv.appendChild(fragment); + // // 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 ) + // const htmlLabel = tempDiv.innerHTML; + // const allPMainElements = this.getInvolvedPMain(range); + // const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))]; + // if (allIds.length > 0) { + // this.updateBubblePosition(range); + // const rootItem = this.wordList.find((item) => item.am_id == allIds[0]); - this.currentSelection = { - // 将 label 设置为包含标签的 HTML 字符串 - label: htmlLabel, - mainId: allIds[0], - index: this.wordList.indexOf(rootItem), + // this.currentSelection = { + // // 将 label 设置为包含标签的 HTML 字符串 + // label: htmlLabel, + // mainId: allIds[0], + // index: this.wordList.indexOf(rootItem), - content: rootItem ? rootItem.content : '' - }; + // content: rootItem ? rootItem.content : '' + // }; - this.currentId = allIds[0]; - this.currentData = rootItem; - } - } else { - this.currentTag = ''; - this.currentTagData = {}; - this.currentSelection = { - label: '', - mainId: '', - index: 0, - content: {} - }; - } - }); + // this.currentId = allIds[0]; + // this.currentData = rootItem; + // } + // } else { + // this.currentTag = ''; + // this.currentTagData = {}; + // this.currentSelection = { + // label: '', + // mainId: '', + // index: 0, + // content: {} + // }; + // } + // }); + this._onDocumentSelectionChange = this.handleDocumentSelectionChange.bind(this); + this._onDocumentMouseUp = this.handleDocumentMouseUp.bind(this); + this._onManuscriptMouseDown = this.handleManuscriptMouseDown.bind(this); + document.addEventListener('selectionchange', this._onDocumentSelectionChange); + document.addEventListener('mouseup', this._onDocumentMouseUp); + if (this.$refs.scroll) { + this.$refs.scroll.addEventListener('mousedown', this._onManuscriptMouseDown); + } + }, activated() { // 主动触发 MathJax 渲染 @@ -1378,6 +1393,115 @@ export default { return rangePs; }, + handleManuscriptMouseDown(event) { + if (this.isPreview) return; + const root = this.$refs && this.$refs.scroll; + if (!root) return; + if (root.contains(event.target)) { + this.isMouseSelecting = true; + } + }, + handleDocumentMouseUp() { + this.isMouseSelecting = false; + }, + handleDocumentSelectionChange() { + if (this.isPreview) return; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + const plainText = selection.toString().trim(); + + if (plainText !== '' && selection.rangeCount > 0) { + const fragment = range.cloneContents(); + const tempDiv = document.createElement('div'); + tempDiv.appendChild(fragment); + const htmlLabel = tempDiv.innerHTML; + const allPMainElements = this.getInvolvedPMain(range); + const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))]; + if (allIds.length > 0) { + this.updateBubblePosition(range); + const rootItem = this.wordList.find((item) => item.am_id == allIds[0]); + + this.currentSelection = { + label: htmlLabel, + mainId: allIds[0], + index: this.wordList.indexOf(rootItem), + content: rootItem ? rootItem.content : '' + }; + + this.currentId = allIds[0]; + this.currentData = rootItem; + } + } else { + this.currentTag = ''; + this.currentTagData = {}; + this.currentSelection = { + label: '', + mainId: '', + index: 0, + content: {} + }; + } + this.scheduleSyncSelectedIdsFromRange(); + }, + scheduleSyncSelectedIdsFromRange() { + if (this._selectionSyncToCheckboxesTimer) { + clearTimeout(this._selectionSyncToCheckboxesTimer); + } + this._selectionSyncToCheckboxesTimer = setTimeout(() => { + this.syncSelectedIdsFromRangeInternal(); + }, 80); + }, + syncSelectedIdsFromRangeInternal() { + if (this.isPreview || this.isInternalAction) return; + const scrollRoot = this.$refs && this.$refs.scroll; + if (!scrollRoot) return; + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; + const range = selection.getRangeAt(0); + const common = range.commonAncestorContainer; + const commonElement = common && common.nodeType === 1 ? common : common && common.parentElement; + if (!commonElement || !scrollRoot.contains(commonElement)) return; + + const nodes = Array.from(scrollRoot.querySelectorAll('.drop-target[main-id]')); + const touchedMainIds = []; + nodes.forEach((node) => { + const id = node.getAttribute('main-id'); + if (!id || id === 'References') return; + try { + if (range.intersectsNode(node)) { + touchedMainIds.push(id); + } + } catch (e) {} + }); + if (!touchedMainIds.length) return; + + const indexMap = {}; + this.wordList.forEach((item, idx) => { + if (item && item.am_id != null) { + indexMap[String(item.am_id)] = idx; + } + }); + const indices = [...new Set(touchedMainIds.map((id) => indexMap[String(id)]).filter((v) => Number.isInteger(v)))]; + if (!indices.length) return; + const lo = Math.min(...indices); + const hi = Math.max(...indices); + const rangeIds = this.wordList + .slice(lo, hi + 1) + .map((item) => (item && item.am_id != null ? item.am_id : null)) + .filter((id) => id != null); + if (!rangeIds.length) return; + + const existingSet = new Set(this.selectedIds || []); + rangeIds.forEach((id) => existingSet.add(id)); + const ordered = this.wordList + .map((item) => (item && item.am_id != null ? item.am_id : null)) + .filter((id) => id != null && existingSet.has(id)); + this.selectedIds = ordered; + this.$forceUpdate(); + }, handleUnbindLink(type) { const rootItem = this.wordList.find((item) => item.am_id == this.currentTagData.main_id);