mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
275 lines
8.4 KiB
TypeScript
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;
|
|
}
|
|
}
|