feat(editor): table block supports drag-and-drop sorting (#10065)

close: BS-2477
This commit is contained in:
zzj3720
2025-02-10 14:14:53 +00:00
parent 964f2e1bfd
commit c78d6b81c6
15 changed files with 496 additions and 49 deletions

View File

@@ -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;
} }
} }

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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;
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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',
},
]);

View File

@@ -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;
} }
} }

View File

@@ -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>();

View File

@@ -0,0 +1,6 @@
export const cleanSelection = () => {
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
};

View File

@@ -1,4 +1,5 @@
type OffsetList = number[]; import type { OffsetList } from './types';
type CellOffsets = { type CellOffsets = {
rows: OffsetList; rows: OffsetList;
columns: OffsetList; columns: OffsetList;

View File

@@ -0,0 +1,3 @@
export * from './cell-select';
export * from './linear-move';
export * from './types';

View File

@@ -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,
};
};

View File

@@ -0,0 +1 @@
export type OffsetList = number[];

View File

@@ -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 {

View File

@@ -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';