'use strict'; const React = require("react"); const createReactClass = require("create-react-class"); const throttleit = require("throttleit"); const euclideanDistance = require("euclidean-distance"); const Window = require("./window"); module.exports = createReactClass({ displayName: "WindowManager", getDefaultProps: function () { return { windowMoveThreshold: 6 }; }, getInitialState: function () { return { movingWindow: null, activeWindow: null, windowPositions: {} }; }, lastMousePositionX: null, lastMousePositionY: null, componentDidMount: function () { /* Due to how synthetic events work in React, we need to separate out the 'get coordinates from event' and 'do something with the coordinates' step; if we throttle the coordinate extraction logic, we'll run into problems when synthetic events have already been cleared for reuse by the time we try to obtain the coordinates. Therefore, `processMouseMove` contains all the throttled logic, whereas the coordinate extraction happens on *every* mousemove event. */ this.processMouseMove = throttleit(() => { /* Yes, the below check is there for a reason; just in case the `movingWindow` state changes between the call to the throttled wrapper and the wrapped function itself. */ if (this.state.movingWindow != null) { let thresholdMet = this.state.movingWindowThresholdMet; /* Since we're outside of a React-controlled handler, we can't rely on React to batch updates. Therefore, we have to do our own impromptu batching. */ let stateToUpdate = {}; if (thresholdMet === false) { let origin = [this.state.movingWindowOriginX, this.state.movingWindowOriginY]; let position = [this.lastMousePositionX, this.lastMousePositionY]; if (euclideanDistance(origin, position) > this.props.windowMoveThreshold) { thresholdMet = true; stateToUpdate.thresholdMet = true; } } if (thresholdMet === true) { /* TODO: Consider handling this move out-of-band, to avoid going through React's rendering cycle for every move event. */ stateToUpdate.windowPositions = Object.assign(this.state.windowPositions, { [this.state.movingWindow]: { x: this.lastMousePositionX - this.state.movingWindowBarX, y: this.lastMousePositionY - this.state.movingWindowBarY } }); } /* FIXME: Move this into the store? */ this.setState(stateToUpdate); } }, 10); }, handleMouseMove: function(event) { if (this.state.movingWindow != null) { this.lastMousePositionX = event.pageX; this.lastMousePositionY = event.pageY; this.processMouseMove(); } }, handleMouseUp: function () { this.setState({ movingWindow: null }); }, handleTitleMouseDown: function (window, event, barX, barY) { this.setState({ movingWindow: window.id, movingWindowThresholdMet: false, movingWindowBarX: barX, movingWindowBarY: barY, movingWindowOriginX: event.pageX, movingWindowOriginY: event.pageY }); }, render: function () { /* TODO: Tiled layout */ return (
{this.props.children} {this.props.store.getAll().toArray().map((window_) => { let x, y; let overrideWindowPosition = this.state.windowPositions[window_.id]; if (overrideWindowPosition != null) { x = overrideWindowPosition.x; y = overrideWindowPosition.y; } else { x = window_.initialX; y = window_.initialY; } let windowStyle = { transform: `translate(${x}px, ${y}px)`, width: window_.width, height: window_.height, zIndex: window_.zIndex }; let handlers = { onTitleMouseDown: (...args) => { return this.handleTitleMouseDown(window_, ...args); }, onMouseDown: () => { this.props.store.focus(window_.id); }, onClose: () => { this.props.store.close(window_.id); } }; return ( {window_.contents} ); })}
); } });