mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-15 07:54:18 +02:00
add drag/drop from lists into queue
This commit is contained in:
@@ -119,6 +119,44 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.container.data-row.dragged-over-top::before {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
height: 2px;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary);
|
||||
}
|
||||
|
||||
.container.data-row.dragged-over-top.dragged-over-first-cell::before {
|
||||
right: -9999px;
|
||||
left: -9999px;
|
||||
margin-right: 9999px;
|
||||
margin-left: 9999px;
|
||||
}
|
||||
|
||||
.container.data-row.dragged-over-bottom::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
height: 2px;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary);
|
||||
}
|
||||
|
||||
.container.data-row.dragged-over-bottom.dragged-over-first-cell::after {
|
||||
right: -9999px;
|
||||
left: -9999px;
|
||||
margin-right: 9999px;
|
||||
margin-left: 9999px;
|
||||
}
|
||||
|
||||
.container.data-row > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -126,6 +164,8 @@
|
||||
|
||||
.container.data-row {
|
||||
cursor: pointer;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
|
||||
@@ -35,13 +35,17 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {}
|
||||
|
||||
export interface ItemTableListInnerColumn extends ItemTableListColumn {
|
||||
controls: ItemControls;
|
||||
dragRef?: null | React.Ref<HTMLDivElement>;
|
||||
isDraggedOver?: 'bottom' | 'top' | null;
|
||||
isDragging?: boolean;
|
||||
type: TableColumn;
|
||||
}
|
||||
|
||||
@@ -49,9 +53,149 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
|
||||
const type = props.columns[props.columnIndex].id as TableColumn;
|
||||
|
||||
const isHeaderEnabled = !!props.enableHeader;
|
||||
const isDataRow = isHeaderEnabled ? props.rowIndex > 0 : true;
|
||||
const item = isDataRow ? props.data[props.rowIndex] : null;
|
||||
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item;
|
||||
|
||||
const {
|
||||
isDraggedOver,
|
||||
isDragging: isDraggingLocal,
|
||||
ref: dragRef,
|
||||
} = useDragDrop<HTMLDivElement>({
|
||||
drag: {
|
||||
getId: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(item as any, props.internalState);
|
||||
|
||||
console.log('getId', draggedItems);
|
||||
|
||||
return draggedItems.map((draggedItem) => draggedItem.id);
|
||||
},
|
||||
getItem: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(item as any, props.internalState);
|
||||
|
||||
console.log('getItem', draggedItems);
|
||||
|
||||
return draggedItems;
|
||||
},
|
||||
itemType: props.itemType,
|
||||
onDragStart: () => {
|
||||
if (!item || !isDataRow || !props.internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(item as any, props.internalState);
|
||||
|
||||
props.internalState.setDragging(draggedItems);
|
||||
},
|
||||
onDrop: () => {
|
||||
if (props.internalState) {
|
||||
props.internalState.setDragging([]);
|
||||
}
|
||||
},
|
||||
operation:
|
||||
props.itemType === LibraryItem.QUEUE_SONG
|
||||
? [DragOperation.REORDER, DragOperation.ADD]
|
||||
: [DragOperation.ADD],
|
||||
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
|
||||
},
|
||||
drop: {
|
||||
canDrop: () => {
|
||||
if (props.itemType === LibraryItem.QUEUE_SONG) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getData: () => {
|
||||
return {
|
||||
id: [(item as unknown as { id: string }).id],
|
||||
item: [item as unknown as unknown[]],
|
||||
itemType: props.itemType,
|
||||
type: DragTargetMap[props.itemType] || DragTarget.GENERIC,
|
||||
};
|
||||
},
|
||||
onDrag: () => {
|
||||
return;
|
||||
},
|
||||
onDragLeave: () => {
|
||||
return;
|
||||
},
|
||||
onDrop: (args) => {
|
||||
if (args.self.type === DragTarget.QUEUE_SONG) {
|
||||
const sourceServerId = (
|
||||
args.source.item?.[0] as unknown as { _serverId: string }
|
||||
)._serverId;
|
||||
|
||||
const sourceItemType = args.source.itemType as LibraryItem;
|
||||
|
||||
const droppedOnUniqueId = (
|
||||
args.self.item?.[0] as unknown as { _uniqueId: string }
|
||||
)._uniqueId;
|
||||
|
||||
switch (args.source.type) {
|
||||
case DragTarget.ALBUM: {
|
||||
props.playerContext.addToQueueByFetch(
|
||||
sourceServerId,
|
||||
args.source.id,
|
||||
sourceItemType,
|
||||
{ edge: args.edge, uniqueId: droppedOnUniqueId },
|
||||
);
|
||||
break;
|
||||
}
|
||||
case DragTarget.ALBUM_ARTIST: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.ARTIST: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.GENRE: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.PLAYLIST: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.QUEUE_SONG: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.SONG: {
|
||||
break;
|
||||
}
|
||||
case DragTarget.TABLE_COLUMN: {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
},
|
||||
isEnabled: shouldEnableDrag,
|
||||
});
|
||||
|
||||
const isDragging =
|
||||
item && typeof item === 'object' && 'id' in item && props.internalState
|
||||
? props.internalState.isDragging((item as any).id)
|
||||
: isDraggingLocal;
|
||||
|
||||
const controls = props.controls;
|
||||
|
||||
const dragProps = {
|
||||
dragRef: shouldEnableDrag ? dragRef : null,
|
||||
isDraggedOver: isDraggedOver === 'top' || isDraggedOver === 'bottom' ? isDraggedOver : null,
|
||||
isDragging,
|
||||
};
|
||||
|
||||
if (isHeaderEnabled && props.rowIndex === 0) {
|
||||
return <TableColumnHeaderContainer {...props} controls={controls} type={type} />;
|
||||
}
|
||||
@@ -59,22 +203,22 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
|
||||
switch (type) {
|
||||
case TableColumn.ACTIONS:
|
||||
case TableColumn.SKIP:
|
||||
return <ActionsColumn {...props} controls={controls} type={type} />;
|
||||
return <ActionsColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.ALBUM_ARTIST:
|
||||
return <AlbumArtistsColumn {...props} controls={controls} type={type} />;
|
||||
return <AlbumArtistsColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.ALBUM_COUNT:
|
||||
case TableColumn.PLAY_COUNT:
|
||||
case TableColumn.SONG_COUNT:
|
||||
return <CountColumn {...props} controls={controls} type={type} />;
|
||||
return <CountColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.ARTIST:
|
||||
return <ArtistsColumn {...props} controls={controls} type={type} />;
|
||||
return <ArtistsColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.BIOGRAPHY:
|
||||
case TableColumn.COMMENT:
|
||||
return <TextColumn {...props} controls={controls} type={type} />;
|
||||
return <TextColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.BIT_RATE:
|
||||
case TableColumn.BPM:
|
||||
@@ -82,50 +226,52 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
|
||||
case TableColumn.DISC_NUMBER:
|
||||
case TableColumn.TRACK_NUMBER:
|
||||
case TableColumn.YEAR:
|
||||
return <NumericColumn {...props} controls={controls} type={type} />;
|
||||
return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.DATE_ADDED:
|
||||
case TableColumn.RELEASE_DATE:
|
||||
return <DateColumn {...props} controls={controls} type={type} />;
|
||||
return <DateColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.DURATION:
|
||||
return <DurationColumn {...props} controls={controls} type={type} />;
|
||||
return <DurationColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.GENRE:
|
||||
return <GenreColumn {...props} controls={controls} type={type} />;
|
||||
return <GenreColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.GENRE_BADGE:
|
||||
return <GenreBadgeColumn {...props} controls={controls} type={type} />;
|
||||
return <GenreBadgeColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.IMAGE:
|
||||
return <ImageColumn {...props} controls={controls} type={type} />;
|
||||
return <ImageColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.LAST_PLAYED:
|
||||
return <RelativeDateColumn {...props} controls={controls} type={type} />;
|
||||
return <RelativeDateColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.PATH:
|
||||
return <PathColumn {...props} controls={controls} type={type} />;
|
||||
return <PathColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.ROW_INDEX:
|
||||
return <RowIndexColumn {...props} controls={controls} type={type} />;
|
||||
return <RowIndexColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.SIZE:
|
||||
return <SizeColumn {...props} controls={controls} type={type} />;
|
||||
return <SizeColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.TITLE:
|
||||
return <TitleColumn {...props} controls={controls} type={type} />;
|
||||
return <TitleColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.TITLE_COMBINED:
|
||||
return <TitleCombinedColumn {...props} controls={controls} type={type} />;
|
||||
return (
|
||||
<TitleCombinedColumn {...props} {...dragProps} controls={controls} type={type} />
|
||||
);
|
||||
|
||||
case TableColumn.USER_FAVORITE:
|
||||
return <FavoriteColumn {...props} controls={controls} type={type} />;
|
||||
return <FavoriteColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
case TableColumn.USER_RATING:
|
||||
return <RatingColumn {...props} controls={controls} type={type} />;
|
||||
return <RatingColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
default:
|
||||
return <DefaultColumn {...props} controls={controls} type={type} />;
|
||||
return <DefaultColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,6 +283,9 @@ export const TableColumnTextContainer = (
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
controls: ItemControls;
|
||||
dragRef?: null | React.Ref<HTMLDivElement>;
|
||||
isDraggedOver?: 'bottom' | 'top' | null;
|
||||
isDragging?: boolean;
|
||||
type: TableColumn;
|
||||
},
|
||||
) => {
|
||||
@@ -149,57 +298,8 @@ export const TableColumnTextContainer = (
|
||||
? props.internalState.isSelected((item as any).id)
|
||||
: false;
|
||||
|
||||
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item;
|
||||
|
||||
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
|
||||
drag: {
|
||||
getId: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(
|
||||
item as any,
|
||||
props.itemType,
|
||||
props.internalState,
|
||||
);
|
||||
return draggedItems.map((draggedItem) => draggedItem.id);
|
||||
},
|
||||
getItem: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [item];
|
||||
},
|
||||
onDragStart: () => {
|
||||
if (!item || !isDataRow || !props.internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(
|
||||
item as any,
|
||||
props.itemType,
|
||||
props.internalState,
|
||||
);
|
||||
props.internalState.setDragging(draggedItems);
|
||||
},
|
||||
onDrop: () => {
|
||||
if (props.internalState) {
|
||||
props.internalState.setDragging([]);
|
||||
}
|
||||
},
|
||||
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
|
||||
},
|
||||
isEnabled: shouldEnableDrag,
|
||||
});
|
||||
|
||||
const isDragging =
|
||||
item && typeof item === 'object' && 'id' in item && props.internalState
|
||||
? props.internalState.isDragging((item as any).id)
|
||||
: isDraggingLocal;
|
||||
|
||||
const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null);
|
||||
const isDragging = props.isDragging ?? false;
|
||||
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDataRow || !containerRef.current) return;
|
||||
@@ -209,13 +309,17 @@ export const TableColumnTextContainer = (
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
// Find all cells in the same row and add hover class
|
||||
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`);
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Remove hover class from all cells in the same row
|
||||
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`);
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
||||
};
|
||||
|
||||
@@ -226,7 +330,53 @@ export const TableColumnTextContainer = (
|
||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight]);
|
||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
||||
|
||||
// Apply dragged over state to all cells in the row so border can span entire row
|
||||
useEffect(() => {
|
||||
if (!isDataRow || !containerRef.current) return;
|
||||
|
||||
const rowIndex = props.rowIndex;
|
||||
const draggedOverState = props.isDraggedOver;
|
||||
|
||||
if (draggedOverState) {
|
||||
// Find all cells in the same row and add dragged over class
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell, index) => {
|
||||
if (draggedOverState === 'top') {
|
||||
cell.classList.add(styles.draggedOverTop);
|
||||
cell.classList.remove(styles.draggedOverBottom);
|
||||
// Mark first cell so border can span full width
|
||||
if (index === 0) {
|
||||
cell.classList.add(styles.draggedOverFirstCell);
|
||||
} else {
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
}
|
||||
} else if (draggedOverState === 'bottom') {
|
||||
cell.classList.add(styles.draggedOverBottom);
|
||||
cell.classList.remove(styles.draggedOverTop);
|
||||
// Mark first cell so border can span full width
|
||||
if (index === 0) {
|
||||
cell.classList.add(styles.draggedOverFirstCell);
|
||||
} else {
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Remove dragged over classes from all cells in the same row
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => {
|
||||
cell.classList.remove(styles.draggedOverTop);
|
||||
cell.classList.remove(styles.draggedOverBottom);
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
});
|
||||
}
|
||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||
|
||||
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
@@ -259,6 +409,8 @@ export const TableColumnTextContainer = (
|
||||
[styles.center]: props.columns[props.columnIndex].align === 'center',
|
||||
[styles.compact]: props.size === 'compact',
|
||||
[styles.dataRow]: isDataRow,
|
||||
[styles.draggedOverBottom]: isDataRow && props.isDraggedOver === 'bottom',
|
||||
[styles.draggedOverTop]: isDataRow && props.isDraggedOver === 'top',
|
||||
[styles.dragging]: isDataRow && isDragging,
|
||||
[styles.large]: props.size === 'large',
|
||||
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
||||
@@ -274,7 +426,7 @@ export const TableColumnTextContainer = (
|
||||
props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0,
|
||||
[styles.withVerticalBorder]: props.enableVerticalBorders,
|
||||
})}
|
||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||
onClick={handleCellClick}
|
||||
ref={mergedRef}
|
||||
style={props.style}
|
||||
@@ -299,6 +451,9 @@ export const TableColumnContainer = (
|
||||
className?: string;
|
||||
containerStyle?: CSSProperties;
|
||||
controls: ItemControls;
|
||||
dragRef?: null | React.Ref<HTMLDivElement>;
|
||||
isDraggedOver?: 'bottom' | 'top' | null;
|
||||
isDragging?: boolean;
|
||||
type: TableColumn;
|
||||
},
|
||||
) => {
|
||||
@@ -311,57 +466,8 @@ export const TableColumnContainer = (
|
||||
? props.internalState.isSelected((item as any).id)
|
||||
: false;
|
||||
|
||||
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item;
|
||||
|
||||
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
|
||||
drag: {
|
||||
getId: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(
|
||||
item as any,
|
||||
props.itemType,
|
||||
props.internalState,
|
||||
);
|
||||
return draggedItems.map((draggedItem) => draggedItem.id);
|
||||
},
|
||||
getItem: () => {
|
||||
if (!item || !isDataRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [item];
|
||||
},
|
||||
onDragStart: () => {
|
||||
if (!item || !isDataRow || !props.internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedItems = getDraggedItems(
|
||||
item as any,
|
||||
props.itemType,
|
||||
props.internalState,
|
||||
);
|
||||
props.internalState.setDragging(draggedItems);
|
||||
},
|
||||
onDrop: () => {
|
||||
if (props.internalState) {
|
||||
props.internalState.setDragging([]);
|
||||
}
|
||||
},
|
||||
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
|
||||
},
|
||||
isEnabled: shouldEnableDrag,
|
||||
});
|
||||
|
||||
const isDragging =
|
||||
item && typeof item === 'object' && 'id' in item && props.internalState
|
||||
? props.internalState.isDragging((item as any).id)
|
||||
: isDraggingLocal;
|
||||
|
||||
const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null);
|
||||
const isDragging = props.isDragging ?? false;
|
||||
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDataRow || !containerRef.current) return;
|
||||
@@ -371,13 +477,17 @@ export const TableColumnContainer = (
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
// Find all cells in the same row and add hover class
|
||||
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`);
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Remove hover class from all cells in the same row
|
||||
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`);
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
||||
};
|
||||
|
||||
@@ -388,7 +498,53 @@ export const TableColumnContainer = (
|
||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight]);
|
||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
||||
|
||||
// Apply dragged over state to all cells in the row so border can span entire row
|
||||
useEffect(() => {
|
||||
if (!isDataRow || !containerRef.current) return;
|
||||
|
||||
const rowIndex = props.rowIndex;
|
||||
const draggedOverState = props.isDraggedOver;
|
||||
|
||||
if (draggedOverState) {
|
||||
// Find all cells in the same row and add dragged over class
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell, index) => {
|
||||
if (draggedOverState === 'top') {
|
||||
cell.classList.add(styles.draggedOverTop);
|
||||
cell.classList.remove(styles.draggedOverBottom);
|
||||
// Mark first cell so border can span full width
|
||||
if (index === 0) {
|
||||
cell.classList.add(styles.draggedOverFirstCell);
|
||||
} else {
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
}
|
||||
} else if (draggedOverState === 'bottom') {
|
||||
cell.classList.add(styles.draggedOverBottom);
|
||||
cell.classList.remove(styles.draggedOverTop);
|
||||
// Mark first cell so border can span full width
|
||||
if (index === 0) {
|
||||
cell.classList.add(styles.draggedOverFirstCell);
|
||||
} else {
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Remove dragged over classes from all cells in the same row
|
||||
const allCells = document.querySelectorAll(
|
||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
||||
);
|
||||
allCells.forEach((cell) => {
|
||||
cell.classList.remove(styles.draggedOverTop);
|
||||
cell.classList.remove(styles.draggedOverBottom);
|
||||
cell.classList.remove(styles.draggedOverFirstCell);
|
||||
});
|
||||
}
|
||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||
|
||||
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
@@ -421,6 +577,8 @@ export const TableColumnContainer = (
|
||||
[styles.center]: props.columns[props.columnIndex].align === 'center',
|
||||
[styles.compact]: props.size === 'compact',
|
||||
[styles.dataRow]: isDataRow,
|
||||
[styles.draggedOverBottom]: isDataRow && props.isDraggedOver === 'bottom',
|
||||
[styles.draggedOverTop]: isDataRow && props.isDraggedOver === 'top',
|
||||
[styles.dragging]: isDataRow && isDragging,
|
||||
[styles.large]: props.size === 'large',
|
||||
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
||||
@@ -436,7 +594,7 @@ export const TableColumnContainer = (
|
||||
props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0,
|
||||
[styles.withVerticalBorder]: props.enableVerticalBorders,
|
||||
})}
|
||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||
onClick={handleCellClick}
|
||||
ref={mergedRef}
|
||||
style={{ ...props.containerStyle, ...props.style }}
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-table-grid-container {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-table-pinned-rows-container {
|
||||
|
||||
@@ -11,6 +11,7 @@ import React, {
|
||||
Ref,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -26,6 +27,7 @@ import { useDefaultItemListControls } from '/@/renderer/components/item-list/hel
|
||||
import {
|
||||
ItemListStateActions,
|
||||
ItemListStateItem,
|
||||
ItemListStateItemWithRequiredProperties,
|
||||
useItemListState,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
||||
@@ -34,8 +36,45 @@ import {
|
||||
ItemListHandle,
|
||||
ItemTableListColumnConfig,
|
||||
} from '/@/renderer/components/item-list/types';
|
||||
import {
|
||||
PlayerContext,
|
||||
usePlayerContext,
|
||||
} from '/@/renderer/features/player/context/player-context';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
/**
|
||||
* Type guard to check if an item has the required properties (id and serverId)
|
||||
* Similar to the type guard used in ItemCard
|
||||
*/
|
||||
const hasRequiredItemProperties = (item: unknown): item is { id: string; serverId: string } => {
|
||||
return (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'id' in item &&
|
||||
typeof (item as any).id === 'string' &&
|
||||
'serverId' in item &&
|
||||
typeof (item as any).serverId === 'string'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if an item has the required properties for ItemListStateItemWithRequiredProperties
|
||||
*/
|
||||
const hasRequiredStateItemProperties = (
|
||||
item: unknown,
|
||||
): item is ItemListStateItemWithRequiredProperties => {
|
||||
return (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'id' in item &&
|
||||
typeof (item as any).id === 'string' &&
|
||||
'_serverId' in item &&
|
||||
typeof (item as any)._serverId === 'string' &&
|
||||
'itemType' in item &&
|
||||
typeof (item as any).itemType === 'string'
|
||||
);
|
||||
};
|
||||
|
||||
interface VirtualizedTableGridProps {
|
||||
calculatedColumnWidths: number[];
|
||||
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
||||
@@ -64,9 +103,11 @@ interface VirtualizedTableGridProps {
|
||||
pinnedRightColumnRef: React.RefObject<HTMLDivElement>;
|
||||
pinnedRowCount: number;
|
||||
pinnedRowRef: React.RefObject<HTMLDivElement>;
|
||||
playerContext: PlayerContext;
|
||||
showLeftShadow: boolean;
|
||||
showRightShadow: boolean;
|
||||
size: 'compact' | 'default' | 'large';
|
||||
tableId: string;
|
||||
totalColumnCount: number;
|
||||
totalRowCount: number;
|
||||
}
|
||||
@@ -100,9 +141,11 @@ const VirtualizedTableGrid = React.memo(
|
||||
pinnedRightColumnRef,
|
||||
pinnedRowCount,
|
||||
pinnedRowRef,
|
||||
playerContext,
|
||||
showLeftShadow,
|
||||
showRightShadow,
|
||||
size,
|
||||
tableId,
|
||||
totalColumnCount,
|
||||
totalRowCount,
|
||||
}: VirtualizedTableGridProps) => {
|
||||
@@ -129,7 +172,9 @@ const VirtualizedTableGrid = React.memo(
|
||||
internalState,
|
||||
itemType,
|
||||
onRowClick,
|
||||
playerContext,
|
||||
size,
|
||||
tableId,
|
||||
}),
|
||||
[
|
||||
cellPadding,
|
||||
@@ -146,9 +191,11 @@ const VirtualizedTableGrid = React.memo(
|
||||
enableVerticalBorders,
|
||||
getRowHeight,
|
||||
internalState,
|
||||
playerContext,
|
||||
itemType,
|
||||
onRowClick,
|
||||
size,
|
||||
tableId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -430,7 +477,9 @@ export interface TableItemProps {
|
||||
internalState: ItemListStateActions;
|
||||
itemType: ItemTableListProps['itemType'];
|
||||
onRowClick?: (item: any, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
playerContext: PlayerContext;
|
||||
size?: ItemTableListProps['size'];
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
interface ItemTableListProps {
|
||||
@@ -484,10 +533,11 @@ export const ItemTableList = ({
|
||||
rowHeight,
|
||||
size = 'default',
|
||||
}: ItemTableListProps) => {
|
||||
const tableId = useId();
|
||||
const totalItemCount = enableHeader ? data.length + 1 : data.length;
|
||||
const parsedColumns = useMemo(() => parseTableColumns(columns), [columns]);
|
||||
const columnCount = parsedColumns.length;
|
||||
|
||||
const playerContext = usePlayerContext();
|
||||
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -614,7 +664,9 @@ export const ItemTableList = ({
|
||||
getRowHeight: () => DEFAULT_ROW_HEIGHT,
|
||||
internalState: {} as ItemListStateActions,
|
||||
itemType,
|
||||
playerContext,
|
||||
size,
|
||||
tableId,
|
||||
};
|
||||
|
||||
for (let i = 0; i < adjustedIndex; i++) {
|
||||
@@ -632,11 +684,9 @@ export const ItemTableList = ({
|
||||
},
|
||||
[
|
||||
enableHeader,
|
||||
rowHeight,
|
||||
size,
|
||||
DEFAULT_ROW_HEIGHT,
|
||||
cellPadding,
|
||||
parsedColumns,
|
||||
data,
|
||||
enableAlternateRowColors,
|
||||
enableExpansion,
|
||||
enableHorizontalBorders,
|
||||
@@ -644,7 +694,11 @@ export const ItemTableList = ({
|
||||
enableSelection,
|
||||
enableVerticalBorders,
|
||||
itemType,
|
||||
data,
|
||||
playerContext,
|
||||
size,
|
||||
tableId,
|
||||
DEFAULT_ROW_HEIGHT,
|
||||
rowHeight,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -989,7 +1043,7 @@ export const ItemTableList = ({
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(item: any, event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!enableSelection || !item) {
|
||||
if (!enableSelection || !item || !hasRequiredItemProperties(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1007,29 +1061,35 @@ export const ItemTableList = ({
|
||||
// Remove this item from selection
|
||||
const currentSelected = internalState.getSelected();
|
||||
const filteredSelected = currentSelected.filter(
|
||||
(selectedItem) => selectedItem.id !== item.id,
|
||||
(selectedItem): selectedItem is ItemListStateItemWithRequiredProperties =>
|
||||
hasRequiredStateItemProperties(selectedItem) &&
|
||||
selectedItem.id !== item.id,
|
||||
);
|
||||
internalState.setSelected(filteredSelected);
|
||||
} else {
|
||||
// Add this item to selection
|
||||
const currentSelected = internalState.getSelected();
|
||||
const newSelected = [...currentSelected, itemListItem];
|
||||
const validSelected = currentSelected.filter(hasRequiredStateItemProperties);
|
||||
const newSelected: ItemListStateItemWithRequiredProperties[] = [
|
||||
...validSelected,
|
||||
itemListItem as ItemListStateItemWithRequiredProperties,
|
||||
];
|
||||
internalState.setSelected(newSelected);
|
||||
}
|
||||
}
|
||||
// Check if shift key is held for range selection
|
||||
else if (event.shiftKey) {
|
||||
const selectedItems = internalState.getSelected();
|
||||
const lastSelectedItem = selectedItems[selectedItems.length - 1];
|
||||
const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
|
||||
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
|
||||
|
||||
if (lastSelectedItem) {
|
||||
// Find the indices of the last selected item and current item
|
||||
const lastIndex = data.findIndex(
|
||||
(d) =>
|
||||
d && typeof d === 'object' && 'id' in d && d.id === lastSelectedItem.id,
|
||||
(d) => hasRequiredItemProperties(d) && d.id === lastSelectedItem.id,
|
||||
);
|
||||
const currentIndex = data.findIndex(
|
||||
(d) => d && typeof d === 'object' && 'id' in d && d.id === item.id,
|
||||
(d) => hasRequiredItemProperties(d) && d.id === item.id,
|
||||
);
|
||||
|
||||
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||
@@ -1037,20 +1097,15 @@ export const ItemTableList = ({
|
||||
const startIndex = Math.min(lastIndex, currentIndex);
|
||||
const stopIndex = Math.max(lastIndex, currentIndex);
|
||||
|
||||
const rangeItems: ItemListStateItem[] = [];
|
||||
const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
|
||||
for (let i = startIndex; i <= stopIndex; i++) {
|
||||
const rangeItem = data[i];
|
||||
if (
|
||||
rangeItem &&
|
||||
typeof rangeItem === 'object' &&
|
||||
'id' in rangeItem &&
|
||||
'serverId' in rangeItem
|
||||
) {
|
||||
if (hasRequiredItemProperties(rangeItem)) {
|
||||
rangeItems.push({
|
||||
_serverId: (rangeItem as any).serverId,
|
||||
id: (rangeItem as any).id,
|
||||
_serverId: rangeItem.serverId,
|
||||
id: rangeItem.id,
|
||||
itemType,
|
||||
});
|
||||
} as ItemListStateItemWithRequiredProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1061,7 +1116,10 @@ export const ItemTableList = ({
|
||||
// Deselect the range
|
||||
const currentSelected = internalState.getSelected();
|
||||
const filteredSelected = currentSelected.filter(
|
||||
(selectedItem) =>
|
||||
(
|
||||
selectedItem,
|
||||
): selectedItem is ItemListStateItemWithRequiredProperties =>
|
||||
hasRequiredStateItemProperties(selectedItem) &&
|
||||
!rangeItems.some(
|
||||
(rangeItem) => rangeItem.id === selectedItem.id,
|
||||
),
|
||||
@@ -1070,7 +1128,12 @@ export const ItemTableList = ({
|
||||
} else {
|
||||
// Select the range
|
||||
const currentSelected = internalState.getSelected();
|
||||
const newSelected = [...currentSelected];
|
||||
const validSelected = currentSelected.filter(
|
||||
hasRequiredStateItemProperties,
|
||||
);
|
||||
const newSelected: ItemListStateItemWithRequiredProperties[] = [
|
||||
...validSelected,
|
||||
];
|
||||
rangeItems.forEach((rangeItem) => {
|
||||
if (!newSelected.some((selected) => selected.id === rangeItem.id)) {
|
||||
newSelected.push(rangeItem);
|
||||
@@ -1081,19 +1144,24 @@ export const ItemTableList = ({
|
||||
}
|
||||
} else {
|
||||
// No previous selection, just toggle this item
|
||||
internalState.toggleSelected(itemListItem);
|
||||
internalState.toggleSelected(
|
||||
itemListItem as ItemListStateItemWithRequiredProperties,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Regular click - deselect all others and select only this item
|
||||
// If this item is already the only selected item, deselect it
|
||||
const selectedItems = internalState.getSelected();
|
||||
const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
|
||||
const isOnlySelected =
|
||||
selectedItems.length === 1 && selectedItems[0].id === item.id;
|
||||
validSelectedItems.length === 1 && validSelectedItems[0].id === item.id;
|
||||
|
||||
if (isOnlySelected) {
|
||||
internalState.clearSelected();
|
||||
} else {
|
||||
internalState.setSelected([itemListItem]);
|
||||
internalState.setSelected([
|
||||
itemListItem as ItemListStateItemWithRequiredProperties,
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1108,12 +1176,13 @@ export const ItemTableList = ({
|
||||
e.stopPropagation();
|
||||
|
||||
const selected = internalState.getSelected();
|
||||
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||
let currentIndex = -1;
|
||||
|
||||
if (selected.length > 0) {
|
||||
const lastSelected = selected[selected.length - 1];
|
||||
if (validSelected.length > 0) {
|
||||
const lastSelected = validSelected[validSelected.length - 1];
|
||||
currentIndex = data.findIndex(
|
||||
(d: any) => d && typeof d === 'object' && 'id' in d && d.id === lastSelected.id,
|
||||
(d) => hasRequiredItemProperties(d) && d.id === lastSelected.id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1131,13 +1200,13 @@ export const ItemTableList = ({
|
||||
// Handle Shift + Arrow for incremental range selection (matches shift+click behavior)
|
||||
if (e.shiftKey) {
|
||||
const selectedItems = internalState.getSelected();
|
||||
const lastSelectedItem = selectedItems[selectedItems.length - 1];
|
||||
const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
|
||||
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
|
||||
|
||||
if (lastSelectedItem) {
|
||||
// Find the indices of the last selected item and new item
|
||||
const lastIndex = data.findIndex(
|
||||
(d: any) =>
|
||||
d && typeof d === 'object' && 'id' in d && d.id === lastSelectedItem.id,
|
||||
(d) => hasRequiredItemProperties(d) && d.id === lastSelectedItem.id,
|
||||
);
|
||||
|
||||
if (lastIndex !== -1 && newIndex !== -1) {
|
||||
@@ -1145,26 +1214,26 @@ export const ItemTableList = ({
|
||||
const startIndex = Math.min(lastIndex, newIndex);
|
||||
const stopIndex = Math.max(lastIndex, newIndex);
|
||||
|
||||
const rangeItems: ItemListStateItem[] = [];
|
||||
const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
|
||||
for (let i = startIndex; i <= stopIndex; i++) {
|
||||
const rangeItem = data[i];
|
||||
if (
|
||||
rangeItem &&
|
||||
typeof rangeItem === 'object' &&
|
||||
'id' in rangeItem &&
|
||||
'serverId' in rangeItem
|
||||
) {
|
||||
if (hasRequiredItemProperties(rangeItem)) {
|
||||
rangeItems.push({
|
||||
_serverId: (rangeItem as any).serverId,
|
||||
id: (rangeItem as any).id,
|
||||
_serverId: rangeItem.serverId,
|
||||
id: rangeItem.id,
|
||||
itemType,
|
||||
});
|
||||
} as ItemListStateItemWithRequiredProperties);
|
||||
}
|
||||
}
|
||||
|
||||
// Add range items to selection (matching shift+click behavior)
|
||||
const currentSelected = internalState.getSelected();
|
||||
const newSelected = [...currentSelected];
|
||||
const validSelected = currentSelected.filter(
|
||||
hasRequiredStateItemProperties,
|
||||
);
|
||||
const newSelected: ItemListStateItemWithRequiredProperties[] = [
|
||||
...validSelected,
|
||||
];
|
||||
rangeItems.forEach((rangeItem) => {
|
||||
if (!newSelected.some((selected) => selected.id === rangeItem.id)) {
|
||||
newSelected.push(rangeItem);
|
||||
@@ -1172,38 +1241,44 @@ export const ItemTableList = ({
|
||||
});
|
||||
|
||||
// Ensure the last item in selection is the item at newIndex for incremental extension
|
||||
const newItemListItem: ItemListStateItem = {
|
||||
_serverId: newItem.serverId,
|
||||
id: newItem.id,
|
||||
itemType,
|
||||
};
|
||||
// Remove the new item from its current position if it exists
|
||||
const filteredSelected = newSelected.filter(
|
||||
(item) => item.id !== newItemListItem.id,
|
||||
);
|
||||
// Add it at the end so it becomes the last selected item
|
||||
filteredSelected.push(newItemListItem);
|
||||
internalState.setSelected(filteredSelected);
|
||||
if (hasRequiredItemProperties(newItem)) {
|
||||
const newItemListItem: ItemListStateItemWithRequiredProperties = {
|
||||
_serverId: newItem.serverId,
|
||||
id: newItem.id,
|
||||
itemType,
|
||||
} as ItemListStateItemWithRequiredProperties;
|
||||
// Remove the new item from its current position if it exists
|
||||
const filteredSelected = newSelected.filter(
|
||||
(item) => item.id !== newItemListItem.id,
|
||||
);
|
||||
// Add it at the end so it becomes the last selected item
|
||||
filteredSelected.push(newItemListItem);
|
||||
internalState.setSelected(filteredSelected);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No previous selection, just select the new item
|
||||
if (hasRequiredItemProperties(newItem)) {
|
||||
internalState.setSelected([
|
||||
{
|
||||
_serverId: newItem.serverId,
|
||||
id: newItem.id,
|
||||
itemType,
|
||||
} as ItemListStateItemWithRequiredProperties,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Without Shift: select only the new item
|
||||
if (hasRequiredItemProperties(newItem)) {
|
||||
internalState.setSelected([
|
||||
{
|
||||
_serverId: newItem.serverId,
|
||||
id: newItem.id,
|
||||
itemType,
|
||||
},
|
||||
} as ItemListStateItemWithRequiredProperties,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Without Shift: select only the new item
|
||||
internalState.setSelected([
|
||||
{
|
||||
_serverId: newItem.serverId,
|
||||
id: newItem.id,
|
||||
itemType,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const offset = calculateScrollTopForIndex(newIndex);
|
||||
@@ -1304,9 +1379,11 @@ export const ItemTableList = ({
|
||||
pinnedRightColumnRef={pinnedRightColumnRef}
|
||||
pinnedRowCount={pinnedRowCount}
|
||||
pinnedRowRef={pinnedRowRef}
|
||||
playerContext={playerContext}
|
||||
showLeftShadow={showLeftShadow}
|
||||
showRightShadow={showRightShadow}
|
||||
size={size}
|
||||
tableId={tableId}
|
||||
totalColumnCount={totalColumnCount}
|
||||
totalRowCount={totalRowCount}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user