Skip to content

Commit

Permalink
work in progress (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexreardon authored Aug 25, 2017
1 parent cb38e79 commit ce9f997
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 2 deletions.
9 changes: 8 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@
// All blocks must be wrapped in curly braces {}
// Preventing if(condition) return;
// https://eslint.org/docs/rules/curly
"curly": ["error", "all"]
"curly": ["error", "all"],

// Allowing Math.pow rather than forcing `**`
// https://eslint.org/docs/rules/no-restricted-properties
"no-restricted-properties": ["off", {
"object": "Math",
"property": "pow"
}]
}
}
6 changes: 6 additions & 0 deletions src/state/axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const vertical: VerticalAxis = {
start: 'top',
end: 'bottom',
size: 'height',
crossAxisStart: 'left',
crossAxisEnd: 'right',
crossAxisSize: 'width',
};

export const horizontal: HorizontalAxis = {
Expand All @@ -15,4 +18,7 @@ export const horizontal: HorizontalAxis = {
start: 'left',
end: 'right',
size: 'width',
crossAxisStart: 'top',
crossAxisEnd: 'bottom',
crossAxisSize: 'height',
};
43 changes: 43 additions & 0 deletions src/state/get-best-droppable-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

## Rules for finding the best Droppable:

### 1. Find lists on the cross axis

Find the list(s) that are closest on the cross axis

Conditions
1. The list must have one corner with the size (height: vertical) of the source list
2. The list must be visible to the user

If more than one list is as close on the cross axis, then:

### 2. Find the closest corner

Based on the draggable items current center position, we need to find the list that
has the closest corner point. That is the closest list.

We do not need to consider the conditions in step 1 as they have already been applied

## Rules for finding the best location within a Droppable

### If moving on the main axis
Move into the first / last position depending on if you are leaving the front / back of the Droppable

nice

### If moving on the cross axis

#### Moving to empty list

Move to the top (vertical list) / left (horizontal list) of the list

#### Moving to populated list

1. Find the draggable with the closest center position

If there is more than one with the closest - choose the the one closest to the top left corner of the page

2. Move below the item if the Draggables current center position is less than the destination. Otherwise, move above
Below = go below
Above = go above
Equal = go above
131 changes: 131 additions & 0 deletions src/state/get-best-droppable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// @flow
import memoizeOne from 'memoize-one';
import { distance } from './position';
import type {
Axis,
Position,
DimensionFragment,
DraggableId,
DroppableId,
DraggableDimension,
DroppableDimension,
DraggableDimensionMap,
DroppableDimensionMap,
} from '../types';

type DroppableCornerMap = {|
[id: DroppableId]: Position[],
|}

type GetBestDroppableArgs = {|
draggableId: DraggableId,
center: Position,
isMovingForward: boolean,
plane: 'main-axis' | 'cross-axis',
// the droppable the draggable is currently in
droppableId: DroppableId,
droppables: DroppableDimensionMap,
draggables: DraggableDimensionMap,
|}

const sortOnCrossAxis = memoizeOne(
(droppables: DroppableDimensionMap, axis: Axis): DroppableDimension[] =>
Object.keys(droppables)
.map((key: DroppableId): DroppableDimension => droppables[key])
.sort((a: DroppableDimension, b: DroppableDimension) => (
a.page.withMargin[axis.crossAxisStart] - b.page.withMargin[axis.crossAxisStart]
)
)
);

type IsWithResultFn = (number) => boolean;

const isWithin = (lowerBound: number, upperBound: number): IsWithResultFn =>
(value: number): boolean => value <= upperBound && value >= lowerBound;

export default ({
isMovingForward,
draggableId,
center,
droppableId,
droppables,
draggables,
}: GetBestDroppableArgs): ?DroppableId => {
const draggable: DraggableDimension = draggables[draggableId];
const source: DroppableDimension = droppables[droppableId];
const axis: Axis = source.axis;

const sorted: DroppableDimension[] = sortOnCrossAxis(droppables, axis);

const candidates: DroppableDimension[] =
// 1. Remove the source droppable from the list
sorted.filter((droppable: DroppableDimension): boolean => droppable !== source)
// 2. Get only droppables that are on the desired side
.filter((droppable: DroppableDimension): boolean => {
if (isMovingForward) {
// is the droppable in front of the source on the cross axis?
return source.page.withMargin[axis.crossAxisEnd] <=
droppable.page.withMargin[axis.crossAxisStart];
}
// is the droppable behind the source on the cross axis?
return droppable.page.withMargin[axis.crossAxisEnd] <=
source.page.withMargin[axis.crossAxisStart];
})
// 3. is there any overlap on the main axis?
.filter((droppable: DroppableDimension): boolean => {
const sourceFragment: DimensionFragment = source.page.withMargin;
const destinationFragment: DimensionFragment = droppable.page.withMargin;

const isBetweenSourceBounds = isWithin(
sourceFragment[axis.start],
sourceFragment[axis.end]
);
const isBetweenDestBounds = isWithin(
destinationFragment[axis.start],
destinationFragment[axis.end]
);

return isBetweenSourceBounds(destinationFragment[axis.start]) ||
isBetweenSourceBounds(destinationFragment[axis.end]) ||
isBetweenDestBounds(sourceFragment[axis.start]) ||
isBetweenDestBounds(sourceFragment[axis.end]);
})
// 4. Find the droppables that have the same cross axis value as the first item
.filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean =>
droppable.page.withMargin[axis.crossAxisStart] ===
array[0].page.withMargin[axis.crossAxisStart]
);

// no possible candidates
if (!candidates.length) {
return null;
}

// only one result - all done!
if (candidates.length === 1) {
return candidates[0].id;
}

// At this point we have a number of candidates that
// all have the same axis.crossAxisStart value.
// Now need to consider the main axis as a tiebreaker

// 1. Get the distance to all of the corner points
// 2. Find the closest corner to current center
// 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0}
const items: DroppableDimension[] =
candidates.map((droppable: DroppableDimension): DroppableCornerMap => {
const fragment: DimensionFragment = droppable.page.withMargin;
const first: Position = {
x: fragment[axis.crossAxisStart],
y: fragment[axis.start],
};
const second: Position = {
x: 2,
y: 3,
};
return {
[droppable.id]: [first, second],
};
});
};
5 changes: 5 additions & 0 deletions src/state/get-best-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @flow

