mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 18:25:30 +00:00
231 lines
6.1 KiB
JavaScript
231 lines
6.1 KiB
JavaScript
import React from "react";
|
|
import PropTypes from "prop-types";
|
|
import { connect } from "react-redux";
|
|
|
|
import {
|
|
snapDiffManyToMany,
|
|
boundingBox,
|
|
snapWithinDiff,
|
|
snap,
|
|
traceConnection,
|
|
applyDiff,
|
|
applyMultipleDiffs
|
|
} from "../snapUtils";
|
|
import { getWindowPositions } from "../selectors";
|
|
import { updateWindowPositions } from "../actionCreators";
|
|
|
|
const WINDOW_HEIGHT = 116;
|
|
const WINDOW_WIDTH = 275;
|
|
|
|
const abuts = (a, b) => {
|
|
// TODO: This is kinda a hack. They should really be touching, not just within snapping distance.
|
|
// Also, overlapping should not count.
|
|
const wouldMoveTo = snap(a, b);
|
|
return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined;
|
|
};
|
|
|
|
class WindowManager extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.windowNodes = {};
|
|
this.getRef = this.getRef.bind(this);
|
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
this.centerWindows = this.centerWindows.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
window.addEventListener("resize", this.centerWindows);
|
|
this.centerWindows();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener("resize", this.centerWindows);
|
|
}
|
|
|
|
centerWindows() {
|
|
let offsetLeft = 0;
|
|
let offsetTop = 0;
|
|
// https://j11y.io/snippets/get-document-height-cross-browser/
|
|
let width = Math.max(
|
|
document.documentElement.scrollWidth,
|
|
document.documentElement.clientWidth,
|
|
document.documentElement.offsetWidth
|
|
);
|
|
let height = Math.max(
|
|
document.documentElement.scrollHeight,
|
|
document.documentElement.clientHeight,
|
|
document.documentElement.offsetHeight
|
|
);
|
|
|
|
const { container } = this.props;
|
|
if (container != null) {
|
|
offsetLeft = container.offsetLeft;
|
|
offsetTop = container.offsetTop;
|
|
width = container.scrollWidth;
|
|
height = container.scrollHeight;
|
|
}
|
|
const windowPositions = {};
|
|
const keys = this.windowKeys();
|
|
const totalHeight = keys.length * WINDOW_HEIGHT;
|
|
keys.forEach((key, i) => {
|
|
const offset = WINDOW_HEIGHT * i;
|
|
windowPositions[key] = {
|
|
x: offsetLeft + (width / 2 - WINDOW_WIDTH / 2),
|
|
y: offsetTop + (height / 2 - totalHeight / 2 + offset)
|
|
};
|
|
});
|
|
this.props.updateWindowPositions(windowPositions);
|
|
}
|
|
|
|
getRef(key, node) {
|
|
// If we are unmounting, the node might be null;
|
|
this.windowNodes[key] = node;
|
|
}
|
|
|
|
//
|
|
getWindowNodes() {
|
|
return this.windowKeys()
|
|
.map(key => {
|
|
const node = this.windowNodes[key];
|
|
return node && this.nodeInfo(node, key);
|
|
})
|
|
|
|
.filter(Boolean);
|
|
}
|
|
|
|
nodeInfo(node, key) {
|
|
const child = node.childNodes[0];
|
|
const { height, width } = child.getBoundingClientRect();
|
|
const { offsetLeft, offsetTop } = node;
|
|
return { key, x: offsetLeft, y: offsetTop, height, width };
|
|
}
|
|
|
|
movingAndStationaryNodes(key) {
|
|
const windows = this.getWindowNodes();
|
|
const targetNode = windows.find(node => node.key === key);
|
|
|
|
let movingSet = new Set([targetNode]);
|
|
// Only the main window brings other windows along.
|
|
if (key === "main") {
|
|
const findAllConnected = traceConnection(abuts);
|
|
movingSet = findAllConnected(windows, targetNode);
|
|
}
|
|
|
|
const stationary = windows.filter(w => !movingSet.has(w));
|
|
const moving = Array.from(movingSet);
|
|
|
|
return [moving, stationary];
|
|
}
|
|
|
|
handleMouseDown(key, e) {
|
|
if (!e.target.classList.contains("draggable")) {
|
|
return;
|
|
}
|
|
// Prevent dragging from highlighting text.
|
|
e.preventDefault();
|
|
|
|
const [moving, stationary] = this.movingAndStationaryNodes(key);
|
|
|
|
const mouseStart = { x: e.clientX, y: e.clientY };
|
|
const browserSize = {
|
|
width: document.documentElement.scrollWidth,
|
|
height: document.documentElement.scrollHeight
|
|
};
|
|
|
|
const box = boundingBox(moving);
|
|
|
|
const handleMouseMove = ee => {
|
|
const proposedDiff = {
|
|
x: ee.clientX - mouseStart.x,
|
|
y: ee.clientY - mouseStart.y
|
|
};
|
|
|
|
const proposedWindows = moving.map(node => ({
|
|
...node,
|
|
...applyDiff(node, proposedDiff)
|
|
}));
|
|
|
|
const proposedBox = {
|
|
...box,
|
|
...applyDiff(box, proposedDiff)
|
|
};
|
|
|
|
const snapDiff = snapDiffManyToMany(proposedWindows, stationary);
|
|
|
|
const withinDiff = snapWithinDiff(proposedBox, browserSize);
|
|
|
|
const finalDiff = applyMultipleDiffs(proposedDiff, snapDiff, withinDiff);
|
|
|
|
const windowPositionDiff = moving.reduce((diff, window) => {
|
|
diff[window.key] = applyDiff(window, finalDiff);
|
|
return diff;
|
|
}, {});
|
|
|
|
this.props.updateWindowPositions(windowPositionDiff);
|
|
};
|
|
|
|
const removeListeners = () => {
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
window.removeEventListener("mouseup", removeListeners);
|
|
};
|
|
|
|
window.addEventListener("mouseup", removeListeners);
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
}
|
|
|
|
// Keys for the visible windows
|
|
windowKeys() {
|
|
// TODO: Iterables can probably do this better.
|
|
return Object.keys(this.props.windows).filter(
|
|
key => !!this.props.windows[key]
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const style = {
|
|
position: "absolute"
|
|
};
|
|
|
|
const parentStyle = {
|
|
position: "absolute",
|
|
width: 0,
|
|
height: 0,
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
return (
|
|
<div style={parentStyle}>
|
|
{this.windowKeys().map(key => {
|
|
const position = this.props.windowPositions[key];
|
|
return (
|
|
position && (
|
|
<div
|
|
onMouseDown={e => this.handleMouseDown(key, e)}
|
|
ref={node => this.getRef(key, node)}
|
|
style={{ ...style, left: position.x, top: position.y }}
|
|
key={key}
|
|
>
|
|
{this.props.windows[key]}
|
|
</div>
|
|
)
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
WindowManager.propTypes = {
|
|
windows: PropTypes.object.isRequired,
|
|
container: PropTypes.instanceOf(Element)
|
|
};
|
|
|
|
const mapStateToProps = state => ({
|
|
windowPositions: getWindowPositions(state)
|
|
});
|
|
|
|
const mapDispatchToProps = {
|
|
updateWindowPositions
|
|
};
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(WindowManager);
|