UNPKG

e-virt-table

Version:

A powerful data table based on canvas. You can use it as data grid、Microsoft Excel or Google sheets. It supports virtual scroll、cell edit etc.

281 lines 9.47 kB
class TextSelector { constructor(ctx) { Object.defineProperty(this, "ctx", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "layouts", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "activeCellKey", { enumerable: true, configurable: true, writable: true, value: '' }); Object.defineProperty(this, "selectionStart", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "selectionEnd", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "pending", { enumerable: true, configurable: true, writable: true, value: null }); this.ctx = ctx; this.ctx.on('registerTextLayout', (key, layout) => this.layouts.set(key, layout)); this.ctx.on('mousedown', (e) => this.onMouseDown(e)); this.ctx.on('mousemove', (e) => this.onMouseMove(e)); this.ctx.on('mouseup', () => this.onMouseUp()); this.ctx.on('cellHeaderHoverChange', (cell) => this.yieldToColumnDrag(cell)); this.ctx.on('cellHoverChange', () => this.yieldToColumnDrag()); this.ctx.on('keydown', (e) => this.onCopyKeydown(e)); this.ctx.on('outsideMousedown', () => this.resetSelection()); } get canSelect() { return this.ctx.config.ENABLE_TEXT_SELECTION && !this.ctx.editing && !this.ctx.finding; } /** 跨表头列或从表头拖入 body 时,让位给列/区域选择 */ yieldToColumnDrag(target) { const anchor = this.ctx.focusCellHeader; if (!this.ctx.textSelecting || !anchor) { return; } if (target) { const min = anchor.colIndex; const max = anchor.colIndex + anchor.colspan - 1; const tMin = target.colIndex; const tMax = target.colIndex + target.colspan - 1; if (tMin >= min && tMax <= max) { return; } } this.resetSelection(); } onMouseDown(e) { if (!this.canSelect || !this.ctx.isTarget(e)) { return; } if (!this.ctx.containerElement.contains(document.activeElement)) { this.ctx.containerElement.focus({ preventScroll: true }); } const hit = this.hitTest(e); if (!hit) { this.resetSelection(); return; } const { offsetX, offsetY } = this.ctx.getOffset(e); this.pending = { cellKey: hit.cellKey, index: hit.index, startX: offsetX, startY: offsetY }; } onMouseMove(e) { if (!this.canSelect) { return; } if (!this.ctx.textSelecting) { this.tryStartDrag(e); } if (!this.ctx.textSelecting) { return; } const layout = this.layouts.get(this.activeCellKey); if (!layout) { return; } const { offsetX, offsetY } = this.ctx.getOffset(e); const index = this.hitTestText(layout, offsetX, offsetY); if (index !== null) { this.selectionEnd = index; this.updateSelection(); } } tryStartDrag(e) { if (!this.pending || !this.ctx.mousedown) { return; } const { offsetX, offsetY } = this.ctx.getOffset(e); const dx = Math.abs(offsetX - this.pending.startX); const dy = Math.abs(offsetY - this.pending.startY); if (dx < TextSelector.DRAG_THRESHOLD && dy < TextSelector.DRAG_THRESHOLD) { return; } const hit = this.hitTest(e); if (!hit || hit.cellKey !== this.pending.cellKey) { this.pending = null; return; } this.activeCellKey = this.pending.cellKey; this.selectionStart = this.pending.index; this.selectionEnd = hit.index; this.pending = null; this.ctx.textSelecting = true; this.updateSelection(); } onMouseUp() { this.pending = null; this.ctx.textSelecting = false; this.ctx.textSelectionStr = this.getSelectedText(); if (this.ctx.textSelectionStr) { this.ctx.emit('drawView'); } } onCopyKeydown(e) { if (!this.canSelect || !((e.ctrlKey || e.metaKey) && e.code === 'KeyC')) { return; } const text = this.getSelectedText(); if (!text) { return; } e.preventDefault(); navigator.clipboard?.writeText(text).catch((error) => console.error('Copy Failure:', error)); } /** * 命中最顶层文字(固定列/表头后绘制会覆盖普通单元格,取最后一个命中)。 */ hitTest(e) { const { offsetX, offsetY } = this.ctx.getOffset(e); let hit = null; for (const [cellKey, layout] of this.layouts) { const index = this.hitTestText(layout, offsetX, offsetY); if (index !== null) { hit = { cellKey, index }; } } return hit; } updateSelection() { this.ctx.textSelectionStr = this.getSelectedText(); this.ctx.emit('drawView'); } hitTestText(layout, mouseX, mouseY) { const { glyphs, contentY, lineHeight, lines } = layout; if (!glyphs.length) { return null; } const lineIndex = Math.floor((mouseY - contentY) / lineHeight); if (lineIndex < 0 || lineIndex >= lines.length) { return null; } const lineGlyphs = glyphs.filter((g) => g.line === lineIndex); if (!lineGlyphs.length) { return null; } const pad = 2; const lineTop = contentY + lineIndex * lineHeight; if (mouseY < lineTop - pad || mouseY >= lineTop + lineHeight + pad) { return null; } const first = lineGlyphs[0]; const last = lineGlyphs[lineGlyphs.length - 1]; if (mouseX < first.x - pad || mouseX > last.x + last.width + pad) { return null; } if (mouseX <= first.x) { return first.index; } for (const glyph of lineGlyphs) { if (mouseX < glyph.x + glyph.width / 2) { return glyph.index; } } return last.index + 1; } clearLayouts() { this.layouts.clear(); } draw() { if (!this.ctx.config.ENABLE_TEXT_SELECTION) { return; } const range = this.getSelectionRange(); const layout = range && this.layouts.get(this.activeCellKey); if (layout) { this.drawTextSelection(layout, range); } } drawTextSelection(layout, range, selectedColor = '#fff', selectionBgColor = '#3b82f6') { if (range.start >= range.end) { return; } const byLine = new Map(); for (const glyph of layout.glyphs) { if (glyph.index < range.start || glyph.index >= range.end) { continue; } const list = byLine.get(glyph.line) ?? []; list.push(glyph); byLine.set(glyph.line, list); } if (!byLine.size) { return; } const ctx = this.ctx.paint.getCtx(); ctx.save(); ctx.font = layout.font; ctx.textBaseline = 'top'; ctx.textAlign = 'left'; byLine.forEach((lineGlyphs) => { const first = lineGlyphs[0]; const last = lineGlyphs[lineGlyphs.length - 1]; ctx.fillStyle = selectionBgColor; ctx.fillRect(first.x, first.y - 1, last.x + last.width - first.x, first.height); ctx.fillStyle = selectedColor; for (const glyph of lineGlyphs) { ctx.fillText(glyph.char, glyph.x, glyph.y); } }); ctx.restore(); } getSelectionRange() { if (this.selectionStart === null || this.selectionEnd === null) { return null; } return { start: Math.min(this.selectionStart, this.selectionEnd), end: Math.max(this.selectionStart, this.selectionEnd), }; } getSelectedText() { const range = this.getSelectionRange(); const layout = range && this.layouts.get(this.activeCellKey); if (!layout || range.start >= range.end) { return ''; } return layout.chars.slice(range.start, range.end).join(''); } resetSelection() { this.activeCellKey = ''; this.selectionStart = null; this.selectionEnd = null; this.pending = null; this.ctx.textSelecting = false; this.ctx.textSelectionStr = ''; this.ctx.emit('drawView'); } destroy() { this.layouts.clear(); this.resetSelection(); } } Object.defineProperty(TextSelector, "DRAG_THRESHOLD", { enumerable: true, configurable: true, writable: true, value: 4 }); export default TextSelector; //# sourceMappingURL=TextSelector.js.map