diff --git a/packages/app/package.json b/packages/app/package.json index 66ffc257..4efa4536 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,11 +34,14 @@ "@capacitor/storage": "1.2.4", "@capgo/capacitor-updater": "^4.12.8", "@corenode/utils": "0.28.26", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", "@emotion/css": "11.0.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@loadable/component": "5.15.2", "@mui/material": "^5.11.9", + "@paciolan/remote-component": "^2.13.0", "@tsmx/human-readable": "^1.0.7", "antd": "^5.2.1", "antd-mobile": "^5.0.0-rc.17", @@ -171,4 +174,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/app/src/components/SortableList/index.jsx b/packages/app/src/components/SortableList/index.jsx new file mode 100644 index 00000000..5cc55782 --- /dev/null +++ b/packages/app/src/components/SortableList/index.jsx @@ -0,0 +1,268 @@ +import React from "react" +import { Button } from "antd" +import { Icons, createIconRender } from "components/Icons" +import classnames from "classnames" +import useLongPress from "hooks/useLongPress" + +import { + DndContext, + TouchSensor, + MouseSensor, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragOverlay, + defaultDropAnimationSideEffects, +} from "@dnd-kit/core" +import { + SortableContext, + arrayMove, + sortableKeyboardCoordinates, + useSortable, +} from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" + +import "./index.less" + +export const SortableItemChildrenContext = React.createContext({ + attributes: {}, + listeners: undefined, + ref() { }, + activeDrag: true, +}) + +export const SortableItemContext = React.createContext({ + activeDrag: true, + onLongPress() { }, + setActiveDrag() { }, +}) + +export function DragHandle() { + const { attributes, listeners, ref, activeDrag } = React.useContext(SortableItemChildrenContext) + + return ( + + ) +} + +export function SortableOverlay({ children }) { + const dropAnimationConfig = { + sideEffects: defaultDropAnimationSideEffects({ + styles: { + active: { + opacity: "0.4" + } + } + }) + } + + return ( + {children} + ) +} + +export function SortableItem({ + children, + id, +}) { + const { activeDrag, onLongPress } = React.useContext(SortableItemContext) + + const { + attributes, + isDragging, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + } = useSortable({ id }) + + const context = React.useMemo( + () => ({ + attributes, + listeners, + ref: setActivatorNodeRef, + activeDrag: activeDrag + }), + [attributes, listeners, setActivatorNodeRef, activeDrag] + ) + + const style = { + opacity: isDragging ? 0.4 : undefined, + transform: CSS.Translate.toString(transform), + transition, + } + + return ( + +
  • + {children} + +
  • +
    + ) +} + +export const DragActiveActions = ({ + actions +}) => { + const { activeDrag, setActiveDrag } = React.useContext(SortableItemContext) + + return
    + + }) + } +
    +} + +export const SortableList = ({ + items, + onChange, + renderItem, + activationConstraint, + useDragOverlay, + useActiveDragActions = true, + activeDragActions, +}) => { + const [active, setActive] = React.useState(null) + const [activeDrag, setActiveDrag] = React.useState(false) + + const activeItem = React.useMemo(() => items.find((item) => item.id === active?.id), [active, items]) + + const context = React.useMemo( + () => ({ + activeDrag, + onLongPress: () => setActiveDrag(true), + setActiveDrag: setActiveDrag, + }), + ) + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint, + }), + useSensor(TouchSensor, { + activationConstraint, + }), + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ) + + const onDragStart = ({ active }) => { + if (!activeDrag) { + return + } + + setActive(active) + } + + const onDragEnd = ({ active, over }) => { + if (!activeDrag) { + return + } + + if (over && active.id !== over?.id) { + const activeIndex = items.findIndex(({ id }) => id === active.id); + const overIndex = items.findIndex(({ id }) => id === over.id); + + onChange(arrayMove(items, activeIndex, overIndex)); + } + + setActive(null); + } + + const onDragCancel = () => { + setActive(null) + } + + return + + +
      + { + items.map((item, index) => ( + + { + renderItem(item, index) + } + + )) + } +
    + + { + useActiveDragActions && + } +
    +
    + + { + useDragOverlay && + {activeItem ? renderItem(activeItem) : null} + + } +
    +} \ No newline at end of file diff --git a/packages/app/src/components/SortableList/index.less b/packages/app/src/components/SortableList/index.less new file mode 100644 index 00000000..3e00612a --- /dev/null +++ b/packages/app/src/components/SortableList/index.less @@ -0,0 +1,109 @@ +.shortable-list { + display: flex; + flex-direction: column; + + gap: 10px; + padding: 0; + + list-style: none; + + transition: all 0.2s ease-in-out; + + &.active-drag { + //border: 1px dashed var(--border-color); + border-radius: 12px; + + padding: 10px; + + background-color: var(--background-color-accent); + } +} + +.sortable-item { + display: flex; + justify-content: space-between; + + flex-grow: 1; + align-items: center; + + //box-shadow: 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 0 1px calc(3px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.15); + //border-radius: calc(4px / var(--scale-x, 1)); + + box-sizing: border-box; + list-style: none; +} + +.drag-handle { + display: flex; + + width: 0px; + padding: 0px; + + opacity: 0; + + align-items: center; + justify-content: center; + + flex: 0 0 auto; + + touch-action: none; + + cursor: var(--cursor, pointer); + + border-radius: 5px; + border: none; + outline: none; + + appearance: none; + background-color: transparent; + -webkit-tap-highlight-color: transparent; + + transition: all 0.2s ease-in-out; + + &.active { + width: 12px; + padding: 15px; + + opacity: 1; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } + + svg { + flex: 0 0 auto; + + margin: auto; + + height: 100%; + + overflow: visible; + + fill: #919eab; + } +} + +.drag-actions { + display: flex; + + flex-direction: row; + + align-items: center; + + gap: 10px; + + width: 0px; + height: 0px; + + opacity: 0; + + transition: all 0.2s ease-in-out; + + &.active { + width: 100%; + height: 30px; + + opacity: 1; + } +} \ No newline at end of file