File Property reorder autoscroll.

This commit is contained in:
Robin E. R. Davies
2025-06-23 22:33:35 -04:00
parent 130cda3773
commit 78539c1795
5 changed files with 341 additions and 141 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
window.addEventListener .... done wrong everywhere!
touch scroll in file propertty dialog.
channel bindings help dialog. <ChannelBindingHelpDialog
Tooltips on touch ui?
draggable UI: scrolloffset!?
-3
View File
@@ -19,9 +19,6 @@ div {
min-width: 0;
}
.draggable-button-base {
touch-action: none;
}
@media not all /* seems to be ok in current chrome (prefers-color-scheme: light) */{
input[type="number"].scrollMod::-webkit-outer-spin-button,
+133 -60
View File
@@ -26,10 +26,11 @@ import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
export interface DraggableButtonBaseProps extends ButtonBaseProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement>) => boolean;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void;
onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>) => void;
longPressDelay? : number;
onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>| React.MouseEvent<HTMLButtonElement>) => void;
instantMouseLongPress?: boolean;
longPressDelay?: number;
}
interface Point {
@@ -58,37 +59,43 @@ function screenToClient(e: React.PointerEvent): Point {
export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
// Ensure that the props are spread correctly
const { onClick, onLongPressStart, onLongPressEnd,longPressDelay, ...rest } = props;
const { onClick, onLongPressStart, onLongPressMove:
doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props;
let [hTimeout, setHTimeout] = React.useState<number | null>(null);
let [pointerId, setPointerId] = React.useState<number | null>(null);
let [longPressed, setLongPressed] = React.useState<boolean>(false);
let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false);
let [pointerDownPoint, setPointerDownPoint] = React.useState<Point >({x: 0, y: 0});
let [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 });
let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null);
function handleSuppressClick(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
setSuppressClick(false);
e.stopPropagation();
e.preventDefault();
setSuppressClick(false);
}
function setSuppressClick(value: boolean) {
if (value !== clickSuppressed) {
setClickSuppressed(value);
if (value) {
window.addEventListener(
"click",
handleSuppressClick,
{
once: true,
capture: true
});
} else {
window.removeEventListener("click", handleSuppressClick);
setClickSuppressed(value);
}
function startLongPress(currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>)
{
if (hTimeout !== null) {
window.clearTimeout(hTimeout);
setHTimeout(null);
}
if (props.onLongPressStart) {
if (!props.onLongPressStart(currentTarget, e)) {
return;
}
}
setLongPressedElement(currentTarget);
setLongPressed(true);
setSuppressClick(true);
currentTarget.style.touchAction = "none"; // prevent scrolling.
// console.log("DraggableButtonBase: long press started");
}
function cancelLongPress() {
@@ -99,23 +106,106 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
setPointerId(null);
setLongPressed(false);
setSuppressClick(false);
if (longPressedElement) {
longPressedElement.style.touchAction = "";
setLongPressedElement(null);
}
if (longPressed) {
// console.log("DraggableButtonBase: long press canceled");
}
}
function stopLongPress(e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) {
if (longPressed) {
if (props.onLongPressEnd) {
props.onLongPressEnd(e);
}
}
cancelLongPress();
}
const handleTouchMove = (e: TouchEvent) => {
if (longPressed) {
e.preventDefault(); // Prevent scrolling during long press
e.stopPropagation();
}
}
const handleTouchstart = (e: TouchEvent) => {
if (longPressed) {
e.preventDefault(); // Prevent default touch behavior during long press
e.stopPropagation();
}
}
useEffect(() => {
let hTouchMove = (e: TouchEvent) => {
handleTouchMove(e);
}
addEventListener("touchmove", hTouchMove, { passive: false, capture: true });
let hTouchStart = (e: TouchEvent) => {
handleTouchstart(e);
}
addEventListener("touchstart", hTouchStart, { passive: false, capture: true });
return () => {
removeEventListener("touchmove", hTouchMove, { capture: true });
removeEventListener("touchstart", hTouchStart, { capture: true });
setSuppressClick(false);
cancelLongPress();
};
}, []);
useEffect(() => {
if (clickSuppressed) {
let hclick = (e: MouseEvent) => {
handleSuppressClick(e);
};
window.addEventListener("click",hclick, { capture: true });
let hTimeout: number | null = null;
hTimeout = window.setTimeout(() => {
setSuppressClick(false);
hTimeout = null;
}, 100);
return () => {
if (hTimeout !== null) {
window.clearTimeout(hTimeout);
}
window.removeEventListener("click", hclick, { capture: true });
}
} else {
return ()=>{};
}
}, [clickSuppressed]);
useEffect(() => {
if (longPressed) {
const suppressTouchMove = (e: TouchEvent) => {
e.preventDefault();
}
window.addEventListener("touchmove", suppressTouchMove, { capture: true, passive: false } );
return () => {
window.removeEventListener("touchmove", suppressTouchMove, {capture: true})
}
}
else {
return () => {
};
}
}, [longPressed]);
return (
<ButtonBase
{...rest}
className="draggable-button-base"
style={{ touchAction: longPressed ? "none" : undefined, ...props.style }}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
if (e.button === -1) { // touch long press
startLongPress(e.currentTarget as HTMLButtonElement,e);
}
return false;
}}
onPointerDown={(e) => {
@@ -130,19 +220,14 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
setPointerDownPoint(screenToClient(e));
let currentTarget = e.currentTarget as HTMLButtonElement;
if (longPressDelay === undefined || longPressDelay >= 0)
{
if (longPressDelay === undefined || longPressDelay >= 0) {
let delay = longPressDelay ?? 1250;
if (instantMouseLongPress === true && e.pointerType === "mouse") {
delay = 0;
}
setHTimeout(window.setTimeout(() => {
setHTimeout(null);
if (props.onLongPressStart) {
if (!props.onLongPressStart(currentTarget,e))
{
return;
}
}
setLongPressed(true);
setSuppressClick(true);
}, longPressDelay??1250));
startLongPress(currentTarget, e);
}, delay));
}
e.preventDefault();
e.stopPropagation();
@@ -150,17 +235,17 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
}}
onPointerMove={(e) => {
if (e.pointerId === pointerId) {
if(longPressed) {
if (props.onLongPressMove) {
props.onLongPressMove(e);
if (longPressed) {
if (doLongPressMove) {
doLongPressMove(e);
}
} else {
let clientPoint = screenToClient(e);
let dx = pointerDownPoint.x- clientPoint.x;
let dx = pointerDownPoint.x - clientPoint.x;
let dy = pointerDownPoint.y - clientPoint.y;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
cancelLongPress();
stopLongPress(e);
}
}
}
@@ -171,14 +256,8 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
onPointerUp={(e) => {
if (e.pointerId === pointerId) {
e.currentTarget.releasePointerCapture(e.pointerId);
setPointerId(null);
if (longPressed) {
if (props.onLongPressEnd) {
props.onLongPressEnd(e);
}
}
cancelLongPress();
if (e.isPropagationStopped()) {
stopLongPress(e);
if (e.isPropagationStopped() || e.isDefaultPrevented()) {
setSuppressClick(true); // only way to cancel the click
return;
}
@@ -187,30 +266,24 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
}
}}
// onTouchStart={(e)=> {
// //e.preventDefault();
// }}
// onTouchMove={(e)=> {
// //e.preventDefault();
// }}
onTouchStart={(e)=> {
//e.preventDefault();
}}
onClick={(e) => {
if (clickSuppressed) {
e.stopPropagation();
e.preventDefault();
setSuppressClick(false);
} else {
if (props.onClick) {
props.onClick(e);
if (onClick) {
onClick(e);
}
}
}}
onPointerCancelCapture={(e) => {
if (pointerId !== null) {
e.currentTarget.releasePointerCapture(e.pointerId);
if (longPressed && props.onLongPressEnd) {
props.onLongPressEnd(e)
}
cancelLongPress();
stopLongPress(e);
}
}}>
{props.children}
+195 -67
View File
@@ -63,6 +63,13 @@ import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48;
const AUTOSCROLL_SCROLL_PER_SECOND = 500;
const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000;
interface Point {
x: number;
y: number;
@@ -93,7 +100,7 @@ const audioFileExtensions: { [name: string]: boolean } = {
".ra": true
};
function screenToClient(currentTarget: HTMLElement, e: React.PointerEvent): Point {
function screenToClient(currentTarget: HTMLElement, e: React.PointerEvent | React.MouseEvent): Point {
let rect = currentTarget.getBoundingClientRect();
const x = e.clientX + rect.left;
const y = e.clientY + rect.right;
@@ -157,11 +164,14 @@ export interface FilePropertyDialogState {
};
class DragState {
activeItem: string = "";
height: number = 0;
from: number = 0;
to: number = 0;
interface DragState {
activeItem: string;
height: number;
dragElement: HTMLElement;
dragElementDy: number;
from: number;
to: number;
lastMousePoint: Point;
};
export default withStyles(
@@ -323,8 +333,6 @@ export default withStyles(
longPressStartPoint: Point | null = null;
dragFromPosition: number = -1;
dragToPosition: number = -1;
maxPosition: number = 0;
handleMultisSelectClose() {
@@ -336,24 +344,29 @@ export default withStyles(
handleLongPressStart(
currentTarget: HTMLButtonElement,
e: React.PointerEvent<HTMLButtonElement>,
e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>,
fileEntry: FileEntry): boolean {
if (this.state.reordering) {
if (!this.isTracksDirectory()) {
return false;
}
this.dragFromPosition = parseInt(currentTarget.getAttribute("data-position") as string, 10);
this.dragToPosition = this.dragToPosition;
let element = currentTarget;
element.style.position = "relative";
element.style.zIndex = "1000";
element.style.top = "5px";
element.style.left = "5px";
element.style.background = isDarkMode() ? "#555" : "#EEF" // xxx: dark mode.
let index = parseInt(element.getAttribute("data-position") as string, 10);
if (this.listContainerElementRef) {
this.longPressStartPoint = screenToClient(currentTarget,e);
this.longPressStartPoint = screenToClient(currentTarget, e);
let dragState: DragState = {
activeItem: fileEntry.pathname,
dragElement: element,
height: element.clientHeight,
dragElementDy: 0,
from: index,
to: index,
lastMousePoint: {...this.longPressStartPoint}
};
this.setState({ dragState: dragState });
}
return true;
} else if (!this.state.multiSelect) {
if (fileEntry.isProtected) {
@@ -370,6 +383,48 @@ export default withStyles(
return false;
}
dragTarget: HTMLButtonElement | null = null;
updateReorderPosition(element: HTMLButtonElement, point: Point) {
if (this.longPressStartPoint) {
let dragDy = point.y - this.longPressStartPoint.y;
let height = element.clientHeight;
let dragFromPosition = parseInt(element.getAttribute("data-position") as string, 10);
let elementOffset = dragFromPosition * height;
let newOffset = elementOffset + dragDy;
if (newOffset < 0) {
dragDy = -elementOffset;
newOffset = 0;
}
let maxOffset = (this.maxPosition - 1) * height;
if (newOffset > maxOffset) {
dragDy = maxOffset - elementOffset;
newOffset = maxOffset;
}
let toPosition = Math.floor((newOffset + height / 2) / height);
if (toPosition < 0) {
toPosition = 0;
}
if (toPosition > this.maxPosition) {
toPosition = this.maxPosition;
}
// console.log("drag from " + dragFromPosition + " to " + toPosition + " dy: " + dragDy);
this.setState({
dragState: {
activeItem: element.getAttribute("data-pathname") || "",
dragElement: element,
height: height,
dragElementDy: dragDy,
from: dragFromPosition,
to: toPosition,
lastMousePoint: point
}
});
}
}
handleLongPressMove(e: React.PointerEvent<HTMLButtonElement>) {
if (this.state.reordering) {
if (!this.isTracksDirectory()) {
@@ -377,37 +432,10 @@ export default withStyles(
}
if (this.listContainerElementRef) {
let element = e.target as HTMLButtonElement;
let point = screenToClient(e.currentTarget,e);
if (this.longPressStartPoint) {
let dy = point.y - this.longPressStartPoint.y;
element.style.left = (5) + "px";
element.style.top = (5 + dy) + "px";
let height = element.clientHeight;
let positionChange_: number = 0;
if (dy > 0) {
positionChange_ = Math.floor((dy + height / 2) / height);
} else {
positionChange_ = Math.ceil((dy - height / 2) / height)
}
let toPosition = this.dragFromPosition + positionChange_;
if (toPosition < 0) {
toPosition = 0;
}
if (toPosition > this.maxPosition) {
toPosition = this.maxPosition;
}
if (toPosition !== this.dragToPosition) {
this.dragToPosition = toPosition;
this.setState({
dragState: {
activeItem: element.getAttribute("data-pathname") || "",
height: height,
from: this.dragFromPosition,
to: toPosition
}
});
}
}
let point = screenToClient(e.currentTarget, e);
this.updateReorderPosition(element, point);
this.startAutoScroll()
}
}
}
@@ -457,7 +485,7 @@ export default withStyles(
this.model.moveAudioFile(
this.state.currentDirectory, from, to);
}
handleLongPressEnd(e: React.PointerEvent<HTMLButtonElement>, fileEntry: FileEntry) {
handleLongPressEnd(e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>, fileEntry: FileEntry) {
if (this.state.reordering) {
if (!this.isTracksDirectory()) {
return;
@@ -471,14 +499,8 @@ export default withStyles(
e.preventDefault();
e.stopPropagation();
let element = e.currentTarget as HTMLButtonElement;
element.style.position = "";
element.style.zIndex = "";
element.style.top = "";
element.style.left = "";
element.style.background = ""; // xxx: dark mode.
this.setState({ dragState: null });
this.stopAutoScroll();
} else if (!this.state.multiSelect) {
let selectedFile = fileEntry.pathname;
if (selectedFile === "") {
@@ -526,6 +548,7 @@ export default withStyles(
this.requestScroll = true;
}
componentWillUnmount() {
this.stopAutoScroll();
this.cancelProgressTimeout();
super.componentWillUnmount();
@@ -925,7 +948,98 @@ export default withStyles(
return (<InsertDriveFileIcon style={style} />);
}
listContainerElementRef: HTMLDivElement | null = null;
private listContainerElementRef: HTMLDivElement | null = null;
private scrollContainerElementRef: HTMLDivElement | null = null;
private autoScrollTimer: number | null = null;
handleAutoScrollTick() {
let dragState = this.state.dragState;
if (!dragState) return;
let scrollContainer = this.scrollContainerElementRef;
if (!scrollContainer) {
return;
}
let scrollBounds_ = scrollContainer.getBoundingClientRect();
let scrollClientTop = scrollBounds_.top;
let scrollClientBottom = scrollClientTop + scrollContainer.clientHeight;
let dragBounds = dragState.dragElement?.getBoundingClientRect();
let dy = 0;
let y = scrollContainer.scrollTop;
if (dragBounds.top < scrollClientTop + AUTOSCROLL_THRESHOLD) {
dy = -AUTOSCROLL_SCROLL_RATE;
y = scrollContainer.scrollTop + dy;
if (y < 0) y = 0;
dy = y - scrollContainer.scrollTop;
} else if (dragBounds.bottom > scrollClientBottom - AUTOSCROLL_THRESHOLD) {
dy = AUTOSCROLL_SCROLL_RATE;
y = scrollContainer.scrollTop + dy;
let maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight;
if (y > maxScrollTop) {
y = maxScrollTop;
}
dy = y - scrollContainer.scrollTop;
} else {
return;
}
if (this.longPressStartPoint === null) {
return;
}
if (dy === 0) {
return;
}
this.longPressStartPoint = {
x: this.longPressStartPoint.x,
y: this.longPressStartPoint.y - dy
};
let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y;
let originalY = dragState.from * dragState.height;
let newY = originalY + dragElementDy;
if (newY < 0)
{
newY = 0;
dragElementDy = -originalY;
}
let maxDy = (this.maxPosition - 1) * dragState.height;
if (newY > maxDy) {
newY = maxDy;
dragElementDy = maxDy - originalY;
}
let newTo = Math.floor((newY + dragState.height / 2) / dragState.height);
let newDragState = { ...dragState };
newDragState.dragElementDy = dragElementDy;
newDragState.to = newTo;
// console.log("autoscroll from " + dragState.from + " to " + newTo + " dy: " + dragElementDy);
// avoid jitter.
newDragState.dragElement.style.top = (newDragState.dragElementDy + 5) + "px";
this.setState({ dragState: newDragState });
scrollContainer.scrollTo({ left: 0, top: y, behavior: "instant" });
this.startAutoScroll();
}
stopAutoScroll() {
if (this.autoScrollTimer !== null) {
clearTimeout(this.autoScrollTimer);
this.autoScrollTimer = null;
}
}
startAutoScroll() {
this.stopAutoScroll();
this.autoScrollTimer = setTimeout(
() => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY);
}
render() {
const isTracksDirectory = this.isTracksDirectory();
@@ -1146,7 +1260,9 @@ export default withStyles(
<DialogContent style={{
paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"),
position: "relative"
}}>
}}
ref={(element: HTMLDivElement | null) => { this.scrollContainerElementRef = element; }}
>
<div style={{
paddingLeft: 16, paddingRight: 16, paddingTop: 8, paddingBottom: 8,
@@ -1210,7 +1326,10 @@ export default withStyles(
scrollRef = (element) => { this.onScrollRef(element) };
}
let dragOffset = 0;
let dragLeft = 0;
let dragState = this.state.dragState;
let zIndex: number | undefined = undefined;
let background: string | undefined = undefined;
if (dragState && value.metadata) {
if (myTrackPosition !== dragState.from) {
if (myTrackPosition >= dragState.to && myTrackPosition < dragState.from) {
@@ -1218,21 +1337,31 @@ export default withStyles(
} else if (myTrackPosition > dragState.from && myTrackPosition <= dragState.to) {
dragOffset = -dragState.height;
}
} else {
dragOffset = dragState.dragElementDy + 5;
dragLeft = 5;
zIndex = 1000;
background = isDarkMode() ? "#555" : "#EEF";
}
}
let dragButtonStyle: React.CSSProperties = {
width: columnWidth, flex: "0 0 auto", height: (value.metadata && !compactVertical) ? 64 : 48,
position: "relative",
top: dragOffset,
left: dragLeft,
zIndex: zIndex,
background: background,
};
return (
<DraggableButtonBase key={value.pathname}
longPressDelay={this.state.reordering ? 0 : undefined}
longPressDelay={this.state.reordering ? 300 : undefined}
data-position={dataPosition}
data-pathname={
value.metadata ? value.metadata.fileName : null
}
instantMouseLongPress={this.state.reordering}
style={{
width: columnWidth, flex: "0 0 auto", height: (value.metadata && !compactVertical) ? 64 : 48,
position: "relative",
top: dragOffset + "px",
}}
style={dragButtonStyle}
onClick={(e) => this.handleFileClick(e, value)}
onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }}
@@ -1559,7 +1688,7 @@ export default withStyles(
await this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty);
let index = newFileList.indexOf(file);
if (index !== -1) {
newFileList.splice(index,1);
newFileList.splice(index, 1);
}
}
@@ -1570,8 +1699,7 @@ export default withStyles(
if (!this.mounted) {
return;
}
if (newFileList.length === 0 )
{
if (newFileList.length === 0) {
this.setState({
multiSelect: false,
selectedFiles: []
+10 -10
View File
@@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) {
if (valueTooltip === undefined) // no timeout if there's a value tooltip
{
if (t > 0) {
console.log("ToolTipEx: starting timeout for ", t);
// console.log("ToolTipEx: starting timeout for ", t);
handle = window.setTimeout(() => {
setOpen(true);
},t);
@@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) {
}
return () => {
if (handle !== null) {
console.log("ToolTipEx: clearing timeout for ", t);
// console.log("ToolTipEx: clearing timeout for ", t);
window.clearTimeout(handle);
}
};
@@ -181,39 +181,39 @@ function ToolTipEx(props: ToolTipExProps) {
return (
<div
onClickCapture={(e) => {
console.log("ToolTipEx: onClickCapture");
// console.log("ToolTipEx: onClickCapture");
handleClickCapture(e);
}}
onMouseEnter={(e)=> {
console.log("ToolTipEx: onMouseEnter");
// console.log("ToolTipEx: onMouseEnter");
handleMouseEnter(e);
}}
onMouseLeave={(e)=> {
console.log("ToolTipEx: onMouseLeave");
// console.log("ToolTipEx: onMouseLeave");
handleMouseLeave(e);
}}
onPointerCancelCapture={(e) => {
console.log("ToolTipEx: onPointerCancelCapture");
// console.log("ToolTipEx: onPointerCancelCapture");
handlePointerCancel(e);
}}
onContextMenuCapture={(e) => {
console.log("ToolTipEx: onContextMenuCapture");
// console.log("ToolTipEx: onContextMenuCapture");
handleConextMenu(e);
return false;
}}
onPointerDownCapture={(e) => {
console.log("ToolTipEx: onPointerDownCapture");
// console.log("ToolTipEx: onPointerDownCapture");
handlePointerDownCapture(e);
}}
onPointerMoveCapture={(e) => {
console.log("ToolTipEx: onPointerMoveCapture");
// console.log("ToolTipEx: onPointerMoveCapture");
handlePointerMoveCapture(e);
}}
onPointerUpCapture={(e) => {
console.log("ToolTipEx: onPointerUpCapture");
// console.log("ToolTipEx: onPointerUpCapture");
handlePointerUpCapture(e);
}}
>