mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(editor): table block supports drag-and-drop sorting (#10065)
close: BS-2477
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
|||||||
import { DefaultColumnWidth, DefaultRowHeight } from './consts';
|
import { DefaultColumnWidth, DefaultRowHeight } from './consts';
|
||||||
import type { TableDataManager } from './table-data-manager';
|
import type { TableDataManager } from './table-data-manager';
|
||||||
|
|
||||||
|
export const AddButtonComponentName = 'affine-table-add-button';
|
||||||
export class AddButton extends SignalWatcher(
|
export class AddButton extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
@@ -322,6 +323,6 @@ export class AddButton extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-table-add-button': AddButton;
|
[AddButtonComponentName]: AddButton;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { AddButton } from './add-button';
|
import { AddButton, AddButtonComponentName } from './add-button';
|
||||||
import { SelectionLayer } from './selection-layer';
|
import { SelectionLayer, SelectionLayerComponentName } from './selection-layer';
|
||||||
import { TableBlockComponent } from './table-block';
|
import { TableBlockComponent, TableBlockComponentName } from './table-block';
|
||||||
import { TableCell } from './table-cell';
|
import { TableCell, TableCellComponentName } from './table-cell';
|
||||||
|
|
||||||
export function effects() {
|
export function effects() {
|
||||||
customElements.define('affine-table', TableBlockComponent);
|
customElements.define(TableBlockComponentName, TableBlockComponent);
|
||||||
customElements.define('affine-table-cell', TableCell);
|
customElements.define(TableCellComponentName, TableCell);
|
||||||
customElements.define('affine-table-add-button', AddButton);
|
customElements.define(AddButtonComponentName, AddButton);
|
||||||
customElements.define('affine-table-selection-layer', SelectionLayer);
|
customElements.define(SelectionLayerComponentName, SelectionLayer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
domToOffsets,
|
domToOffsets,
|
||||||
getAreaByOffsets,
|
getAreaByOffsets,
|
||||||
|
getTargetIndexByDraggingOffset,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import type { UIEventStateContext } from '@blocksuite/block-std';
|
import type { UIEventStateContext } from '@blocksuite/block-std';
|
||||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||||
@@ -14,6 +15,13 @@ import {
|
|||||||
TableSelectionData,
|
TableSelectionData,
|
||||||
} from './selection-schema';
|
} from './selection-schema';
|
||||||
import type { TableBlockComponent } from './table-block';
|
import type { TableBlockComponent } from './table-block';
|
||||||
|
import {
|
||||||
|
createColumnDragPreview,
|
||||||
|
createRowDragPreview,
|
||||||
|
type TableCell,
|
||||||
|
TableCellComponentName,
|
||||||
|
} from './table-cell';
|
||||||
|
import { cleanSelection } from './utils';
|
||||||
type Cells = string[][];
|
type Cells = string[][];
|
||||||
const TEXT = 'text/plain';
|
const TEXT = 'text/plain';
|
||||||
export class SelectionController implements ReactiveController {
|
export class SelectionController implements ReactiveController {
|
||||||
@@ -44,7 +52,7 @@ export class SelectionController implements ReactiveController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const onMove = (event: MouseEvent) => {
|
const onMove = (event: MouseEvent) => {
|
||||||
this.dataManager.draggingColumnId$.value = columnId;
|
this.dataManager.widthAdjustColumnId$.value = columnId;
|
||||||
this.dataManager.virtualWidth$.value = {
|
this.dataManager.virtualWidth$.value = {
|
||||||
columnId,
|
columnId,
|
||||||
width: Math.max(
|
width: Math.max(
|
||||||
@@ -55,7 +63,7 @@ export class SelectionController implements ReactiveController {
|
|||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
const width = this.dataManager.virtualWidth$.value?.width;
|
const width = this.dataManager.virtualWidth$.value?.width;
|
||||||
this.dataManager.draggingColumnId$.value = undefined;
|
this.dataManager.widthAdjustColumnId$.value = undefined;
|
||||||
this.dataManager.virtualWidth$.value = undefined;
|
this.dataManager.virtualWidth$.value = undefined;
|
||||||
if (width) {
|
if (width) {
|
||||||
this.dataManager.setColumnWidth(columnId, width);
|
this.dataManager.setColumnWidth(columnId, width);
|
||||||
@@ -76,14 +84,199 @@ export class SelectionController implements ReactiveController {
|
|||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dragHandle = target.closest('[data-width-adjust-column-id]');
|
const widthAdjustColumn = target.closest('[data-width-adjust-column-id]');
|
||||||
if (dragHandle instanceof HTMLElement) {
|
if (widthAdjustColumn instanceof HTMLElement) {
|
||||||
this.widthAdjust(dragHandle, event);
|
this.widthAdjust(widthAdjustColumn, event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const columnDragHandle = target.closest('[data-drag-column-id]');
|
||||||
|
if (columnDragHandle instanceof HTMLElement) {
|
||||||
|
this.columnDrag(columnDragHandle, event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rowDragHandle = target.closest('[data-drag-row-id]');
|
||||||
|
if (rowDragHandle instanceof HTMLElement) {
|
||||||
|
this.rowDrag(rowDragHandle, event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.onDragStart(event);
|
this.onDragStart(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
startColumnDrag(x: number, columnDragHandle: HTMLElement) {
|
||||||
|
const columnId = columnDragHandle.dataset['dragColumnId'];
|
||||||
|
if (!columnId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cellRect = columnDragHandle.closest('td')?.getBoundingClientRect();
|
||||||
|
const containerRect = this.host.getBoundingClientRect();
|
||||||
|
if (!cellRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const initialDiffX = x - cellRect.left;
|
||||||
|
const cells = Array.from(
|
||||||
|
this.host.querySelectorAll(`td[data-column-id="${columnId}"]`)
|
||||||
|
).map(td => td.closest(TableCellComponentName) as TableCell);
|
||||||
|
const firstCell = cells[0];
|
||||||
|
if (!firstCell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const draggingIndex = firstCell.columnIndex;
|
||||||
|
const columns = Array.from(
|
||||||
|
this.host.querySelectorAll(`td[data-row-id="${firstCell?.row?.rowId}"]`)
|
||||||
|
).map(td => td.getBoundingClientRect());
|
||||||
|
const columnOffsets = columns.flatMap((column, index) =>
|
||||||
|
index === columns.length - 1 ? [column.left, column.right] : [column.left]
|
||||||
|
);
|
||||||
|
const columnDragPreview = createColumnDragPreview(cells);
|
||||||
|
columnDragPreview.style.top = `${cellRect.top - containerRect.top - 0.5}px`;
|
||||||
|
columnDragPreview.style.left = `${cellRect.left - containerRect.left}px`;
|
||||||
|
columnDragPreview.style.width = `${cellRect.width}px`;
|
||||||
|
this.host.append(columnDragPreview);
|
||||||
|
document.body.style.pointerEvents = 'none';
|
||||||
|
const onMove = (x: number) => {
|
||||||
|
const { targetIndex, isForward } = getTargetIndexByDraggingOffset(
|
||||||
|
columnOffsets,
|
||||||
|
draggingIndex,
|
||||||
|
x - initialDiffX
|
||||||
|
);
|
||||||
|
if (targetIndex != null) {
|
||||||
|
this.dataManager.ui.columnIndicatorIndex$.value = isForward
|
||||||
|
? targetIndex + 1
|
||||||
|
: targetIndex;
|
||||||
|
} else {
|
||||||
|
this.dataManager.ui.columnIndicatorIndex$.value = undefined;
|
||||||
|
}
|
||||||
|
columnDragPreview.style.left = `${x - initialDiffX - containerRect.left}px`;
|
||||||
|
};
|
||||||
|
const onEnd = () => {
|
||||||
|
const targetIndex = this.dataManager.ui.columnIndicatorIndex$.value;
|
||||||
|
this.dataManager.ui.columnIndicatorIndex$.value = undefined;
|
||||||
|
document.body.style.pointerEvents = 'auto';
|
||||||
|
columnDragPreview.remove();
|
||||||
|
if (targetIndex != null) {
|
||||||
|
this.dataManager.moveColumn(
|
||||||
|
draggingIndex,
|
||||||
|
targetIndex === 0 ? undefined : targetIndex - 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
onMove,
|
||||||
|
onEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
columnDrag(columnDragHandle: HTMLElement, event: MouseEvent) {
|
||||||
|
let drag: { onMove: (x: number) => void; onEnd: () => void } | undefined =
|
||||||
|
undefined;
|
||||||
|
const initialX = event.clientX;
|
||||||
|
const onMove = (event: MouseEvent) => {
|
||||||
|
const diffX = event.clientX - initialX;
|
||||||
|
if (!drag && Math.abs(diffX) > 10) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
cleanSelection();
|
||||||
|
this.setSelected(undefined);
|
||||||
|
drag = this.startColumnDrag(initialX, columnDragHandle);
|
||||||
|
}
|
||||||
|
drag?.onMove(event.clientX);
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
drag?.onEnd();
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', onMove);
|
||||||
|
window.addEventListener('mouseup', onUp);
|
||||||
|
}
|
||||||
|
startRowDrag(y: number, rowDragHandle: HTMLElement) {
|
||||||
|
const rowId = rowDragHandle.dataset['dragRowId'];
|
||||||
|
if (!rowId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cellRect = rowDragHandle.closest('td')?.getBoundingClientRect();
|
||||||
|
const containerRect = this.host.getBoundingClientRect();
|
||||||
|
if (!cellRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const initialDiffY = y - cellRect.top;
|
||||||
|
const cells = Array.from(
|
||||||
|
this.host.querySelectorAll(`td[data-row-id="${rowId}"]`)
|
||||||
|
).map(td => td.closest(TableCellComponentName) as TableCell);
|
||||||
|
const firstCell = cells[0];
|
||||||
|
if (!firstCell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const draggingIndex = firstCell.rowIndex;
|
||||||
|
const rows = Array.from(
|
||||||
|
this.host.querySelectorAll(
|
||||||
|
`td[data-column-id="${firstCell?.column?.columnId}"]`
|
||||||
|
)
|
||||||
|
).map(td => td.getBoundingClientRect());
|
||||||
|
const rowOffsets = rows.flatMap((row, index) =>
|
||||||
|
index === rows.length - 1 ? [row.top, row.bottom] : [row.top]
|
||||||
|
);
|
||||||
|
const rowDragPreview = createRowDragPreview(cells);
|
||||||
|
rowDragPreview.style.left = `${cellRect.left - containerRect.left}px`;
|
||||||
|
rowDragPreview.style.top = `${cellRect.top - containerRect.top - 0.5}px`;
|
||||||
|
rowDragPreview.style.height = `${cellRect.height}px`;
|
||||||
|
this.host.append(rowDragPreview);
|
||||||
|
document.body.style.pointerEvents = 'none';
|
||||||
|
const onMove = (y: number) => {
|
||||||
|
const { targetIndex, isForward } = getTargetIndexByDraggingOffset(
|
||||||
|
rowOffsets,
|
||||||
|
draggingIndex,
|
||||||
|
y - initialDiffY
|
||||||
|
);
|
||||||
|
if (targetIndex != null) {
|
||||||
|
this.dataManager.ui.rowIndicatorIndex$.value = isForward
|
||||||
|
? targetIndex + 1
|
||||||
|
: targetIndex;
|
||||||
|
} else {
|
||||||
|
this.dataManager.ui.rowIndicatorIndex$.value = undefined;
|
||||||
|
}
|
||||||
|
rowDragPreview.style.top = `${y - initialDiffY - containerRect.top}px`;
|
||||||
|
};
|
||||||
|
const onEnd = () => {
|
||||||
|
const targetIndex = this.dataManager.ui.rowIndicatorIndex$.value;
|
||||||
|
this.dataManager.ui.rowIndicatorIndex$.value = undefined;
|
||||||
|
document.body.style.pointerEvents = 'auto';
|
||||||
|
rowDragPreview.remove();
|
||||||
|
if (targetIndex != null) {
|
||||||
|
this.dataManager.moveRow(
|
||||||
|
draggingIndex,
|
||||||
|
targetIndex === 0 ? undefined : targetIndex - 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
onMove,
|
||||||
|
onEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rowDrag(rowDragHandle: HTMLElement, event: MouseEvent) {
|
||||||
|
let drag: { onMove: (x: number) => void; onEnd: () => void } | undefined =
|
||||||
|
undefined;
|
||||||
|
const initialY = event.clientY;
|
||||||
|
const onMove = (event: MouseEvent) => {
|
||||||
|
const diffY = event.clientY - initialY;
|
||||||
|
if (!drag && Math.abs(diffY) > 10) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
cleanSelection();
|
||||||
|
this.setSelected(undefined);
|
||||||
|
drag = this.startRowDrag(initialY, rowDragHandle);
|
||||||
|
}
|
||||||
|
drag?.onMove(event.clientY);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
const onUp = () => {
|
||||||
|
drag?.onEnd();
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', onMove);
|
||||||
|
window.addEventListener('mouseup', onUp);
|
||||||
|
}
|
||||||
readonly doCopyOrCut = (selection: TableAreaSelection, isCut: boolean) => {
|
readonly doCopyOrCut = (selection: TableAreaSelection, isCut: boolean) => {
|
||||||
const columns = this.dataManager.uiColumns$.value;
|
const columns = this.dataManager.uiColumns$.value;
|
||||||
const rows = this.dataManager.uiRows$.value;
|
const rows = this.dataManager.uiRows$.value;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Rect = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectionLayerComponentName = 'affine-table-selection-layer';
|
||||||
export class SelectionLayer extends SignalWatcher(
|
export class SelectionLayer extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
@@ -105,6 +106,6 @@ export class SelectionLayer extends SignalWatcher(
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-table-selection-layer': SelectionLayer;
|
[SelectionLayerComponentName]: SelectionLayer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from './table-block.css';
|
} from './table-block.css';
|
||||||
import { TableDataManager } from './table-data-manager';
|
import { TableDataManager } from './table-data-manager';
|
||||||
|
|
||||||
|
export const TableBlockComponentName = 'affine-table';
|
||||||
export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel> {
|
export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel> {
|
||||||
private _dataManager: TableDataManager | null = null;
|
private _dataManager: TableDataManager | null = null;
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
|
|||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||||
|
this.style.position = 'relative';
|
||||||
}
|
}
|
||||||
|
|
||||||
override get topContenteditableElement() {
|
override get topContenteditableElement() {
|
||||||
@@ -191,6 +193,6 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-table': TableBlockComponent;
|
[TableBlockComponentName]: TableBlockComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,15 +107,52 @@ export const threePointerIconDotStyle = style({
|
|||||||
backgroundColor: threePointerIconColorVar,
|
backgroundColor: threePointerIconColorVar,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
});
|
});
|
||||||
|
export const indicatorStyle = style({
|
||||||
export const widthDragHandleStyle = style({
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-1px',
|
|
||||||
height: 'calc(100% + 2px)',
|
|
||||||
right: '-3px',
|
|
||||||
width: '5px',
|
|
||||||
backgroundColor: cssVarV2.table.indicator.activated,
|
backgroundColor: cssVarV2.table.indicator.activated,
|
||||||
cursor: 'ew-resize',
|
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
|
pointerEvents: 'none',
|
||||||
});
|
});
|
||||||
|
export const columnIndicatorStyle = style([
|
||||||
|
indicatorStyle,
|
||||||
|
{
|
||||||
|
top: '-1px',
|
||||||
|
height: 'calc(100% + 2px)',
|
||||||
|
width: '5px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const columnRightIndicatorStyle = style([
|
||||||
|
columnIndicatorStyle,
|
||||||
|
{
|
||||||
|
cursor: 'ew-resize',
|
||||||
|
right: '-3px',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const columnLeftIndicatorStyle = style([
|
||||||
|
columnIndicatorStyle,
|
||||||
|
{
|
||||||
|
left: '-2px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const rowIndicatorStyle = style([
|
||||||
|
indicatorStyle,
|
||||||
|
{
|
||||||
|
left: '-1px',
|
||||||
|
width: 'calc(100% + 2px)',
|
||||||
|
height: '5px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const rowBottomIndicatorStyle = style([
|
||||||
|
rowIndicatorStyle,
|
||||||
|
{
|
||||||
|
bottom: '-3px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const rowTopIndicatorStyle = style([
|
||||||
|
rowIndicatorStyle,
|
||||||
|
{
|
||||||
|
top: '-2px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { TextBackgroundDuotoneIcon } from '@blocksuite/affine-components/icons';
|
import { TextBackgroundDuotoneIcon } from '@blocksuite/affine-components/icons';
|
||||||
import {
|
import {
|
||||||
DefaultInlineManagerExtension,
|
DefaultInlineManagerExtension,
|
||||||
type RichText,
|
RichText,
|
||||||
} from '@blocksuite/affine-components/rich-text';
|
} from '@blocksuite/affine-components/rich-text';
|
||||||
import type { TableColumn, TableRow } from '@blocksuite/affine-model';
|
import type { TableColumn, TableRow } from '@blocksuite/affine-model';
|
||||||
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
|
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
@@ -49,16 +49,19 @@ import {
|
|||||||
import type { TableBlockComponent } from './table-block';
|
import type { TableBlockComponent } from './table-block';
|
||||||
import {
|
import {
|
||||||
cellContainerStyle,
|
cellContainerStyle,
|
||||||
|
columnLeftIndicatorStyle,
|
||||||
columnOptionsCellStyle,
|
columnOptionsCellStyle,
|
||||||
columnOptionsStyle,
|
columnOptionsStyle,
|
||||||
|
columnRightIndicatorStyle,
|
||||||
|
rowBottomIndicatorStyle,
|
||||||
rowOptionsCellStyle,
|
rowOptionsCellStyle,
|
||||||
rowOptionsStyle,
|
rowOptionsStyle,
|
||||||
|
rowTopIndicatorStyle,
|
||||||
threePointerIconDotStyle,
|
threePointerIconDotStyle,
|
||||||
threePointerIconStyle,
|
threePointerIconStyle,
|
||||||
widthDragHandleStyle,
|
|
||||||
} from './table-cell.css';
|
} from './table-cell.css';
|
||||||
import type { TableDataManager } from './table-data-manager';
|
import type { TableDataManager } from './table-data-manager';
|
||||||
|
export const TableCellComponentName = 'affine-table-cell';
|
||||||
export class TableCell extends SignalWatcher(
|
export class TableCell extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
@@ -89,6 +92,9 @@ export class TableCell extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor selectionController!: SelectionController;
|
accessor selectionController!: SelectionController;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor height: number | undefined;
|
||||||
|
|
||||||
get hoverColumnIndex$() {
|
get hoverColumnIndex$() {
|
||||||
return this.dataManager.hoverColumnIndex$;
|
return this.dataManager.hoverColumnIndex$;
|
||||||
}
|
}
|
||||||
@@ -447,6 +453,7 @@ export class TableCell extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
return html`<div class=${columnOptionsCellStyle}>
|
return html`<div class=${columnOptionsCellStyle}>
|
||||||
<div
|
<div
|
||||||
|
data-drag-column-id=${column.columnId}
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
[columnOptionsStyle]: true,
|
[columnOptionsStyle]: true,
|
||||||
})}
|
})}
|
||||||
@@ -470,6 +477,7 @@ export class TableCell extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
return html`<div class=${rowOptionsCellStyle}>
|
return html`<div class=${rowOptionsCellStyle}>
|
||||||
<div
|
<div
|
||||||
|
data-drag-row-id=${row.rowId}
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
[rowOptionsStyle]: true,
|
[rowOptionsStyle]: true,
|
||||||
})}
|
})}
|
||||||
@@ -483,7 +491,7 @@ export class TableCell extends SignalWatcher(
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
renderOptionsButton() {
|
renderOptionsButton() {
|
||||||
if (!this.row || !this.column) {
|
if (this.readonly || !this.row || !this.column) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@@ -525,33 +533,115 @@ export class TableCell extends SignalWatcher(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWidthDragHandle() {
|
showColumnIndicator$ = computed(() => {
|
||||||
|
const indicatorIndex =
|
||||||
|
this.dataManager.ui.columnIndicatorIndex$.value ?? -1;
|
||||||
|
if (indicatorIndex === 0 && this.columnIndex === 0) {
|
||||||
|
return 'left';
|
||||||
|
}
|
||||||
|
if (indicatorIndex - 1 === this.columnIndex) {
|
||||||
|
return 'right';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
showRowIndicator$ = computed(() => {
|
||||||
|
const indicatorIndex = this.dataManager.ui.rowIndicatorIndex$.value ?? -1;
|
||||||
|
if (indicatorIndex === 0 && this.rowIndex === 0) {
|
||||||
|
return 'top';
|
||||||
|
}
|
||||||
|
if (indicatorIndex - 1 === this.rowIndex) {
|
||||||
|
return 'bottom';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
renderRowIndicator() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const columnIndex = this.columnIndex;
|
||||||
|
const isFirstColumn = columnIndex === 0;
|
||||||
|
const isLastColumn =
|
||||||
|
columnIndex === this.dataManager.uiColumns$.value.length - 1;
|
||||||
|
const showIndicator = this.showRowIndicator$.value;
|
||||||
|
const style = (show: boolean) =>
|
||||||
|
styleMap({
|
||||||
|
opacity: show ? 1 : 0,
|
||||||
|
borderRadius: isFirstColumn
|
||||||
|
? '3px 0 0 3px'
|
||||||
|
: isLastColumn
|
||||||
|
? '0 3px 3px 0'
|
||||||
|
: '0',
|
||||||
|
});
|
||||||
|
const indicator0 =
|
||||||
|
this.rowIndex === 0
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style=${style(showIndicator === 'top')}
|
||||||
|
class=${rowTopIndicatorStyle}
|
||||||
|
></div>
|
||||||
|
`
|
||||||
|
: nothing;
|
||||||
|
return html`
|
||||||
|
${indicator0}
|
||||||
|
<div
|
||||||
|
style=${style(showIndicator === 'bottom')}
|
||||||
|
class=${rowBottomIndicatorStyle}
|
||||||
|
></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
renderColumnIndicator() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
const hoverColumnId$ = this.dataManager.hoverDragHandleColumnId$;
|
const hoverColumnId$ = this.dataManager.hoverDragHandleColumnId$;
|
||||||
const draggingColumnId$ = this.dataManager.draggingColumnId$;
|
const draggingColumnId$ = this.dataManager.widthAdjustColumnId$;
|
||||||
const rowIndex = this.rowIndex;
|
const rowIndex = this.rowIndex;
|
||||||
const isFirstRow = rowIndex === 0;
|
const isFirstRow = rowIndex === 0;
|
||||||
const isLastRow = rowIndex === this.dataManager.uiRows$.value.length - 1;
|
const isLastRow = rowIndex === this.dataManager.uiRows$.value.length - 1;
|
||||||
const show =
|
const showWidthAdjustIndicator =
|
||||||
draggingColumnId$.value === this.column?.columnId ||
|
draggingColumnId$.value === this.column?.columnId ||
|
||||||
hoverColumnId$.value === this.column?.columnId;
|
hoverColumnId$.value === this.column?.columnId;
|
||||||
return html`<div
|
const showIndicator = this.showColumnIndicator$.value;
|
||||||
@mouseenter=${() => {
|
const style = (show: boolean) =>
|
||||||
hoverColumnId$.value = this.column?.columnId;
|
styleMap({
|
||||||
}}
|
|
||||||
@mouseleave=${() => {
|
|
||||||
hoverColumnId$.value = undefined;
|
|
||||||
}}
|
|
||||||
style=${styleMap({
|
|
||||||
opacity: show ? 1 : 0,
|
opacity: show ? 1 : 0,
|
||||||
borderRadius: isFirstRow
|
borderRadius: isFirstRow
|
||||||
? '3px 3px 0 0'
|
? '3px 3px 0 0'
|
||||||
: isLastRow
|
: isLastRow
|
||||||
? '0 0 3px 3px'
|
? '0 0 3px 3px'
|
||||||
: '0',
|
: '0',
|
||||||
})}
|
});
|
||||||
data-width-adjust-column-id=${this.column?.columnId}
|
const indicator0 =
|
||||||
class=${widthDragHandleStyle}
|
this.columnIndex === 0
|
||||||
></div>`;
|
? html`
|
||||||
|
<div
|
||||||
|
style=${style(showIndicator === 'left')}
|
||||||
|
class=${columnLeftIndicatorStyle}
|
||||||
|
></div>
|
||||||
|
`
|
||||||
|
: nothing;
|
||||||
|
const mouseEnter = () => {
|
||||||
|
hoverColumnId$.value = this.column?.columnId;
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
hoverColumnId$.value = undefined;
|
||||||
|
};
|
||||||
|
return html` ${indicator0}
|
||||||
|
<div
|
||||||
|
@mouseenter=${mouseEnter}
|
||||||
|
@mouseleave=${mouseLeave}
|
||||||
|
style=${style(showWidthAdjustIndicator || showIndicator === 'right')}
|
||||||
|
data
|
||||||
|
-
|
||||||
|
width
|
||||||
|
-
|
||||||
|
adjust
|
||||||
|
-
|
||||||
|
column
|
||||||
|
-
|
||||||
|
id=${this.column?.columnId}
|
||||||
|
class=${columnRightIndicatorStyle}
|
||||||
|
></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
richText$ = signal<RichText>();
|
richText$ = signal<RichText>();
|
||||||
@@ -666,12 +756,79 @@ export class TableCell extends SignalWatcher(
|
|||||||
: null}"
|
: null}"
|
||||||
data-parent-flavour="affine:table"
|
data-parent-flavour="affine:table"
|
||||||
></rich-text>
|
></rich-text>
|
||||||
${this.renderOptionsButton()} ${this.renderWidthDragHandle()}
|
${this.renderOptionsButton()} ${this.renderColumnIndicator()}
|
||||||
|
${this.renderRowIndicator()}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createColumnDragPreview = (cells: TableCell[]) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.position = 'absolute';
|
||||||
|
container.style.opacity = '0.8';
|
||||||
|
container.style.display = 'flex';
|
||||||
|
container.style.flexDirection = 'column';
|
||||||
|
container.style.zIndex = '1000';
|
||||||
|
container.style.boxShadow = '0 0 10px 0 rgba(0, 0, 0, 0.1)';
|
||||||
|
container.style.backgroundColor = cssVarV2.layer.background.primary;
|
||||||
|
cells.forEach((cell, index) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const td = cell.querySelector('td');
|
||||||
|
if (index !== 0) {
|
||||||
|
div.style.borderTop = `1px solid ${cssVarV2.layer.insideBorder.border}`;
|
||||||
|
}
|
||||||
|
if (td) {
|
||||||
|
div.style.height = `${td.getBoundingClientRect().height}px`;
|
||||||
|
}
|
||||||
|
if (cell.text) {
|
||||||
|
const text = new RichText();
|
||||||
|
text.style.padding = '8px 12px';
|
||||||
|
text.yText = cell.text;
|
||||||
|
text.readonly = true;
|
||||||
|
text.attributesSchema = cell.inlineManager?.getSchema();
|
||||||
|
text.attributeRenderer = cell.inlineManager?.getRenderer();
|
||||||
|
text.embedChecker = cell.inlineManager?.embedChecker ?? (() => false);
|
||||||
|
div.append(text);
|
||||||
|
}
|
||||||
|
container.append(div);
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRowDragPreview = (cells: TableCell[]) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.position = 'absolute';
|
||||||
|
container.style.opacity = '0.8';
|
||||||
|
container.style.display = 'flex';
|
||||||
|
container.style.flexDirection = 'row';
|
||||||
|
container.style.zIndex = '1000';
|
||||||
|
container.style.boxShadow = '0 0 10px 0 rgba(0, 0, 0, 0.1)';
|
||||||
|
container.style.backgroundColor = cssVarV2.layer.background.primary;
|
||||||
|
cells.forEach((cell, index) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const td = cell.querySelector('td');
|
||||||
|
if (index !== 0) {
|
||||||
|
div.style.borderLeft = `1px solid ${cssVarV2.layer.insideBorder.border}`;
|
||||||
|
}
|
||||||
|
if (td) {
|
||||||
|
div.style.width = `${td.getBoundingClientRect().width}px`;
|
||||||
|
}
|
||||||
|
if (cell.text) {
|
||||||
|
const text = new RichText();
|
||||||
|
text.style.padding = '8px 12px';
|
||||||
|
text.yText = cell.text;
|
||||||
|
text.readonly = true;
|
||||||
|
text.attributesSchema = cell.inlineManager?.getSchema();
|
||||||
|
text.attributeRenderer = cell.inlineManager?.getRenderer();
|
||||||
|
text.embedChecker = cell.inlineManager?.embedChecker ?? (() => false);
|
||||||
|
div.append(text);
|
||||||
|
}
|
||||||
|
container.append(div);
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
const threePointerIcon = (vertical: boolean = false) => {
|
const threePointerIcon = (vertical: boolean = false) => {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@@ -688,6 +845,6 @@ const threePointerIcon = (vertical: boolean = false) => {
|
|||||||
};
|
};
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-table-cell': TableCell;
|
[TableCellComponentName]: TableCell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import type { TableAreaSelection } from './selection-schema';
|
|||||||
|
|
||||||
export class TableDataManager {
|
export class TableDataManager {
|
||||||
constructor(private readonly model: TableBlockModel) {}
|
constructor(private readonly model: TableBlockModel) {}
|
||||||
|
ui = {
|
||||||
|
columnIndicatorIndex$: signal<number>(),
|
||||||
|
rowIndicatorIndex$: signal<number>(),
|
||||||
|
};
|
||||||
hoverColumnIndex$ = signal<number>();
|
hoverColumnIndex$ = signal<number>();
|
||||||
hoverRowIndex$ = signal<number>();
|
hoverRowIndex$ = signal<number>();
|
||||||
hoverDragHandleColumnId$ = signal<string>();
|
hoverDragHandleColumnId$ = signal<string>();
|
||||||
draggingColumnId$ = signal<string>();
|
widthAdjustColumnId$ = signal<string>();
|
||||||
virtualColumnCount$ = signal<number>(0);
|
virtualColumnCount$ = signal<number>(0);
|
||||||
virtualRowCount$ = signal<number>(0);
|
virtualRowCount$ = signal<number>(0);
|
||||||
virtualWidth$ = signal<{ columnId: string; width: number } | undefined>();
|
virtualWidth$ = signal<{ columnId: string; width: number } | undefined>();
|
||||||
|
|||||||
6
blocksuite/affine/block-table/src/utils.ts
Normal file
6
blocksuite/affine/block-table/src/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const cleanSelection = () => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
type OffsetList = number[];
|
import type { OffsetList } from './types';
|
||||||
|
|
||||||
type CellOffsets = {
|
type CellOffsets = {
|
||||||
rows: OffsetList;
|
rows: OffsetList;
|
||||||
columns: OffsetList;
|
columns: OffsetList;
|
||||||
3
blocksuite/affine/shared/src/utils/drag-helper/index.ts
Normal file
3
blocksuite/affine/shared/src/utils/drag-helper/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './cell-select';
|
||||||
|
export * from './linear-move';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { OffsetList } from './types';
|
||||||
|
export const getTargetIndexByDraggingOffset = (
|
||||||
|
offsets: OffsetList,
|
||||||
|
draggingIndex: number,
|
||||||
|
indicatorLeft: number
|
||||||
|
) => {
|
||||||
|
const originalStart = offsets[draggingIndex];
|
||||||
|
const originalWidth = offsets[draggingIndex + 1] - originalStart;
|
||||||
|
const indicatorRight = indicatorLeft + originalWidth;
|
||||||
|
const isForward = indicatorLeft > originalStart;
|
||||||
|
const startIndex = isForward ? draggingIndex + 1 : 0;
|
||||||
|
const endIndex = isForward ? offsets.length - 1 : draggingIndex - 1;
|
||||||
|
if (isForward) {
|
||||||
|
for (let i = endIndex; i >= startIndex; i--) {
|
||||||
|
const blockCenter = (offsets[i] + offsets[i + 1]) / 2;
|
||||||
|
if (indicatorRight > blockCenter) {
|
||||||
|
return {
|
||||||
|
targetIndex: i,
|
||||||
|
isForward,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
const blockCenter = (offsets[i] + offsets[i + 1]) / 2;
|
||||||
|
if (indicatorLeft < blockCenter) {
|
||||||
|
return {
|
||||||
|
targetIndex: i,
|
||||||
|
isForward,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
targetIndex: undefined,
|
||||||
|
isForward,
|
||||||
|
};
|
||||||
|
};
|
||||||
1
blocksuite/affine/shared/src/utils/drag-helper/types.ts
Normal file
1
blocksuite/affine/shared/src/utils/drag-helper/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type OffsetList = number[];
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { generateKeyBetween } from 'fractional-indexing';
|
import { generateKeyBetween } from 'fractional-indexing';
|
||||||
|
|
||||||
|
function hasSamePrefix(a: string, b: string) {
|
||||||
|
return a.startsWith(b) || b.startsWith(a);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generate a key between a and b, the result key is always satisfied with a < result < b.
|
* generate a key between a and b, the result key is always satisfied with a < result < b.
|
||||||
* the key always has a random suffix, so there is no need to worry about collision.
|
* the key always has a random suffix, so there is no need to worry about collision.
|
||||||
@@ -59,7 +63,7 @@ export function generateFractionalIndexingKeyBetween(
|
|||||||
return generateKeyBetween(aSubkey, null) + '0' + postfix();
|
return generateKeyBetween(aSubkey, null) + '0' + postfix();
|
||||||
} else if (aSubkey !== null && bSubkey !== null) {
|
} else if (aSubkey !== null && bSubkey !== null) {
|
||||||
// generate a key between a and b
|
// generate a key between a and b
|
||||||
if (aSubkey === bSubkey && a !== null && b !== null) {
|
if (hasSamePrefix(aSubkey, bSubkey) && a !== null && b !== null) {
|
||||||
// conflict, if the subkeys are the same, generate a key between fullkeys
|
// conflict, if the subkeys are the same, generate a key between fullkeys
|
||||||
return generateKeyBetween(a, b) + '0' + postfix();
|
return generateKeyBetween(a, b) + '0' + postfix();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export * from './auto-scroll';
|
export * from './auto-scroll';
|
||||||
export * from './button-popper';
|
export * from './button-popper';
|
||||||
export * from './cell-select';
|
|
||||||
export * from './collapsed';
|
export * from './collapsed';
|
||||||
export * from './dnd';
|
export * from './dnd';
|
||||||
export * from './dom';
|
export * from './dom';
|
||||||
|
export * from './drag-helper';
|
||||||
export * from './edgeless';
|
export * from './edgeless';
|
||||||
export * from './event';
|
export * from './event';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
|
|||||||
Reference in New Issue
Block a user