// finds best location for a draggable moving between droppables

// will return the offset for the draggable to move, and the impact of the drag
7 changes: 7 additions & 0 deletions src/state/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ export const patch = (line: 'x' | 'y', value: number): Position => ({
y: line === 'y' ? value : 0,
});

// Returns the distance between two points
// https://www.mathsisfun.com/algebra/distance-2-points.html
export const distance = (point1: Position, point2: Position): number =>
Math.sqrt(
Math.pow((point2.x - point1.x), 2) +
Math.pow((point2.y - point1.y), 2)
);
6 changes: 6 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type VerticalAxis = {|
start: 'top',
end: 'bottom',
size: 'height',
crossAxisStart: 'left',
crossAxisEnd: 'right',
crossAxisSize: 'width',
|}

export type HorizontalAxis = {|
Expand All @@ -29,6 +32,9 @@ export type HorizontalAxis = {|
start: 'left',
end: 'right',
size: 'width',
crossAxisStart: 'top',
crossAxisEnd: 'bottom',
crossAxisSize: 'height',
|}

export type Axis = VerticalAxis | HorizontalAxis
Expand Down
41 changes: 40 additions & 1 deletion test/unit/state/position.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// @flow
import { add, subtract, isEqual, negate, patch } from '../../../src/state/position';
import {
add,
subtract,
isEqual,
negate,
patch,
distance,
} from '../../../src/state/position';
import type { Position } from '../../../src/types';

const point1: Position = {
Expand All @@ -10,6 +17,7 @@ const point2: Position = {
x: 2,
y: 1,
};
const origin: Position = { x: 0, y: 0 };

describe('position', () => {
describe('add', () => {
Expand Down Expand Up @@ -64,4 +72,35 @@ describe('position', () => {
expect(patch('y', 5)).toEqual({ x: 0, y: 5 });
});
});

describe('distance', () => {
describe('on the same axis', () => {
it('should return the distance between two positive values', () => {
const a = { x: 0, y: 2 };
const b = { x: 0, y: 5 };
expect(distance(a, b)).toEqual(3);
});

it('should return the distance between two negative values', () => {
const a = { x: 0, y: -2 };
const b = { x: 0, y: -5 };
expect(distance(a, b)).toEqual(3);
});

it('should return the distance between a positive and negative value', () => {
const a = { x: 0, y: -2 };
const b = { x: 0, y: 3 };
expect(distance(a, b)).toEqual(5);
});
});

describe('with axis shift', () => {
it('should account for a shift in plane', () => {
// a '3, 4, 5' triangle
// https://www.mathsisfun.com/pythagoras.html
const target = { x: 3, y: 4 };
expect(distance(origin, target)).toEqual(5);
});
});
});
});

0 comments on commit ce9f997

Please sign in to comment.