import React, { ReactNode } from "react"; import { connect } from "react-redux"; import * as SnapUtils from "../snapUtils"; import * as Selectors from "../selectors"; import * as Actions from "../actionCreators"; import { WindowInfo, Dispatch, WindowPositions, AppState, WindowId, } from "../types"; const abuts = (a: SnapUtils.Box, b: SnapUtils.Box) => { // TODO: This is kinda a hack. They should really be touching, not just within snapping distance. // Also, overlapping should not count. const wouldMoveTo = SnapUtils.snap(a, b); return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined; }; interface StateProps { windowsInfo: WindowInfo[]; browserWindowSize: { height: number; width: number }; getWindowHidden(windowId: WindowId): boolean; } interface DispatchProps { updateWindowPositions(positions: WindowPositions, center: boolean): void; clearWindowFocus(): void; } interface OwnProps { windows: { [windowId: string]: ReactNode }; container: HTMLElement; } type Props = StateProps & DispatchProps & OwnProps; class WindowManager extends React.Component { movingAndStationaryNodes(key: WindowId): [WindowInfo[], WindowInfo[]] { const windows = this.props.windowsInfo.filter( w => this.props.windows[w.key] != null && !this.props.getWindowHidden(w.key) ); const targetNode = windows.find(node => node.key === key); if (targetNode == null) { throw new Error(`Tried to move a node that does not exist: ${key}`); } let movingSet = new Set([targetNode]); // Only the main window brings other windows along. if (key === "main") { const findAllConnected = SnapUtils.traceConnection(abuts); movingSet = findAllConnected(windows, targetNode); } const stationary = windows.filter(w => !movingSet.has(w)); const moving = Array.from(movingSet); return [moving, stationary]; } handleMouseDown = (key: WindowId, e: React.MouseEvent) => { if (!(e.target as HTMLElement).classList.contains("draggable")) { return; } if (this.props.getWindowHidden(key)) { // The user may be clicking on full screen Milkdrop. return; } const [moving, stationary] = this.movingAndStationaryNodes(key); const mouseStart = { x: e.clientX, y: e.clientY }; const { browserWindowSize } = this.props; const box = SnapUtils.boundingBox(moving); const handleMouseMove = (ee: MouseEvent) => { const proposedDiff = { x: ee.clientX - mouseStart.x, y: ee.clientY - mouseStart.y, }; const proposedWindows = moving.map(node => ({ ...node, ...SnapUtils.applyDiff(node, proposedDiff), })); const proposedBox = { ...box, ...SnapUtils.applyDiff(box, proposedDiff), }; const snapDiff = SnapUtils.snapDiffManyToMany( proposedWindows, stationary ); const withinDiff = SnapUtils.snapWithinDiff( proposedBox, browserWindowSize ); const finalDiff = SnapUtils.applyMultipleDiffs( proposedDiff, snapDiff, withinDiff ); const windowPositionDiff = moving.reduce( (diff: { [windowId: string]: SnapUtils.Diff }, window) => { diff[window.key] = SnapUtils.applyDiff(window, finalDiff); return diff; }, {} ); this.props.updateWindowPositions(windowPositionDiff, false); }; const removeListeners = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", removeListeners); }; window.addEventListener("mouseup", removeListeners); window.addEventListener("mousemove", handleMouseMove); }; render() { const style: React.CSSProperties = { position: "absolute", top: 0, left: 0, }; const windows = this.props.windowsInfo.filter( w => this.props.windows[w.key] ); return windows.map(w => (
{ const { currentTarget, relatedTarget } = e; if ( currentTarget === relatedTarget || currentTarget.contains(relatedTarget) ) { return; } this.props.clearWindowFocus(); }} onMouseDown={(e: React.MouseEvent) => { this.handleMouseDown(w.key, e); }} style={{ ...style, transform: `translate(${w.x}px, ${w.y}px)` }} > {this.props.windows[w.key]}
)); } } const mapStateToProps = (state: AppState): StateProps => ({ windowsInfo: Selectors.getWindowsInfo(state), getWindowHidden: Selectors.getWindowHidden(state), browserWindowSize: Selectors.getBrowserWindowSize(state), }); const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => { return { updateWindowPositions: (positions: WindowPositions) => dispatch(Actions.updateWindowPositions(positions)), clearWindowFocus: () => dispatch(Actions.setFocusedWindow(null)), }; }; export default connect( mapStateToProps, mapDispatchToProps )(WindowManager);