Skip to content

Commit

Permalink
fix issues with drag and drop and begin refactor of dnd reducers to u…
Browse files Browse the repository at this point in the history
…se redux toolkit
  • Loading branch information
jthrilly committed Dec 8, 2023
1 parent ff53f0f commit 799ebc6
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 33 deletions.
319 changes: 319 additions & 0 deletions lib/interviewer/behaviours/DragAndDrop/DragAndDropSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import { createSlice } from '@reduxjs/toolkit';
import { isEmpty, thru } from 'lodash';
// import { markHitAll, markHitSource, markHitTarget } from './utils';

type Target = {
id: string;
x: number;
y: number;
width: number;
height: number;
accepts?: string[];
isOver?: boolean;
willAccept?: boolean;
};

type Obstacle = {
id: string;
x: number;
y: number;
width: number;
height: number;
isOver?: boolean;
};

type Source = {
id: string;
x: number;
y: number;
width: number;
height: number;
setValidMove: (valid: boolean) => void;
} | null;

type State = {
targets: Target[];
obstacles: Obstacle[];
source: Source;
};

const initialState: State = {
targets: [],
obstacles: [],
source: null,
};

// Since accepts() is a weak link and can throw errors if not carefully written.
const defaultSource = {
meta: {},
};

export const willAccept = (
accepts: (source: Source) => boolean,
source: Source,
) => {
try {
return accepts({
...defaultSource,
...source,
});
} catch (e) {
console.warn('Error in accept() function', e, source); // eslint-disable-line no-console
return false;
}
};

export const markOutOfBounds = (source: Source) => {
const isOutOfBounds =
source.x > window.innerWidth ||
source.x < 0 ||
source.y > window.innerHeight ||
source.y < 0;

return isOutOfBounds;
};

export const markHitTarget = ({
target,
source,
}: {
target: Target;
source: Source;
}) => {
if (!source) {
return { ...target, isOver: false, willAccept: false };
}

const isOver =
source.x >= target.x &&
source.x <= target.x + target.width &&
source.y >= target.y &&
source.y <= target.y + target.height;

return {
...target,
isOver,
willAccept: target.accepts ? willAccept(target.accepts, source) : false,
};
};

export const markHitTargets = ({
targets,
source,
}: {
targets: Target[];
source: Source;
}) => targets.map((target) => markHitTarget({ target, source }));

export const markHitSource = ({
targets,
source,
}: {
targets: Target[];
source: Source;
}) =>
thru(source, (s) => {
if (isEmpty(s)) {
return s;
}

return {
...s,
isOver: targets.filter((t) => t.isOver).length > 0,
isOutOfBounds: markOutOfBounds(s),
};
});

export const markHitAll = ({
targets,
obstacles,
source,
...rest
}: {
targets: Target[];
obstacles: Obstacle[];
source: Source;
}) => {
const targetsWithHits = markHitTargets({ targets, source });
const obstaclesWithHits = markHitTargets({ targets: obstacles, source });
const sourceWithHits = markHitSource({ targets: targetsWithHits, source });

return {
...rest,
targets: targetsWithHits,
obstacles: obstaclesWithHits,
source: sourceWithHits,
};
};

export const resetHits = ({ targets, ...rest }: { targets: Target[] }) => ({
targets: targets.map((target) => {
const { isOver, willAccept, ...rest } = target;
return rest;
}),
...rest,
});

const triggerDrag = (state: State, source: Source) => {
const hits = markHitAll({
...state,
source: {
...state.source,
...source,
},
});

source.setValidMove(true);



if (hits.obstacles.some((obstacle) => obstacle.isOver) || hits.source.isOutOfBounds) {
source.setValidMove(false);
return;
}

hits.targets.filter((target) => target.isOver && target.willAccept).forEach((target) => {
source.setValidMove(true);
console.log('target', target);
target.onDrag(hits.source);
});
};

const triggerDrop = (state: State, source: Source) => {
const hits = markHitAll({
...state,
source: {
...state.source,
...source,
},
});


hits.targets.filter((target) => target.willAccept).forEach((target) => {
target.onDragEnd(hits.source);
});

if (hits.obstacles.some((obstacle) => obstacle.isOver)) {
return;
}

hits.targets.filter((target) => target.isOver && target.willAccept).forEach((target) => {
target.onDrop(hits.source);
});
};

