From 645a88e7e35d15cec6106709c42b071bec045e0d Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 2 Aug 2023 02:52:07 +0800 Subject: ... --- FrontEnd/src/migrating/center/TimelineBoard.tsx | 390 ++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 FrontEnd/src/migrating/center/TimelineBoard.tsx (limited to 'FrontEnd/src/migrating/center/TimelineBoard.tsx') diff --git a/FrontEnd/src/migrating/center/TimelineBoard.tsx b/FrontEnd/src/migrating/center/TimelineBoard.tsx new file mode 100644 index 00000000..b3ccdf8c --- /dev/null +++ b/FrontEnd/src/migrating/center/TimelineBoard.tsx @@ -0,0 +1,390 @@ +import * as React from "react"; +import classnames from "classnames"; +import { Link } from "react-router-dom"; + +import { TimelineBookmark } from "@/http/bookmark"; + +import TimelineLogo from "../common/TimelineLogo"; +import LoadFailReload from "../common/LoadFailReload"; +import FlatButton from "../common/button/FlatButton"; +import Card from "../common/Card"; +import Spinner from "../common/Spinner"; +import IconButton from "../common/button/IconButton"; + +interface TimelineBoardItemProps { + timeline: TimelineBookmark; + // In height. + offset?: number; + // In px. + arbitraryOffset?: number; + // If not null, will disable navigation on click. + actions?: { + onDelete: () => void; + onMove: { + start: (e: React.PointerEvent) => void; + moving: (e: React.PointerEvent) => void; + end: (e: React.PointerEvent) => void; + }; + }; +} + +const TimelineBoardItem: React.FC = ({ + timeline, + arbitraryOffset, + offset, + actions, +}) => { + const content = ( + <> + + + {timeline.timelineOwner}/{timeline.timelineName} + + + {actions != null ? ( +
+ + { + e.currentTarget.setPointerCapture(e.pointerId); + actions.onMove.start(e); + }} + onPointerUp={(e) => { + actions.onMove.end(e); + try { + e.currentTarget.releasePointerCapture(e.pointerId); + } catch (_) { + void null; + } + }} + onPointerMove={actions.onMove.moving} + /> +
+ ) : null} + + ); + + const offsetStyle: React.CSSProperties = { + transform: + arbitraryOffset != null + ? `translate(0,${arbitraryOffset}px)` + : offset != null + ? `translate(0,${offset * 100}%)` + : undefined, + transition: offset != null ? "transform 0.5s" : undefined, + zIndex: arbitraryOffset != null ? 1 : undefined, + }; + + return actions == null ? ( + + {content} + + ) : ( +
+ {content} +
+ ); +}; + +interface TimelineBoardItemContainerProps { + timelines: TimelineBookmark[]; + editHandler?: { + // offset may exceed index range plusing index. + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => void; + onDelete: (owner: string, timeline: string) => void; + }; +} + +const TimelineBoardItemContainer: React.FC = ({ + timelines, + editHandler, +}) => { + const [moveState, setMoveState] = React.useState(null); + + return ( + <> + {timelines.map((timeline, index) => { + const height = 48; + + let offset: number | undefined = undefined; + let arbitraryOffset: number | undefined = undefined; + if (moveState != null) { + if (index === moveState.index) { + arbitraryOffset = moveState.offset; + } else { + if (moveState.offset >= 0) { + const offsetCount = Math.round(moveState.offset / height); + if ( + index > moveState.index && + index <= moveState.index + offsetCount + ) { + offset = -1; + } else { + offset = 0; + } + } else { + const offsetCount = Math.round(-moveState.offset / height); + if ( + index < moveState.index && + index >= moveState.index - offsetCount + ) { + offset = 1; + } else { + offset = 0; + } + } + } + } + + return ( + { + editHandler.onDelete( + timeline.timelineOwner, + timeline.timelineName + ); + }, + onMove: { + start: (e) => { + if (moveState != null) return; + setMoveState({ + index, + offset: 0, + startPointY: e.clientY, + }); + }, + moving: (e) => { + if (moveState == null) return; + setMoveState({ + index, + offset: e.clientY - moveState.startPointY, + startPointY: moveState.startPointY, + }); + }, + end: () => { + if (moveState != null) { + const offsetCount = Math.round( + moveState.offset / height + ); + editHandler.onMove( + timeline.timelineOwner, + timeline.timelineName, + moveState.index, + offsetCount + ); + } + setMoveState(null); + }, + }, + } + : undefined + } + /> + ); + })} + + ); +}; + +interface TimelineBoardUIProps { + title?: string | null; + state: "offline" | "loading" | "loaded"; + timelines: TimelineBookmark[]; + onReload: () => void; + className?: string; + editHandler?: { + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => void; + onDelete: (owner: string, timeline: string) => void; + }; +} + +const TimelineBoardUI: React.FC = (props) => { + const { title, state, timelines, className, editHandler } = props; + + const editable = editHandler != null; + + const [editing, setEditing] = React.useState(false); + + return ( + +
+ {title != null &&

{title}

} + {editable && + (editing ? ( + { + setEditing(false); + }} + /> + ) : ( + { + setEditing(true); + }} + /> + ))} +
+ {(() => { + if (state === "loading") { + return ( +
+ +
+ ); + } else if (state === "offline") { + return ( +
+ +
+ ); + } else { + return ( + { + if (index + offset >= timelines.length) { + offset = timelines.length - index - 1; + } else if (index + offset < 0) { + offset = -index; + } + editHandler.onMove(owner, timeline, index, offset); + }, + } + : undefined + } + /> + ); + } + })()} +
+ ); +}; + +export interface TimelineBoardProps { + title?: string | null; + className?: string; + load: () => Promise; + editHandler?: { + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => Promise; + onDelete: (owner: string, timeline: string) => Promise; + }; +} + +const TimelineBoard: React.FC = ({ + className, + title, + load, + editHandler, +}) => { + const [state, setState] = React.useState<"offline" | "loading" | "loaded">( + "loading" + ); + const [timelines, setTimelines] = React.useState([]); + + React.useEffect(() => { + let subscribe = true; + if (state === "loading") { + void load().then( + (timelines) => { + if (subscribe) { + setState("loaded"); + setTimelines(timelines); + } + }, + () => { + setState("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [load, state]); + + return ( + { + setState("loaded"); + }} + editHandler={ + typeof timelines === "object" && editHandler != null + ? { + onMove: (owner, timeline, index, offset) => { + const newTimelines = timelines.slice(); + const [t] = newTimelines.splice(index, 1); + newTimelines.splice(index + offset, 0, t); + setTimelines(newTimelines); + editHandler + .onMove(owner, timeline, index, offset) + .then(null, () => { + setTimelines(timelines); + }); + }, + onDelete: (owner, timeline) => { + const newTimelines = timelines.slice(); + newTimelines.splice( + timelines.findIndex( + (t) => + t.timelineOwner === owner && t.timelineName === timeline + ), + 1 + ); + setTimelines(newTimelines); + editHandler.onDelete(owner, timeline).then(null, () => { + setTimelines(timelines); + }); + }, + } + : undefined + } + /> + ); +}; + +export default TimelineBoard; -- cgit v1.2.3