Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(database): drag to fill cells #6895

Merged
merged 17 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { ShadowlessElement } from '@blocksuite/block-std';
import { assertEquals } from '@blocksuite/global/utils';
import { DocCollection, Text, type Y } from '@blocksuite/store';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';

import { tRichText } from '../../../../logical/data-type.js';
import type { DataViewTable } from '../table-view.js';
import type { TableViewSelection } from '../types.js';

@customElement('data-view-drag-to-fill')
export class DragToFillElement extends ShadowlessElement {
static override styles = css`
.drag-to-fill {
border-radius: 50%;
box-sizing: border-box;
background-color: var(--affine-background-primary-color);
border: 2px solid var(--affine-primary-color);
position: absolute;
cursor: ns-resize;
width: 10px;
height: 10px;
transform: translate(-50%, -50%);
pointer-events: auto;
user-select: none;
transition: scale 0.2s ease;
z-index: 2;
}
.drag-to-fill.dragging {
scale: 1.1;
}
`;

@state()
dragging = false;

dragToFillRef = createRef<HTMLDivElement>();

override render() {
// TODO add tooltip
return html`<div
${ref(this.dragToFillRef)}
draggable="true"
data-drag-to-fill="true"
class="drag-to-fill ${this.dragging ? 'dragging' : ''}"
></div>`;
}
}

export function fillSelectionWithFocusCellData(
host: DataViewTable,
selection: TableViewSelection
) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection;

const focusCell = host.selectionController.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
);

if (!focusCell) return;

