From ce9f9970cf44339752626f005c29df08e873d50b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 25 Aug 2017 16:19:52 +1000 Subject: [PATCH] work in progress (#59) --- .eslintrc | 9 +- src/state/axis.js | 6 ++ src/state/get-best-droppable-rules.md | 43 +++++++++ src/state/get-best-droppable.js | 131 ++++++++++++++++++++++++++ src/state/get-best-location.js | 5 + src/state/position.js | 7 ++ src/types.js | 6 ++ test/unit/state/position.spec.js | 41 +++++++- 8 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 src/state/get-best-droppable-rules.md create mode 100644 src/state/get-best-droppable.js create mode 100644 src/state/get-best-location.js diff --git a/.eslintrc b/.eslintrc index d573cc4fe9..57c598119f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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" + }] } } \ No newline at end of file diff --git a/src/state/axis.js b/src/state/axis.js index aa1a553fd7..1ff8c48c2b 100644 --- a/src/state/axis.js +++ b/src/state/axis.js @@ -7,6 +7,9 @@ export const vertical: VerticalAxis = { start: 'top', end: 'bottom', size: 'height', + crossAxisStart: 'left', + crossAxisEnd: 'right', + crossAxisSize: 'width', }; export const horizontal: HorizontalAxis = { @@ -15,4 +18,7 @@ export const horizontal: HorizontalAxis = { start: 'left', end: 'right', size: 'width', + crossAxisStart: 'top', + crossAxisEnd: 'bottom', + crossAxisSize: 'height', }; diff --git a/src/state/get-best-droppable-rules.md b/src/state/get-best-droppable-rules.md new file mode 100644 index 0000000000..0ec7b58094 --- /dev/null +++ b/src/state/get-best-droppable-rules.md @@ -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 \ No newline at end of file diff --git a/src/state/get-best-droppable.js b/src/state/get-best-droppable.js new file mode 100644 index 0000000000..b78c419356 --- /dev/null +++ b/src/state/get-best-droppable.js @@ -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], + }; + }); +}; diff --git a/src/state/get-best-location.js b/src/state/get-best-location.js new file mode 100644 index 0000000000..6ff684a4ae --- /dev/null +++ b/src/state/get-best-location.js @@ -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 diff --git a/src/state/position.js b/src/state/position.js index 045d4307f9..f792d736bd 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -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) + ); diff --git a/src/types.js b/src/types.js index 0dcba025d0..0b12d8aa41 100644 --- a/src/types.js +++ b/src/types.js @@ -21,6 +21,9 @@ export type VerticalAxis = {| start: 'top', end: 'bottom', size: 'height', + crossAxisStart: 'left', + crossAxisEnd: 'right', + crossAxisSize: 'width', |} export type HorizontalAxis = {| @@ -29,6 +32,9 @@ export type HorizontalAxis = {| start: 'left', end: 'right', size: 'width', + crossAxisStart: 'top', + crossAxisEnd: 'bottom', + crossAxisSize: 'height', |} export type Axis = VerticalAxis | HorizontalAxis diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js index dde74ee3d7..6ba24b25c3 100644 --- a/test/unit/state/position.spec.js +++ b/test/unit/state/position.spec.js @@ -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 = { @@ -10,6 +17,7 @@ const point2: Position = { x: 2, y: 1, }; +const origin: Position = { x: 0, y: 0 }; describe('position', () => { describe('add', () => { @@ -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); + }); + }); + }); });