mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
implement double click handler on default controls
This commit is contained in:
@@ -176,6 +176,7 @@ export const ItemCardControls = ({
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
controls?.onMore?.({
|
||||
event: e,
|
||||
internalState,
|
||||
@@ -183,6 +184,10 @@ export const ItemCardControls = ({
|
||||
itemType,
|
||||
});
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{controls?.onExpand && (
|
||||
@@ -260,14 +265,25 @@ interface SecondaryButtonProps {
|
||||
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
const SecondaryButton = ({ className, icon, onClick }: SecondaryButtonProps) => {
|
||||
const SecondaryButton = ({
|
||||
className,
|
||||
icon,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
}: SecondaryButtonProps & { onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(styles.secondaryButton, className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onClick?.(e);
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onDoubleClick?.(e);
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} size="lg" />
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Image } from '/@/shared/components/image/image';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useDoubleClick } from '/@/shared/hooks/use-double-click';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
@@ -138,22 +139,50 @@ const CompactItemCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleClick = useDoubleClick({
|
||||
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onDoubleClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
e.preventDefault();
|
||||
controls.onMore?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
@@ -170,6 +199,7 @@ const CompactItemCard = ({
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
@@ -242,22 +272,50 @@ const DefaultItemCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleClick = useDoubleClick({
|
||||
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onDoubleClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
e.preventDefault();
|
||||
controls.onMore?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
@@ -274,6 +332,7 @@ const DefaultItemCard = ({
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
@@ -390,22 +449,50 @@ const PosterItemCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleClick = useDoubleClick({
|
||||
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onDoubleClick?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger selection if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onClick?.({
|
||||
e.preventDefault();
|
||||
controls.onMore?.({
|
||||
event: e,
|
||||
internalState,
|
||||
item: data as any,
|
||||
@@ -424,6 +511,7 @@ const PosterItemCard = ({
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
|
||||
@@ -2,11 +2,30 @@ import {
|
||||
ItemTableListInnerColumn,
|
||||
TableColumnContainer,
|
||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { ItemListItem } from '/@/renderer/components/item-list/types';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
export const ActionsColumn = (props: ItemTableListInnerColumn) => {
|
||||
const row: any = (props.data as (any | undefined)[])[props.rowIndex];
|
||||
|
||||
const handleActionClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (row !== undefined) {
|
||||
props.controls.onMore?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: row as ItemListItem,
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionDoubleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
if (row !== undefined) {
|
||||
return (
|
||||
<TableColumnContainer {...props}>
|
||||
@@ -17,6 +36,8 @@ export const ActionsColumn = (props: ItemTableListInnerColumn) => {
|
||||
color: 'muted',
|
||||
size: 'md',
|
||||
}}
|
||||
onDoubleClick={handleActionDoubleClick}
|
||||
onClick={handleActionClick}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
|
||||
@@ -22,6 +22,8 @@ export const FavoriteColumn = (props: ItemTableListInnerColumn) => {
|
||||
size: 'md',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
props.controls.onFavorite?.({
|
||||
event,
|
||||
favorite: !row,
|
||||
@@ -30,6 +32,10 @@ export const FavoriteColumn = (props: ItemTableListInnerColumn) => {
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useDoubleClick } from '/@/shared/hooks/use-double-click';
|
||||
import { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
@@ -426,19 +427,43 @@ export const TableColumnTextContainer = (
|
||||
}
|
||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||
|
||||
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
const target = event.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
const handleClick = useDoubleClick({
|
||||
onDoubleClick: (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isDataRow && item) {
|
||||
props.controls.onDoubleClick?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSingleClick: (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
const target = event.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDataRow && item && props.enableSelection) {
|
||||
props.controls.onClick?.({
|
||||
if (isDataRow && item && props.enableSelection) {
|
||||
props.controls.onClick?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isDataRow && item) {
|
||||
event.preventDefault();
|
||||
props.controls.onMore?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
@@ -478,7 +503,8 @@ export const TableColumnTextContainer = (
|
||||
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
|
||||
})}
|
||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||
onClick={handleCellClick}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
ref={mergedRef}
|
||||
style={props.style}
|
||||
>
|
||||
@@ -604,19 +630,43 @@ export const TableColumnContainer = (
|
||||
}
|
||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||
|
||||
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
const target = event.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
const handleClick = useDoubleClick({
|
||||
onDoubleClick: (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isDataRow && item) {
|
||||
props.controls.onDoubleClick?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSingleClick: (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Don't trigger row selection if clicking on interactive elements
|
||||
const target = event.target as HTMLElement;
|
||||
const isInteractiveElement = target.closest(
|
||||
'button, a, input, select, textarea, [role="button"]',
|
||||
);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
if (isInteractiveElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDataRow && item && props.enableSelection) {
|
||||
props.controls.onClick?.({
|
||||
if (isDataRow && item && props.enableSelection) {
|
||||
props.controls.onClick?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
itemType: props.itemType,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isDataRow && item) {
|
||||
event.preventDefault();
|
||||
props.controls.onMore?.({
|
||||
event,
|
||||
internalState: props.internalState,
|
||||
item: item as ItemListItem,
|
||||
@@ -656,7 +706,8 @@ export const TableColumnContainer = (
|
||||
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
|
||||
})}
|
||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||
onClick={handleCellClick}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
ref={mergedRef}
|
||||
style={{ ...props.containerStyle, ...props.style }}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export const useDoubleClick = ({
|
||||
latency = 160,
|
||||
onDoubleClick = () => null,
|
||||
onSingleClick = () => null,
|
||||
}: {
|
||||
latency?: number;
|
||||
onDoubleClick?: (e: any) => void;
|
||||
onSingleClick?: (e: any) => void;
|
||||
}) => {
|
||||
const clickCountRef = useRef(0);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: any) => {
|
||||
clickCountRef.current += 1;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout to determine if it's a single or double click
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (clickCountRef.current === 1) {
|
||||
onSingleClick(e);
|
||||
} else if (clickCountRef.current === 2) {
|
||||
onDoubleClick(e);
|
||||
}
|
||||
|
||||
clickCountRef.current = 0;
|
||||
}, latency);
|
||||
},
|
||||
[latency, onDoubleClick, onSingleClick],
|
||||
);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return handleClick;
|
||||
};
|
||||
Reference in New Issue
Block a user