-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
69b71c1
commit 3009a4c
Showing
13 changed files
with
767 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
190 changes: 190 additions & 0 deletions
190
developer-extension/src/panel/components/tabs/eventsTab/columnDrag.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import type { RefObject } from 'react' | ||
import React, { useState, useEffect } from 'react' | ||
import { Box, Text } from '@mantine/core' | ||
import { BORDER_RADIUS } from '../../../uiUtils' | ||
import type { Coordinates } from './drag' | ||
import { initDrag } from './drag' | ||
import type { EventListColumn } from './columnUtils' | ||
import { getColumnTitle } from './columnUtils' | ||
import { HORIZONTAL_PADDING, VERTICAL_PADDING } from './grid' | ||
|
||
/** Number of pixel to determine if the cursor is close enough of a position to trigger an action */ | ||
const ACTION_DISTANCE_THRESHOLD = 20 | ||
|
||
export function ColumnDrag({ | ||
headerRowRef, | ||
columns, | ||
onColumnsChange, | ||
}: { | ||
headerRowRef: RefObject<HTMLDivElement> | ||
columns: EventListColumn[] | ||
onColumnsChange: (columns: EventListColumn[]) => void | ||
}) { | ||
const [drag, setDrag] = useState<DragState | null>(null) | ||
|
||
useEffect(() => { | ||
if (columns.length > 1) { | ||
const { stop } = initColumnDrag(headerRowRef.current!, setDrag, columns, onColumnsChange) | ||
return stop | ||
} | ||
}, [columns]) | ||
|
||
return drag && <DragGhost columns={columns} drag={drag} /> | ||
} | ||
|
||
function DragGhost({ columns, drag }: { columns: EventListColumn[]; drag: DragState }) { | ||
return ( | ||
<Box | ||
sx={{ | ||
position: 'fixed', | ||
opacity: 0.5, | ||
borderRadius: BORDER_RADIUS, | ||
|
||
top: drag.targetRect.top, | ||
height: drag.targetRect.height, | ||
transform: 'translateX(-50%)', | ||
left: drag.position.x, | ||
background: 'grey', | ||
|
||
paddingTop: VERTICAL_PADDING, | ||
paddingBottom: VERTICAL_PADDING, | ||
paddingLeft: HORIZONTAL_PADDING, | ||
paddingRight: HORIZONTAL_PADDING, | ||
cursor: 'grabbing', | ||
|
||
...(drag.action?.type === 'insert' && { | ||
width: 0, | ||
left: drag.action.place.xPosition, | ||
background: 'green', | ||
color: 'transparent', | ||
paddingRight: 3, | ||
paddingLeft: 3, | ||
}), | ||
|
||
...(drag.action?.type === 'delete' && { | ||
top: drag.targetRect.y + (drag.position.y - drag.startPosition.y), | ||
background: 'red', | ||
}), | ||
}} | ||
> | ||
<Text weight="bold">{getColumnTitle(columns[drag.columnIndex])}</Text> | ||
</Box> | ||
) | ||
} | ||
|
||
function getClosestCell(target: HTMLElement) { | ||
if (target.closest('button, .mantine-Popover-dropdown')) { | ||
return null | ||
} | ||
return target.closest('[data-header-cell]') | ||
} | ||
|
||
interface DragState { | ||
targetRect: DOMRect | ||
startPosition: Coordinates | ||
position: Coordinates | ||
action?: DragAction | ||
moved: boolean | ||
insertPlaces: Place[] | ||
columnIndex: number | ||
} | ||
|
||
interface Place { | ||
index: number | ||
xPosition: number | ||
} | ||
|
||
type DragAction = { type: 'delete' } | { type: 'insert'; place: Place } | ||
|
||
function initColumnDrag( | ||
target: HTMLElement, | ||
onColumnDragStateChanges: (state: DragState | null) => void, | ||
columns: EventListColumn[], | ||
onColumnsChange: (columns: EventListColumn[]) => void | ||
) { | ||
let state: DragState | null = null | ||
|
||
return initDrag({ | ||
target, | ||
|
||
onStart({ target, position }) { | ||
const targetCell = getClosestCell(target) | ||
if (!targetCell) { | ||
return false | ||
} | ||
const siblings = Array.from(targetCell.parentElement!.children) | ||
const columnIndex = siblings.indexOf(targetCell) | ||
|
||
state = { | ||
targetRect: targetCell.getBoundingClientRect(), | ||
insertPlaces: siblings.flatMap((sibling, index) => { | ||
if (sibling === targetCell) { | ||
return [] | ||
} | ||
return { | ||
xPosition: sibling.getBoundingClientRect()[index < columnIndex ? 'left' : 'right'], | ||
index, | ||
} | ||
}), | ||
startPosition: position, | ||
position, | ||
moved: false, | ||
action: undefined, | ||
columnIndex, | ||
} | ||
onColumnDragStateChanges(state) | ||
}, | ||
|
||
onMove({ position }) { | ||
if (!state) { | ||
return | ||
} | ||
let action: DragAction | undefined | ||
if (Math.abs(state.startPosition.y - position.y) > ACTION_DISTANCE_THRESHOLD) { | ||
action = { type: 'delete' } | ||
} else { | ||
const insertPlace = state.insertPlaces.find( | ||
({ xPosition }) => Math.abs(position.x - xPosition) < ACTION_DISTANCE_THRESHOLD | ||
) | ||
if (insertPlace) { | ||
action = { type: 'insert', place: insertPlace } | ||
} | ||
} | ||
|
||
state = { ...state, action, position, moved: true } | ||
onColumnDragStateChanges(state) | ||
}, | ||
|
||
onDrop() { | ||
if (!state) { | ||
return | ||
} | ||
|
||
if (state.action) { | ||
switch (state.action.type) { | ||
case 'delete': { | ||
const newColumns = columns.slice() | ||
newColumns.splice(state.columnIndex, 1) | ||
onColumnsChange(newColumns) | ||
break | ||
} | ||
case 'insert': { | ||
const newColumns = columns.slice() | ||
const [column] = newColumns.splice(state.columnIndex, 1) | ||
newColumns.splice(state.action.place.index, 0, column) | ||
onColumnsChange(newColumns) | ||
break | ||
} | ||
} | ||
} | ||
|
||
state = null | ||
onColumnDragStateChanges(state) | ||
}, | ||
|
||
onAbort() { | ||
state = null | ||
onColumnDragStateChanges(state) | ||
}, | ||
}) | ||
} |
26 changes: 26 additions & 0 deletions
26
developer-extension/src/panel/components/tabs/eventsTab/columnUtils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export type EventListColumn = | ||
| { type: 'date' } | ||
| { type: 'description' } | ||
| { type: 'type' } | ||
| { type: 'field'; path: string } | ||
|
||
export const DEFAULT_COLUMNS: EventListColumn[] = [{ type: 'date' }, { type: 'type' }, { type: 'description' }] | ||
|
||
export function includesColumn(existingColumns: EventListColumn[], newColumn: EventListColumn) { | ||
return existingColumns.some((column) => { | ||
if (column.type === 'field' && newColumn.type === 'field') { | ||
return column.path === newColumn.path | ||
} | ||
return column.type === newColumn.type | ||
}) | ||
} | ||
|
||
export function getColumnTitle(column: EventListColumn) { | ||
return column.type === 'date' | ||
? 'Date' | ||
: column.type === 'description' | ||
? 'Description' | ||
: column.type === 'type' | ||
? 'Type' | ||
: column.path | ||
} |
121 changes: 121 additions & 0 deletions
121
developer-extension/src/panel/components/tabs/eventsTab/drag.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
export interface Coordinates { | ||
x: number | ||
y: number | ||
} | ||
|
||
/** | ||
* This is a framework agnostic drag implementation that works as a state machine: | ||
* | ||
* ``` | ||
* (init) | ||
* | | ||
* [onStart] | ||
* | \______________ | ||
* | \ | ||
* <returns true> <returns false> | ||
* | ____ | | ||
* | / \ (end) | ||
* [onMove] ) | ||
* | \ \____/ | ||
* | \______________ | ||
* | \ | ||
* <drop in the window> | | ||
* | <stop() called or drop out of the window> | ||
* | | | ||
* [onDrop] [onAbort] | ||
* | _________________/ | ||
* |/ | ||
* (end) | ||
* ``` | ||
*/ | ||
export function initDrag({ | ||
target, | ||
onStart, | ||
onMove, | ||
onAbort, | ||
onDrop, | ||
}: { | ||
target: HTMLElement | ||
onStart: (event: { target: HTMLElement; position: Coordinates }) => boolean | void | ||
onMove: (event: { position: Coordinates }) => void | ||
onDrop: () => void | ||
onAbort: () => void | ||
}) { | ||
type DragState = | ||
| { isDragging: false } | ||
| { | ||
isDragging: true | ||
removeListeners: () => void | ||
} | ||
|
||
let state: DragState = { | ||
isDragging: false, | ||
} | ||
|
||
target.addEventListener('pointerdown', onPointerDown, { capture: true }) | ||
|
||
return { | ||
stop: () => { | ||
endDrag(true) | ||
target.removeEventListener('pointerdown', onPointerDown, { capture: true }) | ||
}, | ||
} | ||
|
||
function onPointerDown(event: PointerEvent) { | ||
if ( | ||
state.isDragging || | ||
event.buttons !== 1 || | ||
onStart({ target: event.target as HTMLElement, position: { x: event.clientX, y: event.clientY } }) === false | ||
) { | ||
return | ||
} | ||
|
||
event.preventDefault() | ||
|
||
state = { | ||
isDragging: true, | ||
removeListeners: () => { | ||
removeEventListener('pointerup', onPointerUp, { capture: true }) | ||
removeEventListener('pointermove', onPointerMove, { capture: true }) | ||
}, | ||
} | ||
|
||
addEventListener('pointerup', onPointerUp, { capture: true }) | ||
addEventListener('pointermove', onPointerMove, { capture: true }) | ||
} | ||
|
||
function onPointerUp(_event: PointerEvent) { | ||
endDrag(false) | ||
} | ||
|
||
function onPointerMove(event: PointerEvent) { | ||
if (!state.isDragging) { | ||
return | ||
} | ||
|
||
if (event.buttons !== 1) { | ||
// The user might have released the click outside of the window | ||
endDrag(true) | ||
return | ||
} | ||
|
||
onMove({ | ||
position: { | ||
x: event.clientX, | ||
y: event.clientY, | ||
}, | ||
}) | ||
} | ||
|
||
function endDrag(abort: boolean) { | ||
if (state.isDragging) { | ||
if (abort) { | ||
onAbort() | ||
} else { | ||
onDrop() | ||
} | ||
state.removeListeners() | ||
state = { isDragging: false } | ||
} | ||
} | ||
} |
Oops, something went wrong.