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(editor): kanban mobile drag and drop #9411

Open
wants to merge 12 commits into
base: canary
Choose a base branch
from
Open
7 changes: 1 addition & 6 deletions blocksuite/affine/block-database/src/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
type ViewMeta,
} from '@blocksuite/data-view';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { IS_MOBILE } from '@blocksuite/global/env';
import { assertExists } from '@blocksuite/global/utils';
import { type BlockModel, nanoid, Text } from '@blocksuite/store';
import { computed, type ReadonlySignal } from '@preact/signals-core';
Expand Down Expand Up @@ -69,11 +68,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
});

readonly$: ReadonlySignal<boolean> = computed(() => {
return (
this._model.doc.readonly ||
// TODO(@L-Sun): use block level readonly
IS_MOBILE
);
return this._model.doc.readonly;
});

rows$: ReadonlySignal<string[]> = computed(() => {
Expand Down
2 changes: 2 additions & 0 deletions blocksuite/affine/data-view/src/core/utils/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const startDrag = <
const clear = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
window.removeEventListener('pointercancel', up);
window.removeEventListener('keydown', keydown);
document.body.style.cursor = oldCursor;
ops.onClear();
Expand Down Expand Up @@ -63,6 +64,7 @@ export const startDrag = <
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
window.addEventListener('pointercancel', up);
window.addEventListener('keydown', keydown);

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const styles = css`
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.05);
border-radius: 8px;
background-color: var(--affine-background-kanban-card-color);
touch-action: none;
alsuren marked this conversation as resolved.
Show resolved Hide resolved
}

.mobile-card-header {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { ReactiveController } from 'lit';

import { assertExists, Point, Rect } from '@blocksuite/global/utils';
import { computed } from '@preact/signals-core';

import type { MobileDataViewKanban } from '../kanban-view.js';

import { autoScrollOnBoundary } from '../../../../core/utils/auto-scroll.js';
import { startDrag } from '../../../../core/utils/drag.js';
import { MobileKanbanCard } from '../card.js';
import { MobileKanbanGroup } from '../group.js';

export class MobileKanbanDragController implements ReactiveController {
dragStart = (ele: MobileKanbanCard, evt: PointerEvent) => {
const eleRect = ele.getBoundingClientRect();
const offsetLeft = evt.x - eleRect.left;
const offsetTop = evt.y - eleRect.top;
const preview = createDragPreview(
ele,
evt.x - offsetLeft,
evt.y - offsetTop
);
const currentGroup = ele.closest('mobile-kanban-group');
const drag = startDrag<
| { type: 'out'; callback: () => void }
| {
type: 'self';
key: string;
position: InsertToPosition;
}
| undefined,
PointerEvent
>(evt, {
onDrag: () => undefined,
onMove: evt => {
if (!(evt.target instanceof HTMLElement)) {
return;
}
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!Rect.fromDOM(this.host).isPointIn(Point.from(evt))) {
const callback = this.host.props.onDrag;
if (callback) {
this.dropPreview.remove();
return {
type: 'out',
callback: callback(evt, ele.cardId),
};
}
return;
}
const result = this.shooIndicator(evt, ele);
if (result) {
return {
type: 'self',
key: result.group.group.key,
position: result.position,
};
}
return;
},
onClear: () => {
preview.remove();
this.dropPreview.remove();
cancelScroll();
},
onDrop: result => {
if (!result) {
return;
}
if (result.type === 'out') {
result.callback();
return;
}
if (result && currentGroup) {
currentGroup.group.manager.moveCardTo(
ele.cardId,
currentGroup.group.key,
result.key,
result.position
);
}
},
});
const cancelScroll = autoScrollOnBoundary(
this.scrollContainer,
computed(() => {
return {
left: drag.mousePosition.value.x,
right: drag.mousePosition.value.x,
top: drag.mousePosition.value.y,
bottom: drag.mousePosition.value.y,
};
})
);
};

dropPreview = createDropPreview();

getInsertPosition = (
evt: MouseEvent
):
| { group: MobileKanbanGroup; card?: MobileKanbanCard; position: InsertToPosition }
| undefined => {
const eles = document.elementsFromPoint(evt.x, evt.y);
const target = eles.find(v => v instanceof MobileKanbanGroup) as MobileKanbanGroup;
if (target) {
const card = getCardByPoint(target, evt.y);
return {
group: target,
card,
position: card
? {
before: true,
id: card.cardId,
}
: 'end',
};
} else {
return;
}
};

shooIndicator = (
evt: MouseEvent,
self: MobileKanbanCard | undefined
): { group: MobileKanbanGroup; position: InsertToPosition } | undefined => {
const position = this.getInsertPosition(evt);
if (position) {
this.dropPreview.display(position.group, self, position.card);
} else {
this.dropPreview.remove();
}
return position;
};

get scrollContainer() {
const scrollContainer = this.host.querySelector(
'.mobile-kanban-groups'
) as HTMLElement;
assertExists(scrollContainer);
return scrollContainer;
}

constructor(private host: MobileDataViewKanban) {
this.host.addController(this);
}

hostConnected() {
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (target instanceof Element) {
const cell = target.closest('mobile-kanban-cell');
if (cell?.isEditing) {
return;
}
cell?.selectCurrentCell(false);
const card = target.closest('mobile-kanban-card');
if (card) {
this.dragStart(card, event);
}
}
return true;
})
);
}
}

const createDragPreview = (card: MobileKanbanCard, x: number, y: number) => {
const preOpacity = card.style.opacity;
card.style.opacity = '0.5';
const div = document.createElement('div');
const kanbanCard = new MobileKanbanCard();
kanbanCard.cardId = card.cardId;
kanbanCard.view = card.view;
kanbanCard.isFocus = true;
kanbanCard.style.backgroundColor = 'var(--affine-background-primary-color)';
div.append(kanbanCard);
div.className = 'with-data-view-css-variable';
div.style.width = `${card.getBoundingClientRect().width}px`;
div.style.position = 'fixed';
// div.style.pointerEvents = 'none';
div.style.transform = 'rotate(-3deg)';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.zIndex = '9999';
document.body.append(div);
return {
display(x: number, y: number) {
div.style.left = `${Math.round(x)}px`;
div.style.top = `${Math.round(y)}px`;
},
remove() {
card.style.opacity = preOpacity;
div.remove();
},
};
};
const createDropPreview = () => {
const div = document.createElement('div');
div.style.height = '2px';
div.style.borderRadius = '1px';
div.style.backgroundColor = 'var(--affine-primary-color)';
div.style.boxShadow = '0px 0px 8px 0px rgba(30, 150, 235, 0.35)';
return {
display(
group: MobileKanbanGroup,
self: MobileKanbanCard | undefined,
card?: MobileKanbanCard
) {
const target = card ?? group.querySelector('.add-card');
assertExists(target);
if (target.previousElementSibling === self || target === self) {
div.remove();
return;
}
if (target.previousElementSibling === div) {
return;
}
target.insertAdjacentElement('beforebegin', div);
},
remove() {
div.remove();
},
};
};

const getCardByPoint = (
group: MobileKanbanGroup,
y: number
): MobileKanbanCard | undefined => {
const cards = Array.from(
group.querySelectorAll('mobile-kanban-card')
);
const positions = cards.map(v => {
const rect = v.getBoundingClientRect();
return (rect.top + rect.bottom) / 2;
});
const index = positions.findIndex(v => v > y);
return cards[index];
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';

import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../types.js';

import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../types.js';
import { MobileKanbanDragController } from './controller/drag.js';

const styles = css`
mobile-data-view-kanban {
Expand Down Expand Up @@ -51,6 +53,8 @@ export class MobileDataViewKanban extends DataViewBase<
> {
static override styles = styles;

private dragController = new MobileKanbanDragController(this);

renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
Expand Down Expand Up @@ -94,8 +98,8 @@ export class MobileDataViewKanban extends DataViewBase<
},
hideIndicator: () => {},
moveTo: () => {},
showIndicator: () => {
return false;
showIndicator: evt => {
return this.dragController.shooIndicator(evt, undefined) != null;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { html } from 'lit/static-html.js';

import type { DataViewRenderer } from '../../../core/data-view.js';
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js';

import { openDetail, popCardMenu } from './menu.js';

const styles = css`
Expand All @@ -22,6 +23,7 @@ const styles = css`
border-radius: 8px;
transition: background-color 100ms ease-in-out;
background-color: var(--affine-background-kanban-card-color);
touch-action: none;
}

affine-data-view-kanban-card:hover {
Expand Down Expand Up @@ -125,15 +127,15 @@ export class KanbanCard extends SignalWatcher(
) {
static override styles = styles;

private readonly clickEdit = (e: MouseEvent) => {
private clickEdit = (e: MouseEvent) => {
e.stopPropagation();
const selection = this.getSelection();
if (selection) {
openDetail(this.dataViewEle, this.cardId, selection);
}
};

private readonly clickMore = (e: MouseEvent) => {
private clickMore = (e: MouseEvent) => {
e.stopPropagation();
const selection = this.getSelection();
const ele = e.currentTarget as HTMLElement;
Expand All @@ -156,7 +158,7 @@ export class KanbanCard extends SignalWatcher(
}
};

private readonly contextMenu = (e: MouseEvent) => {
private contextMenu = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const selection = this.getSelection();
Expand Down
Loading
Loading