Skip to content

Commit

Permalink
feat(database): drag to fill cells (#6895)
Browse files Browse the repository at this point in the history
Co-authored-by: 3720 <zuozijian1994@gmail.com>
  • Loading branch information
golok727 and zzj3720 committed Apr 29, 2024
1 parent 941f580 commit c35727b
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 12 deletions.
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

0 comments on commit c35727b

Please sign in to comment.