import * as React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; import { TimelineBookmark } from "~src/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;