fix: dnd 전반적인 상호작용 향상 및 mobile dnd preview 추가

This commit is contained in:
sechan100 2024-10-16 11:23:00 +09:00
parent e0a4fb2af7
commit 9265a4d7fd
8 changed files with 238 additions and 62 deletions

View file

@ -2,7 +2,10 @@ import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { sassPlugin } from 'esbuild-sass-plugin';
import fs from "fs";
import fs from "node:fs";
import path from "path";
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@ -11,6 +14,7 @@ if you want to view the source, please visit the github repository of this plugi
`;
const prod = process.argv[2] === "production";
const local = process.argv[2] === "local";
// CSS를 하나로 합치기 위한 rename 플러그인
const renamePlugin = {
@ -76,6 +80,43 @@ const context = await esbuild.context({
if(prod) {
await context.rebuild();
process.exit(0);
} else if(local) {
await context.rebuild();
console.log("\n === 클라우드로 플러그인 복사 시작 ===");
await copyPluginFilesTo_iCloud();
console.log("=== 완료 ===");
process.exit(0);
} else {
await context.watch();
}
async function copyPluginFilesTo_iCloud() {
const filesToCopy = [
'main.js',
'manifest.json',
'styles.css',
];
const destinationDir = '/Users/sechan/Library/Mobile Documents/iCloud~md~obsidian/Documents/cloud-test/.obsidian/plugins/daily-routine-2/';
// const destinationDir = '/Users/sechan/local/obsidian/plugin-dev';
// 현재 파일의 URL을 파일 경로로 변환
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 복사 작업
for(const file of filesToCopy) {
const sourcePath = path.join(__dirname, file);
const destPath = path.join(destinationDir, file);
try {
await fs.promises.copyFile(sourcePath, destPath);
console.log(`DONE: ${file}`);
} catch (error) {
console.error(`Failed to copy ${file}:`, error);
}
}
}

View file

@ -6,6 +6,7 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"local": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs local",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],

View file

@ -8,6 +8,8 @@ import { useRoutineNoteState } from "./task-context";
import _, { DebouncedFunc } from "lodash";
import { routineNoteArchiver } from "entities/archive";
import { routineManager } from "entities/routine/routine";
import { DRAG_PRESS_DELAY } from "./constants";
import { useDragLayer } from 'react-dnd'
@ -33,7 +35,7 @@ interface TaskProps<T extends TaskEntity> {
onTaskClick?: (task: T) => void;
}
export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onTaskClick }: TaskProps<T>) => {
export const Task = <T extends TaskEntity>({ className, task, onOptionClick: onOptionClick_props, onTaskClick }: TaskProps<T>) => {
// 루틴 체크와 클릭 ///////////////////////////////////////////////////////////////////////
const [checked, setChecked] = useState(task.checked);
@ -62,12 +64,12 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
///////////////////////////////////////////////////////////////////////////
// option 클릭시 콜백함수
const optionClick = useCallback((e: React.MouseEvent) => {
const onOptionClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if(onOptionClick){
onOptionClick(task);
if(onOptionClick_props){
onOptionClick_props(task);
}
}, [task, onOptionClick])
}, [task, onOptionClick_props])
// taskRef ///////////////////////////////////////////////////////////////
@ -79,7 +81,7 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
const [dragState, setDragState] = useState<DragState>({type: "idle"});
const [ routineNote, setRoutineNote ] = useRoutineNoteState();
const [{isDragging}, drag, preview] = useDrag({
const [{isDragging}, drag] = useDrag({
type: "task",
item(){
setDragState({type: "dragging"});
@ -103,8 +105,8 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
const dropTargetAvailableHeightPerHixboxes = dropTargetHeight * hitBoundary;
// RETURN
if(y < dropElOffset.top + dropTargetAvailableHeightPerHixboxes) return "top";
if(y > dropElOffset.bottom - dropTargetAvailableHeightPerHixboxes) return "bottom";
if(y < dropElOffset.top + dropTargetAvailableHeightPerHixboxes) return "bottom";
if(y > dropElOffset.bottom - dropTargetAvailableHeightPerHixboxes) return "top";
return null;
}, [])
@ -159,15 +161,29 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
const touchDebounce = useRef<DebouncedFunc<() => void>>(
_.debounce(() => {
setDragState({type: "ready"});
}, 500)
}, DRAG_PRESS_DELAY)
);
// touchstart, touchend 이벤트를 통해 dragState 변경
const touchStart = useCallback(() => touchDebounce.current(), [])
const touchStart = useCallback(() => {
touchDebounce.current()
}, [])
const touchEnd = useCallback(() => {
touchDebounce.current.cancel();
/**
* NOTE: 모바일에서 touchstart 500ms dragState가 ready로 .
* , dragging으로, touchend가 . ready이다.
* dragState가 ready일 , option .
*
* contextMenu modal이 .
* 버그: https://forum.obsidian.md/t/hold-tap-on-the-note-or-folder-opens-bottom-menu-twice/58572
*/
if(dragState.type === "ready" && onOptionClick_props){
onOptionClick_props(task);
}
setDragState({type: "idle"});
}, [])
}, [dragState.type, onOptionClick_props, task])
return (
@ -181,11 +197,9 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
"dr-task--dragging": isDragging
})}
onClick={onClick}
onContextMenu={optionClick}
onTouchStart={touchStart}
onTouchEnd={touchEnd}
onTouchCancel={touchEnd}
onTouchMove={touchEnd}
>
<div className="dr-task__container">
{/* MAIN */}
@ -200,7 +214,7 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
{/* 옵션 */}
<div
className="dr-task__option"
onClick={optionClick}
onClick={onOptionClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -216,3 +230,64 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
</div>
)
}
interface TaskPreviewProps {
task: TaskEntity;
style: React.CSSProperties;
}
export const TaskPreview = ({ task, style }: TaskPreviewProps) => {
const { currentOffset } = useDragLayer((monitor) => ({
currentOffset: monitor.getSourceClientOffset(),
}));
const getItemStyles = (style: React.CSSProperties) => {
if (!currentOffset) {
return { display: 'none' };
}
const { x, y } = currentOffset;
const transform = `translate(${x - 45}px, ${y - 39}px)`;
return {
...style,
backgroundColor: "var(--background-primary)",
boxShadow: "0 0 0.5em 0.5em rgba(0, 0, 0, 0.1)",
transform,
WebkitTransform: transform,
};
};
return (
<div
className={clsx("dr-task", "dr-task-preview", { "dr-task--checked": task.checked })}
style={getItemStyles(style)}
>
<div className="dr-task__container">
{/* MAIN */}
<div className="dr-task__main">
<span className="dr-task__cbx">
<svg viewBox="0 0 14 12">
<polyline points="1 7.6 5 11 13 1"></polyline>
</svg>
</span>
<span className="dr-task__name">{task.name}</span>
</div>
{/* 옵션 */}
<div className="dr-task__option">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
</svg>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,8 @@
/**
*
*/
export const DRAG_PRESS_DELAY = 300;

View file

@ -0,0 +1,80 @@
import { Task } from "entities/routine-note";
///////////////////////////////////////////////////////
import React, { CSSProperties, useCallback, useMemo } from "react";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import {
DndProvider,
// TouchTransition,
// MouseTransition,
MultiBackendOptions,
createTransition,
} from "react-dnd-multi-backend";
import { Preview } from "react-dnd-preview";
import { DRAG_PRESS_DELAY } from "./constants";
import { TaskPreview } from "./Task";
interface PreviewContextProps {
task: Task;
style: CSSProperties;
}
const PreviewContext = ({ task, style }: PreviewContextProps) => {
return <TaskPreview task={task} style={style} />
}
export const TaskDndContext = ({ children }: {children: React.ReactNode }) => {
const [ backend, setBackend ] = React.useState<"touch" | "html5">('html5');
const MouseTransition = useMemo(() => createTransition('mousedown', (event) => {
const b = event.type.contains('mouse')
if(b) setBackend('html5');
return b;
}), [setBackend])
const TouchTransition = useMemo(() => createTransition('touchstart', (event) => {
const b = event.type.contains('touch')
if(b) setBackend('touch');
return b;
}), [setBackend])
const HTML5toTouch: MultiBackendOptions = useMemo<MultiBackendOptions>(() => {
return {
backends: [
{
id: "html5",
backend: HTML5Backend,
transition: MouseTransition,
},
{
id: "touch",
backend: TouchBackend,
options: {
enableMouseEvents: false,
delayTouchStart: DRAG_PRESS_DELAY,
ignoreContextMenu: false
},
transition: TouchTransition
}
],
}
}, [MouseTransition, TouchTransition])
return (
<DndProvider options={HTML5toTouch}>
{children}
{backend === 'touch' && <Preview>{ ({ item, style }) => {
// @ts-ignore
const task = item?.task as Task;
return <PreviewContext task={task} style={style} />
}}</Preview>}
</DndProvider>
)
}

View file

@ -1,42 +1,21 @@
import { RoutineNote } from "entities/routine-note";
import { Task } from "entities/routine-note";
///////////////////////////////////////////////////////
import React, { Dispatch, SetStateAction, useState } from "react";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import {
DndProvider,
TouchTransition,
MouseTransition,
Preview
} from "react-dnd-multi-backend";
import { createUseStateSyncedStore, UseStateRv } from "shared/zustand/create-use-state-synced-store";
import { TaskDndContext } from "./dnd-context";
const {StoreProvider, useStoreHook} = createUseStateSyncedStore<RoutineNote>();
export const useRoutineNoteState = useStoreHook;
const HTML5toTouch = {
backends: [
{
id: "html5",
backend: HTML5Backend,
transition: MouseTransition,
},
{
id: "touch",
backend: TouchBackend,
options: {
enableMouseEvents: false,
delayTouchStart: 500
},
preview: false,
transition: TouchTransition
}
],
};
interface TaskContextProps {
useRoutineNoteState: UseStateRv<RoutineNote>;
@ -45,18 +24,9 @@ interface TaskContextProps {
export const TaskContext = ({ useRoutineNoteState, children }: TaskContextProps) => {
return (
<StoreProvider useState={useRoutineNoteState}>
<DndProvider options={HTML5toTouch}>
<TaskDndContext>
{children}
{/* <Preview>{
({item, itemType, monitor, ref, style}) => {
// @ts-ignore
const task = item?.task as Task;
return <div className="item-list__item" style={style}>{task.name}</div>
}}
</Preview> */}
</DndProvider>
</TaskDndContext>
</StoreProvider>
)
}
export const useRoutineNoteState = useStoreHook;
}

View file

@ -149,7 +149,8 @@
left: 0;
width: 100%;
height: 100%;
border-radius: 5px;
background-color: rgba(203, 203, 203, 0.5);
border-radius: 5px;
// background-color: rgba(203, 203, 203, 0.5);
background-color: hsla(var(--color-accent-1-hsl), 0.5)
}
}

File diff suppressed because one or more lines are too long