if (rowsSelection && columnsSelection) {
assertEquals(
columnsSelection.start,
columnsSelection.end,
'expected selections on a single column'
);

const curCol = focusCell.column; // we are sure that we are always in the same column while iterating through rows
const focusData = curCol.getValue(focusCell.rowId);

const draggingColIdx = columnsSelection.start;
const { start, end } = rowsSelection;

for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue;

const cellContainer = host.selectionController.getCellContainer(
groupKey,
i,
draggingColIdx
);

if (!cellContainer) continue;

const curRowId = cellContainer.rowId;

if (tRichText.is(curCol.dataType)) {
// title column gives Y.Text and text col gives Text
const focusCellText = focusData as Y.Text | Text;

const delta = focusCellText.toDelta();
const curCellText = curCol.getValue(curRowId);
if (curCellText) {
const text =
curCol.type === 'title'
? new Text(curCellText as Y.Text)
: (curCellText as Text);

text.clear();
text.applyDelta(delta);
} else {
const newText = new DocCollection.Y.Text();
newText.applyDelta(delta);
curCol.setValue(curRowId, newText);
}
} else {
curCol.setValue(curRowId, focusData);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ import type {
MultiSelection,
TableViewSelection,
} from '../types.js';
import {
DragToFillElement,
fillSelectionWithFocusCellData,
} from './drag-to-fill.js';

export class TableSelectionController implements ReactiveController {
__selectionElement = new SelectionElement();
__dragToFillElement = new DragToFillElement();

private get dragToFillDraggable() {
return this.__dragToFillElement.dragToFillRef.value;
}

private get focusSelectionElement() {
return this.__selectionElement.focusRef.value;
Expand Down Expand Up @@ -48,6 +57,7 @@ export class TableSelectionController implements ReactiveController {
public hostConnected() {
requestAnimationFrame(() => {
this.tableContainer.append(this.__selectionElement);
this.tableContainer.append(this.__dragToFillElement);
});
this.handleDragEvent();
this.handleSelectionChange();
Expand Down Expand Up @@ -107,13 +117,41 @@ export class TableSelectionController implements ReactiveController {
);
}

private getFocusCellContainer = () => {
if (!this._tableViewSelection) return null;
const { groupKey, focus } = this._tableViewSelection;

const dragStartCell = this.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
);
return dragStartCell ?? null;
};

private resolveDragStartTarget(
target: HTMLElement
): [cell: DatabaseCellContainer | null, fillValues: boolean] {
let cell: DatabaseCellContainer | null;
const fillValues = !!target.dataset.dragToFill;
if (fillValues) {
const focusCellContainer = this.getFocusCellContainer();
assertExists(focusCellContainer);
cell = focusCellContainer;
} else {
cell = target.closest('affine-database-cell-container');
}
return [cell, fillValues];
}

private handleDragEvent() {
this.host.disposables.add(
this.host.handleEvent('dragStart', context => {
const event = context.get('pointerState').raw;
const target = event.target;
if (target instanceof Element) {
const cell = target.closest('affine-database-cell-container');
if (target instanceof HTMLElement) {
const [cell, fillValues] = this.resolveDragStartTarget(target);

if (cell) {
const selection = this.selection;
if (
Expand All @@ -124,7 +162,7 @@ export class TableSelectionController implements ReactiveController {
) {
return false;
}
this.startDrag(event, cell);
this.startDrag(event, cell, fillValues);
event.preventDefault();
return true;
}
Expand Down Expand Up @@ -234,7 +272,11 @@ export class TableSelectionController implements ReactiveController {
};
}

startDrag(evt: PointerEvent, cell: DatabaseCellContainer) {
startDrag(
evt: PointerEvent,
cell: DatabaseCellContainer,
fillValues?: boolean
) {
const groupKey = cell.closest('affine-data-view-table-group')?.group?.key;
const table = this.tableContainer;
const scrollContainer = table.parentElement;
Expand Down Expand Up @@ -278,12 +320,22 @@ export class TableSelectionController implements ReactiveController {
x: evt.x,
y: evt.y,
}),
onDrag: () => undefined,
onDrag: () => {
if (fillValues) this.__dragToFillElement.dragging = true;
return undefined;
},
onMove: ({ x, y }) => {
const tableRect = table.getBoundingClientRect();
const startX = tableRect.left + startOffsetX;
const startY = tableRect.top + startOffsetY;
const selection = offsetToSelection(startX, x, startY, y);

if (fillValues)
selection.column = {
start: cell.columnIndex,
end: cell.columnIndex,
};

select(selection);
return selection;
},
Expand All @@ -292,6 +344,10 @@ export class TableSelectionController implements ReactiveController {
return;
}
select(selection);
if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData(this.host, this.selection);
}
},
onClear: () => {
cancelScroll();
Expand Down Expand Up @@ -412,11 +468,21 @@ export class TableSelectionController implements ReactiveController {

const isRowSelection =
tableSelection?.rowsSelection && !tableSelection?.columnsSelection;

const rowSel = tableSelection?.rowsSelection;

const isDragElemDragging = this.__dragToFillElement.dragging;
const isEditing = !!tableSelection?.isEditing;

const showDragToFillHandle =
!isEditing && ((rowSel && isDragElemDragging) || !rowSel);

this.updateFocusSelectionStyle(
tableSelection?.groupKey,
tableSelection?.focus,
isRowSelection,
tableSelection?.isEditing
isEditing,
showDragToFillHandle
);
return true;
};
Expand Down Expand Up @@ -476,10 +542,13 @@ export class TableSelectionController implements ReactiveController {
groupKey: string | undefined,
focus?: CellFocus,
isRowSelection?: boolean,
isEditing = false
isEditing = false,
showDragToFillHandle = false
) {
const div = this.focusSelectionElement;
if (!div) return;
const dragToFill = this.dragToFillDraggable;

if (!div || !dragToFill) return;
if (focus && !isRowSelection) {
// Check if row is removed.
const rows = this.rows(groupKey) ?? [];
Expand All @@ -493,17 +562,30 @@ export class TableSelectionController implements ReactiveController {
focus.columnIndex
);
const tableRect = this.tableContainer.getBoundingClientRect();
div.style.left = `${left - tableRect.left / scale}px`;
div.style.top = `${top - 1 - tableRect.top / scale}px`;
div.style.width = `${width + 1}px`;
div.style.height = `${height + 1}px`;

const x = left - tableRect.left / scale;
const y = top - 1 - tableRect.top / scale;
const w = width + 1;
const h = height + 1;
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.width = `${w}px`;
div.style.height = `${h}px`;
div.style.borderColor = 'var(--affine-primary-color)';
div.style.borderStyle = this.__dragToFillElement.dragging
? 'dashed'
: 'solid';
div.style.boxShadow = isEditing
? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)'
: 'unset';
div.style.display = 'block';

dragToFill.style.left = `${x + w}px`;
dragToFill.style.top = `${y + h}px`;
dragToFill.style.display = showDragToFillHandle ? 'block' : 'none';
} else {
div.style.display = 'none';
dragToFill.style.display = 'none';
}
}

Expand Down
Loading