PromucFlow_constructor/app/client/src/reflow/reflowUtils.ts
Abhinav Jha 4361db4269
feat: Automatic height updates for widgets based on contents (Auto Height) (#18341)
* added multi select back

* (WIP): Complete the dynamc height update logic

* (WIP): Dynamic height logic

* (WIP): Container computation logic, Next steps: Prevent reflow when resize is disabled. Fix logic of widgets randomly changing positions (Debug)

* Fix logic in container computations

* Integrate for PoC

* fixed the no initial load dynamic height updates

* Stop vertical resize and reflow when dynamic height is enabled for a widget

* added another container in text widget

* enabled dynamic height for container widgets

* removed dynamic height feature from list widget

* Fixed Button and Input components height increase

* added an experiment to overflow the content if maxHEight is less

* removed the ref of Textwidget by mistake, added it back

* fixed text widget height overflow problem with a little hack

* added long labels with text

* fixed the table scroll issue

* overflow fixed for json form widget

* added extra 8px height for Switch, Rating and Checkbox Height

* (WIP): Resolve issues

* (WIP): Fix widget padding issue

* added overflow container for Radio and Switch group widgets

* (WIP): Have modals work with dynamic height

* added the overlay and the handles

* added dragging behavior to the dots

* fixed the overlapping with the selection tool

* (WIP): Fix issues reported

* now we can update the property pane values back from overlay handles

* now we can update the property pane values back from overlay handles

* (WIP): Fix table widget

* Fix package.json

* Remove unit tests temporarily

* Fix unit test

* (WIP): Fix modal resize. Fix cursors. Fix border issue on non-resizable widgets

* fetch component heights using the requestAnimationFrame callback

* behavioural changes

* (WIP): Fix issues on the platform

* Update main container size appropriately

* more behavioural changes

* overlay now only be visible when hovering over the dots

* grid showing and widget reselecting

* added onfocus and onblur events to property pane listeners

* added onfocus and onblur events to property pane listeners

* added a range slider for min and max

* added demarcations for slider values

* (WIP): Fix platform workflows for dynamic height

* Fix issues with widgets

* Fix removed import

* - Add missing cypress files

* set the limits

* limit increase on change

* Fix z-index of min max limit indicators. Fix unused-vars warnings

* Fix Table Widget and Text Widget issues

* Fix: all the bugs in the bug master list for DH (#16268)

* changed the zindex for the signifiers

* showing signifiers only when the widget is selected

* made changes suggested by Momcilo

* activate the dots when the fields are active

* created a new centered dot handle

* removed overlays on focus and made the border more like deisgn

* handles on top of other widgets

* hide the overlay when multiple widgets are selected

* added a white border

* added a white border

* bug #15509 resolved

* changed the minDynamicHeightLimit to 2 instead of 4 to fix the Bug #15527

* removed the height auto fix from BaseInputComponent to fix the Bug #15388

* removed the condition to not ccalculate dynamic height when the row difference is less than 2 to fix the bug 15353

* made fixes for the bug #16307

* made fixes for the bug #16308

* made fixes for bug 16310

* made fixes for the bug #16402

* removed some log statements

* made fixes for the bug #16407

* fixed label problem found in the issue #16543

* made fixes for the issue #16547

* made fixes for the bug #16492

* redeploy

* (WIP): Fix to make this branch functional

* imported LabelWithTooltip back from design system

* signifier is now centered

* filled the signifier with primary color

* overlay hidden while dragging

* made the signifier dashed border also draggable

* Fix issue #16590 (#16798)

* set the limits to 4 rows

* replaced the static 40 value

* added signifiers for modal widget

* added signifiers for modal widget

* tried solving the scroll issue for widgets when there are limits

* solved the height problem using ResizeObserver

* (WIP): Fix maxDynamicHeight issue with container widgets:

* made the changes as per the review

* fixed the issue for input widget when label gets out of border

* hide text widget overflow options if auto height is enabled

* (WIP): In view mode, invisible widgets now donot take space (#16920)

* (WIP): In view mode, invisible widgets now donot take space

* (WIP): Enable the feature where invisible widgets in view mode don't take space to all widgets irrespective of the dynamic height feature

* Remove Replay conditional

* removed the scroll container for container type widgets

* removed the scroll container for container type widgets

* updated the hook to set overflow none for text widget

* fixed the should dynamic height logic to respect the min height limit

* Modal widget adheres to dynamic height (#16995)

* Modal widget adheres to dynamic height

* WIP: POC: fix dynamic height issues  (#16996)

Fix height less than 4 issue. Fix JSONForm adherence to min and max height

* POC: Dynamic height undo redo issue (#17085)

* Revert debouce timeout

* (WIP): Fix issue with undo-redo in dynamic height

* fix: Dynamic height issue fixes (#17153)

* Dynamic height issue fixes
==

- Fix issue where nested widgets did not ensure parent dynamic height updates
- Fix issue where Modal widget updates came in subsequent renders
- Fix issue where JSONForm collapses
- Fix performance issue for independent updates

* Use functions to get min and max dynamic height

* Fix issue where variable might have been undefined

* added the dynamic container into the deploy mode as well

* added overflow-x hidden when overflow-y is active in the dynamic height container

* fix: Dynamic height Issue fixes (#17204)

Fix preview mode invisible widgets. Fix Tabs widget dynamic height.

* removed a console.log statement

* removed the slider control file

* imported the LabelWithTooltip from the repo rather than ds

* word-break CSS rules added for Switch and Checkbox widget when Dynamic Height is enabled

* abstracted the check for dynamic height with limits enabled as isDynamicHeightWithLimitsEnabledForWidget

* abstracted the static value of 10 in dynamic height overlay to GridDefaults

* abstracted min and max dynamic height limits to getters

* fix: replaced all the refs for simpler widgets (#17353)

* replaced all the refs for simpler widgets

* removed the updateDynamicHeight from componentDidUpdate in BaseWidget

* added back lifecycle methods back to BaseWidget

* removed the contentRef from SwitchGroup and Table

* updating the height from the auto height with limits as well

* some hacks to make the limits work

* working solution

* used setTimeout to send an update to updateDynamicHeight from overlay update

* removed a log

* added requestanimationframe in settimeout

Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* Fix issues caused during merge

* Remove unneeded derived property

* removed more unnecessary code which should have been removed after removing the ref dependency

* fixed the maxDynamicHeight issue

* Fix issue where property configs were not being sent

* fix: Auto Height Feature - add selectors for tests (#17687)

Add selectors for auto height cypress tests

* fix: removed height auto default theme (#17415)

removed height auto css rule from the default theme

Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>

* fix: Auto Height Feature - Resolve issues and restructure code (#17686)

* Fix issues in dynamic height. Restructure code and reduce abstraction leaks

* Fix typescript issues

* Update based on review comments. Comment migrations, as a cyclic import is causing the jest tests to fail.

* Remove unused imports

* Decrease code nesting

* added the base styles for the overlay like position and z-index in its styled component css

* used the isDynamicHeightEnabled prop to set the height of SwitchGroup and RadioGroup widgets from 32px to 100% in case of inline mode

* fix: Auto Height - Resolve issues (#17737)

* Fix Tabs Widget showTabs toggle based auto height. Revert removal of BaseWidget code. Remove box-intersect and use a bruteforce algorithm. Add base logic for having containers collapse due to hidden child widgets

* Hide scroll contents and overflow property pane controls when dynamic height is enabled

* Removed the class property expectedHeight from BaseWidget as it is not useful in the overlay logic after some changes

* fixed the left alignment issue of label in the rich text editor by adding some styles applied only when the dynamic height is enabled

* fixed the input field stretching issue in case of Dynamic height by adding some CSS styles when isDynamicHeight is true

* Fix failing modal widget cypress tests

* Fix issue with scrollContents and Tabs Widget defaulTab

* added a little bit padding of 4px to the right of scroll container of dynamic height with limit

* Add test locators for resize handles

* removed the dynamic height logic from the table widget

* fix: Auto-Height invisible widgets (#17849)

* Fix issue where invisible widgets were still taking space

* Make sure to collapse only if dynamic height is enabled

* Fix issues with reflow (not the invisible widgets)

* Fix container min height issues

* Fix reflow with original bottom and top values. Testing needed

* Fix invisible widgets

* fix: enabled dynamic height for stat box widget (#17971)

enabled dynamic height for stat box widget

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: added a min height to rich text editor so that it does not collapse (#17970)

added a min height to rich text editor so that it does not collapse

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* Fix issue with resizing auto height widget

* Add helper text to educate users regarding the scroll disconnect in WYSIWYG

* fix: Auto Height Fixes (#18111)

AUTO HEIGHT FIXES

- Fix JSONForm height discrepancy
- Fix issue where widgets moved below the other
- Fix droptarget height after parent container resize

* fix: sliced up the DynamicHeightOverlay component a little bit (#18100)

* sliced up the DynamicHeightOverlay component a little bit

* more refactoring

* more refactoring

* used release event emitter and refactored more

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: rich text editor center alignment issue (#18142)

* removed the center alignment from rich text editor

* dummy commit

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: old DSL container collapse (#18160)

* Fix issue where old containers from old DSLs used to collapse when auto height was enabled

* Fix issue where old containers don't allow new widgets to be added when auto height is enabled, this is because the shouldScrollContents is undefined

* fix: input widgets issue (#18172)

fixed the auto height not working issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: preview deploy mode (#18174)

fixed the preview and deploy mode

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits label intersection with handle dot (#18186)

fixed the position of the limits label to the right so that it will not intersect with the handle dot

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits rich text editor min height (#18187)

decrease the min height of the RTE so that it does not have the boundary issue with the max limit when auto height with limits is enabled

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: grammatical error in the help text (#18188)

changed react to reacts in the helpText of the dynamic height property in the proeprty pane

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height tabs double scroll (#18210)

solved the issue by disabling the scroll for the child canvas widget in the tabs widget

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: auto height limits resizing (#18213)

* fixed the auto height limits resizing issue

* made the auto height overlay independent of isResizing and used its own property to show the grid

* some more refactoring

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* dummy commit

* fix: old apps container issue (#18255)

filtered out the widgets which are detached from layout

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: fixing auto height in childless containers. (#18263)

fixing auto height in childless containers.

* task: Dynamic height reflow fixes in Branch (#18244)

dynamic height reflow fixes

* fix: compact label issue and min and max limits numeric input (#18282)

fixed compact label issue and turned min and max limits to numeric input

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: LabelWithTooltip help icon fix

* fix: NaN and min limit for min and max (#18284)

* fixed compact label issue and turned min and max limits to numeric input

* fixed NaN and set min to be 4

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: validation issues for min max (#18286)

* fixed compact label issue and turned min and max limits to numeric input

* fixed NaN and set min to be 4

* validations start working min max

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* added a full stop to container scroll helper text

* validations start working min max

* dummy commit

* feat: stop resizing auto height widgets vertically because of Drag n Drop Reflow (#18267)

* reflow fixes

* stop resizing auto height widgets vertically because of Drag n Drop Reflow

* feat: Analytics for Dynamic height (#18279)

* Fix canvas min height issue and invisible widgets issue and remove logs and fix issue where widgets overlapped when coming back from preview mode to edit mode

* Fix issue with containers not respecting auto height and decreasing height

* Fix issue with modal widget not hugging contents, and container widgets never become visible after going invisible

* Fix issue where existing containers don't have correct min height for child canvas

* fix: canvasLevelsReducers test (#18301)

fixed the canvasLevelsReducers test

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: removed auto height min max config from widget features (#18316)

removed auto height min max config from widget features

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: Fixing Modal Height updates (#18317)

Fixing Modal Height updates

* fix: text widget background auto height (#18319)

added background color of Text widget back to the auto height container

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* test: cypress tests for auto height (#17676)

* Added tests for dynamic height

* updated tests for another usecase

* moved locators into commonfile

* updated common method

* added tests for some more widgets

* Added tests for jsonForm / Form widget

* Updated the test

* updated test for multiple text widgets

* updated test with few more usecases

* updated the dsl

* updated tests for text change

* updated tests based on new changes

* updated cypress test fixes

* fix: auto height container merge poc wrt release (#18334)

updated the poc wrt PR already merged in the release regarding the auto height container

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: renamed auto height overlay components and added some tests (#18333)

* renamed auto height overlay components and added some tests

* replaced the 10 value with GridDefaults

* avoiding event to reach drop target

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* updated tests

* Merge all code into one branch

* Fix failing AutoHeightcontainer test

* fix: Fix reflow computations which were causing widget overlap (#18300)

* Fix reflow computations which were causing widget overlap

* Fix issues with parent container height and overlapping widgets

* Remove console logs

* Revert comment

* Fix issues related to reflow of containers

* feat: Making getEffectedBoxes a Recursive function in autoHeight Reflow (#18336)

Making getEffectedBoxes a Recursive function in autoHeight Reflow

* Return null for invisible widgets from withWidgetProps

* Remove duplicate import

Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>

* Remove missed console log

* fix: Label position gets deselected on selecting already selected option (#18298)

* fix: Label position gets deselected on selecting the already selected value

* Added migration for Currency & Phone input widgets

* simplify migration function using a utility

* combine conditions

* Increments LATEST_PAGE_VERSION

* Update DynamicHeight_Visibility_spec.js

updated a check wrt auto height

* Handling Modals for canvas size calculations

* fix: migrate label position test failing issue (#18365)

fixed migrate label postition test failing issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* removed the two unwanted imports from DSLMigrations to fix client build

* fix: Auto height zero and limits issue (#18366)

fixed the auto height zero and limits issue

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* fix: Auto height regression issues (#18367)

* Fix auto height regression issues #18367

* feat: auto height migrations (#18368)

Add auto height migrations

* Increase file caching size

* Use manual array for list of auto height enabled widgets

* Fix cypress test dsl versions

* Revert changes to shouldUpdateHeightDynamically

* Update test results based on code changes

* Marginally increase the workbox file size cache

* review comment incorporated for test spec

* Update container auto height property on drop

* added small wait for validation

Co-authored-by: Ankur Singhal <ankur@appsmith.com>
Co-authored-by: rahulramesha <rahul@appsmith.com>
Co-authored-by: Abhinav Jha <zatanna@Abhinavs-iMac.lan>
Co-authored-by: Ankur Singhal <ankursinghal@Ankurs-MacBook-Pro-2.local>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>
Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Co-authored-by: Albin <albin@appsmith.com>
Co-authored-by: Aswath K <aswath.sana@gmail.com>
Co-authored-by: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com>
Co-authored-by: Apple <nandan@thinkify.io>
2022-11-23 15:18:23 +05:30

1901 lines
56 KiB
TypeScript

import { OccupiedSpace } from "constants/CanvasEditorConstants";
import { cloneDeep, isUndefined } from "lodash";
import { Rect } from "utils/boxHelpers";
import {
CollidingSpace,
CollidingSpaceMap,
CollisionAccessors,
CollisionMap,
CollisionTree,
CollisionTreeCache,
GridProps,
MathComparators,
MovementLimitMap,
OrientationAccessors,
PrevReflowState,
ReflowDirection,
ReflowedSpace,
ReflowedSpaceMap,
SecondOrderCollisionMap,
SpaceAttributes,
SpaceMap,
SpaceMovementMap,
} from "./reflowTypes";
/**
* This method is used while calculating MovementMap of the reflowed spaces,
* if a particular space's movement is already calculated once, this method returns a boolean
* if the current calculation should replace the previous calculation
*
* @param oldMovement
* @param newMovement
* @param direction
* @returns boolean
*/
export function shouldReplaceOldMovement(
oldMovement: ReflowedSpace,
newMovement: ReflowedSpace,
direction: ReflowDirection,
) {
if (!oldMovement) return true;
const { directionIndicator, isHorizontal } = getAccessor(direction);
const distanceKey = isHorizontal ? "X" : "Y";
const dimensionKey = isHorizontal ? "width" : "height";
const oldDistance = oldMovement[distanceKey],
newDistance = newMovement[distanceKey];
const oldDimension = oldMovement[dimensionKey] || 0,
newDimension = newMovement[dimensionKey] || 0;
//if either one is undefined and other one is a number, or if both are undefined it should replace
if (
(oldDistance === undefined && newDistance !== undefined) ||
(oldDistance !== undefined && newDistance === undefined) ||
(oldDistance === undefined && newDistance === undefined)
)
return true;
if (oldDistance === undefined || newDistance === undefined) {
return false;
}
// if old movement is in the opposite direction return false
if (oldDistance > 0 !== newDistance > 0) return false;
return newDistance === oldDistance
? compareNumbers(newDimension, oldDimension, directionIndicator > 0)
: compareNumbers(newDistance, oldDistance, directionIndicator > 0);
}
/**
* for calculating all the spaces a particular space is colliding with,
* we have to resize the space through out it's movement in the direction
* for example, if a space is moved by 2 rows in the BOTTOM direction,
* then the space's bottom dimension is increased by 2 rows
*
* @param space
* @param accessors
* @returns resized Dimensions of the space
*/
export function getResizedDimensions(
space: CollidingSpace,
{
direction,
directionIndicator,
parallelMax,
parallelMin,
}: CollisionAccessors,
) {
const reflowedPosition = { ...space, children: [] };
reflowedPosition[direction] =
reflowedPosition.collidingValue +
directionIndicator *
(reflowedPosition[parallelMax] - reflowedPosition[parallelMin]);
return reflowedPosition;
}
/**
* sort the collidingSpaces with respect to the distance from the newSpacePositions
* eg, for the colliding spaces of dragging/moving spaces,
* it is sorted by distance from dragging/moving spaces
*
* @param collidingSpaces
* @param staticPosition
* @param isAscending
*/
export function sortCollidingSpacesByDistance(
collidingSpaces: CollidingSpace[],
isAscending = true,
) {
const distanceComparator = getDistanceComparator(isAscending);
collidingSpaces.sort(distanceComparator);
}
/**
* This is a comparator for colliding spaces to sort them by distance from dragging/moving spaces
*
* @param isAscending
* @returns comparator function
*/
function getDistanceComparator(isAscending = true) {
return function(spaceA: CollidingSpace, spaceB: CollidingSpace) {
const accessorA = getAccessor(spaceA.direction);
const accessorB = getAccessor(spaceB.direction);
const distanceA = Math.abs(
spaceA.collidingValue - spaceA[accessorA.oppositeDirection],
);
const distanceB = Math.abs(
spaceB.collidingValue - spaceB[accessorB.oppositeDirection],
);
return isAscending ? distanceB - distanceA : distanceA - distanceB;
};
}
/**
* Method to generate object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*
* @param existingMovementLimits
* @param spaceMovementMap
* @param delta
* @param beforeLimit
* @returns object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*/
export function getShouldReflow(
existingMovementLimits: MovementLimitMap,
spaceMovementMap: SpaceMovementMap | undefined,
delta = { X: 0, Y: 0 },
beforeLimit = false,
) {
if (!spaceMovementMap) return;
const movementKeys = Object.keys(spaceMovementMap || {});
for (const movementKey of movementKeys) {
const spaceMovements = spaceMovementMap[movementKey];
let canHorizontalMove = true,
canVerticalMove = true;
for (const movementLimit of spaceMovements) {
const {
coordinateKey,
directionalIndicator,
isHorizontal,
maxMovement,
} = movementLimit;
const canMove = compareNumbers(
delta[coordinateKey],
maxMovement,
directionalIndicator < 0,
beforeLimit,
);
if (!canMove) {
if (isHorizontal) canHorizontalMove = false;
else canVerticalMove = false;
}
}
let prevCanHorizontalMove = true,
prevCanVerticalMove = true;
if (existingMovementLimits[movementKey]) {
({
canHorizontalMove: prevCanHorizontalMove,
canVerticalMove: prevCanVerticalMove,
} = existingMovementLimits[movementKey]);
}
existingMovementLimits[movementKey] = {
canVerticalMove: canVerticalMove && prevCanVerticalMove,
canHorizontalMove: canHorizontalMove && prevCanHorizontalMove,
};
}
}
/**
* initializes canHorizontalMove and canVerticalMove as true for all moving/resizing spaces
*
* @param newSpacePositions
* @returns object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*/
export function initializeMovementLimitMap(
newSpacePositions: OccupiedSpace[],
): MovementLimitMap {
const movementLimitMap: MovementLimitMap = {};
for (const spacePosition of newSpacePositions) {
movementLimitMap[spacePosition.id] = {
canHorizontalMove: true,
canVerticalMove: true,
};
}
return movementLimitMap;
}
/**
* Should return X and Y absolute coordinates of the Dragging/moving spaces relative to the original space positions
*
* @param OGSpacePositionsMap
* @param newSpacePositionsMap
* @param direction
* @returns Object with X, Y
*/
export function getDelta(
OGSpacePositionsMap: SpaceMap,
newSpacePositionsMap: SpaceMap,
direction: ReflowDirection,
) {
const tempId = Object.keys(OGSpacePositionsMap)[0];
const OGSpacePosition = OGSpacePositionsMap[tempId];
const newSpacePosition = newSpacePositionsMap[tempId];
let X = OGSpacePosition.left - newSpacePosition.left,
Y = OGSpacePosition.top - newSpacePosition.top;
if (direction.indexOf("|") > 0) {
const [verticalDirection, horizontalDirection] = direction.split("|");
const { direction: xDirection } = getAccessor(
horizontalDirection as ReflowDirection,
);
const { direction: yDirection } = getAccessor(
verticalDirection as ReflowDirection,
);
X = OGSpacePosition[xDirection] - newSpacePosition[xDirection];
Y = OGSpacePosition[yDirection] - newSpacePosition[yDirection];
return { X, Y };
}
const { direction: directionalAccessor, isHorizontal } = getAccessor(
direction,
);
const diff =
OGSpacePosition[directionalAccessor] -
newSpacePosition[directionalAccessor];
if (isHorizontal) X = diff;
else Y = diff;
return { X, Y };
}
/**
* This method checks if the occupied spaces are overlapping with newSpacePositions,
* and generates a map with the overlapping spaces with the direction of collision
*
* @param newSpacePositions Dragging/Moving Spaces
* @param occupiedSpaces
* @param direction
* @param prevCollidingSpaceMap
* @param isHorizontalMove
* @param prevSpacesMap
* @param forceDirection
* @param primaryCollisionMap
* @returns collision spaces Map
*/
export function getCollidingSpaceMap(
newSpacePositions: OccupiedSpace[],
occupiedSpaces: OccupiedSpace[],
direction: ReflowDirection,
prevCollidingSpaceMap: CollidingSpaceMap,
isHorizontalMove?: boolean,
prevSpacesMap?: SpaceMap,
forceDirection = false,
primaryCollisionMap?: CollisionMap,
) {
let isColliding = false;
const collidingSpaceMap: CollisionMap = {};
let order = 1;
const orientationalAccessor = getOrientationAccessor(isHorizontalMove);
const oppositeOrientationalAccessor = getOrientationAccessor(
!isHorizontalMove,
);
for (const newSpacePosition of newSpacePositions) {
for (const occupiedSpace of occupiedSpaces) {
if (areOverlapping(occupiedSpace, newSpacePosition)) {
isColliding = true;
const currentSpaceId = occupiedSpace.id;
//sometimes direction mentioned cannot be trusted a direction is intelligently calculated
let movementDirection = getCorrectedDirection(
occupiedSpace,
prevSpacesMap && prevSpacesMap[newSpacePosition.id]
? prevSpacesMap[newSpacePosition.id]
: undefined,
direction,
forceDirection,
prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor],
isHorizontalMove,
);
// if in case this is a second run of the getMovementMap,
//direction should be same as first or primary run.
if (
primaryCollisionMap &&
primaryCollisionMap[occupiedSpace.id] &&
primaryCollisionMap[occupiedSpace.id].collidingId ===
newSpacePosition.id
) {
movementDirection = primaryCollisionMap[occupiedSpace.id].direction;
}
// if incase of the previous run of the entire reflow algorithm, then even though it might be in
//the opposite orientation of current, we should still consider the previous direction
if (
prevCollidingSpaceMap &&
prevCollidingSpaceMap[oppositeOrientationalAccessor] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][
occupiedSpace.id
] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][occupiedSpace.id]
.collidingId === newSpacePosition.id
) {
movementDirection =
prevCollidingSpaceMap[oppositeOrientationalAccessor][
occupiedSpace.id
].direction;
}
const {
direction: directionAccessor,
directionIndicator,
isHorizontal,
} = getAccessor(movementDirection);
if (isHorizontal !== isHorizontalMove) continue;
// If this particular space is already colliding with another dragging space,
// then the highest in the particular direction will override the lowest value
const currentCollidingSpace = collidingSpaceMap[currentSpaceId];
if (
!currentCollidingSpace ||
(currentCollidingSpace &&
compareNumbers(
newSpacePosition[directionAccessor],
currentCollidingSpace.collidingValue,
directionIndicator > 0,
))
) {
collidingSpaceMap[currentSpaceId] = {
...occupiedSpace,
direction: movementDirection,
collidingValue: newSpacePosition[directionAccessor],
collidingId: newSpacePosition.id,
isHorizontal,
order,
};
order++;
}
}
}
}
return {
isColliding,
collidingSpaceMap,
};
}
/**
* This method is used while generating a collision tree,
* This checks if the newSpacePosition is overlapping with occupiedSpaces
* and creates a array of those spaces with the direction of collision,
* This is usually the direction passed down but with some exceptions
* based on prevReflowState and isDirectCollidingSpace, it can be different
*
* @param newSpacePosition
* @param OGPosition
* @param globalDirection
* @param direction
* @param gridProps
* @param prevReflowState
* @param globalCollisionMap
* @param occupiedSpaces
* @param occupiedSpaces
* @param isDirectCollidingSpace
* @returns Colliding Spaces Array
*/
export function getCollidingSpacesInDirection(
newSpacePosition: CollidingSpace,
OGPosition: OccupiedSpace,
globalDirection: ReflowDirection,
direction: ReflowDirection,
gridProps: GridProps,
prevReflowState: PrevReflowState,
globalCollisionMap: CollisionMap,
occupiedSpaces?: OccupiedSpace[],
isDirectCollidingSpace = false,
) {
const collidingSpaces: CollidingSpace[] = [];
const occupiedSpacesInDirection = filterSpaceByDirection(
newSpacePosition,
occupiedSpaces,
direction,
);
const currentOccupiedSpaces = filterSpaceByDirection(
newSpacePosition,
occupiedSpaces,
getOppositeDirection(direction),
);
const accessor = getAccessor(direction);
const { prevMovementMap, prevSecondOrderCollisionMap } = prevReflowState;
let order = 1;
for (const occupiedSpace of currentOccupiedSpaces) {
// determines if the space acn be added to the list of colliding spaces, if so in what direction
const {
changedDirection,
collidingValue,
isHorizontal,
shouldAddToArray,
} = ShouldAddToCollisionSpacesArray(
newSpacePosition,
OGPosition,
occupiedSpace,
direction,
accessor,
isDirectCollidingSpace,
gridProps,
globalDirection,
prevMovementMap,
prevSecondOrderCollisionMap,
);
let currentDirection = direction,
currentCollidingValue = newSpacePosition[accessor.direction],
isCurrentHorizontal = accessor.isHorizontal;
if (collidingValue !== undefined && changedDirection) {
currentDirection = changedDirection;
currentCollidingValue = collidingValue;
isCurrentHorizontal = !!isHorizontal;
}
const currentAccessor = getAccessor(currentDirection);
const collidingSpace = globalCollisionMap[occupiedSpace.id];
// If the space already collides with a moving/dragging space and it's collisionValue is farther than current's Then don't add.
if (
collidingSpace &&
collidingSpace.direction === currentDirection &&
compareNumbers(
collidingSpace.collidingValue,
currentCollidingValue,
currentAccessor.directionIndicator > 0,
)
)
continue;
if (shouldAddToArray) {
collidingSpaces.push({
...occupiedSpace,
direction: currentDirection,
collidingValue: currentCollidingValue,
collidingId: newSpacePosition.id,
isHorizontal: isCurrentHorizontal,
order,
});
order++;
}
}
return {
collidingSpaces,
occupiedSpacesInDirection,
skipCollisionTree: false,
};
}
/**
* Checks if the collidingSpace is overlapping with newSpacePosition
* and check if collidingSpace should be added to the collidingSpaceArray
* and return and object with boolean and other variables if there is a change in direction
*
* @param newSpacePosition
* @param OGPosition
* @param collidingSpace
* @param direction
* @param accessor
* @param isDirectCollidingSpace
* @param gridProps
* @param globalDirection
* @param prevMovementMap
* @param prevSecondOrderCollisionMap
* @returns object with boolean if the occupied space is to be added to array, also mentions if there is a change in direction
*/
export function ShouldAddToCollisionSpacesArray(
newSpacePosition: CollidingSpace,
OGPosition: OccupiedSpace,
collidingSpace: OccupiedSpace,
direction: ReflowDirection,
accessor: CollisionAccessors,
isDirectCollidingSpace: boolean,
gridProps: GridProps,
globalDirection: ReflowDirection,
prevMovementMap?: ReflowedSpaceMap,
prevSecondOrderCollisionMap?: SecondOrderCollisionMap,
) {
// not intersecting then dont add to array
if (!areOverlapping(collidingSpace, newSpacePosition))
return { shouldAddToArray: false };
const {
direction: directionAccessor,
directionIndicator,
isHorizontal,
oppositeDirection,
parallelMax,
parallelMin,
} = accessor;
const prevCollisionMap: {
children: {
[key: string]: any;
};
} = (prevSecondOrderCollisionMap &&
prevSecondOrderCollisionMap[newSpacePosition.id]) || {
children: {},
};
// if these two spaces previously collided then it calculates how it collided
if (prevCollisionMap.children[collidingSpace.id]) {
return getCollisionStatusBasedOnPrevValue(
newSpacePosition,
collidingSpace,
direction,
accessor,
gridProps,
prevCollisionMap,
prevMovementMap,
);
}
const movementDirectionAccessor = accessor.isHorizontal
? "directionX"
: "directionY";
// if it collided in the previous run in this particular orientation the previous direction is considered
if (
prevMovementMap &&
prevMovementMap[collidingSpace.id] &&
prevMovementMap[collidingSpace.id][movementDirectionAccessor]
) {
const prevDirection =
prevMovementMap[collidingSpace.id][movementDirectionAccessor];
const shouldAddToArray = prevDirection === direction;
return { shouldAddToArray };
}
const reflowedSpacePosition = {
...newSpacePosition,
[accessor.oppositeDirection]: newSpacePosition.collidingValue,
};
// if it does not collide in it's current reflowed position then it should not be added to the branch
if (
!isDirectCollidingSpace &&
globalDirection !== direction &&
!areOverlapping(reflowedSpacePosition, collidingSpace)
)
return { shouldAddToArray: false };
// if this particular space does not collide directly with dragging spaces then can directly be added to list
if (!isDirectCollidingSpace) return { shouldAddToArray: true };
// next to condition return true if they dont have enough data to move forward because it is the first time reflow algorithm is run
if (!prevSecondOrderCollisionMap) return { shouldAddToArray: true };
if (!prevMovementMap || !prevMovementMap[newSpacePosition.id])
return { shouldAddToArray: true };
const { [OGPosition.id]: prevStaticSpace } = getModifiedOccupiedSpacesMap(
{ [OGPosition.id]: { ...OGPosition } },
prevMovementMap,
!isHorizontal,
gridProps,
parallelMax,
parallelMin,
);
//determines if should be added by comparing previous value to now,
// for example if previous bottom of dragging space is lesser than top of colliding space,
//then it should collide in the bottom direction
const shouldAddToArray = compareNumbers(
collidingSpace[oppositeDirection],
prevStaticSpace[directionAccessor],
directionIndicator > 0,
true,
);
const currentStaticSpace = {
...newSpacePosition,
[oppositeDirection]: newSpacePosition.collidingValue,
};
const correctedDirection = getCorrectedDirection(
collidingSpace,
prevStaticSpace,
globalDirection,
);
const correctedAccessor = getAccessor(correctedDirection);
// if this cant be added in mentioned direction but still intersects, then we determine the correct direction
// but if it says the currentDirection is same as old one, it should not add
if (
!shouldAddToArray &&
areOverlapping(collidingSpace, currentStaticSpace) &&
correctedDirection !== direction &&
correctedAccessor.isHorizontal !== isHorizontal
) {
let collidingValue = newSpacePosition.collidingValue;
if (isHorizontal !== correctedAccessor.isHorizontal) {
collidingValue = currentStaticSpace[correctedAccessor.direction];
}
return {
shouldAddToArray: true,
changedDirection: correctedDirection,
collidingValue: collidingValue,
isHorizontal: correctedAccessor.isHorizontal,
};
}
return { shouldAddToArray };
}
/**
* method to filter occupiedSpaces to be after a newSpacePosition in direction
* eg, if the direction is BOTTOM, this methods returns all the occupiedSpaces below the newSpacePosition
*
* @param newSpacePosition
* @param occupiedSpaces
* @param direction
* @returns filtered array of occupied space
*/
export function filterSpaceByDirection(
newSpacePosition: OccupiedSpace,
occupiedSpaces: OccupiedSpace[] | undefined,
direction: ReflowDirection,
): OccupiedSpace[] {
let filteredSpaces: OccupiedSpace[] = [];
const {
direction: directionAccessor,
directionIndicator,
oppositeDirection,
} = getAccessor(direction);
if (occupiedSpaces) {
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
if (
occupiedSpace.id === newSpacePosition.id ||
occupiedSpace.parentId === newSpacePosition.id
) {
return false;
}
return compareNumbers(
occupiedSpace[directionAccessor],
newSpacePosition[oppositeDirection],
directionIndicator > 0,
);
});
}
return filteredSpaces;
}
/**
* filters out occupiedSpaces, and returns array without a space with id
*
* @param id
* @param occupiedSpaces
* @returns filtered occupied spaces
*/
export function filterSpaceById(
id: string,
occupiedSpaces: OccupiedSpace[] | undefined,
): OccupiedSpace[] {
let filteredSpaces: OccupiedSpace[] = [];
if (occupiedSpaces) {
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
return occupiedSpace.id !== id && occupiedSpace.parentId !== id;
});
}
return filteredSpaces;
}
/**
* filters out occupiedSpaceMap and removes spaces with ids of newSpacePositionsMap
*
* @param newSpacePositionsMap
* @param occupiedSpaceMap
* @mutates occupiedSpaceMap
*/
export function filterCommonSpaces(
newSpacePositionsMap: { [key: string]: any },
occupiedSpaceMap: SpaceMap,
) {
const keysToFilter = Object.keys(newSpacePositionsMap);
for (const key of keysToFilter) {
if (occupiedSpaceMap[key]) {
delete occupiedSpaceMap[key];
}
}
}
/**
* easily the most important method in the algorithm
*
* @param r1 space1
* @param r2 space 2
* @returns boolean if it is colliding
*/
function areOverlapping(r1: Rect, r2: Rect) {
return !(
r2.left >= r1.right ||
r2.right <= r1.left ||
r2.top >= r1.bottom ||
r2.bottom <= r1.top
);
}
/**
* This method checks if collidingSpace can collide
* with the moving/dragging spaces in a particular direction
* if they cant collide in that direction a direction is determined based on prevPositions
*
* @param collidingSpace
* @param prevPositions
* @param direction
* @param forceDirection
* @param prevCollidingSpaces
* @param isHorizontalMove
* @returns direction
*/
function getCorrectedDirection(
collidingSpace: OccupiedSpace,
prevPositions: OccupiedSpace | undefined,
direction: ReflowDirection,
forceDirection = false,
prevCollidingSpaces?: CollisionMap,
isHorizontalMove?: boolean,
): ReflowDirection {
if (forceDirection) return direction;
// if previously collided in that direction then it should be that direction
if (prevCollidingSpaces && prevCollidingSpaces[collidingSpace.id]) {
return prevCollidingSpaces[collidingSpace.id].direction;
}
let primaryDirection: ReflowDirection = direction,
secondaryDirection: ReflowDirection | undefined = undefined;
// this is for composite directions for resizing while dragging the corner handles
if (direction.indexOf("|") >= 0) {
const directions = direction.split("|");
if (isHorizontalMove) {
primaryDirection = directions[1] as ReflowDirection;
secondaryDirection = directions[0] as ReflowDirection;
} else {
primaryDirection = directions[0] as ReflowDirection;
secondaryDirection = directions[1] as ReflowDirection;
}
}
// if first run the return direction
if (!prevPositions) return primaryDirection;
const primaryAccessors = getAccessor(primaryDirection);
// check if they can collide based on previous location of the dragging space
const isCorrectDirection = compareNumbers(
collidingSpace[primaryAccessors.oppositeDirection],
prevPositions[primaryAccessors.direction],
primaryAccessors.directionIndicator > 0,
true,
);
if (isCorrectDirection) {
return primaryDirection;
} else if (secondaryDirection) {
return secondaryDirection;
}
//if all the conditions doesn't match then we have to determine manually
return getVerifiedDirection(
collidingSpace,
prevPositions,
primaryDirection,
primaryAccessors.isHorizontal,
);
}
/**
* if spaces cannot possibly collide in certain direction,
* this method provides the direction it is most likely to collide in
* This is determined by checking the previous positions of the dragging/moving spaces
* with the dimension of collidingSpace
*
* @param collidingSpace
* @param prevPositions
* @param direction
* @param isHorizontalMove
* @returns direction
*/
function getVerifiedDirection(
collidingSpace: OccupiedSpace,
prevPositions: OccupiedSpace,
direction: ReflowDirection,
isHorizontalMove: boolean,
) {
// determines direction by comparing if it can collide in all the directions
if (isHorizontalMove) {
if (collidingSpace.bottom <= prevPositions.top) {
return ReflowDirection.TOP;
} else if (collidingSpace.top >= prevPositions.bottom) {
return ReflowDirection.BOTTOM;
} else if (
direction !== ReflowDirection.RIGHT &&
collidingSpace.left >= prevPositions.right
) {
return ReflowDirection.RIGHT;
} else if (
direction !== ReflowDirection.LEFT &&
collidingSpace.right <= prevPositions.left
) {
return ReflowDirection.LEFT;
}
} else {
if (collidingSpace.right <= prevPositions.left) {
return ReflowDirection.LEFT;
} else if (collidingSpace.left >= prevPositions.right) {
return ReflowDirection.RIGHT;
} else if (
direction !== ReflowDirection.TOP &&
collidingSpace.bottom <= prevPositions.top
) {
return ReflowDirection.TOP;
} else if (
direction !== ReflowDirection.BOTTOM &&
collidingSpace.top >= prevPositions.bottom
) {
return ReflowDirection.BOTTOM;
}
}
return direction;
}
/**
* compares numbers and returns boolean
* @param numberA
* @param numberB
* @param isGreaterThan
* @param isEqual
* @returns boolean
*/
export function compareNumbers(
numberA: number,
numberB: number,
isGreaterThan: boolean,
isEqual = false,
): boolean {
if (isGreaterThan) {
if (isEqual) {
return numberA >= numberB;
}
return numberA > numberB;
}
if (isEqual) {
return numberA <= numberB;
}
return numberA < numberB;
}
/**
* gets opposite direction
* @param direction
* @returns ReflowDirection
*/
export function getOppositeDirection(
direction: ReflowDirection,
): ReflowDirection {
const directionalAccessors = getAccessor(direction);
return directionalAccessors.oppositeDirection.toUpperCase() as ReflowDirection;
}
/**
* Accessors are used to access space's dimension based on direction
* These are string accessors to get the dimension of the space in a direction
*
* @param direction
* @returns accessors
*/
export function getAccessor(direction: ReflowDirection): CollisionAccessors {
switch (direction) {
case ReflowDirection.LEFT:
return {
direction: SpaceAttributes.left,
oppositeDirection: SpaceAttributes.right,
perpendicularMax: SpaceAttributes.bottom,
perpendicularMin: SpaceAttributes.top,
parallelMax: SpaceAttributes.right,
parallelMin: SpaceAttributes.left,
mathComparator: MathComparators.max,
directionIndicator: -1,
isHorizontal: true,
plane: "horizontal",
};
case ReflowDirection.RIGHT:
return {
direction: SpaceAttributes.right,
oppositeDirection: SpaceAttributes.left,
perpendicularMax: SpaceAttributes.bottom,
perpendicularMin: SpaceAttributes.top,
parallelMax: SpaceAttributes.right,
parallelMin: SpaceAttributes.left,
mathComparator: MathComparators.min,
directionIndicator: 1,
isHorizontal: true,
plane: "horizontal",
};
case ReflowDirection.TOP:
return {
direction: SpaceAttributes.top,
oppositeDirection: SpaceAttributes.bottom,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.max,
directionIndicator: -1,
isHorizontal: false,
plane: "vertical",
};
case ReflowDirection.BOTTOM:
return {
direction: SpaceAttributes.bottom,
oppositeDirection: SpaceAttributes.top,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.min,
directionIndicator: 1,
isHorizontal: false,
plane: "vertical",
};
}
return {
direction: SpaceAttributes.bottom,
oppositeDirection: SpaceAttributes.top,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.min,
directionIndicator: 1,
isHorizontal: false,
plane: "vertical",
};
}
/**
* get Max X coordinate of the the space,
* MaxX is the maximum a reflowed space can move in the X axis before it should start to resize,
*
* @param collisionTree
* @param gridProps
* @param direction
* @param occupiedLength
* @param maxOccupiedSpace
* @param shouldResize
* @returns number
*/
export function getMaxX(
collisionTree: CollisionTree,
gridProps: GridProps,
direction: ReflowDirection,
occupiedLength: number,
maxOccupiedSpace: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
const movementLimit = shouldResize ? occupiedLength : maxOccupiedSpace;
let maxX = collisionTree[accessors.direction] - movementLimit;
if (direction === ReflowDirection.RIGHT) {
maxX =
gridProps.maxGridColumns -
collisionTree[accessors.direction] -
movementLimit;
}
return accessors.directionIndicator * maxX * gridProps.parentColumnSpace;
}
/**
* get Max Y coordinate of the the space
* MaxY is the maximum a reflowed space can move in the Y axis before it should start to resize,
*
* @param collisionTree
* @param gridProps
* @param direction
* @param occupiedLength
* @param maxOccupiedSpace
* @param shouldResize
* @returns number
*/
export function getMaxY(
collisionTree: CollisionTree,
gridProps: GridProps,
direction: ReflowDirection,
occupiedLength: number,
maxOccupiedSpace: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
const movementLimit = shouldResize ? occupiedLength : maxOccupiedSpace;
let maxY =
(collisionTree[accessors.direction] - movementLimit) *
gridProps.parentRowSpace;
if (direction === ReflowDirection.BOTTOM) {
maxY = Infinity;
}
return accessors.directionIndicator * maxY;
}
/**
* calculates the reflowed distance i.e, X or Y of the reflowed space
* this distance indicates the absolute value by which the reflowed space has to move rom it's original position
*
* @param collisionTree
* @param direction
* @param maxDistance
* @param distanceBeforeCollision
* @param actualDimension
* @param emptySpaces
* @param snapGridSpace
* @param expandableCanvas
* @returns distance in number
*/
export function getReflowDistance(
collisionTree: CollisionTree,
direction: ReflowDirection,
maxDistance: number,
distanceBeforeCollision: number,
actualDimension: number,
emptySpaces: number,
snapGridSpace: number,
expandableCanvas = false,
) {
const accessors = getAccessor(direction);
const originalDimension =
(collisionTree[accessors.parallelMax] -
collisionTree[accessors.parallelMin]) *
snapGridSpace;
const value =
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
snapGridSpace *
-1;
const maxValue = Math[accessors.mathComparator](value, maxDistance);
if (expandableCanvas) {
return maxValue;
}
return accessors.directionIndicator < 0
? maxValue
: maxValue + originalDimension - actualDimension;
}
/**
* returns the reflowed dimension (width or height) of the reflowed space
* It returns the original dimension if it has not reached the canvas borders
*
* @param collisionTree
* @param direction
* @param travelDistance
* @param maxDistance
* @param distanceBeforeCollision
* @param snapGridSpace
* @param emptySpaces
* @param minDimension
* @param shouldResize
* @returns resized width or height of space
*/
export function getReflowedDimension(
collisionTree: CollisionTree,
direction: ReflowDirection,
travelDistance: number,
maxDistance: number,
distanceBeforeCollision: number,
snapGridSpace: number,
emptySpaces: number,
minDimension: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
if (direction === ReflowDirection.TOP && collisionTree.fixedHeight)
return collisionTree.fixedHeight * snapGridSpace;
const currentDistanceBeforeCollision =
travelDistance +
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
snapGridSpace;
const originalDimension =
collisionTree[accessors.parallelMax] - collisionTree[accessors.parallelMin];
if (!shouldResize) {
return originalDimension * snapGridSpace;
}
const resizeThreshold = maxDistance + currentDistanceBeforeCollision;
const resizeLimit =
resizeThreshold +
(originalDimension - minDimension) *
snapGridSpace *
accessors.directionIndicator;
let shrink = 0;
const canResize = compareNumbers(
travelDistance,
resizeThreshold,
accessors.directionIndicator > 0,
true,
);
if (canResize) {
shrink = Math[accessors.mathComparator](travelDistance, resizeLimit);
shrink = shrink - resizeThreshold;
}
return originalDimension * snapGridSpace - Math.abs(shrink);
}
/**
* check the limits of each movement map
* and replace with previous run's movement values if it has already reached the limit
*
* @param movementMap
* @param prevMovementMap
* @param movementLimit
* @returns
*/
export function getLimitedMovementMap(
movementMap: ReflowedSpaceMap | undefined,
prevMovementMap: ReflowedSpaceMap,
movementLimit: { canVerticalMove: boolean; canHorizontalMove: boolean },
): ReflowedSpaceMap {
if (!movementMap) return {};
const { canHorizontalMove, canVerticalMove } = movementLimit;
if (!canVerticalMove && !canHorizontalMove) {
return prevMovementMap;
}
if (!canVerticalMove) {
return replaceMovementMapByDirection(movementMap, prevMovementMap, false);
}
if (!canHorizontalMove) {
return replaceMovementMapByDirection(movementMap, prevMovementMap, true);
}
return movementMap;
}
/**
* replace movement of the reflowed space with previous run's movement of the reflowed space
*
* @param movementMap
* @param prevMovementMap
* @param replaceHorizontal
* @returns
*/
function replaceMovementMapByDirection(
movementMap: ReflowedSpaceMap,
prevMovementMap: ReflowedSpaceMap,
replaceHorizontal: boolean,
): ReflowedSpaceMap {
const checkKey = replaceHorizontal ? "X" : "Y";
const currentMovementMap = { ...movementMap };
const movementMapIds = Object.keys(movementMap);
for (const spaceId of movementMapIds) {
if (currentMovementMap[spaceId][checkKey] !== undefined) {
delete currentMovementMap[spaceId];
if (prevMovementMap[spaceId]) {
currentMovementMap[spaceId] = { ...prevMovementMap[spaceId] };
}
}
}
return currentMovementMap;
}
/**
* on Container exit, the exited container and the widgets behind it should reflow in opposite direction,
* So this method checks if that it the case and sets it in the opposite direction
*
* @param collidingSpaceMap
* @param exitContainerId
* @param direction
* changes reference of collidingSpaceMap
*/
export function changeExitContainerDirection(
collidingSpaceMap: CollisionMap,
exitContainerId: string | undefined,
direction: ReflowDirection,
) {
if (!exitContainerId || !collidingSpaceMap[exitContainerId]) {
return;
}
const oppDirection = getOppositeDirection(direction);
const { directionIndicator, oppositeDirection } = getAccessor(oppDirection);
const collidingSpaces: CollidingSpace[] = Object.values(collidingSpaceMap);
const oppositeFrom = collidingSpaceMap[exitContainerId][oppositeDirection];
const oppositeSpaceIds = collidingSpaces
.filter((collidingSpace: CollidingSpace) => {
return compareNumbers(
collidingSpace[oppositeDirection],
oppositeFrom,
directionIndicator > 0,
true,
);
})
.map((collidingSpace: CollidingSpace) => collidingSpace.id);
for (const spaceId of oppositeSpaceIds) {
collidingSpaceMap[spaceId].direction = oppDirection;
}
}
/**
* Convert an array of spaces to map of spaces
*
* @param spacesArray
* @returns space map
*/
export function getSpacesMapFromArray(
spacesArray: OccupiedSpace[] | undefined,
) {
if (!spacesArray) return {};
const spacesMap: SpaceMap = {};
for (const space of spacesArray) {
spacesMap[space.id] = space;
}
return spacesMap;
}
/**
* build a collision array to Collision map structure
*
* @param spacesArray
* @returns space map
*/
export function buildArrayToCollisionMap(
collidingSpaces: CollidingSpace[] | undefined,
) {
if (!collidingSpaces) return {};
const collidingSpaceMap: CollisionMap = {};
let order = 1;
for (const collidingSpace of collidingSpaces) {
const { directionIndicator } = getAccessor(collidingSpace.direction);
const prevCollidingSpace = collidingSpaceMap[collidingSpace.id];
if (
!prevCollidingSpace ||
(prevCollidingSpace &&
prevCollidingSpace.direction === collidingSpace.direction &&
compareNumbers(
collidingSpace.collidingValue,
prevCollidingSpace.collidingValue,
directionIndicator > 0,
))
) {
collidingSpaceMap[collidingSpace.id] = { ...collidingSpace, order };
order++;
}
}
return collidingSpaceMap;
}
/**
* move the occupied spaces to previous opposite orientation run's position only in a particular orientation
* eg, if the current orientation is horizontal, then the occupiedSpacesMap's top and bottom positions are modified
* to match previously reflowed values
*
* @param occupiedSpacesMap
* @param prevMovementMap
* @param isHorizontal
* @param gridProps
* @param directionMax
* @param directionMin
* @returns modified occupied space map
*/
export function getModifiedOccupiedSpacesMap(
occupiedSpacesMap: SpaceMap,
prevMovementMap: ReflowedSpaceMap | undefined,
isHorizontal: boolean,
gridProps: GridProps,
directionMax: SpaceAttributes,
directionMin: SpaceAttributes,
) {
if (!prevMovementMap) return cloneDeep(occupiedSpacesMap);
const spaceKeys = Object.keys(occupiedSpacesMap);
const directionalOccupiedSpacesMap: SpaceMap = {};
const displaceMentAccessor = isHorizontal ? "Y" : "X";
const dimensionAccessor = isHorizontal ? "height" : "width";
const gridGap = isHorizontal
? gridProps.parentRowSpace
: gridProps.parentColumnSpace;
for (const key of spaceKeys) {
const movement =
(prevMovementMap[key] && prevMovementMap[key][displaceMentAccessor]) || 0;
const dimension =
prevMovementMap[key] && prevMovementMap[key][dimensionAccessor];
const currentSpace = occupiedSpacesMap[key];
directionalOccupiedSpacesMap[key] = {
...currentSpace,
[directionMin]:
currentSpace[directionMin] + Math.round(movement / gridGap),
[directionMax]: dimension
? currentSpace[directionMin] +
Math.round((movement + dimension) / gridGap)
: currentSpace[directionMax] + Math.round(movement / gridGap),
};
}
return directionalOccupiedSpacesMap;
}
/**
* Modifies a single CollidingSpace to previous opposite orientation run's position only in a particular orientation
* eg, if the current orientation is horizontal, then the collidingSpace's top and bottom positions are modified
* to match previously reflowed values
*
* @param collidingSpace
* @param OGOccupiedSpacesMap
* @param prevMovementMap
* @param isHorizontal
* @param gridProps
* @param directionMax
* @param directionMin
* @returns modified collidingSpace positions
*/
export function getModifiedCollidingSpace(
collidingSpace: CollidingSpace,
OGOccupiedSpacesMap: SpaceMap,
prevMovementMap: ReflowedSpaceMap | undefined,
isHorizontal: boolean,
gridProps: GridProps,
directionMax: SpaceAttributes,
directionMin: SpaceAttributes,
) {
if (!prevMovementMap) return { ...collidingSpace };
const displaceMentAccessor = isHorizontal ? "Y" : "X";
const dimensionAccessor = isHorizontal ? "height" : "width";
const gridGap = isHorizontal
? gridProps.parentRowSpace
: gridProps.parentColumnSpace;
const spaceId = collidingSpace.id;
const OGCollidingSpacePosition = OGOccupiedSpacesMap[spaceId];
const movement =
(prevMovementMap[spaceId] &&
prevMovementMap[spaceId][displaceMentAccessor]) ||
0;
const dimension =
prevMovementMap[spaceId] && prevMovementMap[spaceId][dimensionAccessor];
const currentCollidingSpace = {
...collidingSpace,
[directionMin]:
OGCollidingSpacePosition[directionMin] + Math.round(movement / gridGap),
[directionMax]: dimension
? OGCollidingSpacePosition[directionMin] +
Math.round((movement + dimension) / gridGap)
: OGCollidingSpacePosition[directionMax] + Math.round(movement / gridGap),
};
return currentCollidingSpace;
}
/**
* Check if the new CollidingSpaces are colliding with the new Space positions,
* if so it is to be added to the Collision map of that new Space position's colliding map
*
* @param collidingSpace
* @param OGCollidingSpacePosition
* @param globalDirection
* @param direction
* @param newSpacePositions
* @param globalCollidingSpaces
* @param insertionIndex
* @param globalProcessedNodes
* @param collidingSpaceMap
* @param prevReflowState
* @param isSecondRun boolean to indicate if it is being run for the second time
* @returns boolean to stop moving any further
*/
export function checkReCollisionWithOtherNewSpacePositions(
collidingSpace: CollidingSpace,
OGCollidingSpacePosition: OccupiedSpace,
globalDirection: ReflowDirection,
direction: ReflowDirection,
newSpacePositions: OccupiedSpace[],
globalCollidingSpaces: CollidingSpace[],
insertionIndex: number,
globalProcessedNodes: CollisionTreeCache,
collidingSpaceMap: CollisionMap,
prevReflowState: PrevReflowState,
isSecondRun: boolean,
): boolean {
const accessor = getAccessor(direction);
const { isHorizontal: globalIsHorizontal } = getAccessor(globalDirection);
const { prevCollidingSpaceMap, prevSpacesMap } = prevReflowState;
const orientationalAccessor = getOrientationAccessor(globalIsHorizontal);
const oppositeOrientationalAccessor = getOrientationAccessor(
!globalIsHorizontal,
);
let stopCollisionCheck = false;
for (const newSpacePosition of newSpacePositions) {
if (areOverlapping(newSpacePosition, collidingSpace)) {
//If it is already colliding directly with a moving/Dragging space then no need to check
if (
collidingSpaceMap[collidingSpace.id] &&
collidingSpaceMap[collidingSpace.id].collidingId === newSpacePosition.id
)
continue;
let currentDirection = getCorrectedDirection(
collidingSpace,
prevSpacesMap && prevSpacesMap[newSpacePosition.id],
globalDirection,
false,
prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor],
globalIsHorizontal,
);
//this is to check if the same two widgets collide again it should be in the same direction even if they have opposite orientation
if (
prevCollidingSpaceMap &&
prevCollidingSpaceMap[oppositeOrientationalAccessor] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][
collidingSpace.id
] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][collidingSpace.id]
.collidingId === newSpacePosition.id
) {
currentDirection =
prevCollidingSpaceMap[oppositeOrientationalAccessor][
collidingSpace.id
].direction;
}
//if this is being run for the second orientation, no matter what direction is predicted,
//it has to collide in the passed direction
if (isSecondRun) currentDirection = direction;
const currentAccessors = getAccessor(currentDirection);
const currentCollidingSpace: CollidingSpace = {
...OGCollidingSpacePosition,
direction: currentDirection,
collidingId: newSpacePosition.id,
collidingValue: newSpacePosition[currentAccessors.direction],
isHorizontal: currentAccessors.isHorizontal,
order: 0,
};
if (
currentDirection === direction &&
compareNumbers(
currentCollidingSpace.collidingValue,
collidingSpace.collidingValue,
accessor.directionIndicator > 0,
)
) {
stopCollisionCheck = true;
collidingSpaceMap[collidingSpace.id] = currentCollidingSpace;
globalCollidingSpaces.splice(
insertionIndex + 1,
0,
currentCollidingSpace,
);
delete globalProcessedNodes[collidingSpace.id];
}
}
}
return stopCollisionCheck;
}
/**
* If exact same spaces collide with each other again in the current run and previous run
* calculate the direction they collide in
*
* @param staticSpace
* @param collidingSpace
* @param direction
* @param accessor
* @param gridProps
* @param prevCollisionMap
* @param prevMovementMap
* @returns
*/
function getCollisionStatusBasedOnPrevValue(
staticSpace: CollidingSpace,
collidingSpace: OccupiedSpace,
direction: ReflowDirection,
accessor: CollisionAccessors,
gridProps: GridProps,
prevCollisionMap: {
children: {
[key: string]: any;
};
},
prevMovementMap: ReflowedSpaceMap | undefined,
): {
shouldAddToArray: boolean;
changedDirection?: ReflowDirection;
collidingValue?: number;
isHorizontal?: boolean;
} {
const prevCollisionSpace = prevCollisionMap.children[collidingSpace.id];
const reflowedSpacePosition = {
...staticSpace,
[accessor.oppositeDirection]: staticSpace.collidingValue,
};
// If it previously as well collided in the current direction, then add to the Colliding spaces List
if (prevCollisionSpace.direction === direction) {
return { shouldAddToArray: true };
} //if it is previously collided in the same orientation but current colliding value is lesser the dont add
else if (
prevCollisionSpace.isHorizontal === accessor.isHorizontal &&
compareNumbers(
staticSpace.collidingValue,
collidingSpace[accessor.direction],
accessor.directionIndicator > 0,
)
) {
return {
shouldAddToArray: false,
};
} //if it is previously collided in the same orientation but current colliding value is greater then add in the previous direction
else if (prevCollisionSpace.isHorizontal === accessor.isHorizontal) {
return {
shouldAddToArray: true,
changedDirection: prevCollisionSpace.direction,
collidingValue: staticSpace.collidingValue,
isHorizontal: prevCollisionSpace.isHorizontal,
};
} else if (!areOverlapping(reflowedSpacePosition, collidingSpace)) {
return { shouldAddToArray: false };
} else {
const localAccessor = getAccessor(prevCollisionSpace.direction);
const dimensionAccessor = localAccessor.isHorizontal ? "width" : "height";
const gridGap = localAccessor.isHorizontal
? gridProps.parentColumnSpace
: gridProps.parentRowSpace;
const dimension =
prevMovementMap &&
prevMovementMap[staticSpace.id] &&
prevMovementMap[staticSpace.id][dimensionAccessor] !== undefined
? Math.round(
(prevMovementMap[staticSpace.id][dimensionAccessor] || 0) / gridGap,
)
: staticSpace[localAccessor.parallelMax] -
staticSpace[localAccessor.parallelMin];
const orientationalDimension = localAccessor.directionIndicator * dimension;
return {
shouldAddToArray: true,
changedDirection: prevCollisionSpace.direction,
collidingValue:
staticSpace[localAccessor.oppositeDirection] + orientationalDimension,
isHorizontal: prevCollisionSpace.isHorizontal,
};
}
}
/**
* get orientation accessor for first and second run
* @param isHorizontal
* @returns orientation object
*/
export function getOrientationAccessors(
isHorizontal: boolean,
): OrientationAccessors {
return isHorizontal
? { primary: "horizontal", secondary: "vertical" }
: { primary: "vertical", secondary: "horizontal" };
}
/**
* get maximum and minimum space attributes in both orientation
* eg, if primary orientation is horizontal the,
* primary max is bottom, min is top and
* secondary max is right, min is left
*
* @param accessor
* @returns
*/
export function getMaxSpaceAttributes(accessor: CollisionAccessors) {
const {
parallelMax,
parallelMin,
perpendicularMax,
perpendicularMin,
} = accessor;
return {
primary: { max: perpendicularMax, min: perpendicularMin },
secondary: { max: parallelMax, min: parallelMin },
};
}
/**
* get a particular orientation accessor based on orientation
* @param isHorizontal
* @returns horizontal or vertical
*/
export function getOrientationAccessor(isHorizontal?: boolean) {
return isHorizontal ? "horizontal" : "vertical";
}
/**
* method to get sorted occupied spaces based on direction
*
* @param occupiedSpacesMap all the occupied spaces map on the canvas
* @param accessors collision accessors to access the space's/block's data based on direction
* @returns array, sorted occupied spaces based on direction
*/
export function getSortedOccupiedSpaces(
occupiedSpacesMap: SpaceMap,
accessors: CollisionAccessors,
) {
const sortedOccupiedSpaces = Object.values(occupiedSpacesMap);
sortedOccupiedSpaces.sort((a, b) => {
return a[accessors.direction] - b[accessors.direction];
});
return sortedOccupiedSpaces;
}
/**
* method to get sorted new spaces based on direction
*
* @param newSpacePositions new/current positions array of the space/block
* @param accessors collision accessors to access the space's/block's data based on direction
* @returns array, sorted new spaces based on direction
*/
export function getSortedNewPositions(
newSpacePositionsMap: SpaceMap,
accessors: CollisionAccessors,
) {
const newSpacePositions = Object.values(newSpacePositionsMap);
newSpacePositions
.sort((a, b) => {
return a[accessors.direction] - b[accessors.direction];
})
.map((a) => {
return { ...a, order: true };
});
return newSpacePositions;
}
/**
* method to get sorted colliding spaces based on previous collision order
*
* @param collidingSpaceMap direct collision spaces map of the current new space positions
* @param isHorizontal boolean to indicate if the orientation is horizontal
* @param primaryCollisionMap direct collision spaces map on the previous run of the algorithm
* @returns array, sorted occupied spaces based on direction
*/
export function getSortedCollidingSpaces(
collidingSpaceMap: CollisionMap,
isHorizontal: boolean,
prevCollisionMap: CollisionMap,
) {
const collidingSpaces = Object.values(collidingSpaceMap).filter(
(a) => a.isHorizontal === isHorizontal,
);
if (!collidingSpaces.length) return [];
collidingSpaces.sort((a, b) => {
const collisionKeyA = a.id,
collisionKeyB = b.id;
if (prevCollisionMap) {
if (prevCollisionMap[collisionKeyA] && prevCollisionMap[collisionKeyB]) {
return (
prevCollisionMap[collisionKeyA].order -
prevCollisionMap[collisionKeyB].order
);
} else if (prevCollisionMap[collisionKeyA]) return -1;
else if (prevCollisionMap[collisionKeyB]) return 1;
}
return a.order - b.order;
});
return collidingSpaces;
}
/**
* method to get a calculated direction based on previous space positions
*
* @param newSpacePositionsMap new/current positions map of the space/block
* @param prevSpacesMap previous space positions map of the space/block
* @param passedDirection ReflowedDirection Passed from the main method
* @returns calculated direction
*/
export function getCalculatedDirection(
newSpacePositionsMap: SpaceMap,
prevSpacesMap: SpaceMap,
passedDirection: ReflowDirection,
) {
if (passedDirection.indexOf("|") >= 0) return [passedDirection];
for (const key in newSpacePositionsMap) {
if (newSpacePositionsMap[key] && prevSpacesMap[key]) {
const { left: newLeft, top: newTop } = newSpacePositionsMap[key];
const { left: prevLeft, top: prevTop } = prevSpacesMap[key];
if (newTop !== prevTop && newLeft !== prevLeft) {
return [
compareNumbers(newTop, prevTop, true)
? ReflowDirection.BOTTOM
: ReflowDirection.TOP,
compareNumbers(newLeft, prevLeft, true)
? ReflowDirection.RIGHT
: ReflowDirection.LEFT,
];
}
if (newTop !== prevTop)
return compareNumbers(newTop, prevTop, true)
? [ReflowDirection.BOTTOM]
: [ReflowDirection.TOP];
if (newLeft !== prevLeft)
return compareNumbers(newLeft, prevLeft, true)
? [ReflowDirection.RIGHT]
: [ReflowDirection.LEFT];
return [passedDirection];
}
}
return [passedDirection];
}
/**
* Returns the bottom most row among all the widget
* @param newPositions
* @returns number, the bottom most row
*/
export function getBottomMostRow(newPositions: OccupiedSpace[]): number {
let maxBottomRow = 0;
for (const newPosition of newPositions) {
maxBottomRow = Math.max(maxBottomRow, newPosition.bottom);
}
return maxBottomRow;
}
/**
* Returns a boolean to indicate if the colliding Space is to be processed if it is already processed once
*
* @param collidingSpace
* @param globalProcessedNodes
* @returns boolean, if true will process the colliding Space while generating a Tree.
*/
export function checkProcessNodeForTree(
collidingSpace: CollidingSpace,
globalProcessedNodes: CollisionTreeCache,
) {
if (!globalProcessedNodes[collidingSpace.id])
return { shouldProcessNode: true };
const direction = collidingSpace.direction;
const oppositeDirection = getOppositeDirection(direction);
// If the current node is already processed in the opposite direction the return false
if (!isUndefined(globalProcessedNodes[collidingSpace.id][oppositeDirection]))
return { shouldProcessNode: false };
const { directionIndicator } = getAccessor(direction);
// if the current node is not processed return true or if it is processed but
// the current colliding value is greater than previous' return true
if (
isUndefined(
globalProcessedNodes[collidingSpace.id][collidingSpace.direction],
) ||
compareNumbers(
collidingSpace.collidingValue,
globalProcessedNodes[collidingSpace.id][direction].value,
directionIndicator > 0,
)
)
return { shouldProcessNode: true };
//if collision values are equal, return the cached values to be used in calculation
const {
childNode,
currentEmptySpaces,
occupiedLength,
occupiedSpace,
value,
} = globalProcessedNodes[collidingSpace.id][direction];
if (collidingSpace.collidingValue === value)
return {
shouldProcessNode: false,
currentChildNode: childNode,
occupiedLength,
occupiedSpace,
currentEmptySpaces,
};
return {
shouldProcessNode: false,
};
}
/**
* This is to get the colliding value relative to the edge of the canvas.
* eg, If a widget is colliding with another, near the ege of the canvas.
* After the point where the widget resizes full and can'yt move or resize, then the colliding value also should not increase
*
* @param accessors
* @param collidingValue
* @param direction
* @param gridProps
* @param occupiedLength
* @returns number, colliding value to the edge of canvas
*/
export function getRelativeCollidingValue(
accessors: CollisionAccessors,
collidingValue: number,
direction: ReflowDirection,
{ maxGridColumns }: GridProps,
occupiedLength?: number,
): number {
if (direction === ReflowDirection.BOTTOM || !occupiedLength)
return collidingValue;
let calculatedCollidingValue = occupiedLength;
if (direction === ReflowDirection.RIGHT)
calculatedCollidingValue = maxGridColumns - calculatedCollidingValue;
// return the maximum or minimum based on the direction
return Math[accessors.mathComparator](
calculatedCollidingValue,
collidingValue,
);
}