const HIGHLIGHT_NAME = 'bms-table-text-copy-highlight' const EDITABLE_SELECTOR = [ 'button', 'a', 'input', 'textarea', 'select', '[contenteditable="true"]', '[role="button"]', '.el-button', '.el-checkbox', '.el-radio', '.el-switch', '.el-input', '.el-select', '.el-date-editor', '.el-cascader', '.el-autocomplete', '.el-input-number' ].join(',') const contexts = new Map() let activeSelection = null let dragging = false let hasDragged = false let suppressClickRoot = null let listenersInstalled = false function supportsHighlights() { return Boolean(window.CSS?.highlights && typeof window.Highlight === 'function') } function clearHighlight() { window.CSS?.highlights?.delete(HIGHLIGHT_NAME) } function clearSelection() { activeSelection = null dragging = false hasDragged = false clearHighlight() } function getContextForCell(cell) { for (let element = cell; element; element = element.parentElement) { const context = contexts.get(element) if (context?.enabled) return context } return null } function getRows(bodyWrapper) { return Array.from(bodyWrapper.querySelectorAll('tbody > tr.el-table__row')) .filter(row => row.getClientRects().length > 0) } function resolveCell(target) { const element = target instanceof Element ? target : target?.parentElement const cell = element?.closest('td') const bodyWrapper = cell?.closest('.el-table__body-wrapper') const row = cell?.closest('tbody > tr.el-table__row') if (!cell || !bodyWrapper || !row || cell.matches('.el-table-column--selection, .el-table__expand-column')) return null if (element?.closest(EDITABLE_SELECTOR)) return null const context = getContextForCell(cell) if (!context || !context.el.contains(bodyWrapper)) return null const rows = getRows(bodyWrapper) const rowIndex = rows.indexOf(row) const colIndex = Array.from(row.children).indexOf(cell) if (rowIndex < 0 || colIndex < 0) return null return { context, bodyWrapper, rowIndex, colIndex, cell } } function cellTextRoot(cell) { return cell.querySelector('.cell') || cell } function cellTextNodes(cell) { const walker = document.createTreeWalker(cellTextRoot(cell), NodeFilter.SHOW_TEXT) const nodes = [] let node while ((node = walker.nextNode())) { if (node.nodeValue) nodes.push(node) } return nodes } function cellText(cell) { return cellTextNodes(cell).map(node => node.nodeValue).join('') } function caretOffsetInCell(cell, clientX, clientY) { const root = cellTextRoot(cell) let point = null if (document.caretRangeFromPoint) { const range = document.caretRangeFromPoint(clientX, clientY) if (range) point = { node: range.startContainer, offset: range.startOffset } } else if (document.caretPositionFromPoint) { const position = document.caretPositionFromPoint(clientX, clientY) if (position) point = { node: position.offsetNode, offset: position.offset } } if (point && (point.node === root || root.contains(point.node))) { const range = document.createRange() range.selectNodeContents(root) range.setEnd(point.node, point.offset) return Math.min(range.toString().length, cellText(cell).length) } const rect = root.getBoundingClientRect() return clientX <= rect.left + rect.width / 2 ? 0 : cellText(cell).length } function textRangesInCell(cell, startOffset, endOffset) { const ranges = [] let cursor = 0 cellTextNodes(cell).forEach(node => { const length = node.nodeValue.length const start = Math.max(startOffset, cursor) const end = Math.min(endOffset, cursor + length) if (end > start) { const range = document.createRange() range.setStart(node, start - cursor) range.setEnd(node, end - cursor) ranges.push(range) } cursor += length }) return ranges } function selectedTextSegments() { if (!activeSelection) return [] const firstRow = Math.min(activeSelection.anchorRow, activeSelection.focusRow) const lastRow = Math.max(activeSelection.anchorRow, activeSelection.focusRow) const rows = getRows(activeSelection.bodyWrapper) const segments = [] for (let rowIndex = firstRow; rowIndex <= lastRow; rowIndex += 1) { const cell = rows[rowIndex]?.children[activeSelection.colIndex] if (!cell) continue const value = cellText(cell) let start = 0 let end = value.length if (activeSelection.anchorRow === activeSelection.focusRow) { start = Math.min(activeSelection.anchorOffset, activeSelection.focusOffset) end = Math.max(activeSelection.anchorOffset, activeSelection.focusOffset) } else if (rowIndex === activeSelection.anchorRow) { if (activeSelection.anchorRow < activeSelection.focusRow) start = activeSelection.anchorOffset else end = activeSelection.anchorOffset } else if (rowIndex === activeSelection.focusRow) { if (activeSelection.focusRow > activeSelection.anchorRow) end = activeSelection.focusOffset else start = activeSelection.focusOffset } start = Math.max(0, Math.min(start, value.length)) end = Math.max(start, Math.min(end, value.length)) segments.push({ cell, start, end, text: value.slice(start, end) }) } return segments } function renderHighlight() { clearHighlight() if (!activeSelection || !supportsHighlights()) return const ranges = selectedTextSegments().flatMap(segment => textRangesInCell(segment.cell, segment.start, segment.end)) if (ranges.length) window.CSS.highlights.set(HIGHLIGHT_NAME, new window.Highlight(...ranges)) } function handleMouseDown(event) { suppressClickRoot = null clearSelection() if (event.button !== 0 || !supportsHighlights()) return const cell = resolveCell(event.target) if (!cell) return const offset = caretOffsetInCell(cell.cell, event.clientX, event.clientY) activeSelection = { context: cell.context, bodyWrapper: cell.bodyWrapper, colIndex: cell.colIndex, anchorRow: cell.rowIndex, focusRow: cell.rowIndex, anchorOffset: offset, focusOffset: offset, startX: event.clientX, startY: event.clientY } dragging = true event.preventDefault() } function handleMouseMove(event) { if (!dragging || !activeSelection) return if (Math.abs(event.clientX - activeSelection.startX) + Math.abs(event.clientY - activeSelection.startY) > 2) { hasDragged = true } const cell = resolveCell(document.elementFromPoint(event.clientX, event.clientY)) if (!cell || cell.context !== activeSelection.context || cell.bodyWrapper !== activeSelection.bodyWrapper || cell.colIndex !== activeSelection.colIndex) return const offset = caretOffsetInCell(cell.cell, event.clientX, event.clientY) if (activeSelection.focusRow === cell.rowIndex && activeSelection.focusOffset === offset) return activeSelection.focusRow = cell.rowIndex activeSelection.focusOffset = offset renderHighlight() event.preventDefault() } function handleMouseUp() { if (!dragging) return dragging = false if (hasDragged && activeSelection) { suppressClickRoot = activeSelection.context.el requestAnimationFrame(() => { suppressClickRoot = null }) } } function handleDoubleClick(event) { const cell = resolveCell(event.target) if (!cell) return const textLength = cellText(cell.cell).length if (!textLength) return activeSelection = { context: cell.context, bodyWrapper: cell.bodyWrapper, colIndex: cell.colIndex, anchorRow: cell.rowIndex, focusRow: cell.rowIndex, anchorOffset: 0, focusOffset: textLength } dragging = false hasDragged = false renderHighlight() event.preventDefault() } function handleClick(event) { const root = suppressClickRoot if (!root || !root.contains(event.target)) return event.preventDefault() event.stopImmediatePropagation() suppressClickRoot = null } function handleCopy(event) { if (!activeSelection || dragging || !event.clipboardData) return const target = event.target instanceof Element ? event.target : event.target?.parentElement if (target?.closest(EDITABLE_SELECTOR)) return const text = selectedTextSegments().map(segment => segment.text).join('\n') if (!text) return event.clipboardData.setData('text/plain', text) event.preventDefault() } function installListeners() { if (listenersInstalled) return document.addEventListener('mousedown', handleMouseDown, true) document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) document.addEventListener('dblclick', handleDoubleClick) document.addEventListener('click', handleClick, true) document.addEventListener('copy', handleCopy, true) listenersInstalled = true } function removeListeners() { if (!listenersInstalled) return document.removeEventListener('mousedown', handleMouseDown, true) document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) document.removeEventListener('dblclick', handleDoubleClick) document.removeEventListener('click', handleClick, true) document.removeEventListener('copy', handleCopy, true) listenersInstalled = false } function isEnabled(value) { if (value && typeof value === 'object') return value.enabled !== false return value !== false } export default { mounted(el, binding) { contexts.set(el, { el, enabled: isEnabled(binding.value) }) installListeners() }, updated(el, binding) { const context = contexts.get(el) if (!context) return context.enabled = isEnabled(binding.value) if (!context.enabled || activeSelection?.context === context) clearSelection() }, unmounted(el) { const context = contexts.get(el) if (activeSelection?.context === context) clearSelection() contexts.delete(el) if (contexts.size === 0) removeListeners() } }