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
JavaScript
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