Skip to content

Commit

Permalink
fix(web): Simplify the logic for tag drag and dropping
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed May 18, 2024
1 parent 1fee129 commit e5fd9ee
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 127 deletions.
72 changes: 31 additions & 41 deletions apps/web/components/dashboard/tags/AllTagsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Toggle } from "@/components/ui/toggle";
import { toast } from "@/components/ui/use-toast";
import { useDragAndDrop } from "@/lib/drag-and-drop";
import { api } from "@/lib/trpc";
import { ArrowDownAZ, Combine } from "lucide-react";
import Draggable from "react-draggable";

import type { ZGetTagResponse } from "@hoarder/shared/types/tags";
Expand Down Expand Up @@ -75,27 +76,26 @@ export default function AllTagsView({
}: {
initialData: ZGetTagResponse[];
}) {
const [draggingEnabled, toggleDraggingEnabled] = React.useState(false);
const [sortByName, toggleSortByName] = React.useState(false);

const { dragState, handleDrag, handleDragStart, handleDragEnd } =
useDragAndDrop(
"data-id",
"data-id",
(dragSourceId: string, dragTargetId: string) => {
mergeTag({
fromTagIds: [dragSourceId],
intoTagId: dragTargetId,
});
},
);
const [draggingEnabled, setDraggingEnabled] = React.useState(false);
const [sortByName, setSortByName] = React.useState(false);

const { handleDragStart, handleDragEnd } = useDragAndDrop(
"data-id",
"data-id",
(dragSourceId: string, dragTargetId: string) => {
mergeTag({
fromTagIds: [dragSourceId],
intoTagId: dragTargetId,
});
},
);

function handleSortByNameChange(): void {
toggleSortByName(!sortByName);
function toggleSortByName(): void {
setSortByName(!sortByName);
}

function handleDraggableChange(): void {
toggleDraggingEnabled(!draggingEnabled);
function toggleDraggingEnabled(): void {
setDraggingEnabled(!draggingEnabled);
}

const { mutate: mergeTag } = useMergeTag({
Expand Down Expand Up @@ -131,6 +131,7 @@ export default function AllTagsView({
const { data } = api.tags.list.useQuery(undefined, {
initialData: { tags: initialData },
});

// Sort tags by usage desc
const allTags = data.tags.sort(sortByName ? byNameSorter : byUsageSorter);

Expand All @@ -148,28 +149,15 @@ export default function AllTagsView({
key={t.id}
axis="both"
onStart={handleDragStart}
onDrag={handleDrag}
onStop={handleDragEnd}
disabled={!draggingEnabled}
defaultClassNameDragging={
"position-relative z-10 pointer-events-none"
}
position={
!dragState.dragSourceId
? {
x: dragState.initialX ?? 0,
y: dragState.initialY ?? 0,
}
: undefined
}
position={{ x: 0, y: 0 }}
>
<div className="group relative flex cursor-grab" data-id={t.id}>
<TagPill
id={t.id}
name={t.name}
count={t.count}
isDraggable={draggingEnabled}
/>
<div className="cursor-grab" data-id={t.id}>
<TagPill id={t.id} name={t.name} count={t.count} />
</div>
</Draggable>
))}
Expand All @@ -182,22 +170,24 @@ export default function AllTagsView({
};
return (
<>
<div className="float-right">
<div className="flex justify-end gap-x-2">
<Toggle
variant="outline"
aria-label="Toggle bold"
pressed={draggingEnabled}
onPressedChange={handleDraggableChange}
onPressedChange={toggleDraggingEnabled}
>
Allow Merging via Drag&Drop
<Combine className="mr-2 size-4" />
Drag & Drop Merging
<InfoTooltip size={15} className="my-auto ml-2" variant="explain">
<p>Drag and drop tags on each other to merge them</p>
</InfoTooltip>
</Toggle>
<Toggle
variant="outline"
aria-label="Toggle bold"
pressed={sortByName}
onPressedChange={handleSortByNameChange}
onPressedChange={toggleSortByName}
>
Sort by Name
<ArrowDownAZ className="mr-2 size-4" /> Sort by Name
</Toggle>
</div>
<span className="flex items-center gap-2">
Expand Down
22 changes: 7 additions & 15 deletions apps/web/components/dashboard/tags/TagPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,24 @@ import { X } from "lucide-react";

import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog";

const PILL_STYLE =
"flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background";

export function TagPill({
id,
name,
count,
isDraggable,
}: {
id: string;
name: string;
count: number;
isDraggable: boolean;
}) {
// When the element is draggable, do not generate a link. Links can be dragged into e.g. the tab-bar and therefore dragging the TagPill does not work properly
if (isDraggable) {
return (
<div className={PILL_STYLE} data-id={id}>
{name} <Separator orientation="vertical" /> {count}
</div>
);
}

return (
<div className="group relative flex">
<Link className={PILL_STYLE} href={`/dashboard/tags/${id}`}>
<Link
className={
"flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background"
}
href={`/dashboard/tags/${id}`}
data-id={id}
>
{name} <Separator orientation="vertical" /> {count}
</Link>

Expand Down
101 changes: 30 additions & 71 deletions apps/web/lib/drag-and-drop.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,48 @@
import React from "react";
import { DraggableData, DraggableEvent } from "react-draggable";

export interface DragState {
// The id of the element that is being dragged
dragSourceId: string | null;
// The id of the element that is currently being hovered over
dragTargetId: string | null;
// The position of the elements being dragged such that on drag over, we can revert the position.
initialX: number;
initialY: number;
}

export interface DragAndDropFunctions {
handleDragStart: (e: DraggableEvent, data: DraggableData) => void;
handleDrag: (e: DraggableEvent) => void;
handleDragEnd: () => void;
dragState: DragState;
}

export function useDragAndDrop(
dragSourceIdAttribute: string,
dragTargetIdAttribute: string,
callback: (dragSourceId: string, dragTargetId: string) => void,
): DragAndDropFunctions {
const initialState: DragState = {
dragSourceId: null,
dragTargetId: null,
initialX: 0,
initialY: 0,
};

let currentState: DragState = initialState;

function handleDragStart(e: DraggableEvent, data: DraggableData): void {
const { node } = data;
const id = node.getAttribute(dragSourceIdAttribute);

currentState = {
...initialState,
dragSourceId: id,
initialX: data.x,
initialY: data.y,
};
}

function handleDrag(e: DraggableEvent): void {
const { dragTargetId } = currentState;
const { target } = e;

// Important according to the sample I found
e.preventDefault();

if (target) {
const id = (target as HTMLElement).getAttribute(dragTargetIdAttribute);

if (id !== dragTargetId) {
currentState.dragTargetId = id;
}
}
}

function handleDragEnd(): void {
const { dragSourceId, dragTargetId } = currentState;

if (dragSourceId && dragTargetId && dragSourceId !== dragTargetId) {
/*
onDragOver: (dragSourceId: string, dragTargetId: string) => void,
) {
const [dragSourceId, setDragSourceId] = React.useState<string | null>(null);

const handleDragStart = React.useCallback(
(_e: DraggableEvent, { node }: DraggableData) => {
const id = node.getAttribute(dragSourceIdAttribute);
setDragSourceId(id);
},
[],
);

const handleDragEnd = React.useCallback(
(e: DraggableEvent) => {
const { target } = e;
const dragTargetId = (target as HTMLElement).getAttribute(
dragTargetIdAttribute,
);

if (dragSourceId && dragTargetId && dragSourceId !== dragTargetId) {
/*
As Draggable tries to setState when the
component is unmounted, it is needed to
push onCombine to the event loop queue.
onCombine would be run after setState on
Draggable so it would fix the issue until
they fix it on their end.
*/
setTimeout(() => {
console.log(dragSourceId, dragTargetId);
callback(dragSourceId, dragTargetId);
}, 0);
}

currentState = initialState;
}
setTimeout(() => {
onDragOver(dragSourceId, dragTargetId);
}, 0);
}
setDragSourceId(null);
},
[dragSourceId, onDragOver],
);

return {
dragState: currentState,
handleDragStart,
handleDrag,
handleDragEnd,
};
}

0 comments on commit e5fd9ee

Please sign in to comment.