Files
AFFiNE-Mirror/blocksuite/affine/block-table/src/selection-controller.ts
T

275 lines
8.4 KiB
TypeScript

import {
domToOffsets,
getAreaByOffsets,
} from '@blocksuite/affine-shared/utils';
import type { UIEventStateContext } from '@blocksuite/block-std';
import { IS_MOBILE } from '@blocksuite/global/env';
import { computed } from '@preact/signals-core';
import type { ReactiveController } from 'lit';
import { ColumnMinWidth, DefaultColumnWidth } from './consts';
import {
type TableAreaSelection,
TableSelection,
TableSelectionData,
} from './selection-schema';
import type { TableBlockComponent } from './table-block';
type Cells = string[][];
const TEXT = 'text/plain';
export class SelectionController implements ReactiveController {
constructor(public readonly host: TableBlockComponent) {
this.host.addController(this);
}
hostConnected() {
this.dragListener();
this.host.handleEvent('copy', this.onCopy);
this.host.handleEvent('cut', this.onCut);
this.host.handleEvent('paste', this.onPaste);
}
private get dataManager() {
return this.host.dataManager;
}
private get clipboard() {
return this.host.std.clipboard;
}
widthAdjust(dragHandle: HTMLElement, event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const initialX = event.clientX;
const currentWidth =
dragHandle.closest('td')?.getBoundingClientRect().width ??
DefaultColumnWidth;
const columnId = dragHandle.dataset['widthAdjustColumnId'];
if (!columnId) {
return;
}
const onMove = (event: MouseEvent) => {
this.dataManager.draggingColumnId$.value = columnId;
this.dataManager.virtualWidth$.value = {
columnId,
width: Math.max(
ColumnMinWidth,
event.clientX - initialX + currentWidth
),
};
};
const onUp = () => {
const width = this.dataManager.virtualWidth$.value?.width;
this.dataManager.draggingColumnId$.value = undefined;
this.dataManager.virtualWidth$.value = undefined;
if (width) {
this.dataManager.setColumnWidth(columnId, width);
}
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
dragListener() {
if (IS_MOBILE) {
return;
}
this.host.disposables.addFromEvent(this.host, 'mousedown', event => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const dragHandle = target.closest('[data-width-adjust-column-id]');
if (dragHandle instanceof HTMLElement) {
this.widthAdjust(dragHandle, event);
return;
}
this.onDragStart(event);
});
}
readonly doCopyOrCut = (selection: TableAreaSelection, isCut: boolean) => {
const columns = this.dataManager.uiColumns$.value;
const rows = this.dataManager.uiRows$.value;
const cells: Cells = [];
const deleteCells: { rowId: string; columnId: string }[] = [];
for (let i = selection.rowStartIndex; i <= selection.rowEndIndex; i++) {
const row = rows[i];
if (!row) {
continue;
}
const rowCells: string[] = [];
for (
let j = selection.columnStartIndex;
j <= selection.columnEndIndex;
j++
) {
const column = columns[j];
if (!column) {
continue;
}
const cell = this.dataManager.getCell(row.rowId, column.columnId);
rowCells.push(cell?.text.toString() ?? '');
if (isCut) {
deleteCells.push({ rowId: row.rowId, columnId: column.columnId });
}
}
cells.push(rowCells);
}
if (isCut) {
this.dataManager.clearCells(deleteCells);
}
const text = cells.map(row => row.join('\t')).join('\n');
this.clipboard
.writeToClipboard(items => ({
...items,
[TEXT]: text,
}))
.catch(console.error);
};
onCopy = () => {
const selection = this.getSelected();
if (!selection || selection.type !== 'area') {
return false;
}
this.doCopyOrCut(selection, false);
return true;
};
onCut = () => {
const selection = this.getSelected();
if (!selection || selection.type !== 'area') {
return false;
}
this.doCopyOrCut(selection, true);
return true;
};
doPaste = (plainText: string, selection: TableAreaSelection) => {
try {
const rowTextLists = plainText
.split(/\r?\n/)
.map(line => line.split('\t').map(cell => cell.trim()))
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
const height = rowTextLists.length;
const width = rowTextLists[0]?.length ?? 0;
if (height > 0 && width > 0) {
const columns = this.dataManager.uiColumns$.value;
const rows = this.dataManager.uiRows$.value;
for (let i = selection.rowStartIndex; i <= selection.rowEndIndex; i++) {
const row = rows[i];
if (!row) {
continue;
}
for (
let j = selection.columnStartIndex;
j <= selection.columnEndIndex;
j++
) {
const column = columns[j];
if (!column) {
continue;
}
const text = this.dataManager.getCell(
row.rowId,
column.columnId
)?.text;
if (text) {
const rowIndex = (i - selection.rowStartIndex) % height;
const columnIndex = (j - selection.columnStartIndex) % width;
text.replace(
0,
text.length,
rowTextLists[rowIndex]?.[columnIndex] ?? ''
);
}
}
}
}
} catch (error) {
console.error(error);
}
};
onPaste = (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw;
event.stopPropagation();
const clipboardData = event.clipboardData;
if (!clipboardData) return false;
const selection = this.getSelected();
if (!selection || selection.type !== 'area') {
return false;
}
const plainText = clipboardData.getData('text/plain');
this.doPaste(plainText, selection);
return true;
};
onDragStart(event: MouseEvent) {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const offsets = domToOffsets(this.host, 'tr', 'td');
if (!offsets) return;
const startX = event.clientX;
const startY = event.clientY;
let selected = false;
const initCell = target.closest('affine-table-cell');
if (!initCell) {
selected = true;
}
const onMove = (event: MouseEvent) => {
const target = event.target;
if (target instanceof HTMLElement) {
const cell = target.closest('affine-table-cell');
if (!selected && initCell === cell) {
return;
}
selected = true;
const endX = event.clientX;
const endY = event.clientY;
const [left, right] = startX > endX ? [endX, startX] : [startX, endX];
const [top, bottom] = startY > endY ? [endY, startY] : [startY, endY];
const area = getAreaByOffsets(offsets, top, bottom, left, right);
this.setSelected({
type: 'area',
rowStartIndex: area.top,
rowEndIndex: area.bottom,
columnStartIndex: area.left,
columnEndIndex: area.right,
});
}
};
const onUp = () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
setSelected(
selection: TableSelectionData | undefined,
removeNativeSelection = true
) {
if (selection) {
const previous = this.getSelected();
if (TableSelectionData.equals(previous, selection)) {
return;
}
if (removeNativeSelection) {
getSelection()?.removeAllRanges();
}
this.host.selection.set([
new TableSelection({
blockId: this.host.model.id,
data: selection,
}),
]);
} else {
this.host.selection.clear();
}
}
selected$ = computed(() => this.getSelected());
getSelected(): TableSelectionData | undefined {
const selection = this.host.selection.value.find(
selection => selection.blockId === this.host.model.id
);
return selection?.is(TableSelection) ? selection.data : undefined;
}
}