Main window brings connected windows along.

This commit is contained in:
Jordan Eldredge 2017-08-19 17:00:57 -07:00
parent 2fe465ddf7
commit 994c725f60
3 changed files with 347 additions and 23 deletions

View file

@ -1,11 +1,27 @@
import React from "react";
import { findDOMNode } from "react-dom";
import { snapToMany, snapWithin, applySnap } from "../snapUtils";
import {
snapDiffManyToMany,
boundingBox,
snapWithinDiff,
snap,
traceConnection
} from "../snapUtils";
const WINDOW_HEIGHT = 116;
const WINDOW_WIDTH = 275;
const abuts = (a, b) => {
const wouldMoveTo = snap(a, b);
return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined;
};
const applyDiff = (a, b) => ({
x: a.x + b.x,
y: a.y + b.y
});
class WindowManager extends React.Component {
constructor(props) {
super(props);
@ -42,16 +58,20 @@ class WindowManager extends React.Component {
this.windowNodes.push(node);
}
otherWindows(element) {
const domNodes = this.windowNodes.map(findDOMNode);
return domNodes.filter(node => node !== element);
windows() {
return this.windowNodes.map(findDOMNode);
}
nodeInfo(node) {
otherWindows(element) {
return this.windows.filter(node => node !== element);
}
nodeInfo(node, i) {
const child = node.childNodes[0];
const { height, width } = child.getBoundingClientRect();
const { offsetLeft, offsetTop } = node;
return {
i,
x: offsetLeft,
y: offsetTop,
height,
@ -63,44 +83,71 @@ class WindowManager extends React.Component {
if (!e.target.classList.contains("draggable")) {
return;
}
const mouseStart = {
x: e.clientX,
y: e.clientY
};
const windowStart = this.nodeInfo(e.currentTarget);
const targetNode = this.nodeInfo(e.currentTarget, i);
const otherWindowNodes = this.otherWindows(e.currentTarget).map(
this.nodeInfo
);
const windows = this.windows(e.currentTarget).map(this.nodeInfo);
let movingSet = new Set([targetNode]);
// Only the main window brings other windows along.
if (targetNode.i === 0) {
const findAllConnected = traceConnection(abuts);
movingSet = findAllConnected(windows, targetNode);
}
const stationary = windows.filter(w => !movingSet.has(w));
const moving = Array.from(movingSet);
const browserSize = {
width: window.innerWidth,
height: window.innerHeight
};
const box = boundingBox(moving);
const handleMouseMove = ee => {
const diff = {
const proposedDiff = {
x: ee.clientX - mouseStart.x,
y: ee.clientY - mouseStart.y
};
const proposedWindow = {
...windowStart,
x: windowStart.x + diff.x,
y: windowStart.y + diff.y
const proposedWindows = moving.map(node => ({
...node,
...applyDiff(node, proposedDiff)
}));
const proposedBox = {
...box,
...applyDiff(box, proposedDiff)
};
const snappedToOthers = snapToMany(proposedWindow, otherWindowNodes);
const snappedToBrowser = snapWithin(proposedWindow, browserSize);
const snapDiff = snapDiffManyToMany(proposedWindows, stationary);
const newPosition = applySnap(
proposedWindow,
snappedToBrowser,
snappedToOthers
);
const withinDiff = snapWithinDiff(proposedBox, browserSize);
this.setState({ [i]: { top: newPosition.y, left: newPosition.x } });
let finalDiff = proposedDiff;
if (withinDiff.x !== 0 || withinDiff.y !== 0) {
finalDiff = applyDiff(proposedDiff, withinDiff);
} else {
finalDiff = applyDiff(proposedDiff, snapDiff);
}
const stateDiff = moving.reduce((diff, window) => {
const newWindowLocation = applyDiff(window, finalDiff);
diff[window.i] = {
top: newWindowLocation.y,
left: newWindowLocation.x
};
return diff;
}, {});
this.setState(stateDiff);
};
window.addEventListener("mouseup", () => {

View file

@ -45,6 +45,31 @@ export const snap = (boxA, boxB) => {
return { x, y };
};
export const snapDiff = (a, b) => {
const newPos = snap(a, b);
return {
x: newPos.x === undefined ? 0 : newPos.x - a.x,
y: newPos.y === undefined ? 0 : newPos.y - a.y
};
};
// TODO: Use the first x and y combo
export const snapDiffManyToMany = (as, bs) => {
let x = 0;
let y = 0;
for (const a of as) {
for (const b of bs) {
const diff = snapDiff(a, b);
x = x || diff.x;
y = y || diff.y;
if (x > 0 && y > 0) {
break;
}
}
}
return { x, y };
};
export const snapToMany = (boxA, otherBoxes) => {
let x, y;
@ -75,6 +100,14 @@ export const snapWithin = (boxA, boundingBox) => {
return { x, y };
};
export const snapWithinDiff = (a, b) => {
const newPos = snapWithin(a, b);
return {
x: newPos.x === undefined ? 0 : newPos.x - a.x,
y: newPos.y === undefined ? 0 : newPos.y - a.y
};
};
export const applySnap = (original, ...snaps) =>
snaps.reduce(
(previous, snapped) => ({
@ -84,3 +117,42 @@ export const applySnap = (original, ...snaps) =>
}),
original
);
export const boundingBox = nodes => {
const boxes = nodes.slice();
const firstNode = boxes.pop();
const bounding = {
top: top(firstNode),
right: right(firstNode),
bottom: bottom(firstNode),
left: left(firstNode)
};
boxes.forEach(node => {
bounding.top = Math.min(bounding.top, top(node));
bounding.right = Math.max(bounding.right, right(node));
bounding.bottom = Math.max(bounding.bottom, bottom(node));
bounding.left = Math.min(bounding.left, left(node));
});
return {
x: bounding.left,
y: bounding.top,
width: bounding.right - bounding.left,
height: bounding.bottom - bounding.top
};
};
export const traceConnection = areConnected => (candidates, node) => {
const connected = new Set();
const checkNode = n => {
for (const candidate of candidates) {
if (!connected.has(candidate) && areConnected(candidate, n)) {
connected.add(candidate);
checkNode(candidate);
}
}
};
checkNode(node);
return connected;
};

View file

@ -8,7 +8,10 @@ import {
overlapY,
overlapX,
snapWithin,
applySnap
applySnap,
snapDiff,
boundingBox,
traceConnection
} from "./snapUtils";
describe("side functions", () => {
@ -248,3 +251,205 @@ describe("applySnap function", () => {
expect(actual).toEqual(expected);
});
});
describe("snap diff", () => {
it("does not snap if A and B are obviously far apart", () => {
const a = { x: 10, y: 10, width: 100, height: 100 };
const b = { x: 200, y: 200, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 0, y: 0 };
expect(actual).toEqual(expected);
});
it("snaps the left of A to the right of B", () => {
const a = { x: 120, y: 30, width: 100, height: 100 };
const b = { x: 10, y: 10, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: -10, y: 0 };
expect(actual).toEqual(expected);
});
it("snaps the right of A to the left of B", () => {
const a = { x: 10, y: 30, width: 100, height: 100 };
const b = { x: 120, y: 10, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 10, y: 0 };
expect(actual).toEqual(expected);
});
it("snaps the top of A to the bottom of B", () => {
const a = { x: 30, y: 10, width: 100, height: 100 };
const b = { x: 10, y: 120, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 0, y: 10 };
expect(actual).toEqual(expected);
});
it("snaps the bottom of A to the top of B", () => {
const a = { x: 30, y: 120, width: 100, height: 100 };
const b = { x: 10, y: 10, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 0, y: -10 };
expect(actual).toEqual(expected);
});
it("does not snap to the X axis if A is below B", () => {
const a = { x: 10, y: 10, width: 100, height: 100 };
const b = { x: 110, y: 150, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 0, y: 0 };
expect(actual).toEqual(expected);
});
it("snaps in both axis if the corners are within SNAP_DISTANCE", () => {
const a = { x: 10, y: 10, width: 100, height: 100 };
const b = { x: 120, y: 120, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 10, y: 10 };
expect(actual).toEqual(expected);
});
it("snaps the left of A to the left of B", () => {
const a = { x: 10, y: 10, width: 100, height: 100 };
const b = { x: 15, y: 110, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 5, y: 0 };
expect(actual).toEqual(expected);
});
it("snaps the top of A to the bottom of B", () => {
const a = { x: 10, y: 10, width: 100, height: 100 };
const b = { x: 110, y: 15, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: 0, y: 5 };
expect(actual).toEqual(expected);
});
it("snaps the top left corner of A to the bottom right corner of B", () => {
const a = { x: 110, y: 110, width: 100, height: 100 };
const b = { x: 0, y: 0, width: 100, height: 100 };
const actual = snapDiff(a, b);
const expected = { x: -10, y: -10 };
expect(actual).toEqual(expected);
});
});
describe("snapWithin function", () => {
it("snaps the inner box to the top of the outer box", () => {
const inner = { x: 120, y: 10, width: 100, height: 100 };
const outer = { width: 1000, height: 1000 };
const actual = snapWithin(inner, outer);
const expected = { y: 0 };
expect(actual).toEqual(expected);
});
it("snaps the inner box to the bottom of the outer box", () => {
const inner = { x: 120, y: 910, width: 100, height: 100 };
const outer = { width: 1000, height: 1000 };
const actual = snapWithin(inner, outer);
const expected = { y: 900 };
expect(actual).toEqual(expected);
});
it("snaps the inner box to the right of the outer box", () => {
const inner = { x: 910, y: 120, width: 100, height: 100 };
const outer = { width: 1000, height: 1000 };
const actual = snapWithin(inner, outer);
const expected = { x: 900 };
expect(actual).toEqual(expected);
});
it("snaps the inner box to the left of the outer box", () => {
const inner = { x: 10, y: 120, width: 100, height: 100 };
const outer = { width: 1000, height: 1000 };
const actual = snapWithin(inner, outer);
const expected = { x: 0 };
expect(actual).toEqual(expected);
});
it("snaps the inner box to the top-left of the outer box", () => {
const inner = { x: 10, y: 10, width: 100, height: 100 };
const outer = { width: 1000, height: 1000 };
const actual = snapWithin(inner, outer);
const expected = { x: 0, y: 0 };
expect(actual).toEqual(expected);
});
});
describe("boundingBox function", () => {
it("does not mutate the given array", () => {
const box = { x: 10, y: 10, width: 100, height: 100 };
const boxesA = [box];
const boxesB = [box];
boundingBox(boxesA);
expect(boxesA).toEqual(boxesB);
});
it("returns iteself when called on a single box", () => {
const box = { x: 10, y: 10, width: 100, height: 100 };
expect(boundingBox([box])).toEqual(box);
});
it("returns the larger box when one fits entire within another", () => {
const boxes = [
{ x: 10, y: 10, width: 100, height: 100 },
{ x: 20, y: 20, width: 80, height: 80 }
];
expect(boundingBox(boxes)).toEqual(boxes[0]);
});
it("returns countaing box of two boxes", () => {
const boxes = [
{ x: 10, y: 10, width: 100, height: 100 },
{ x: 120, y: 220, width: 100, height: 100 }
];
expect(boundingBox(boxes)).toEqual({
x: 10,
y: 10,
width: 210,
height: 310
});
});
it("returns countaing box of three boxes", () => {
const boxes = [
{ x: 10, y: 10, width: 100, height: 100 },
{ x: 120, y: 220, width: 100, height: 100 },
{ x: 3, y: 220, width: 100, height: 100 }
];
expect(boundingBox(boxes)).toEqual({
x: 3,
y: 10,
width: 217,
height: 310
});
});
});
describe("applySnap function", () => {
it("does not apply undefined values", () => {
const original = { x: 10, y: 10 };
const snapped = { x: undefined, y: undefined };
const actual = applySnap(original, snapped);
const expected = original;
expect(actual).toEqual(expected);
});
it("preserves other values on original", () => {
const original = { x: 10, y: 10, foo: "bar" };
const snapped = { x: 50, y: 50 };
const actual = applySnap(original, snapped);
const expected = { x: 50, y: 50, foo: "bar" };
expect(actual).toEqual(expected);
});
it('will clobber original with falsy "0" values', () => {
const original = { x: 10, y: 10 };
const snapped = { x: 0, y: 0 };
const actual = applySnap(original, snapped);
expect(actual).toEqual(snapped);
});
it('will clobber original with falsy "0" values', () => {
const original = { x: 10, y: 10 };
const snapped = { x: 0, y: 0 };
const actual = applySnap(original, snapped);
expect(actual).toEqual(snapped);
});
it("can apply multiple snaps", () => {
const original = { x: 10, y: 10 };
const snapped1 = { x: 0 };
const snapped2 = { y: 0 };
const actual = applySnap(original, snapped1, snapped2);
const expected = { x: 0, y: 0 };
expect(actual).toEqual(expected);
});
it("the last snap wins", () => {
const original = { x: 10, y: 10 };
const snapped1 = { x: 50, y: 20 };
const snapped2 = { x: 60, y: 60 };
const actual = applySnap(original, snapped1, snapped2);
const expected = { x: 60, y: 60 };
expect(actual).toEqual(expected);
});
});