const dragAndDropSlice = createSlice({
name: 'dragAndDrop',
initialState,
reducers: {
upsertTarget(state, { payload: { target } }) {
const targets: Target[] = [
...state.targets.filter((t) => t.id !== target.id),
markHitTarget({ target, source: state.source }),
];

const source = markHitSource({
targets,
source: state.source,
});

return {
...state,
targets,
source,
};
},
renameTarget(state, action) {
return {
...state,
targets: state.targets.map((target) => {
if (action.from !== target.id) {
return target;
}

return {
...target,
id: action.to,
};
}),
};
},
removeTarget(state, action) {
const targets = state.targets.filter((t) => t.id !== action.id);
const source = markHitSource({ targets, source: state.source });
return {
...state,
targets,
source,
};
},
upsertObstacle(state, action) {
const obstacles = [
...state.obstacles.filter((o) => o.id !== action.obstacle.id)
markHitTarget({ target: action.obstacle, source: state.source }),
];

const source = markHitSource({
targets: obstacles,
source: state.source,
});

return {
...state,
obstacles,
source,
};
},
removeObstacle(state, action) {
const obstacles = state.obstacles.filter((o) => o.id !== action.id);
const source = markHitSource({
targets: obstacles,
source: state.source,
});

return {
...state,
obstacles,
source,
};
},
dragStart(state, action) {
return markHitAll({
...state,
source: action.source,
});
},
dragMove(state, action) {
if (state.source === null) {
return state;
}

return markHitAll({
...state,
source: {
...state.source,
...action.source,
},
});
},
dragEnd(state, action) {
return resetHits({
...state,
source: null,
});
},
},
});

export const {
upsertTarget,
renameTarget,
removeTarget,
upsertObstacle,
removeObstacle,
dragStart,
dragMove,
dragEnd,
} = dragAndDropSlice.actions;

export default dragAndDropSlice.reducer;
1 change: 1 addition & 0 deletions lib/interviewer/behaviours/DragAndDrop/DragSource.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable react/display-name */
/* eslint-disable react/no-find-dom-node, react/sort-comp, react/jsx-props-no-spreading */

import React, { useEffect, useRef, useState } from 'react';
Expand Down
13 changes: 6 additions & 7 deletions lib/interviewer/behaviours/DragAndDrop/DropTarget.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const dropTarget = (WrappedComponent) => {
update = () => {
this.updateTarget();

this.interval = setTimeout(
() => {
this.animationFrame = requestAnimationFrame(this.update);
},
1000 / maxFramesPerSecond,
);
// this.interval = setTimeout(
// () => {
// this.animationFrame = requestAnimationFrame(this.update);
// },
// 1000 / maxFramesPerSecond,
// );
}

updateTarget = () => {
Expand All @@ -54,7 +54,6 @@ const dropTarget = (WrappedComponent) => {
} = this.props;

const boundingClientRect = getAbsoluteBoundingRect(this.node);

store.dispatch(
actions.upsertTarget({
id,
Expand Down
5 changes: 3 additions & 2 deletions lib/interviewer/behaviours/DragAndDrop/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ const triggerDrag = (state, source) => {
filter(hits.targets, { isOver: true, willAccept: true })
.forEach((target) => {
source.setValidMove(true);
target.onDrag(hits.source);
console.log('target', target);
target.onDrag?.(hits.source);
});
};

Expand All @@ -131,7 +132,7 @@ const triggerDrop = (state, source) => {

filter(hits.targets, { willAccept: true })
.forEach((target) => {
target.onDragEnd(hits.source);
target.onDragEnd?.(hits.source);
});

if (some(hits.obstacles, { isOver: true })) {
Expand Down
9 changes: 5 additions & 4 deletions lib/interviewer/behaviours/DragAndDrop/store.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
import logger from '../../ducks/middleware/logger';
import { configureStore } from '@reduxjs/toolkit';

const store = createStore(
const store = configureStore({
reducer,
applyMiddleware(thunk),
);
middleware: [thunk, logger],
});

export default store;
Loading

0 comments on commit 799ebc6

Please sign in to comment.