- Add in new DND Library (react-beautiful-dnd-grid), resides in client/src/components/trello-board/dnd/lib/index.js
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
35
client/package-lock.json
generated
35
client/package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
@@ -39,6 +40,7 @@
|
||||
"libphonenumber-js": "^1.11.2",
|
||||
"logrocket": "^8.1.0",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.0.0",
|
||||
@@ -74,6 +76,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.11",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.1",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
@@ -5851,6 +5854,11 @@
|
||||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@splitsoftware/splitio-react/node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/@surma/rollup-plugin-off-main-thread": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
||||
@@ -8518,6 +8526,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
@@ -13069,9 +13085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "13.2.0",
|
||||
@@ -15414,11 +15430,6 @@
|
||||
"react-dom": "^16.14.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar/node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/react-color": {
|
||||
"version": "2.19.3",
|
||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||
@@ -18275,6 +18286,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
@@ -39,6 +40,7 @@
|
||||
"libphonenumber-js": "^1.11.2",
|
||||
"logrocket": "^8.1.0",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.0.0",
|
||||
@@ -74,6 +76,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.11",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.1",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
|
||||
67
client/src/components/trello-board/dnd/lib/animation.js
Normal file
67
client/src/components/trello-board/dnd/lib/animation.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { isEqual, origin } from "./state/position";
|
||||
|
||||
export const curves = {
|
||||
outOfTheWay: "cubic-bezier(0.2, 0, 0, 1)",
|
||||
drop: "cubic-bezier(.2,1,.1,1)"
|
||||
};
|
||||
export const combine = {
|
||||
opacity: {
|
||||
// while dropping: fade out totally
|
||||
drop: 0,
|
||||
// while dragging: fade out partially
|
||||
combining: 0.7
|
||||
},
|
||||
scale: {
|
||||
drop: 0.75
|
||||
}
|
||||
};
|
||||
export const timings = {
|
||||
outOfTheWay: 0.2,
|
||||
// greater than the out of the way time
|
||||
// so that when the drop ends everything will
|
||||
// have to be out of the way
|
||||
minDropTime: 0.33,
|
||||
maxDropTime: 0.55
|
||||
};
|
||||
|
||||
// slow timings
|
||||
// uncomment to use
|
||||
// export const timings = {
|
||||
// outOfTheWay: 2,
|
||||
// // greater than the out of the way time
|
||||
// // so that when the drop ends everything will
|
||||
// // have to be out of the way
|
||||
// minDropTime: 3,
|
||||
// maxDropTime: 4,
|
||||
// };
|
||||
|
||||
const outOfTheWayTiming = `${timings.outOfTheWay}s ${curves.outOfTheWay}`;
|
||||
export const placeholderTransitionDelayTime = 0.1;
|
||||
export const transitions = {
|
||||
fluid: `opacity ${outOfTheWayTiming}`,
|
||||
snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`,
|
||||
drop: (duration) => {
|
||||
const timing = `${duration}s ${curves.drop}`;
|
||||
return `transform ${timing}, opacity ${timing}`;
|
||||
},
|
||||
outOfTheWay: `transform ${outOfTheWayTiming}`,
|
||||
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`
|
||||
};
|
||||
const moveTo = (offset) => (isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`);
|
||||
export const transforms = {
|
||||
moveTo,
|
||||
drop: (offset, isCombining) => {
|
||||
const translate = moveTo(offset);
|
||||
if (!translate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// only transforming the translate
|
||||
if (!isCombining) {
|
||||
return translate;
|
||||
}
|
||||
|
||||
// when dropping while combining we also update the scale
|
||||
return `${translate} scale(${combine.scale.drop})`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const average = (values) => {
|
||||
const sum = values.reduce((previous, current) => previous + current, 0);
|
||||
return sum / values.length;
|
||||
};
|
||||
export default (groupSize) => {
|
||||
console.log("Starting average action timer middleware");
|
||||
console.log(`Will take an average every ${groupSize} actions`);
|
||||
const bucket = {};
|
||||
return () => (next) => (action) => {
|
||||
const start = performance.now();
|
||||
const result = next(action);
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
if (!bucket[action.type]) {
|
||||
bucket[action.type] = [duration];
|
||||
return result;
|
||||
}
|
||||
bucket[action.type].push(duration);
|
||||
if (bucket[action.type].length < groupSize) {
|
||||
return result;
|
||||
}
|
||||
console.warn(`Average time for ${action.type}`, average(bucket[action.type]));
|
||||
|
||||
// reset
|
||||
bucket[action.type] = [];
|
||||
return result;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as timings from "../timings";
|
||||
|
||||
export default () => (next) => (action) => {
|
||||
timings.forceEnable();
|
||||
const key = `redux action: ${action.type}`;
|
||||
timings.start(key);
|
||||
const result = next(action);
|
||||
timings.finish(key);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export default (mode = "verbose") =>
|
||||
(store) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (mode === "light") {
|
||||
console.log("🏃 Action:", action.type);
|
||||
return next(action);
|
||||
}
|
||||
console.group(`action: ${action.type}`);
|
||||
console.log("action payload", action.payload);
|
||||
console.log("state before", store.getState());
|
||||
const result = next(action);
|
||||
console.log("state after", store.getState());
|
||||
console.groupEnd();
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default () => (next) => (action) => {
|
||||
const title = `👾 redux (action): ${action.type}`;
|
||||
const startMark = `${action.type}:start`;
|
||||
const endMark = `${action.type}:end`;
|
||||
performance.mark(startMark);
|
||||
const result = next(action);
|
||||
performance.mark(endMark);
|
||||
performance.measure(title, startMark, endMark);
|
||||
return result;
|
||||
};
|
||||
68
client/src/components/trello-board/dnd/lib/debug/timings.js
Normal file
68
client/src/components/trello-board/dnd/lib/debug/timings.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const records = {};
|
||||
let isEnabled = false;
|
||||
const isTimingsEnabled = () => isEnabled;
|
||||
export const forceEnable = () => {
|
||||
isEnabled = true;
|
||||
};
|
||||
|
||||
// Debug: uncomment to enable
|
||||
// forceEnable();
|
||||
|
||||
export const start = (key) => {
|
||||
// we want to strip all the code out for production builds
|
||||
// draw back: can only do timings in dev env (which seems to be fine for now)
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
records[key] = now;
|
||||
}
|
||||
};
|
||||
export const finish = (key) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const previous = records[key];
|
||||
if (!previous) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("cannot finish timing as no previous time found", key);
|
||||
return;
|
||||
}
|
||||
const result = now - previous;
|
||||
const rounded = result.toFixed(2);
|
||||
const style = (() => {
|
||||
if (result < 12) {
|
||||
return {
|
||||
textColor: "green",
|
||||
symbol: "✅"
|
||||
};
|
||||
}
|
||||
if (result < 40) {
|
||||
return {
|
||||
textColor: "orange",
|
||||
symbol: "⚠️"
|
||||
};
|
||||
}
|
||||
return {
|
||||
textColor: "red",
|
||||
symbol: "❌"
|
||||
};
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${style.symbol} %cTiming %c${rounded} %cms %c${key}`,
|
||||
// title
|
||||
"color: blue; font-weight: bold;",
|
||||
// result
|
||||
`color: ${style.textColor}; font-size: 1.1em;`,
|
||||
// ms
|
||||
"color: grey;",
|
||||
// key
|
||||
"color: purple; font-weight: bold;"
|
||||
);
|
||||
}
|
||||
};
|
||||
44
client/src/components/trello-board/dnd/lib/dev-warning.js
Normal file
44
client/src/components/trello-board/dnd/lib/dev-warning.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
// not replacing newlines (which \s does)
|
||||
const spacesAndTabs = /[ \t]{2,}/g;
|
||||
const lineStartWithSpaces = /^[ \t]*/gm;
|
||||
|
||||
// using .trim() to clear the any newlines before the first text and after last text
|
||||
const clean = (value) => value.replace(spacesAndTabs, " ").replace(lineStartWithSpaces, "").trim();
|
||||
const getDevMessage = (message) =>
|
||||
clean(`
|
||||
%creact-beautiful-dnd
|
||||
|
||||
%c${clean(message)}
|
||||
|
||||
%c👷 This is a development only message. It will be removed in production builds.
|
||||
`);
|
||||
export const getFormattedMessage = (message) => [
|
||||
getDevMessage(message),
|
||||
// title (green400)
|
||||
"color: #00C584; font-size: 1.2em; font-weight: bold;",
|
||||
// message
|
||||
"line-height: 1.5",
|
||||
// footer (purple300)
|
||||
"color: #723874;"
|
||||
];
|
||||
const isDisabledFlag = "__react-beautiful-dnd-disable-dev-warnings";
|
||||
|
||||
export function log(type, message) {
|
||||
// no warnings in production
|
||||
if (isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// manual opt out of warnings
|
||||
if (typeof window !== "undefined" && window[isDisabledFlag]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](...getFormattedMessage(message));
|
||||
}
|
||||
|
||||
export const warning = log.bind(null, "warn");
|
||||
export const error = log.bind(null, "error");
|
||||
5
client/src/components/trello-board/dnd/lib/empty.js
Normal file
5
client/src/components/trello-board/dnd/lib/empty.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function noop() {}
|
||||
|
||||
export function identity(value) {
|
||||
return value;
|
||||
}
|
||||
18
client/src/components/trello-board/dnd/lib/index.js
Normal file
18
client/src/components/trello-board/dnd/lib/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// Components
|
||||
export { default as DragDropContext } from "./view/drag-drop-context";
|
||||
export { default as Droppable } from "./view/droppable";
|
||||
export { default as Draggable } from "./view/draggable";
|
||||
|
||||
// Default sensors
|
||||
|
||||
export { useMouseSensor, useTouchSensor, useKeyboardSensor } from "./view/use-sensor-marshal";
|
||||
|
||||
// Utils
|
||||
|
||||
export { resetServerContext } from "./view/drag-drop-context";
|
||||
|
||||
// Public flow types
|
||||
|
||||
// Droppable types
|
||||
|
||||
// Draggable types
|
||||
32
client/src/components/trello-board/dnd/lib/invariant.js
Normal file
32
client/src/components/trello-board/dnd/lib/invariant.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const prefix = "Invariant failed";
|
||||
|
||||
// Want to use this:
|
||||
// export class RbdInvariant extends Error { }
|
||||
// But it causes babel to bring in a lot of code
|
||||
|
||||
export function RbdInvariant(message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
RbdInvariant.prototype.toString = function toString() {
|
||||
return this.message;
|
||||
};
|
||||
|
||||
// A copy-paste of tiny-invariant but with a custom error type
|
||||
// Throw an error if the condition fails
|
||||
export function invariant(condition, message) {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
if (isProduction) {
|
||||
// In production we strip the message but still throw
|
||||
throw new RbdInvariant(prefix);
|
||||
} else {
|
||||
// When not in production we allow the message to pass through
|
||||
// *This block will be removed in production builds*
|
||||
throw new RbdInvariant(`${prefix}: ${message || ""}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
export function isInteger(value) {
|
||||
if (Number.isInteger) {
|
||||
return Number.isInteger(value);
|
||||
}
|
||||
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
|
||||
}
|
||||
|
||||
// Using this helper to ensure there are correct flow types
|
||||
// https://github.com/facebook/flow/issues/2221
|
||||
export function values(map) {
|
||||
if (Object.values) {
|
||||
// $FlowFixMe - Object.values currently does not have good flow support
|
||||
return Object.values(map);
|
||||
}
|
||||
return Object.keys(map).map((key) => map[key]);
|
||||
}
|
||||
|
||||
// Could also extend to pass index and list
|
||||
|
||||
// TODO: swap order
|
||||
export function findIndex(list, predicate) {
|
||||
if (list.findIndex) {
|
||||
return list.findIndex(predicate);
|
||||
}
|
||||
|
||||
// Using a for loop so that we can exit early
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (predicate(list[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Array.prototype.find returns -1 when nothing is found
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function find(list, predicate) {
|
||||
if (list.find) {
|
||||
return list.find(predicate);
|
||||
}
|
||||
const index = findIndex(list, predicate);
|
||||
if (index !== -1) {
|
||||
return list[index];
|
||||
}
|
||||
// Array.prototype.find returns undefined when nothing is found
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Using this rather than Array.from as Array.from adds 2kb to the gzip
|
||||
// document.querySelector actually returns Element[], but flow thinks it is HTMLElement[]
|
||||
// So we downcast the result to Element[]
|
||||
export function toArray(list) {
|
||||
return Array.prototype.slice.call(list);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const dragHandleUsageInstructions = `
|
||||
Press space bar to start a drag.
|
||||
When dragging you can use the arrow keys to move the item around and escape to cancel.
|
||||
Some screen readers may require you to be in focus mode or to use your pass through key
|
||||
`;
|
||||
const position = (index) => index + 1;
|
||||
|
||||
// We cannot list what index the Droppable is in automatically as we are not sure how
|
||||
// the Droppable's have been configured
|
||||
const onDragStart = (start) => `
|
||||
You have lifted an item in position ${position(start.source.index)}
|
||||
`;
|
||||
const withLocation = (source, destination) => {
|
||||
const isInHomeList = source.droppableId === destination.droppableId;
|
||||
const startPosition = position(source.index);
|
||||
const endPosition = position(destination.index);
|
||||
if (isInHomeList) {
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
to position ${endPosition}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
in list ${source.droppableId}
|
||||
to list ${destination.droppableId}
|
||||
in position ${endPosition}
|
||||
`;
|
||||
};
|
||||
const withCombine = (id, source, combine) => {
|
||||
const inHomeList = source.droppableId === combine.droppableId;
|
||||
if (inHomeList) {
|
||||
return `
|
||||
The item ${id}
|
||||
has been combined with ${combine.draggableId}`;
|
||||
}
|
||||
return `
|
||||
The item ${id}
|
||||
in list ${source.droppableId}
|
||||
has been combined with ${combine.draggableId}
|
||||
in list ${combine.droppableId}
|
||||
`;
|
||||
};
|
||||
const onDragUpdate = (update) => {
|
||||
const location = update.destination;
|
||||
if (location) {
|
||||
return withLocation(update.source, location);
|
||||
}
|
||||
const combine = update.combine;
|
||||
if (combine) {
|
||||
return withCombine(update.draggableId, update.source, combine);
|
||||
}
|
||||
return "You are over an area that cannot be dropped on";
|
||||
};
|
||||
const returnedToStart = (source) => `
|
||||
The item has returned to its starting position
|
||||
of ${position(source.index)}
|
||||
`;
|
||||
const onDragEnd = (result) => {
|
||||
if (result.reason === "CANCEL") {
|
||||
return `
|
||||
Movement cancelled.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
}
|
||||
const location = result.destination;
|
||||
const combine = result.combine;
|
||||
if (location) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withLocation(result.source, location)}
|
||||
`;
|
||||
}
|
||||
if (combine) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withCombine(result.draggableId, result.source, combine)}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
The item has been dropped while not over a drop area.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
};
|
||||
const preset = {
|
||||
dragHandleUsageInstructions,
|
||||
onDragStart,
|
||||
onDragUpdate,
|
||||
onDragEnd
|
||||
};
|
||||
export default preset;
|
||||
@@ -0,0 +1,88 @@
|
||||
export const beforeInitialCapture = (args) => ({
|
||||
type: "BEFORE_INITIAL_CAPTURE",
|
||||
payload: args
|
||||
});
|
||||
export const lift = (args) => ({
|
||||
type: "LIFT",
|
||||
payload: args
|
||||
});
|
||||
export const initialPublish = (args) => ({
|
||||
type: "INITIAL_PUBLISH",
|
||||
payload: args
|
||||
});
|
||||
export const publishWhileDragging = (args) => ({
|
||||
type: "PUBLISH_WHILE_DRAGGING",
|
||||
payload: args
|
||||
});
|
||||
export const collectionStarting = () => ({
|
||||
type: "COLLECTION_STARTING",
|
||||
payload: null
|
||||
});
|
||||
export const updateDroppableScroll = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsCombineEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_COMBINE_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const move = (args) => ({
|
||||
type: "MOVE",
|
||||
payload: args
|
||||
});
|
||||
export const moveByWindowScroll = (args) => ({
|
||||
type: "MOVE_BY_WINDOW_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateViewportMaxScroll = (args) => ({
|
||||
type: "UPDATE_VIEWPORT_MAX_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const moveUp = () => ({
|
||||
type: "MOVE_UP",
|
||||
payload: null
|
||||
});
|
||||
export const moveDown = () => ({
|
||||
type: "MOVE_DOWN",
|
||||
payload: null
|
||||
});
|
||||
export const moveRight = () => ({
|
||||
type: "MOVE_RIGHT",
|
||||
payload: null
|
||||
});
|
||||
export const moveLeft = () => ({
|
||||
type: "MOVE_LEFT",
|
||||
payload: null
|
||||
});
|
||||
export const flush = () => ({
|
||||
type: "FLUSH",
|
||||
payload: null
|
||||
});
|
||||
export const animateDrop = (args) => ({
|
||||
type: "DROP_ANIMATE",
|
||||
payload: args
|
||||
});
|
||||
export const completeDrop = (args) => ({
|
||||
type: "DROP_COMPLETE",
|
||||
payload: args
|
||||
});
|
||||
export const drop = (args) => ({
|
||||
type: "DROP",
|
||||
payload: args
|
||||
});
|
||||
export const cancel = () =>
|
||||
drop({
|
||||
reason: "CANCEL"
|
||||
});
|
||||
export const dropPending = (args) => ({
|
||||
type: "DROP_PENDING",
|
||||
payload: args
|
||||
});
|
||||
export const dropAnimationFinished = () => ({
|
||||
type: "DROP_ANIMATION_FINISHED",
|
||||
payload: null
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { add, apply, isEqual, origin } from "../position";
|
||||
|
||||
const smallestSigned = apply((value) => {
|
||||
if (value === 0) {
|
||||
return 0;
|
||||
}
|
||||
return value > 0 ? 1 : -1;
|
||||
});
|
||||
// We need to figure out how much of the movement
|
||||
// cannot be done with a scroll
|
||||
export const getOverlap = (() => {
|
||||
const getRemainder = (target, max) => {
|
||||
if (target < 0) {
|
||||
return target;
|
||||
}
|
||||
if (target > max) {
|
||||
return target - max;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return ({ current, max, change }) => {
|
||||
const targetScroll = add(current, change);
|
||||
const overlap = {
|
||||
x: getRemainder(targetScroll.x, max.x),
|
||||
y: getRemainder(targetScroll.y, max.y)
|
||||
};
|
||||
if (isEqual(overlap, origin)) {
|
||||
return null;
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
})();
|
||||
export const canPartiallyScroll = ({ max: rawMax, current, change }) => {
|
||||
// It is possible for the max scroll to be greater than the current scroll
|
||||
// when there are scrollbars on the cross axis. We adjust for this by
|
||||
// increasing the max scroll point if needed
|
||||
// This will allow movements backwards even if the current scroll is greater than the max scroll
|
||||
const max = {
|
||||
x: Math.max(current.x, rawMax.x),
|
||||
y: Math.max(current.y, rawMax.y)
|
||||
};
|
||||
|
||||
// Only need to be able to move the smallest amount in the desired direction
|
||||
const smallestChange = smallestSigned(change);
|
||||
const overlap = getOverlap({
|
||||
max,
|
||||
current,
|
||||
change: smallestChange
|
||||
});
|
||||
|
||||
// no overlap at all - we can move there!
|
||||
if (!overlap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an x value, but there is no x overlap - then we can scroll on the x!
|
||||
if (smallestChange.x !== 0 && overlap.x === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an y value, but there is no y overlap - then we can scroll on the y!
|
||||
if (smallestChange.y !== 0 && overlap.y === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const canScrollWindow = (viewport, change) =>
|
||||
canPartiallyScroll({
|
||||
current: viewport.scroll.current,
|
||||
max: viewport.scroll.max,
|
||||
change
|
||||
});
|
||||
export const getWindowOverlap = (viewport, change) => {
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
return null;
|
||||
}
|
||||
const max = viewport.scroll.max;
|
||||
const current = viewport.scroll.current;
|
||||
return getOverlap({
|
||||
current,
|
||||
max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const canScrollDroppable = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
|
||||
// Cannot scroll when there is no scrollable
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
return canPartiallyScroll({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const getDroppableOverlap = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return null;
|
||||
}
|
||||
return getOverlap({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Values used to control how the fluid auto scroll feels
|
||||
const config = {
|
||||
// percentage distance from edge of container:
|
||||
startFromPercentage: 0.25,
|
||||
maxScrollAtPercentage: 0.05,
|
||||
// pixels per frame
|
||||
maxPixelScroll: 28,
|
||||
// A function used to ease a percentage value
|
||||
// A simple linear function would be: (percentage) => percentage;
|
||||
// percentage is between 0 and 1
|
||||
// result must be between 0 and 1
|
||||
ease: (percentage) => Math.pow(percentage, 2),
|
||||
durationDampening: {
|
||||
// ms: how long to dampen the speed of an auto scroll from the start of a drag
|
||||
stopDampeningAt: 1200,
|
||||
// ms: when to start accelerating the reduction of duration dampening
|
||||
accelerateAt: 360
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,43 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { invariant } from "../../../invariant";
|
||||
import isPositionInFrame from "../../visibility/is-position-in-frame";
|
||||
import { toDroppableList } from "../../dimension-structures";
|
||||
import { find } from "../../../native-with-fallback";
|
||||
|
||||
const getScrollableDroppables = memoizeOne((droppables) =>
|
||||
toDroppableList(droppables).filter((droppable) => {
|
||||
// exclude disabled droppables
|
||||
if (!droppable.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only want droppables that are scrollable
|
||||
if (!droppable.frame) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
const getScrollableDroppableOver = (target, droppables) => {
|
||||
const maybe = find(getScrollableDroppables(droppables), (droppable) => {
|
||||
invariant(droppable.frame, "Invalid result");
|
||||
return isPositionInFrame(droppable.frame.pageMarginBox)(target);
|
||||
});
|
||||
return maybe;
|
||||
};
|
||||
export default ({ center, destination, droppables }) => {
|
||||
// We need to scroll the best droppable frame we can so that the
|
||||
// placeholder buffer logic works correctly
|
||||
|
||||
if (destination) {
|
||||
const dimension = droppables[destination];
|
||||
if (!dimension.frame) {
|
||||
return null;
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
// 2. If we are not over a droppable - are we over a droppable frame?
|
||||
const dimension = getScrollableDroppableOver(center, droppables);
|
||||
return dimension;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import getScroll from "./get-scroll";
|
||||
import { canScrollDroppable } from "../can-scroll";
|
||||
|
||||
export default ({ droppable, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||
// We know this has a closestScrollable
|
||||
const frame = droppable.frame;
|
||||
|
||||
// this should never happen - just being safe
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
const scroll = getScroll({
|
||||
dragStartTime,
|
||||
container: frame.pageMarginBox,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
return scroll && canScrollDroppable(droppable, scroll) ? scroll : null;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { warning } from "../../../dev-warning";
|
||||
|
||||
export default ({ startOfRange, endOfRange, current }) => {
|
||||
const range = endOfRange - startOfRange;
|
||||
if (range === 0) {
|
||||
warning(`
|
||||
Detected distance range of 0 in the fluid auto scroller
|
||||
This is unexpected and would cause a divide by 0 issue.
|
||||
Not allowing an auto scroll
|
||||
`);
|
||||
return 0;
|
||||
}
|
||||
const currentInRange = current - startOfRange;
|
||||
const percentage = currentInRange / range;
|
||||
return percentage;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export default ({ container, subject, proposedScroll }) => {
|
||||
const isTooBigVertically = subject.height > container.height;
|
||||
const isTooBigHorizontally = subject.width > container.width;
|
||||
|
||||
// not too big on any axis
|
||||
if (!isTooBigHorizontally && !isTooBigVertically) {
|
||||
return proposedScroll;
|
||||
}
|
||||
|
||||
// too big on both axis
|
||||
if (isTooBigHorizontally && isTooBigVertically) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only too big on one axis
|
||||
// Exclude the axis that we cannot scroll on
|
||||
return {
|
||||
x: isTooBigHorizontally ? 0 : proposedScroll.x,
|
||||
y: isTooBigVertically ? 0 : proposedScroll.y
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import getPercentage from "../../get-percentage";
|
||||
import config from "../../config";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
const accelerateAt = config.durationDampening.accelerateAt;
|
||||
const stopAt = config.durationDampening.stopDampeningAt;
|
||||
export default (proposedScroll, dragStartTime) => {
|
||||
const startOfRange = dragStartTime;
|
||||
const endOfRange = stopAt;
|
||||
const now = Date.now();
|
||||
const runTime = now - startOfRange;
|
||||
|
||||
// we have finished the time dampening period
|
||||
if (runTime >= stopAt) {
|
||||
return proposedScroll;
|
||||
}
|
||||
|
||||
// Up to this point we know there is a proposed scroll
|
||||
// but we have not reached our accelerate point
|
||||
// Return the minimum amount of scroll
|
||||
if (runTime < accelerateAt) {
|
||||
return minScroll;
|
||||
}
|
||||
const betweenAccelerateAtAndStopAtPercentage = getPercentage({
|
||||
startOfRange: accelerateAt,
|
||||
endOfRange,
|
||||
current: runTime
|
||||
});
|
||||
const scroll = proposedScroll * config.ease(betweenAccelerateAtAndStopAtPercentage);
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import config from "../../config";
|
||||
|
||||
// all in pixels
|
||||
|
||||
// converts the percentages in the config into actual pixel values
|
||||
export default (container, axis) => {
|
||||
const startScrollingFrom = container[axis.size] * config.startFromPercentage;
|
||||
const maxScrollValueAt = container[axis.size] * config.maxScrollAtPercentage;
|
||||
const thresholds = {
|
||||
startScrollingFrom,
|
||||
maxScrollValueAt
|
||||
};
|
||||
return thresholds;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import getPercentage from "../../get-percentage";
|
||||
import config from "../../config";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
export default (distanceToEdge, thresholds) => {
|
||||
/*
|
||||
// This function only looks at the distance to one edge
|
||||
// Example: looking at bottom edge
|
||||
|----------------------------------|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| | => no scroll in this range
|
||||
| |
|
||||
| |
|
||||
| startScrollingFrom (eg 100px) |
|
||||
| |
|
||||
| | => increased scroll value the closer to maxScrollValueAt
|
||||
| maxScrollValueAt (eg 10px) |
|
||||
| | => max scroll value in this range
|
||||
|----------------------------------|
|
||||
*/
|
||||
|
||||
// too far away to auto scroll
|
||||
if (distanceToEdge > thresholds.startScrollingFrom) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// use max speed when on or over boundary
|
||||
if (distanceToEdge <= thresholds.maxScrollValueAt) {
|
||||
return config.maxPixelScroll;
|
||||
}
|
||||
|
||||
// when just going on the boundary return the minimum integer
|
||||
if (distanceToEdge === thresholds.startScrollingFrom) {
|
||||
return minScroll;
|
||||
}
|
||||
|
||||
// to get the % past startScrollingFrom we will calculate
|
||||
// the % the value is from maxScrollValueAt and then invert it
|
||||
const percentageFromMaxScrollValueAt = getPercentage({
|
||||
startOfRange: thresholds.maxScrollValueAt,
|
||||
endOfRange: thresholds.startScrollingFrom,
|
||||
current: distanceToEdge
|
||||
});
|
||||
const percentageFromStartScrollingFrom = 1 - percentageFromMaxScrollValueAt;
|
||||
const scroll = config.maxPixelScroll * config.ease(percentageFromStartScrollingFrom);
|
||||
|
||||
// scroll will always be a positive integer
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import getValueFromDistance from "./get-value-from-distance";
|
||||
import dampenValueByTime from "./dampen-value-by-time";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
export default ({ distanceToEdge, thresholds, dragStartTime, shouldUseTimeDampening }) => {
|
||||
const scroll = getValueFromDistance(distanceToEdge, thresholds);
|
||||
|
||||
// not enough distance to trigger a minimum scroll
|
||||
// we can bail here
|
||||
if (scroll === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Dampen an auto scroll speed based on duration of drag
|
||||
|
||||
if (!shouldUseTimeDampening) {
|
||||
return scroll;
|
||||
}
|
||||
|
||||
// Once we know an auto scroll should occur based on distance,
|
||||
// we must let at least 1px through to trigger a scroll event an
|
||||
// another auto scroll call
|
||||
|
||||
return Math.max(dampenValueByTime(scroll, dragStartTime), minScroll);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import getDistanceThresholds from "./get-distance-thresholds";
|
||||
import getValue from "./get-value";
|
||||
|
||||
export default ({ container, distanceToEdges, dragStartTime, axis, shouldUseTimeDampening }) => {
|
||||
const thresholds = getDistanceThresholds(container, axis);
|
||||
const isCloserToEnd = distanceToEdges[axis.end] < distanceToEdges[axis.start];
|
||||
if (isCloserToEnd) {
|
||||
return getValue({
|
||||
distanceToEdge: distanceToEdges[axis.end],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
}
|
||||
return (
|
||||
-1 *
|
||||
getValue({
|
||||
distanceToEdge: distanceToEdges[axis.start],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
// A scroll event will only be triggered when there is a value of at least 1px change
|
||||
export default 1;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { apply, isEqual, origin } from "../../../position";
|
||||
import getScrollOnAxis from "./get-scroll-on-axis";
|
||||
import adjustForSizeLimits from "./adjust-for-size-limits";
|
||||
import { horizontal, vertical } from "../../../axis";
|
||||
|
||||
// will replace -0 and replace with +0
|
||||
const clean = apply((value) => (value === 0 ? 0 : value));
|
||||
export default ({ dragStartTime, container, subject, center, shouldUseTimeDampening }) => {
|
||||
// get distance to each edge
|
||||
const distanceToEdges = {
|
||||
top: center.y - container.top,
|
||||
right: container.right - center.x,
|
||||
bottom: container.bottom - center.y,
|
||||
left: center.x - container.left
|
||||
};
|
||||
|
||||
// 1. Figure out which x,y values are the best target
|
||||
// 2. Can the container scroll in that direction at all?
|
||||
// If no for both directions, then return null
|
||||
// 3. Is the center close enough to a edge to start a drag?
|
||||
// 4. Based on the distance, calculate the speed at which a scroll should occur
|
||||
// The lower distance value the faster the scroll should be.
|
||||
// Maximum speed value should be hit before the distance is 0
|
||||
// Negative values to not continue to increase the speed
|
||||
const y = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: vertical,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
const x = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: horizontal,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
const required = clean({
|
||||
x,
|
||||
y
|
||||
});
|
||||
|
||||
// nothing required
|
||||
if (isEqual(required, origin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// need to not scroll in a direction that we are too big to scroll in
|
||||
const limited = adjustForSizeLimits({
|
||||
container,
|
||||
subject,
|
||||
proposedScroll: required
|
||||
});
|
||||
if (!limited) {
|
||||
return null;
|
||||
}
|
||||
return isEqual(limited, origin) ? null : limited;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import getScroll from "./get-scroll";
|
||||
import { canScrollWindow } from "../can-scroll";
|
||||
|
||||
export default ({ viewport, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||
const scroll = getScroll({
|
||||
dragStartTime,
|
||||
container: viewport.frame,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
return scroll && canScrollWindow(viewport, scroll) ? scroll : null;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import rafSchd from "raf-schd";
|
||||
import scroll from "./scroll";
|
||||
import { invariant } from "../../../invariant";
|
||||
import * as timings from "../../../debug/timings";
|
||||
|
||||
export default ({ scrollWindow, scrollDroppable }) => {
|
||||
const scheduleWindowScroll = rafSchd(scrollWindow);
|
||||
const scheduleDroppableScroll = rafSchd(scrollDroppable);
|
||||
let dragging = null;
|
||||
const tryScroll = (state) => {
|
||||
invariant(dragging, "Cannot fluid scroll if not dragging");
|
||||
const { shouldUseTimeDampening, dragStartTime } = dragging;
|
||||
scroll({
|
||||
state,
|
||||
scrollWindow: scheduleWindowScroll,
|
||||
scrollDroppable: scheduleDroppableScroll,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
};
|
||||
const start = (state) => {
|
||||
timings.start("starting fluid scroller");
|
||||
invariant(!dragging, "Cannot start auto scrolling when already started");
|
||||
const dragStartTime = Date.now();
|
||||
let wasScrollNeeded = false;
|
||||
const fakeScrollCallback = () => {
|
||||
wasScrollNeeded = true;
|
||||
};
|
||||
scroll({
|
||||
state,
|
||||
dragStartTime: 0,
|
||||
shouldUseTimeDampening: false,
|
||||
scrollWindow: fakeScrollCallback,
|
||||
scrollDroppable: fakeScrollCallback
|
||||
});
|
||||
dragging = {
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening: wasScrollNeeded
|
||||
};
|
||||
timings.finish("starting fluid scroller");
|
||||
|
||||
// we know an auto scroll is needed - let's do it!
|
||||
if (wasScrollNeeded) {
|
||||
tryScroll(state);
|
||||
}
|
||||
};
|
||||
const stop = () => {
|
||||
// can be called defensively
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
scheduleWindowScroll.cancel();
|
||||
scheduleDroppableScroll.cancel();
|
||||
dragging = null;
|
||||
};
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
scroll: tryScroll
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import getBestScrollableDroppable from "./get-best-scrollable-droppable";
|
||||
import whatIsDraggedOver from "../../droppable/what-is-dragged-over";
|
||||
import getWindowScrollChange from "./get-window-scroll-change";
|
||||
import getDroppableScrollChange from "./get-droppable-scroll-change";
|
||||
|
||||
export default ({ state, dragStartTime, shouldUseTimeDampening, scrollWindow, scrollDroppable }) => {
|
||||
const center = state.current.page.borderBoxCenter;
|
||||
const draggable = state.dimensions.draggables[state.critical.draggable.id];
|
||||
const subject = draggable.page.marginBox;
|
||||
// 1. Can we scroll the viewport?
|
||||
if (state.isWindowScrollAllowed) {
|
||||
const viewport = state.viewport;
|
||||
const change = getWindowScrollChange({
|
||||
dragStartTime,
|
||||
viewport,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
if (change) {
|
||||
scrollWindow(change);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const droppable = getBestScrollableDroppable({
|
||||
center,
|
||||
destination: whatIsDraggedOver(state.impact),
|
||||
droppables: state.dimensions.droppables
|
||||
});
|
||||
if (!droppable) {
|
||||
return;
|
||||
}
|
||||
const change = getDroppableScrollChange({
|
||||
dragStartTime,
|
||||
droppable,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
if (change) {
|
||||
scrollDroppable(droppable.descriptor.id, change);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import createFluidScroller from "./fluid-scroller";
|
||||
import createJumpScroller from "./jump-scroller";
|
||||
|
||||
export default ({ scrollDroppable, scrollWindow, move }) => {
|
||||
const fluidScroller = createFluidScroller({
|
||||
scrollWindow,
|
||||
scrollDroppable
|
||||
});
|
||||
const jumpScroll = createJumpScroller({
|
||||
move,
|
||||
scrollWindow,
|
||||
scrollDroppable
|
||||
});
|
||||
const scroll = (state) => {
|
||||
// Only allowing auto scrolling in the DRAGGING phase
|
||||
if (state.phase !== "DRAGGING") {
|
||||
return;
|
||||
}
|
||||
if (state.movementMode === "FLUID") {
|
||||
fluidScroller.scroll(state);
|
||||
return;
|
||||
}
|
||||
if (!state.scrollJumpRequest) {
|
||||
return;
|
||||
}
|
||||
jumpScroll(state);
|
||||
};
|
||||
const scroller = {
|
||||
scroll,
|
||||
start: fluidScroller.start,
|
||||
stop: fluidScroller.stop
|
||||
};
|
||||
return scroller;
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { invariant } from "../../invariant";
|
||||
import { add, subtract } from "../position";
|
||||
import { canScrollWindow, canScrollDroppable, getWindowOverlap, getDroppableOverlap } from "./can-scroll";
|
||||
import whatIsDraggedOver from "../droppable/what-is-dragged-over";
|
||||
|
||||
export default ({ move, scrollDroppable, scrollWindow }) => {
|
||||
const moveByOffset = (state, offset) => {
|
||||
const client = add(state.current.client.selection, offset);
|
||||
move({
|
||||
client
|
||||
});
|
||||
};
|
||||
const scrollDroppableAsMuchAsItCan = (droppable, change) => {
|
||||
// Droppable cannot absorb any of the scroll
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return change;
|
||||
}
|
||||
const overlap = getDroppableOverlap(droppable, change);
|
||||
|
||||
// Droppable can absorb the entire change
|
||||
if (!overlap) {
|
||||
scrollDroppable(droppable.descriptor.id, change);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Droppable can only absorb a part of the change
|
||||
const whatTheDroppableCanScroll = subtract(change, overlap);
|
||||
scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll);
|
||||
const remainder = subtract(change, whatTheDroppableCanScroll);
|
||||
return remainder;
|
||||
};
|
||||
const scrollWindowAsMuchAsItCan = (isWindowScrollAllowed, viewport, change) => {
|
||||
if (!isWindowScrollAllowed) {
|
||||
return change;
|
||||
}
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
// window cannot absorb any of the scroll
|
||||
return change;
|
||||
}
|
||||
const overlap = getWindowOverlap(viewport, change);
|
||||
|
||||
// window can absorb entire scroll
|
||||
if (!overlap) {
|
||||
scrollWindow(change);
|
||||
return null;
|
||||
}
|
||||
|
||||
// window can only absorb a part of the scroll
|
||||
const whatTheWindowCanScroll = subtract(change, overlap);
|
||||
scrollWindow(whatTheWindowCanScroll);
|
||||
const remainder = subtract(change, whatTheWindowCanScroll);
|
||||
return remainder;
|
||||
};
|
||||
const jumpScroller = (state) => {
|
||||
const request = state.scrollJumpRequest;
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
const destination = whatIsDraggedOver(state.impact);
|
||||
invariant(destination, "Cannot perform a jump scroll when there is no destination");
|
||||
|
||||
// 1. We scroll the droppable first if we can to avoid the draggable
|
||||
// leaving the list
|
||||
|
||||
const droppableRemainder = scrollDroppableAsMuchAsItCan(state.dimensions.droppables[destination], request);
|
||||
|
||||
// droppable absorbed the entire scroll
|
||||
if (!droppableRemainder) {
|
||||
return;
|
||||
}
|
||||
const viewport = state.viewport;
|
||||
const windowRemainder = scrollWindowAsMuchAsItCan(state.isWindowScrollAllowed, viewport, droppableRemainder);
|
||||
|
||||
// window could absorb all the droppable remainder
|
||||
if (!windowRemainder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The entire scroll could not be absorbed by the droppable and window
|
||||
// so we manually move whatever is left
|
||||
moveByOffset(state, windowRemainder);
|
||||
};
|
||||
return jumpScroller;
|
||||
};
|
||||
34
client/src/components/trello-board/dnd/lib/state/axis.js
Normal file
34
client/src/components/trello-board/dnd/lib/state/axis.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export const vertical = {
|
||||
direction: "vertical",
|
||||
line: "y",
|
||||
crossAxisLine: "x",
|
||||
start: "top",
|
||||
end: "bottom",
|
||||
size: "height",
|
||||
crossAxisStart: "left",
|
||||
crossAxisEnd: "right",
|
||||
crossAxisSize: "width"
|
||||
};
|
||||
export const horizontal = {
|
||||
direction: "horizontal",
|
||||
line: "x",
|
||||
crossAxisLine: "y",
|
||||
start: "left",
|
||||
end: "right",
|
||||
size: "width",
|
||||
crossAxisStart: "top",
|
||||
crossAxisEnd: "bottom",
|
||||
crossAxisSize: "height"
|
||||
};
|
||||
export const grid = {
|
||||
direction: "horizontal",
|
||||
grid: true,
|
||||
line: "x",
|
||||
crossAxisLine: "y",
|
||||
start: "left",
|
||||
end: "right",
|
||||
size: "width",
|
||||
crossAxisStart: "top",
|
||||
crossAxisEnd: "bottom",
|
||||
crossAxisSize: "height"
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import removeDraggableFromList from "../remove-draggable-from-list";
|
||||
import isHomeOf from "../droppable/is-home-of";
|
||||
import { emptyGroups } from "../no-impact";
|
||||
import { find } from "../../native-with-fallback";
|
||||
import getDisplacementGroups from "../get-displacement-groups";
|
||||
|
||||
function getIndexOfLastItem(draggables, options) {
|
||||
if (!draggables.length) {
|
||||
return 0;
|
||||
}
|
||||
const indexOfLastItem = draggables[draggables.length - 1].descriptor.index;
|
||||
|
||||
// When in a foreign list there will be an additional one item in the list
|
||||
return options.inHomeList ? indexOfLastItem : indexOfLastItem + 1;
|
||||
}
|
||||
|
||||
function goAtEnd({ insideDestination, inHomeList, displacedBy, destination }) {
|
||||
const newIndex = getIndexOfLastItem(insideDestination, {
|
||||
inHomeList
|
||||
});
|
||||
return {
|
||||
displaced: emptyGroups,
|
||||
displacedBy,
|
||||
at: {
|
||||
type: "REORDER",
|
||||
destination: {
|
||||
droppableId: destination.descriptor.id,
|
||||
index: newIndex
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function calculateReorderImpact({
|
||||
draggable,
|
||||
insideDestination,
|
||||
destination,
|
||||
viewport,
|
||||
displacedBy,
|
||||
last,
|
||||
index,
|
||||
forceShouldAnimate
|
||||
}) {
|
||||
const inHomeList = isHomeOf(draggable, destination);
|
||||
|
||||
// Go into last spot of list
|
||||
if (index == null) {
|
||||
return goAtEnd({
|
||||
insideDestination,
|
||||
inHomeList,
|
||||
displacedBy,
|
||||
destination
|
||||
});
|
||||
}
|
||||
|
||||
// this might be the dragging item
|
||||
const match = find(insideDestination, (item) => item.descriptor.index === index);
|
||||
if (!match) {
|
||||
return goAtEnd({
|
||||
insideDestination,
|
||||
inHomeList,
|
||||
displacedBy,
|
||||
destination
|
||||
});
|
||||
}
|
||||
const withoutDragging = removeDraggableFromList(draggable, insideDestination);
|
||||
const sliceFrom = insideDestination.indexOf(match);
|
||||
const impacted = withoutDragging.slice(sliceFrom);
|
||||
const displaced = getDisplacementGroups({
|
||||
afterDragging: impacted,
|
||||
destination,
|
||||
displacedBy,
|
||||
last,
|
||||
viewport: viewport.frame,
|
||||
forceShouldAnimate
|
||||
});
|
||||
return {
|
||||
displaced,
|
||||
displacedBy,
|
||||
at: {
|
||||
type: "REORDER",
|
||||
destination: {
|
||||
droppableId: destination.descriptor.id,
|
||||
index
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export default (state, id) => {
|
||||
// Ready to go!
|
||||
if (state.phase === "IDLE") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Can lift depending on the type of drop animation
|
||||
if (state.phase !== "DROP_ANIMATING") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// - For a user drop we allow the user to drag other Draggables
|
||||
// immediately as items are most likely already in their home
|
||||
// - For a cancel items will be moving back to their original position
|
||||
// as such it is a cleaner experience to block them from dragging until
|
||||
// the drop animation is complete. Otherwise they will be grabbing
|
||||
// items not in their original position which can lead to bad visuals
|
||||
// Not allowing dragging of the dropping draggable
|
||||
if (state.completed.result.draggableId === id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if dropping - allow lifting
|
||||
// if cancelling - disallow lifting
|
||||
return state.completed.result.reason === "DROP";
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { applyMiddleware, createStore, compose } from "redux";
|
||||
import reducer from "./reducer";
|
||||
import lift from "./middleware/lift";
|
||||
import style from "./middleware/style";
|
||||
import drop from "./middleware/drop/drop-middleware";
|
||||
import scrollListener from "./middleware/scroll-listener";
|
||||
import responders from "./middleware/responders/responders-middleware";
|
||||
import dropAnimationFinish from "./middleware/drop/drop-animation-finish-middleware";
|
||||
import dropAnimationFlushOnScroll from "./middleware/drop/drop-animation-flush-on-scroll-middleware";
|
||||
import dimensionMarshalStopper from "./middleware/dimension-marshal-stopper";
|
||||
import focus from "./middleware/focus";
|
||||
import autoScroll from "./middleware/auto-scroll";
|
||||
import pendingDrop from "./middleware/pending-drop";
|
||||
// We are checking if window is available before using it.
|
||||
// This is needed for universal apps that render the component server side.
|
||||
// Details: https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup
|
||||
const composeEnhancers =
|
||||
process.env.NODE_ENV !== "production" && typeof window !== "undefined" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||
name: "react-beautiful-dnd"
|
||||
})
|
||||
: compose;
|
||||
export default ({ dimensionMarshal, focusMarshal, styleMarshal, getResponders, announce, autoScroller }) =>
|
||||
createStore(
|
||||
reducer,
|
||||
composeEnhancers(
|
||||
applyMiddleware(
|
||||
// ## Debug middleware
|
||||
|
||||
// > uncomment to use
|
||||
// debugging logger
|
||||
// require('../debug/middleware/log').default('light'),
|
||||
// // user timing api
|
||||
// require('../debug/middleware/user-timing').default,
|
||||
// debugging timer
|
||||
// require('../debug/middleware/action-timing').default,
|
||||
// average action timer
|
||||
// require('../debug/middleware/action-timing-average').default(200),
|
||||
|
||||
// ## Application middleware
|
||||
|
||||
// Style updates do not cause more actions. It is important to update styles
|
||||
// before responders are called: specifically the onDragEnd responder. We need to clear
|
||||
// the transition styles off the elements before a reorder to prevent strange
|
||||
// post drag animations in firefox. Even though we clear the transition off
|
||||
// a Draggable - if it is done after a reorder firefox will still apply the
|
||||
// transition.
|
||||
// Must be called before dimension marshal for lifting to apply collecting styles
|
||||
style(styleMarshal),
|
||||
// Stop the dimension marshal collecting anything
|
||||
// when moving into a phase where collection is no longer needed.
|
||||
// We need to stop the marshal before responders fire as responders can cause
|
||||
// dimension registration changes in response to reordering
|
||||
dimensionMarshalStopper(dimensionMarshal),
|
||||
// Fire application responders in response to drag changes
|
||||
lift(dimensionMarshal),
|
||||
drop,
|
||||
// When a drop animation finishes - fire a drop complete
|
||||
dropAnimationFinish,
|
||||
dropAnimationFlushOnScroll,
|
||||
pendingDrop,
|
||||
autoScroll(autoScroller),
|
||||
scrollListener,
|
||||
focus(focusMarshal),
|
||||
// Fire responders for consumers (after update to store)
|
||||
responders(getResponders, announce)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function didStartAfterCritical(draggableId, afterCritical) {
|
||||
return Boolean(afterCritical.effected[draggableId]);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { invariant } from "../../invariant";
|
||||
import createPublisher from "./while-dragging-publisher";
|
||||
import getInitialPublish from "./get-initial-publish";
|
||||
import { warning } from "../../dev-warning";
|
||||
|
||||
function shouldPublishUpdate(registry, dragging, entry) {
|
||||
// do not publish updates for the critical draggable
|
||||
if (entry.descriptor.id === dragging.id) {
|
||||
return false;
|
||||
}
|
||||
// do not publish updates for draggables that are not of a type that we care about
|
||||
if (entry.descriptor.type !== dragging.type) {
|
||||
return false;
|
||||
}
|
||||
const home = registry.droppable.getById(entry.descriptor.droppableId);
|
||||
if (home.descriptor.mode !== "virtual") {
|
||||
warning(`
|
||||
You are attempting to add or remove a Draggable [id: ${entry.descriptor.id}]
|
||||
while a drag is occurring. This is only supported for virtual lists.
|
||||
|
||||
See https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/virtual-lists.md
|
||||
`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default (registry, callbacks) => {
|
||||
let collection = null;
|
||||
const publisher = createPublisher({
|
||||
callbacks: {
|
||||
publish: callbacks.publishWhileDragging,
|
||||
collectionStarting: callbacks.collectionStarting
|
||||
},
|
||||
registry
|
||||
});
|
||||
const updateDroppableIsEnabled = (id, isEnabled) => {
|
||||
invariant(
|
||||
registry.droppable.exists(id),
|
||||
`Cannot update is enabled flag of Droppable ${id} as it is not registered`
|
||||
);
|
||||
|
||||
// no need to update the application state if a collection is not occurring
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point a non primary droppable dimension might not yet be published
|
||||
// but may have its enabled state changed. For now we still publish this change
|
||||
// and let the reducer exit early if it cannot find the dimension in the state.
|
||||
callbacks.updateDroppableIsEnabled({
|
||||
id,
|
||||
isEnabled
|
||||
});
|
||||
};
|
||||
const updateDroppableIsCombineEnabled = (id, isCombineEnabled) => {
|
||||
// no need to update
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
invariant(
|
||||
registry.droppable.exists(id),
|
||||
`Cannot update isCombineEnabled flag of Droppable ${id} as it is not registered`
|
||||
);
|
||||
callbacks.updateDroppableIsCombineEnabled({
|
||||
id,
|
||||
isCombineEnabled
|
||||
});
|
||||
};
|
||||
const updateDroppableScroll = (id, newScroll) => {
|
||||
// no need to update the application state if a collection is not occurring
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
invariant(registry.droppable.exists(id), `Cannot update the scroll on Droppable ${id} as it is not registered`);
|
||||
callbacks.updateDroppableScroll({
|
||||
id,
|
||||
newScroll
|
||||
});
|
||||
};
|
||||
const scrollDroppable = (id, change) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
registry.droppable.getById(id).callbacks.scroll(change);
|
||||
};
|
||||
const stopPublishing = () => {
|
||||
// This function can be called defensively
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
// Stop any pending dom collections or publish
|
||||
publisher.stop();
|
||||
|
||||
// Tell all droppables to stop watching scroll
|
||||
// all good if they where not already listening
|
||||
const home = collection.critical.droppable;
|
||||
registry.droppable.getAllByType(home.type).forEach((entry) => entry.callbacks.dragStopped());
|
||||
|
||||
// Unsubscribe from registry updates
|
||||
collection.unsubscribe();
|
||||
// Finally - clear our collection
|
||||
collection = null;
|
||||
};
|
||||
const subscriber = (event) => {
|
||||
invariant(collection, "Should only be subscribed when a collection is occurring");
|
||||
// The dragging item can be add and removed when using a clone
|
||||
// We do not publish updates for the critical item
|
||||
const dragging = collection.critical.draggable;
|
||||
if (event.type === "ADDITION") {
|
||||
if (shouldPublishUpdate(registry, dragging, event.value)) {
|
||||
publisher.add(event.value);
|
||||
}
|
||||
}
|
||||
if (event.type === "REMOVAL") {
|
||||
if (shouldPublishUpdate(registry, dragging, event.value)) {
|
||||
publisher.remove(event.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
const startPublishing = (request) => {
|
||||
invariant(!collection, "Cannot start capturing critical dimensions as there is already a collection");
|
||||
const entry = registry.draggable.getById(request.draggableId);
|
||||
const home = registry.droppable.getById(entry.descriptor.droppableId);
|
||||
const critical = {
|
||||
draggable: entry.descriptor,
|
||||
droppable: home.descriptor
|
||||
};
|
||||
const unsubscribe = registry.subscribe(subscriber);
|
||||
collection = {
|
||||
critical,
|
||||
unsubscribe
|
||||
};
|
||||
return getInitialPublish({
|
||||
critical,
|
||||
registry,
|
||||
scrollOptions: request.scrollOptions
|
||||
});
|
||||
};
|
||||
const marshal = {
|
||||
// Droppable changes
|
||||
updateDroppableIsEnabled,
|
||||
updateDroppableIsCombineEnabled,
|
||||
scrollDroppable,
|
||||
updateDroppableScroll,
|
||||
// Entry
|
||||
startPublishing,
|
||||
stopPublishing
|
||||
};
|
||||
return marshal;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as timings from "../../debug/timings";
|
||||
import { toDraggableMap, toDroppableMap } from "../dimension-structures";
|
||||
import getViewport from "../../view/window/get-viewport";
|
||||
|
||||
export default ({ critical, scrollOptions, registry }) => {
|
||||
const timingKey = "Initial collection from DOM";
|
||||
timings.start(timingKey);
|
||||
const viewport = getViewport();
|
||||
const windowScroll = viewport.scroll.current;
|
||||
const home = critical.droppable;
|
||||
const droppables = registry.droppable
|
||||
.getAllByType(home.type)
|
||||
.map((entry) => entry.callbacks.getDimensionAndWatchScroll(windowScroll, scrollOptions));
|
||||
const draggables = registry.draggable
|
||||
.getAllByType(critical.draggable.type)
|
||||
.map((entry) => entry.getDimension(windowScroll));
|
||||
const dimensions = {
|
||||
draggables: toDraggableMap(draggables),
|
||||
droppables: toDroppableMap(droppables)
|
||||
};
|
||||
timings.finish(timingKey);
|
||||
const result = {
|
||||
dimensions,
|
||||
critical,
|
||||
viewport
|
||||
};
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as timings from "../../debug/timings";
|
||||
import { origin } from "../position";
|
||||
|
||||
const clean = () => ({
|
||||
additions: {},
|
||||
removals: {},
|
||||
modified: {}
|
||||
});
|
||||
const timingKey = "Publish collection from DOM";
|
||||
export default function createPublisher({ registry, callbacks }) {
|
||||
let staging = clean();
|
||||
let frameId = null;
|
||||
const collect = () => {
|
||||
if (frameId) {
|
||||
return;
|
||||
}
|
||||
callbacks.collectionStarting();
|
||||
frameId = requestAnimationFrame(() => {
|
||||
frameId = null;
|
||||
timings.start(timingKey);
|
||||
const { additions, removals, modified } = staging;
|
||||
const added = Object.keys(additions)
|
||||
.map(
|
||||
// Using the origin as the window scroll. This will be adjusted when processing the published values
|
||||
(id) => registry.draggable.getById(id).getDimension(origin)
|
||||
)
|
||||
// Dimensions are not guarenteed to be ordered in the same order as keys
|
||||
// So we need to sort them so they are in the correct order
|
||||
.sort((a, b) => a.descriptor.index - b.descriptor.index);
|
||||
const updated = Object.keys(modified).map((id) => {
|
||||
const entry = registry.droppable.getById(id);
|
||||
const scroll = entry.callbacks.getScrollWhileDragging();
|
||||
return {
|
||||
droppableId: id,
|
||||
scroll
|
||||
};
|
||||
});
|
||||
const result = {
|
||||
additions: added,
|
||||
removals: Object.keys(removals),
|
||||
modified: updated
|
||||
};
|
||||
staging = clean();
|
||||
timings.finish(timingKey);
|
||||
callbacks.publish(result);
|
||||
});
|
||||
};
|
||||
const add = (entry) => {
|
||||
const id = entry.descriptor.id;
|
||||
staging.additions[id] = entry;
|
||||
staging.modified[entry.descriptor.droppableId] = true;
|
||||
if (staging.removals[id]) {
|
||||
delete staging.removals[id];
|
||||
}
|
||||
collect();
|
||||
};
|
||||
const remove = (entry) => {
|
||||
const descriptor = entry.descriptor;
|
||||
staging.removals[descriptor.id] = true;
|
||||
staging.modified[descriptor.droppableId] = true;
|
||||
if (staging.additions[descriptor.id]) {
|
||||
delete staging.additions[descriptor.id];
|
||||
}
|
||||
collect();
|
||||
};
|
||||
const stop = () => {
|
||||
if (!frameId) {
|
||||
return;
|
||||
}
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = null;
|
||||
staging = clean();
|
||||
};
|
||||
return {
|
||||
add,
|
||||
remove,
|
||||
stop
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { values } from "../native-with-fallback";
|
||||
|
||||
export const toDroppableMap = memoizeOne((droppables) =>
|
||||
droppables.reduce((previous, current) => {
|
||||
previous[current.descriptor.id] = current;
|
||||
return previous;
|
||||
}, {})
|
||||
);
|
||||
export const toDraggableMap = memoizeOne((draggables) =>
|
||||
draggables.reduce((previous, current) => {
|
||||
previous[current.descriptor.id] = current;
|
||||
return previous;
|
||||
}, {})
|
||||
);
|
||||
export const toDroppableList = memoizeOne((droppables) => values(droppables));
|
||||
export const toDraggableList = memoizeOne((draggables) => values(draggables));
|
||||
@@ -0,0 +1,56 @@
|
||||
import { grid, horizontal, vertical } from "../axis";
|
||||
import { origin } from "../position";
|
||||
import getMaxScroll from "../get-max-scroll";
|
||||
import getSubject from "./util/get-subject";
|
||||
|
||||
export default ({ descriptor, isEnabled, isCombineEnabled, isFixedOnPage, direction, client, page, closest }) => {
|
||||
const frame = (() => {
|
||||
if (!closest) {
|
||||
return null;
|
||||
}
|
||||
const { scrollSize, client: frameClient } = closest;
|
||||
|
||||
// scrollHeight and scrollWidth are based on the padding box
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||
const maxScroll = getMaxScroll({
|
||||
scrollHeight: scrollSize.scrollHeight,
|
||||
scrollWidth: scrollSize.scrollWidth,
|
||||
height: frameClient.paddingBox.height,
|
||||
width: frameClient.paddingBox.width
|
||||
});
|
||||
return {
|
||||
pageMarginBox: closest.page.marginBox,
|
||||
frameClient,
|
||||
scrollSize,
|
||||
shouldClipSubject: closest.shouldClipSubject,
|
||||
scroll: {
|
||||
initial: closest.scroll,
|
||||
current: closest.scroll,
|
||||
max: maxScroll,
|
||||
diff: {
|
||||
value: origin,
|
||||
displacement: origin
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
const axis = direction === "vertical" ? vertical : direction === "grid" ? grid : horizontal;
|
||||
const subject = getSubject({
|
||||
page,
|
||||
withPlaceholder: null,
|
||||
axis,
|
||||
frame
|
||||
});
|
||||
const dimension = {
|
||||
descriptor,
|
||||
isCombineEnabled,
|
||||
isFixedOnPage,
|
||||
axis,
|
||||
isEnabled,
|
||||
client,
|
||||
page,
|
||||
frame,
|
||||
subject
|
||||
};
|
||||
return dimension;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export default (draggable, destination) => draggable.descriptor.droppableId === destination.descriptor.id;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { invariant } from "../../invariant";
|
||||
import { negate, subtract } from "../position";
|
||||
import getSubject from "./util/get-subject";
|
||||
|
||||
export default (droppable, newScroll) => {
|
||||
invariant(droppable.frame);
|
||||
const scrollable = droppable.frame;
|
||||
const scrollDiff = subtract(newScroll, scrollable.scroll.initial);
|
||||
// a positive scroll difference leads to a negative displacement
|
||||
// (scrolling down pulls an item upwards)
|
||||
const scrollDisplacement = negate(scrollDiff);
|
||||
|
||||
// Sometimes it is possible to scroll beyond the max point.
|
||||
// This can occur when scrolling a foreign list that now has a placeholder.
|
||||
|
||||
const frame = {
|
||||
...scrollable,
|
||||
scroll: {
|
||||
initial: scrollable.scroll.initial,
|
||||
current: newScroll,
|
||||
diff: {
|
||||
value: scrollDiff,
|
||||
displacement: scrollDisplacement
|
||||
},
|
||||
// TODO: rename 'softMax?'
|
||||
max: scrollable.scroll.max
|
||||
}
|
||||
};
|
||||
const subject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: droppable.subject.withPlaceholder,
|
||||
axis: droppable.axis,
|
||||
frame
|
||||
});
|
||||
const result = {
|
||||
...droppable,
|
||||
frame,
|
||||
subject
|
||||
};
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import whatIsDraggedOver from "./what-is-dragged-over";
|
||||
|
||||
// use placeholder if dragged over
|
||||
export default (descriptor, impact) => whatIsDraggedOver(impact) === descriptor.droppableId;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getRect } from "css-box-model";
|
||||
|
||||
export default (frame, subject) => {
|
||||
const result = getRect({
|
||||
top: Math.max(subject.top, frame.top),
|
||||
right: Math.min(subject.right, frame.right),
|
||||
bottom: Math.min(subject.bottom, frame.bottom),
|
||||
left: Math.max(subject.left, frame.left)
|
||||
});
|
||||
if (result.width <= 0 || result.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getRect } from "css-box-model";
|
||||
import executeClip from "./clip";
|
||||
import { offsetByPosition } from "../../spacing";
|
||||
|
||||
const scroll = (target, frame) => {
|
||||
if (!frame) {
|
||||
return target;
|
||||
}
|
||||
return offsetByPosition(target, frame.scroll.diff.displacement);
|
||||
};
|
||||
const increase = (target, axis, withPlaceholder) => {
|
||||
if (withPlaceholder && withPlaceholder.increasedBy) {
|
||||
return {
|
||||
...target,
|
||||
[axis.end]: target[axis.end] + withPlaceholder.increasedBy[axis.line]
|
||||
};
|
||||
}
|
||||
return target;
|
||||
};
|
||||
const clip = (target, frame) => {
|
||||
if (frame && frame.shouldClipSubject) {
|
||||
return executeClip(frame.pageMarginBox, target);
|
||||
}
|
||||
return getRect(target);
|
||||
};
|
||||
export default ({ page, withPlaceholder, axis, frame }) => {
|
||||
const scrolled = scroll(page.marginBox, frame);
|
||||
const increased = increase(scrolled, axis, withPlaceholder);
|
||||
const clipped = clip(increased, frame);
|
||||
return {
|
||||
page,
|
||||
withPlaceholder,
|
||||
active: clipped
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default (result) => {
|
||||
const { combine, destination } = result;
|
||||
if (destination) {
|
||||
return destination.droppableId;
|
||||
}
|
||||
if (combine) {
|
||||
return combine.droppableId;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default (impact) => {
|
||||
const at = impact.at;
|
||||
if (!at) {
|
||||
return null;
|
||||
}
|
||||
if (at.type === "REORDER") {
|
||||
return at.destination.droppableId;
|
||||
}
|
||||
return at.combine.droppableId;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { invariant } from "../../invariant";
|
||||
import getDraggablesInsideDroppable from "../get-draggables-inside-droppable";
|
||||
import { add, patch } from "../position";
|
||||
import getSubject from "./util/get-subject";
|
||||
import isHomeOf from "./is-home-of";
|
||||
import getDisplacedBy from "../get-displaced-by";
|
||||
|
||||
const getRequiredGrowthForPlaceholder = (droppable, placeholderSize, draggables) => {
|
||||
const axis = droppable.axis;
|
||||
|
||||
// A virtual list will most likely not contain all of the Draggables
|
||||
// so counting them does not help.
|
||||
if (droppable.descriptor.mode === "virtual") {
|
||||
return patch(axis.line, placeholderSize[axis.line]);
|
||||
}
|
||||
|
||||
// TODO: consider margin collapsing?
|
||||
// Using contentBox as that is where the Draggables will sit
|
||||
const availableSpace = droppable.subject.page.contentBox[axis.size];
|
||||
const insideDroppable = getDraggablesInsideDroppable(droppable.descriptor.id, draggables);
|
||||
const spaceUsed = insideDroppable.reduce((sum, dimension) => sum + dimension.client.marginBox[axis.size], 0);
|
||||
const requiredSpace = spaceUsed + placeholderSize[axis.line];
|
||||
const needsToGrowBy = requiredSpace - availableSpace;
|
||||
|
||||
// nothing to do here
|
||||
if (needsToGrowBy <= 0) {
|
||||
return null;
|
||||
}
|
||||
return patch(axis.line, needsToGrowBy);
|
||||
};
|
||||
const withMaxScroll = (frame, max) => ({
|
||||
...frame,
|
||||
scroll: {
|
||||
...frame.scroll,
|
||||
max
|
||||
}
|
||||
});
|
||||
export const addPlaceholder = (droppable, draggable, draggables) => {
|
||||
const frame = droppable.frame;
|
||||
invariant(!isHomeOf(draggable, droppable), "Should not add placeholder space to home list");
|
||||
invariant(!droppable.subject.withPlaceholder, "Cannot add placeholder size to a subject when it already has one");
|
||||
const placeholderSize = getDisplacedBy(droppable.axis, draggable.displaceBy).point;
|
||||
const requiredGrowth = getRequiredGrowthForPlaceholder(droppable, placeholderSize, draggables);
|
||||
const added = {
|
||||
placeholderSize,
|
||||
increasedBy: requiredGrowth,
|
||||
oldFrameMaxScroll: droppable.frame ? droppable.frame.scroll.max : null
|
||||
};
|
||||
if (!frame) {
|
||||
const subject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: added,
|
||||
axis: droppable.axis,
|
||||
frame: droppable.frame
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject
|
||||
};
|
||||
}
|
||||
const maxScroll = requiredGrowth ? add(frame.scroll.max, requiredGrowth) : frame.scroll.max;
|
||||
const newFrame = withMaxScroll(frame, maxScroll);
|
||||
const subject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: added,
|
||||
axis: droppable.axis,
|
||||
frame: newFrame
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
frame: newFrame
|
||||
};
|
||||
};
|
||||
export const removePlaceholder = (droppable) => {
|
||||
const added = droppable.subject.withPlaceholder;
|
||||
invariant(added, "Cannot remove placeholder form subject when there was none");
|
||||
const frame = droppable.frame;
|
||||
if (!frame) {
|
||||
const subject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
axis: droppable.axis,
|
||||
frame: null,
|
||||
// cleared
|
||||
withPlaceholder: null
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject
|
||||
};
|
||||
}
|
||||
const oldMaxScroll = added.oldFrameMaxScroll;
|
||||
invariant(oldMaxScroll, "Expected droppable with frame to have old max frame scroll when removing placeholder");
|
||||
const newFrame = withMaxScroll(frame, oldMaxScroll);
|
||||
const subject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
axis: droppable.axis,
|
||||
frame: newFrame,
|
||||
// cleared
|
||||
withPlaceholder: null
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
frame: newFrame
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { add, subtract } from "../../position";
|
||||
import withViewportDisplacement from "../../with-scroll-change/with-viewport-displacement";
|
||||
|
||||
export default ({ pageBorderBoxCenter, draggable, viewport }) => {
|
||||
const withoutPageScrollChange = withViewportDisplacement(viewport, pageBorderBoxCenter);
|
||||
const offset = subtract(withoutPageScrollChange, draggable.page.borderBox.center);
|
||||
return add(draggable.client.borderBox.center, offset);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import getPageBorderBoxCenterFromImpact from "../get-page-border-box-center";
|
||||
import getClientFromPageBorderBoxCenter from "./get-client-from-page-border-box-center";
|
||||
|
||||
export default ({ impact, draggable, droppable, draggables, viewport, afterCritical }) => {
|
||||
const pageBorderBoxCenter = getPageBorderBoxCenterFromImpact({
|
||||
impact,
|
||||
draggable,
|
||||
draggables,
|
||||
droppable,
|
||||
afterCritical
|
||||
});
|
||||
return getClientFromPageBorderBoxCenter({
|
||||
pageBorderBoxCenter,
|
||||
draggable,
|
||||
viewport
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import whenCombining from "./when-combining";
|
||||
import whenReordering from "./when-reordering";
|
||||
import withDroppableDisplacement from "../../with-scroll-change/with-droppable-displacement";
|
||||
|
||||
const getResultWithoutDroppableDisplacement = ({ impact, draggable, droppable, draggables, afterCritical }) => {
|
||||
const original = draggable.page.borderBox.center;
|
||||
const at = impact.at;
|
||||
if (!droppable) {
|
||||
return original;
|
||||
}
|
||||
if (!at) {
|
||||
return original;
|
||||
}
|
||||
if (at.type === "REORDER") {
|
||||
return whenReordering({
|
||||
impact,
|
||||
draggable,
|
||||
draggables,
|
||||
droppable,
|
||||
afterCritical
|
||||
});
|
||||
}
|
||||
return whenCombining({
|
||||
impact,
|
||||
draggables,
|
||||
afterCritical
|
||||
});
|
||||
};
|
||||
export default (args) => {
|
||||
const withoutDisplacement = getResultWithoutDroppableDisplacement(args);
|
||||
const droppable = args.droppable;
|
||||
const withDisplacement = droppable ? withDroppableDisplacement(droppable, withoutDisplacement) : withoutDisplacement;
|
||||
return withDisplacement;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import { add } from "../../position";
|
||||
import getCombinedItemDisplacement from "../../get-combined-item-displacement";
|
||||
import { tryGetCombine } from "../../get-impact-location";
|
||||
// Returns the client offset required to move an item from its
|
||||
// original client position to its final resting position
|
||||
export default ({ afterCritical, impact, draggables }) => {
|
||||
const combine = tryGetCombine(impact);
|
||||
invariant(combine);
|
||||
const combineWith = combine.draggableId;
|
||||
const center = draggables[combineWith].page.borderBox.center;
|
||||
const displaceBy = getCombinedItemDisplacement({
|
||||
displaced: impact.displaced,
|
||||
afterCritical,
|
||||
combineWith,
|
||||
displacedBy: impact.displacedBy
|
||||
});
|
||||
return add(center, displaceBy);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { offset } from "css-box-model";
|
||||
import { goBefore, goAfter, goIntoStart } from "../move-relative-to";
|
||||
import getDraggablesInsideDroppable from "../../get-draggables-inside-droppable";
|
||||
import { negate } from "../../position";
|
||||
import didStartAfterCritical from "../../did-start-after-critical";
|
||||
// Returns the client offset required to move an item from its
|
||||
// original client position to its final resting position
|
||||
export default ({ impact, draggable, draggables, droppable, afterCritical }) => {
|
||||
const insideDestination = getDraggablesInsideDroppable(droppable.descriptor.id, draggables);
|
||||
const draggablePage = draggable.page;
|
||||
const axis = droppable.axis;
|
||||
|
||||
// this will only happen in a foreign list
|
||||
if (!insideDestination.length) {
|
||||
return goIntoStart({
|
||||
axis,
|
||||
moveInto: droppable.page,
|
||||
isMoving: draggablePage
|
||||
});
|
||||
}
|
||||
const { displaced, displacedBy } = impact;
|
||||
const closestAfter = displaced.all[0];
|
||||
|
||||
// go before the first displaced item
|
||||
// items can only be displaced forwards
|
||||
if (closestAfter) {
|
||||
const closest = draggables[closestAfter];
|
||||
// want to go before where it would be with the displacement
|
||||
|
||||
// target is displaced and is already in it's starting position
|
||||
if (didStartAfterCritical(closestAfter, afterCritical)) {
|
||||
return goBefore({
|
||||
axis,
|
||||
moveRelativeTo: closest.page,
|
||||
isMoving: draggablePage
|
||||
});
|
||||
}
|
||||
|
||||
// target has been displaced during the drag and it is not in its starting position
|
||||
// we need to account for the displacement
|
||||
const withDisplacement = offset(closest.page, displacedBy.point);
|
||||
return goBefore({
|
||||
axis,
|
||||
moveRelativeTo: withDisplacement,
|
||||
isMoving: draggablePage
|
||||
});
|
||||
}
|
||||
|
||||
// Nothing in list is displaced, we should go after the last item
|
||||
|
||||
const last = insideDestination[insideDestination.length - 1];
|
||||
|
||||
// we can just go into our original position if the last item
|
||||
// is the dragging item
|
||||
if (last.descriptor.id === draggable.descriptor.id) {
|
||||
return draggablePage.borderBox.center;
|
||||
}
|
||||
if (didStartAfterCritical(last.descriptor.id, afterCritical)) {
|
||||
// if the item started displaced and it is no longer displaced then
|
||||
// we need to go after it it's non-displaced position
|
||||
|
||||
const page = offset(last.page, negate(afterCritical.displacedBy.point));
|
||||
return goAfter({
|
||||
axis,
|
||||
moveRelativeTo: page,
|
||||
isMoving: draggablePage
|
||||
});
|
||||
}
|
||||
|
||||
// item is in its resting spot. we can go straight after it
|
||||
return goAfter({
|
||||
axis,
|
||||
moveRelativeTo: last.page,
|
||||
isMoving: draggablePage
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { patch } from "../position";
|
||||
|
||||
const distanceFromStartToBorderBoxCenter = (axis, box) => box.margin[axis.start] + box.borderBox[axis.size] / 2;
|
||||
const distanceFromEndToBorderBoxCenter = (axis, box) => box.margin[axis.end] + box.borderBox[axis.size] / 2;
|
||||
|
||||
// We align the moving item against the cross axis start of the target
|
||||
// We used to align the moving item cross axis center with the cross axis center of the target.
|
||||
// However, this leads to a bad experience when reordering columns
|
||||
const getCrossAxisBorderBoxCenter = (axis, target, isMoving) =>
|
||||
target[axis.crossAxisStart] + isMoving.margin[axis.crossAxisStart] + isMoving.borderBox[axis.crossAxisSize] / 2;
|
||||
export const goAfter = ({ axis, moveRelativeTo, isMoving }) =>
|
||||
patch(
|
||||
axis.line,
|
||||
// start measuring from the end of the target
|
||||
moveRelativeTo.marginBox[axis.end] + distanceFromStartToBorderBoxCenter(axis, isMoving),
|
||||
getCrossAxisBorderBoxCenter(axis, moveRelativeTo.marginBox, isMoving)
|
||||
);
|
||||
export const goBefore = ({ axis, moveRelativeTo, isMoving }) =>
|
||||
patch(
|
||||
axis.line,
|
||||
// start measuring from the start of the target
|
||||
moveRelativeTo.marginBox[axis.start] - distanceFromEndToBorderBoxCenter(axis, isMoving),
|
||||
getCrossAxisBorderBoxCenter(axis, moveRelativeTo.marginBox, isMoving)
|
||||
);
|
||||
// moves into the content box
|
||||
export const goIntoStart = ({ axis, moveInto, isMoving }) =>
|
||||
patch(
|
||||
axis.line,
|
||||
moveInto.contentBox[axis.start] + distanceFromStartToBorderBoxCenter(axis, isMoving),
|
||||
getCrossAxisBorderBoxCenter(axis, moveInto.contentBox, isMoving)
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { negate, origin } from "./position";
|
||||
import didStartAfterCritical from "./did-start-after-critical";
|
||||
|
||||
export default ({ displaced, afterCritical, combineWith, displacedBy }) => {
|
||||
const isDisplaced = Boolean(displaced.visible[combineWith] || displaced.invisible[combineWith]);
|
||||
if (didStartAfterCritical(combineWith, afterCritical)) {
|
||||
return isDisplaced ? origin : negate(displacedBy.point);
|
||||
}
|
||||
return isDisplaced ? displacedBy.point : origin;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { patch } from "./position";
|
||||
|
||||
// TODO: memoization needed?
|
||||
export default memoizeOne(function getDisplacedBy(axis, displaceBy) {
|
||||
const displacement = displaceBy[axis.line];
|
||||
return {
|
||||
value: displacement,
|
||||
point: patch(axis.line, displacement)
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { expand, getRect } from "css-box-model";
|
||||
import { isPartiallyVisible } from "./visibility/is-visible";
|
||||
|
||||
const getShouldAnimate = (id, last, forceShouldAnimate) => {
|
||||
// Use a forced value if provided
|
||||
if (typeof forceShouldAnimate === "boolean") {
|
||||
return forceShouldAnimate;
|
||||
}
|
||||
|
||||
// nothing to gauge animation from
|
||||
if (!last) {
|
||||
return true;
|
||||
}
|
||||
const { invisible, visible } = last;
|
||||
|
||||
// it was previously invisible - no animation
|
||||
if (invisible[id]) {
|
||||
return false;
|
||||
}
|
||||
const previous = visible[id];
|
||||
return previous ? previous.shouldAnimate : true;
|
||||
};
|
||||
|
||||
// Note: it is also an optimisation to not render the displacement on
|
||||
// items when they are not longer visible.
|
||||
// This prevents a lot of .render() calls when leaving / entering a list
|
||||
|
||||
function getTarget(draggable, displacedBy) {
|
||||
const marginBox = draggable.page.marginBox;
|
||||
|
||||
// ## Visibility overscanning
|
||||
// We are expanding rather than offsetting the marginBox.
|
||||
// In some cases we want
|
||||
// - the target based on the starting position (such as when dropping outside of any list)
|
||||
// - the target based on the items position without starting displacement (such as when moving inside a list)
|
||||
// To keep things simple we just expand the whole area for this check
|
||||
// The worst case is some minor redundant offscreen movements
|
||||
const expandBy = {
|
||||
// pull backwards into viewport
|
||||
top: displacedBy.point.y,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
// pull backwards into viewport
|
||||
left: displacedBy.point.x
|
||||
};
|
||||
return getRect(expand(marginBox, expandBy));
|
||||
}
|
||||
|
||||
export default function getDisplacementGroups({
|
||||
afterDragging,
|
||||
destination,
|
||||
displacedBy,
|
||||
viewport,
|
||||
forceShouldAnimate,
|
||||
last
|
||||
}) {
|
||||
return afterDragging.reduce(
|
||||
function process(groups, draggable) {
|
||||
const target = getTarget(draggable, displacedBy);
|
||||
const id = draggable.descriptor.id;
|
||||
groups.all.push(id);
|
||||
const isVisible = isPartiallyVisible({
|
||||
target,
|
||||
destination,
|
||||
viewport,
|
||||
withDroppableDisplacement: true
|
||||
});
|
||||
if (!isVisible) {
|
||||
groups.invisible[draggable.descriptor.id] = true;
|
||||
return groups;
|
||||
}
|
||||
|
||||
// item is visible
|
||||
|
||||
const shouldAnimate = getShouldAnimate(id, last, forceShouldAnimate);
|
||||
const displacement = {
|
||||
draggableId: id,
|
||||
shouldAnimate
|
||||
};
|
||||
groups.visible[id] = displacement;
|
||||
return groups;
|
||||
},
|
||||
{
|
||||
all: [],
|
||||
visible: {},
|
||||
invisible: {}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { find } from "../../native-with-fallback";
|
||||
import getDidStartAfterCritical from "../did-start-after-critical";
|
||||
import getDisplacedBy from "../get-displaced-by";
|
||||
import getIsDisplaced from "../get-is-displaced";
|
||||
import removeDraggableFromList from "../remove-draggable-from-list";
|
||||
// exported for testing
|
||||
export const combineThresholdDivisor = 4;
|
||||
export default ({
|
||||
draggable,
|
||||
pageBorderBoxWithDroppableScroll: targetRect,
|
||||
previousImpact,
|
||||
destination,
|
||||
insideDestination,
|
||||
afterCritical
|
||||
}) => {
|
||||
if (!destination.isCombineEnabled) {
|
||||
return null;
|
||||
}
|
||||
const axis = destination.axis;
|
||||
const displacedBy = getDisplacedBy(destination.axis, draggable.displaceBy);
|
||||
const displacement = displacedBy.value;
|
||||
const targetStart = targetRect[axis.start];
|
||||
const targetEnd = targetRect[axis.end];
|
||||
const withoutDragging = removeDraggableFromList(draggable, insideDestination);
|
||||
const combineWith = find(withoutDragging, (child) => {
|
||||
const id = child.descriptor.id;
|
||||
const childRect = child.page.borderBox;
|
||||
const childSize = childRect[axis.size];
|
||||
const threshold = childSize / combineThresholdDivisor;
|
||||
const didStartAfterCritical = getDidStartAfterCritical(id, afterCritical);
|
||||
const isDisplaced = getIsDisplaced({
|
||||
displaced: previousImpact.displaced,
|
||||
id
|
||||
});
|
||||
|
||||
/*
|
||||
Only combining when in the combine region
|
||||
As soon as a boundary is hit then no longer combining
|
||||
*/
|
||||
|
||||
if (didStartAfterCritical) {
|
||||
// In original position
|
||||
// Will combine with item when inside a band
|
||||
if (isDisplaced) {
|
||||
return targetEnd > childRect[axis.start] + threshold && targetEnd < childRect[axis.end] - threshold;
|
||||
}
|
||||
|
||||
// child is now 'displaced' backwards from where it started
|
||||
// want to combine when we move backwards onto it
|
||||
return (
|
||||
targetStart > childRect[axis.start] - displacement + threshold &&
|
||||
targetStart < childRect[axis.end] - displacement - threshold
|
||||
);
|
||||
}
|
||||
|
||||
// item has moved forwards
|
||||
if (isDisplaced) {
|
||||
return (
|
||||
targetEnd > childRect[axis.start] + displacement + threshold &&
|
||||
targetEnd < childRect[axis.end] + displacement - threshold
|
||||
);
|
||||
}
|
||||
|
||||
// is in resting position - being moved backwards on to
|
||||
return targetStart > childRect[axis.start] + threshold && targetStart < childRect[axis.end] - threshold;
|
||||
});
|
||||
if (!combineWith) {
|
||||
return null;
|
||||
}
|
||||
const impact = {
|
||||
// no change to displacement when combining
|
||||
displacedBy,
|
||||
displaced: previousImpact.displaced,
|
||||
at: {
|
||||
type: "COMBINE",
|
||||
combine: {
|
||||
draggableId: combineWith.descriptor.id,
|
||||
droppableId: destination.descriptor.id
|
||||
}
|
||||
}
|
||||
};
|
||||
return impact;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import getDisplacedBy from "../get-displaced-by";
|
||||
import removeDraggableFromList from "../remove-draggable-from-list";
|
||||
import isHomeOf from "../droppable/is-home-of";
|
||||
import { find } from "../../native-with-fallback";
|
||||
import getDidStartAfterCritical from "../did-start-after-critical";
|
||||
import calculateReorderImpact from "../calculate-drag-impact/calculate-reorder-impact";
|
||||
import getIsDisplaced from "../get-is-displaced";
|
||||
import { horizontal, vertical } from "../axis";
|
||||
|
||||
function atIndex({ draggable, closest, inHomeList }) {
|
||||
if (!closest) {
|
||||
return null;
|
||||
}
|
||||
if (!inHomeList) {
|
||||
return closest.descriptor.index;
|
||||
}
|
||||
if (closest.descriptor.index > draggable.descriptor.index) {
|
||||
return closest.descriptor.index - 1;
|
||||
}
|
||||
return closest.descriptor.index;
|
||||
}
|
||||
|
||||
export default ({
|
||||
pageBorderBoxWithDroppableScroll: targetRect,
|
||||
draggable,
|
||||
destination,
|
||||
insideDestination,
|
||||
last,
|
||||
viewport,
|
||||
afterCritical
|
||||
}) => {
|
||||
const axis = destination.axis;
|
||||
const displacedBy = getDisplacedBy(destination.axis, draggable.displaceBy);
|
||||
const displacement = displacedBy.value;
|
||||
const targetStart = targetRect[axis.start];
|
||||
const targetEnd = targetRect[axis.end];
|
||||
const uprightAxis = axis.direction === "horizontal" ? vertical : horizontal;
|
||||
const uprightAxisBoundStart = targetRect[uprightAxis.start];
|
||||
const withoutDragging = removeDraggableFromList(draggable, insideDestination);
|
||||
const closest = find(withoutDragging, (child) => {
|
||||
const id = child.descriptor.id;
|
||||
const childCenter = child.page.borderBox.center[axis.line];
|
||||
if (axis.grid) {
|
||||
const uprightChildCenter = child.page.borderBox.center[uprightAxis.line];
|
||||
if (!(uprightAxisBoundStart < uprightChildCenter)) return false;
|
||||
}
|
||||
const didStartAfterCritical = getDidStartAfterCritical(id, afterCritical);
|
||||
const isDisplaced = getIsDisplaced({
|
||||
displaced: last,
|
||||
id
|
||||
});
|
||||
|
||||
/*
|
||||
Note: we change things when moving *past* the child center - not when it hits the center
|
||||
If we make it when we *hit* the child center then there can be
|
||||
a hit on the next update causing a flicker.
|
||||
- Update 1: targetBottom hits center => displace backwards
|
||||
- Update 2: targetStart is now hitting the displaced center => displace forwards
|
||||
- Update 3: goto 1 (boom)
|
||||
*/
|
||||
|
||||
if (didStartAfterCritical) {
|
||||
// Continue to displace while targetEnd before the childCenter
|
||||
// Move once we *move forward past* the childCenter
|
||||
if (isDisplaced) {
|
||||
return targetEnd <= childCenter;
|
||||
}
|
||||
|
||||
// Has been moved backwards from where it started
|
||||
// Displace forwards when targetStart *moves backwards past* the displaced childCenter
|
||||
return targetStart < childCenter - displacement;
|
||||
}
|
||||
|
||||
// Item has been shifted forward.
|
||||
// Remove displacement when targetEnd moves forward past the displaced center
|
||||
if (isDisplaced) {
|
||||
return targetEnd <= childCenter + displacement;
|
||||
}
|
||||
|
||||
// Item is behind the dragging item
|
||||
// We want to displace it if the targetStart goes *backwards past* the childCenter
|
||||
return targetStart < childCenter;
|
||||
});
|
||||
const newIndex = atIndex({
|
||||
draggable,
|
||||
closest,
|
||||
inHomeList: isHomeOf(draggable, destination)
|
||||
});
|
||||
|
||||
// TODO: index cannot be null?
|
||||
// otherwise return null from there and return empty impact
|
||||
// that was calculate reorder impact does not need to account for a null index
|
||||
return calculateReorderImpact({
|
||||
draggable,
|
||||
insideDestination,
|
||||
destination,
|
||||
viewport,
|
||||
last,
|
||||
displacedBy,
|
||||
index: newIndex
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import getDroppableOver from "../get-droppable-over";
|
||||
import getDraggablesInsideDroppable from "../get-draggables-inside-droppable";
|
||||
import withDroppableScroll from "../with-scroll-change/with-droppable-scroll";
|
||||
import getReorderImpact from "./get-reorder-impact";
|
||||
import getCombineImpact from "./get-combine-impact";
|
||||
import noImpact from "../no-impact";
|
||||
import { offsetRectByPosition } from "../rect";
|
||||
|
||||
export default ({ pageOffset, draggable, draggables, droppables, previousImpact, viewport, afterCritical }) => {
|
||||
const pageBorderBox = offsetRectByPosition(draggable.page.borderBox, pageOffset);
|
||||
const destinationId = getDroppableOver({
|
||||
pageBorderBox,
|
||||
draggable,
|
||||
droppables
|
||||
});
|
||||
|
||||
// not dragging over anything
|
||||
|
||||
if (!destinationId) {
|
||||
// A big design decision was made here to collapse the home list
|
||||
// when not over any list. This yielded the most consistently beautiful experience.
|
||||
return noImpact;
|
||||
}
|
||||
const destination = droppables[destinationId];
|
||||
const insideDestination = getDraggablesInsideDroppable(destination.descriptor.id, draggables);
|
||||
|
||||
// Where the element actually is now.
|
||||
// Need to take into account the change of scroll in the droppable
|
||||
const pageBorderBoxWithDroppableScroll = withDroppableScroll(destination, pageBorderBox);
|
||||
|
||||
// checking combine first so we combine before any reordering
|
||||
return (
|
||||
getCombineImpact({
|
||||
pageBorderBoxWithDroppableScroll,
|
||||
draggable,
|
||||
previousImpact,
|
||||
destination,
|
||||
insideDestination,
|
||||
afterCritical
|
||||
}) ||
|
||||
getReorderImpact({
|
||||
pageBorderBoxWithDroppableScroll,
|
||||
draggable,
|
||||
destination,
|
||||
insideDestination,
|
||||
last: previousImpact.displaced,
|
||||
viewport,
|
||||
afterCritical
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { toDraggableList } from "./dimension-structures";
|
||||
|
||||
export default memoizeOne((droppableId, draggables) => {
|
||||
const result = toDraggableList(draggables)
|
||||
.filter((draggable) => droppableId === draggable.descriptor.droppableId)
|
||||
// Dimensions are not guarenteed to be ordered in the same order as keys
|
||||
// So we need to sort them so they are in the correct order
|
||||
.sort((a, b) => a.descriptor.index - b.descriptor.index);
|
||||
return result;
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { toDroppableList } from "./dimension-structures";
|
||||
import isPositionInFrame from "./visibility/is-position-in-frame";
|
||||
import { distance, patch } from "./position";
|
||||
import isWithin from "./is-within";
|
||||
|
||||
// https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
|
||||
// https://silentmatt.com/rectangle-intersection/
|
||||
function getHasOverlap(first, second) {
|
||||
return (
|
||||
first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top
|
||||
);
|
||||
}
|
||||
|
||||
function getFurthestAway({ pageBorderBox, draggable, candidates }) {
|
||||
// We are not comparing the center of the home list with the target list as it would
|
||||
// give preference to giant lists
|
||||
|
||||
// We are measuring the distance from where the draggable started
|
||||
// to where it is *hitting* the candidate
|
||||
// Note: The hit point might technically not be in the bounds of the candidate
|
||||
|
||||
const startCenter = draggable.page.borderBox.center;
|
||||
const sorted = candidates
|
||||
.map((candidate) => {
|
||||
const axis = candidate.axis;
|
||||
const target = patch(
|
||||
candidate.axis.line,
|
||||
// use the current center of the dragging item on the main axis
|
||||
pageBorderBox.center[axis.line],
|
||||
// use the center of the list on the cross axis
|
||||
candidate.page.borderBox.center[axis.crossAxisLine]
|
||||
);
|
||||
return {
|
||||
id: candidate.descriptor.id,
|
||||
distance: distance(startCenter, target)
|
||||
};
|
||||
})
|
||||
// largest value will be first
|
||||
.sort((a, b) => b.distance - a.distance);
|
||||
|
||||
// just being safe
|
||||
return sorted[0] ? sorted[0].id : null;
|
||||
}
|
||||
|
||||
export default function getDroppableOver({ pageBorderBox, draggable, droppables }) {
|
||||
// We know at this point that some overlap has to exist
|
||||
const candidates = toDroppableList(droppables).filter((item) => {
|
||||
// Cannot be a candidate when disabled
|
||||
if (!item.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot be a candidate when there is no visible area
|
||||
const active = item.subject.active;
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot be a candidate when dragging item is not over the droppable at all
|
||||
if (!getHasOverlap(pageBorderBox, active)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Candidate if the center position is over a droppable
|
||||
if (isPositionInFrame(active)(pageBorderBox.center)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Candidate if an edge is over the cross axis half way point
|
||||
// 3. Candidate if dragging item is totally over droppable on cross axis
|
||||
|
||||
const axis = item.axis;
|
||||
const childCenter = active.center[axis.crossAxisLine];
|
||||
const crossAxisStart = pageBorderBox[axis.crossAxisStart];
|
||||
const crossAxisEnd = pageBorderBox[axis.crossAxisEnd];
|
||||
const isContained = isWithin(active[axis.crossAxisStart], active[axis.crossAxisEnd]);
|
||||
const isStartContained = isContained(crossAxisStart);
|
||||
const isEndContained = isContained(crossAxisEnd);
|
||||
|
||||
// Dragging item is totally covering the active area
|
||||
if (!isStartContained && !isEndContained) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* edges must go beyond the center line in order to avoid
|
||||
* cases were both conditions are satisfied.
|
||||
*/
|
||||
if (isStartContained) {
|
||||
return crossAxisStart < childCenter;
|
||||
}
|
||||
return crossAxisEnd > childCenter;
|
||||
});
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only one candidate - use that!
|
||||
if (candidates.length === 1) {
|
||||
return candidates[0].descriptor.id;
|
||||
}
|
||||
|
||||
// Multiple options returned
|
||||
// Should only occur with really large items
|
||||
// Going to use fallback: distance from home
|
||||
return getFurthestAway({
|
||||
pageBorderBox,
|
||||
draggable,
|
||||
candidates
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { invariant } from "../invariant";
|
||||
|
||||
export default (droppable) => {
|
||||
const frame = droppable.frame;
|
||||
invariant(frame, "Expected Droppable to have a frame");
|
||||
return frame;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export default (descriptor) => ({
|
||||
index: descriptor.index,
|
||||
droppableId: descriptor.droppableId
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export function tryGetDestination(impact) {
|
||||
if (impact.at && impact.at.type === "REORDER") {
|
||||
return impact.at.destination;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function tryGetCombine(impact) {
|
||||
if (impact.at && impact.at.type === "COMBINE") {
|
||||
return impact.at.combine;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function getIsDisplaced({ displaced, id }) {
|
||||
return Boolean(displaced.visible[id] || displaced.invisible[id]);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { invariant } from "../invariant";
|
||||
import getHomeLocation from "./get-home-location";
|
||||
import getDraggablesInsideDroppable from "./get-draggables-inside-droppable";
|
||||
import getDisplacedBy from "./get-displaced-by";
|
||||
import getDisplacementGroups from "./get-displacement-groups";
|
||||
|
||||
export default ({ draggable, home, draggables, viewport }) => {
|
||||
const displacedBy = getDisplacedBy(home.axis, draggable.displaceBy);
|
||||
const insideHome = getDraggablesInsideDroppable(home.descriptor.id, draggables);
|
||||
|
||||
// in a list that does not start at 0 the descriptor.index might be different from the index in the list
|
||||
// eg a list could be: [2,3,4]. A descriptor.index of '2' would actually be in index '0' of the list
|
||||
const rawIndex = insideHome.indexOf(draggable);
|
||||
invariant(rawIndex !== -1, "Expected draggable to be inside home list");
|
||||
const afterDragging = insideHome.slice(rawIndex + 1);
|
||||
const effected = afterDragging.reduce((previous, item) => {
|
||||
previous[item.descriptor.id] = true;
|
||||
return previous;
|
||||
}, {});
|
||||
const afterCritical = {
|
||||
inVirtualList: home.descriptor.mode === "virtual",
|
||||
displacedBy,
|
||||
effected
|
||||
};
|
||||
const displaced = getDisplacementGroups({
|
||||
afterDragging,
|
||||
destination: home,
|
||||
displacedBy,
|
||||
last: null,
|
||||
viewport: viewport.frame,
|
||||
// originally we do not want any animation as we want
|
||||
// everything to be fixed in the same position that
|
||||
// it started in
|
||||
forceShouldAnimate: false
|
||||
});
|
||||
const impact = {
|
||||
displaced,
|
||||
displacedBy,
|
||||
at: {
|
||||
type: "REORDER",
|
||||
destination: getHomeLocation(draggable.descriptor)
|
||||
}
|
||||
};
|
||||
return {
|
||||
impact,
|
||||
afterCritical
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { subtract } from "./position";
|
||||
|
||||
export default ({ scrollHeight, scrollWidth, height, width }) => {
|
||||
const maxScroll = subtract(
|
||||
// full size
|
||||
{
|
||||
x: scrollWidth,
|
||||
y: scrollHeight
|
||||
},
|
||||
// viewport size
|
||||
{
|
||||
x: width,
|
||||
y: height
|
||||
}
|
||||
);
|
||||
const adjustedMaxScroll = {
|
||||
x: Math.max(0, maxScroll.x),
|
||||
y: Math.max(0, maxScroll.y)
|
||||
};
|
||||
return adjustedMaxScroll;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Using function declaration as arrow function does not play well with the %checks syntax
|
||||
export default function isMovementAllowed(state) {
|
||||
return state.phase === "DRAGGING" || state.phase === "COLLECTING";
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// is a value between two other values
|
||||
export default (lowerBound, upperBound) => (value) => lowerBound <= value && value <= upperBound;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { invariant } from "../../invariant";
|
||||
|
||||
const shouldStop = (action) =>
|
||||
action.type === "DROP_COMPLETE" || action.type === "DROP_ANIMATE" || action.type === "FLUSH";
|
||||
export default (autoScroller) => (store) => (next) => (action) => {
|
||||
if (shouldStop(action)) {
|
||||
autoScroller.stop();
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
if (action.type === "INITIAL_PUBLISH") {
|
||||
// letting the action go first to hydrate the state
|
||||
next(action);
|
||||
const state = store.getState();
|
||||
invariant(state.phase === "DRAGGING", "Expected phase to be DRAGGING after INITIAL_PUBLISH");
|
||||
autoScroller.start(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// auto scroll happens in response to state changes
|
||||
// releasing all actions to the reducer first
|
||||
next(action);
|
||||
autoScroller.scroll(store.getState());
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export default (marshal) => () => (next) => (action) => {
|
||||
// Not stopping a collection on a 'DROP' as we want a collection to continue
|
||||
if (
|
||||
// drag is finished
|
||||
action.type === "DROP_COMPLETE" ||
|
||||
action.type === "FLUSH" ||
|
||||
// no longer accepting changes once the drop has started
|
||||
action.type === "DROP_ANIMATE"
|
||||
) {
|
||||
marshal.stopPublishing();
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import { completeDrop } from "../../action-creators";
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
if (action.type !== "DROP_ANIMATION_FINISHED") {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
const state = store.getState();
|
||||
invariant(state.phase === "DROP_ANIMATING", "Cannot finish a drop animating when no drop is occurring");
|
||||
store.dispatch(
|
||||
completeDrop({
|
||||
completed: state.completed
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { dropAnimationFinished } from "../../action-creators";
|
||||
import bindEvents from "../../../view/event-bindings/bind-events";
|
||||
|
||||
export default (store) => {
|
||||
let unbind = null;
|
||||
let frameId = null;
|
||||
|
||||
function clear() {
|
||||
if (frameId) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = null;
|
||||
}
|
||||
if (unbind) {
|
||||
unbind();
|
||||
unbind = null;
|
||||
}
|
||||
}
|
||||
|
||||
return (next) => (action) => {
|
||||
if (action.type === "FLUSH" || action.type === "DROP_COMPLETE" || action.type === "DROP_ANIMATION_FINISHED") {
|
||||
clear();
|
||||
}
|
||||
next(action);
|
||||
if (action.type !== "DROP_ANIMATE") {
|
||||
return;
|
||||
}
|
||||
const binding = {
|
||||
eventName: "scroll",
|
||||
// capture: true will catch all scroll events, event from scroll containers
|
||||
// once: just in case, we only want to ever fire one
|
||||
options: {
|
||||
capture: true,
|
||||
passive: false,
|
||||
once: true
|
||||
},
|
||||
fn: function flushDropAnimation() {
|
||||
const state = store.getState();
|
||||
if (state.phase === "DROP_ANIMATING") {
|
||||
store.dispatch(dropAnimationFinished());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The browser can batch a few scroll events in a single frame
|
||||
// including the one that ended the drag.
|
||||
// Binding after a requestAnimationFrame ensures that any scrolls caused
|
||||
// by the auto scroller are finished
|
||||
// TODO: why is a second window scroll being fired?
|
||||
// It leads to funny drop positions :(
|
||||
frameId = requestAnimationFrame(() => {
|
||||
frameId = null;
|
||||
unbind = bindEvents(window, [binding]);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import { animateDrop, completeDrop, dropPending } from "../../action-creators";
|
||||
import { isEqual } from "../../position";
|
||||
import getDropDuration from "./get-drop-duration";
|
||||
import getNewHomeClientOffset from "./get-new-home-client-offset";
|
||||
import getDropImpact from "./get-drop-impact";
|
||||
import { tryGetCombine, tryGetDestination } from "../../get-impact-location";
|
||||
|
||||
export default ({ getState, dispatch }) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (action.type !== "DROP") {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
const state = getState();
|
||||
const reason = action.payload.reason;
|
||||
|
||||
// Still waiting for a bulk collection to publish
|
||||
// We are now shifting the application into the 'DROP_PENDING' phase
|
||||
if (state.phase === "COLLECTING") {
|
||||
dispatch(
|
||||
dropPending({
|
||||
reason
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Could have occurred in response to an error
|
||||
if (state.phase === "IDLE") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Still waiting for our drop pending to end
|
||||
// TODO: should this throw?
|
||||
const isWaitingForDrop = state.phase === "DROP_PENDING" && state.isWaiting;
|
||||
invariant(!isWaitingForDrop, "A DROP action occurred while DROP_PENDING and still waiting");
|
||||
invariant(state.phase === "DRAGGING" || state.phase === "DROP_PENDING", `Cannot drop in phase: ${state.phase}`);
|
||||
// We are now in the DRAGGING or DROP_PENDING phase
|
||||
|
||||
const critical = state.critical;
|
||||
const dimensions = state.dimensions;
|
||||
const draggable = dimensions.draggables[state.critical.draggable.id];
|
||||
// Only keeping impact when doing a user drop - otherwise we are cancelling
|
||||
|
||||
const { impact, didDropInsideDroppable } = getDropImpact({
|
||||
reason,
|
||||
lastImpact: state.impact,
|
||||
afterCritical: state.afterCritical,
|
||||
onLiftImpact: state.onLiftImpact,
|
||||
home: state.dimensions.droppables[state.critical.droppable.id],
|
||||
viewport: state.viewport,
|
||||
draggables: state.dimensions.draggables
|
||||
});
|
||||
|
||||
// only populating destination / combine if 'didDropInsideDroppable' is true
|
||||
const destination = didDropInsideDroppable ? tryGetDestination(impact) : null;
|
||||
const combine = didDropInsideDroppable ? tryGetCombine(impact) : null;
|
||||
const source = {
|
||||
index: critical.draggable.index,
|
||||
droppableId: critical.droppable.id
|
||||
};
|
||||
const result = {
|
||||
draggableId: draggable.descriptor.id,
|
||||
type: draggable.descriptor.type,
|
||||
source,
|
||||
reason,
|
||||
mode: state.movementMode,
|
||||
// destination / combine will be null if didDropInsideDroppable is true
|
||||
destination,
|
||||
combine
|
||||
};
|
||||
const newHomeClientOffset = getNewHomeClientOffset({
|
||||
impact,
|
||||
draggable,
|
||||
dimensions,
|
||||
viewport: state.viewport,
|
||||
afterCritical: state.afterCritical
|
||||
});
|
||||
const completed = {
|
||||
critical: state.critical,
|
||||
afterCritical: state.afterCritical,
|
||||
result,
|
||||
impact
|
||||
};
|
||||
const isAnimationRequired =
|
||||
// 1. not already in the right spot
|
||||
!isEqual(state.current.client.offset, newHomeClientOffset) ||
|
||||
// 2. doing a combine (we still want to animate the scale and opacity fade)
|
||||
// looking at the result and not the impact as the combine impact is cleared
|
||||
Boolean(result.combine);
|
||||
if (!isAnimationRequired) {
|
||||
dispatch(
|
||||
completeDrop({
|
||||
completed
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
const dropDuration = getDropDuration({
|
||||
current: state.current.client.offset,
|
||||
destination: newHomeClientOffset,
|
||||
reason
|
||||
});
|
||||
const args = {
|
||||
newHomeClientOffset,
|
||||
dropDuration,
|
||||
completed
|
||||
};
|
||||
dispatch(animateDrop(args));
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { distance as getDistance } from "../../position";
|
||||
import { timings } from "../../../animation";
|
||||
|
||||
const { minDropTime, maxDropTime } = timings;
|
||||
const dropTimeRange = maxDropTime - minDropTime;
|
||||
const maxDropTimeAtDistance = 1500;
|
||||
// will bring a time lower - which makes it faster
|
||||
const cancelDropModifier = 0.6;
|
||||
export default ({ current, destination, reason }) => {
|
||||
const distance = getDistance(current, destination);
|
||||
// even if there is no distance to travel, we might still need to animate opacity
|
||||
if (distance <= 0) {
|
||||
return minDropTime;
|
||||
}
|
||||
if (distance >= maxDropTimeAtDistance) {
|
||||
return maxDropTime;
|
||||
}
|
||||
|
||||
// * range from:
|
||||
// 0px = 0.33s
|
||||
// 1500px and over = 0.55s
|
||||
// * If reason === 'CANCEL' then speeding up the animation
|
||||
// * round to 2 decimal points
|
||||
|
||||
const percentage = distance / maxDropTimeAtDistance;
|
||||
const duration = minDropTime + dropTimeRange * percentage;
|
||||
const withDuration = reason === "CANCEL" ? duration * cancelDropModifier : duration;
|
||||
// To two decimal points by converting to string and back
|
||||
return Number(withDuration.toFixed(2));
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import recompute from "../../update-displacement-visibility/recompute";
|
||||
import { emptyGroups } from "../../no-impact";
|
||||
|
||||
export default ({ draggables, reason, lastImpact, home, viewport, onLiftImpact }) => {
|
||||
if (!lastImpact.at || reason !== "DROP") {
|
||||
// Dropping outside of a list or the drag was cancelled
|
||||
|
||||
// Going to use the on lift impact
|
||||
// Need to recompute the visibility of the original impact
|
||||
// What is visible can be different to when the drag started
|
||||
|
||||
const recomputedHomeImpact = recompute({
|
||||
draggables,
|
||||
impact: onLiftImpact,
|
||||
destination: home,
|
||||
viewport,
|
||||
// We need the draggables to animate back to their positions
|
||||
forceShouldAnimate: true
|
||||
});
|
||||
return {
|
||||
impact: recomputedHomeImpact,
|
||||
didDropInsideDroppable: false
|
||||
};
|
||||
}
|
||||
|
||||
// use the existing impact
|
||||
if (lastImpact.at.type === "REORDER") {
|
||||
return {
|
||||
impact: lastImpact,
|
||||
didDropInsideDroppable: true
|
||||
};
|
||||
}
|
||||
|
||||
// When merging we remove the movement so that everything
|
||||
// will animate closed
|
||||
const withoutMovement = {
|
||||
...lastImpact,
|
||||
displaced: emptyGroups
|
||||
};
|
||||
return {
|
||||
impact: withoutMovement,
|
||||
didDropInsideDroppable: true
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import whatIsDraggedOver from "../../droppable/what-is-dragged-over";
|
||||
import { subtract } from "../../position";
|
||||
import getClientBorderBoxCenter from "../../get-center-from-impact/get-client-border-box-center";
|
||||
|
||||
export default ({ impact, draggable, dimensions, viewport, afterCritical }) => {
|
||||
const { draggables, droppables } = dimensions;
|
||||
const droppableId = whatIsDraggedOver(impact);
|
||||
const destination = droppableId ? droppables[droppableId] : null;
|
||||
const home = droppables[draggable.descriptor.droppableId];
|
||||
const newClientCenter = getClientBorderBoxCenter({
|
||||
impact,
|
||||
draggable,
|
||||
draggables,
|
||||
// if there is no destination, then we will be dropping back into the home
|
||||
afterCritical,
|
||||
droppable: destination || home,
|
||||
viewport
|
||||
});
|
||||
const offset = subtract(newClientCenter, draggable.client.borderBox.center);
|
||||
return offset;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./drop-middleware";
|
||||
@@ -0,0 +1,31 @@
|
||||
export default (marshal) => {
|
||||
let isWatching = false;
|
||||
return () => (next) => (action) => {
|
||||
if (action.type === "INITIAL_PUBLISH") {
|
||||
isWatching = true;
|
||||
marshal.tryRecordFocus(action.payload.critical.draggable.id);
|
||||
next(action);
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
return;
|
||||
}
|
||||
next(action);
|
||||
if (!isWatching) {
|
||||
return;
|
||||
}
|
||||
if (action.type === "FLUSH") {
|
||||
isWatching = false;
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
return;
|
||||
}
|
||||
if (action.type === "DROP_COMPLETE") {
|
||||
isWatching = false;
|
||||
const result = action.payload.completed.result;
|
||||
|
||||
// give focus to the combine target when combining
|
||||
if (result.combine) {
|
||||
marshal.tryShiftRecord(result.draggableId, result.combine.draggableId);
|
||||
}
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { invariant } from "../../invariant";
|
||||
import { beforeInitialCapture, completeDrop, flush, initialPublish } from "../action-creators";
|
||||
import validateDimensions from "./util/validate-dimensions";
|
||||
|
||||
export default (marshal) =>
|
||||
({ getState, dispatch }) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (action.type !== "LIFT") {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
const { id, clientSelection, movementMode } = action.payload;
|
||||
const initial = getState();
|
||||
|
||||
// flush dropping animation if needed
|
||||
// this can change the descriptor of the dragging item
|
||||
// Will call the onDragEnd responders
|
||||
|
||||
if (initial.phase === "DROP_ANIMATING") {
|
||||
dispatch(
|
||||
completeDrop({
|
||||
completed: initial.completed
|
||||
})
|
||||
);
|
||||
}
|
||||
invariant(getState().phase === "IDLE", "Unexpected phase to start a drag");
|
||||
|
||||
// Removing any placeholders before we capture any starting dimensions
|
||||
dispatch(flush());
|
||||
|
||||
// Let consumers know we are just about to publish
|
||||
// We are only publishing a small amount of information as
|
||||
// things might change as a result of the onBeforeCapture callback
|
||||
dispatch(
|
||||
beforeInitialCapture({
|
||||
draggableId: id,
|
||||
movementMode
|
||||
})
|
||||
);
|
||||
|
||||
// will communicate with the marshal to start requesting dimensions
|
||||
const scrollOptions = {
|
||||
shouldPublishImmediately: movementMode === "SNAP"
|
||||
};
|
||||
const request = {
|
||||
draggableId: id,
|
||||
scrollOptions
|
||||
};
|
||||
// Let's get the marshal started!
|
||||
const { critical, dimensions, viewport } = marshal.startPublishing(request);
|
||||
validateDimensions(critical, dimensions);
|
||||
|
||||
// Okay, we are good to start dragging now
|
||||
dispatch(
|
||||
initialPublish({
|
||||
critical,
|
||||
dimensions,
|
||||
clientSelection,
|
||||
movementMode,
|
||||
viewport
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { drop } from "../action-creators";
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
// Always let the action go through first
|
||||
next(action);
|
||||
if (action.type !== "PUBLISH_WHILE_DRAGGING") {
|
||||
return;
|
||||
}
|
||||
|
||||
// A bulk replace occurred - check if
|
||||
// 1. there is a pending drop
|
||||
// 2. that the pending drop is no longer waiting
|
||||
|
||||
const postActionState = store.getState();
|
||||
|
||||
// no pending drop after the publish
|
||||
if (postActionState.phase !== "DROP_PENDING") {
|
||||
return;
|
||||
}
|
||||
|
||||
// the pending drop is still waiting for completion
|
||||
if (postActionState.isWaiting) {
|
||||
return;
|
||||
}
|
||||
store.dispatch(
|
||||
drop({
|
||||
reason: postActionState.reason
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import { findIndex } from "../../../native-with-fallback";
|
||||
|
||||
export default () => {
|
||||
const entries = [];
|
||||
const execute = (timerId) => {
|
||||
const index = findIndex(entries, (item) => item.timerId === timerId);
|
||||
invariant(index !== -1, "Could not find timer");
|
||||
// delete in place
|
||||
const [entry] = entries.splice(index, 1);
|
||||
entry.callback();
|
||||
};
|
||||
const add = (fn) => {
|
||||
const timerId = setTimeout(() => execute(timerId));
|
||||
const entry = {
|
||||
timerId,
|
||||
callback: fn
|
||||
};
|
||||
entries.push(entry);
|
||||
};
|
||||
const flush = () => {
|
||||
// nothing to flush
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
const shallow = [...entries];
|
||||
// clearing entries in case a callback adds some more callbacks
|
||||
entries.length = 0;
|
||||
shallow.forEach((entry) => {
|
||||
clearTimeout(entry.timerId);
|
||||
entry.callback();
|
||||
});
|
||||
};
|
||||
return {
|
||||
add,
|
||||
flush
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { warning } from "../../../dev-warning";
|
||||
|
||||
export default (announce) => {
|
||||
let wasCalled = false;
|
||||
let isExpired = false;
|
||||
|
||||
// not allowing async announcements
|
||||
const timeoutId = setTimeout(() => {
|
||||
isExpired = true;
|
||||
});
|
||||
const result = (message) => {
|
||||
if (wasCalled) {
|
||||
warning("Announcement already made. Not making a second announcement");
|
||||
return;
|
||||
}
|
||||
if (isExpired) {
|
||||
warning(`
|
||||
Announcements cannot be made asynchronously.
|
||||
Default message has already been announced.
|
||||
`);
|
||||
return;
|
||||
}
|
||||
wasCalled = true;
|
||||
announce(message);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
// getter for isExpired
|
||||
// using this technique so that a consumer cannot
|
||||
// set the isExpired or wasCalled flags
|
||||
result.wasCalled = () => wasCalled;
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./responders-middleware";
|
||||
@@ -0,0 +1,38 @@
|
||||
export const areLocationsEqual = (first, second) => {
|
||||
// if both are null - we are equal
|
||||
if (first == null && second == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if one is null - then they are not equal
|
||||
if (first == null || second == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// compare their actual values
|
||||
return first.droppableId === second.droppableId && first.index === second.index;
|
||||
};
|
||||
export const isCombineEqual = (first, second) => {
|
||||
// if both are null - we are equal
|
||||
if (first == null && second == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// only one is null
|
||||
if (first == null || second == null) {
|
||||
return false;
|
||||
}
|
||||
return first.draggableId === second.draggableId && first.droppableId === second.droppableId;
|
||||
};
|
||||
export const isCriticalEqual = (first, second) => {
|
||||
if (first === second) {
|
||||
return true;
|
||||
}
|
||||
const isDraggableEqual =
|
||||
first.draggable.id === second.draggable.id &&
|
||||
first.draggable.droppableId === second.draggable.droppableId &&
|
||||
first.draggable.type === second.draggable.type &&
|
||||
first.draggable.index === second.draggable.index;
|
||||
const isDroppableEqual = first.droppable.id === second.droppable.id && first.droppable.type === second.droppable.type;
|
||||
return isDraggableEqual && isDroppableEqual;
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import messagePreset from "../../../screen-reader-message-preset";
|
||||
import * as timings from "../../../debug/timings";
|
||||
import getExpiringAnnounce from "./expiring-announce";
|
||||
import getAsyncMarshal from "./async-marshal";
|
||||
import { areLocationsEqual, isCombineEqual, isCriticalEqual } from "./is-equal";
|
||||
import { tryGetCombine, tryGetDestination } from "../../get-impact-location";
|
||||
|
||||
const withTimings = (key, fn) => {
|
||||
timings.start(key);
|
||||
fn();
|
||||
timings.finish(key);
|
||||
};
|
||||
const getDragStart = (critical, mode) => ({
|
||||
draggableId: critical.draggable.id,
|
||||
type: critical.droppable.type,
|
||||
source: {
|
||||
droppableId: critical.droppable.id,
|
||||
index: critical.draggable.index
|
||||
},
|
||||
mode
|
||||
});
|
||||
const execute = (responder, data, announce, getDefaultMessage) => {
|
||||
if (!responder) {
|
||||
announce(getDefaultMessage(data));
|
||||
return;
|
||||
}
|
||||
const willExpire = getExpiringAnnounce(announce);
|
||||
const provided = {
|
||||
announce: willExpire
|
||||
};
|
||||
|
||||
// Casting because we are not validating which data type is going into which responder
|
||||
responder(data, provided);
|
||||
if (!willExpire.wasCalled()) {
|
||||
announce(getDefaultMessage(data));
|
||||
}
|
||||
};
|
||||
export default (getResponders, announce) => {
|
||||
const asyncMarshal = getAsyncMarshal();
|
||||
let dragging = null;
|
||||
const beforeCapture = (draggableId, mode) => {
|
||||
invariant(!dragging, "Cannot fire onBeforeCapture as a drag start has already been published");
|
||||
withTimings("onBeforeCapture", () => {
|
||||
// No use of screen reader for this responder
|
||||
const fn = getResponders().onBeforeCapture;
|
||||
if (fn) {
|
||||
const before = {
|
||||
draggableId,
|
||||
mode
|
||||
};
|
||||
fn(before);
|
||||
}
|
||||
});
|
||||
};
|
||||
const beforeStart = (critical, mode) => {
|
||||
invariant(!dragging, "Cannot fire onBeforeDragStart as a drag start has already been published");
|
||||
withTimings("onBeforeDragStart", () => {
|
||||
// No use of screen reader for this responder
|
||||
const fn = getResponders().onBeforeDragStart;
|
||||
if (fn) {
|
||||
fn(getDragStart(critical, mode));
|
||||
}
|
||||
});
|
||||
};
|
||||
const start = (critical, mode) => {
|
||||
invariant(!dragging, "Cannot fire onBeforeDragStart as a drag start has already been published");
|
||||
const data = getDragStart(critical, mode);
|
||||
dragging = {
|
||||
mode,
|
||||
lastCritical: critical,
|
||||
lastLocation: data.source,
|
||||
lastCombine: null
|
||||
};
|
||||
|
||||
// we will flush this frame if we receive any responder updates
|
||||
asyncMarshal.add(() => {
|
||||
withTimings("onDragStart", () => execute(getResponders().onDragStart, data, announce, messagePreset.onDragStart));
|
||||
});
|
||||
};
|
||||
|
||||
// Passing in the critical location again as it can change during a drag
|
||||
const update = (critical, impact) => {
|
||||
const location = tryGetDestination(impact);
|
||||
const combine = tryGetCombine(impact);
|
||||
invariant(dragging, "Cannot fire onDragMove when onDragStart has not been called");
|
||||
|
||||
// Has the critical changed? Will result in a source change
|
||||
const hasCriticalChanged = !isCriticalEqual(critical, dragging.lastCritical);
|
||||
if (hasCriticalChanged) {
|
||||
dragging.lastCritical = critical;
|
||||
}
|
||||
|
||||
// Has the location changed? Will result in a destination change
|
||||
const hasLocationChanged = !areLocationsEqual(dragging.lastLocation, location);
|
||||
if (hasLocationChanged) {
|
||||
dragging.lastLocation = location;
|
||||
}
|
||||
const hasGroupingChanged = !isCombineEqual(dragging.lastCombine, combine);
|
||||
if (hasGroupingChanged) {
|
||||
dragging.lastCombine = combine;
|
||||
}
|
||||
|
||||
// Nothing has changed - no update needed
|
||||
if (!hasCriticalChanged && !hasLocationChanged && !hasGroupingChanged) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
...getDragStart(critical, dragging.mode),
|
||||
combine,
|
||||
destination: location
|
||||
};
|
||||
asyncMarshal.add(() => {
|
||||
withTimings("onDragUpdate", () =>
|
||||
execute(getResponders().onDragUpdate, data, announce, messagePreset.onDragUpdate)
|
||||
);
|
||||
});
|
||||
};
|
||||
const flush = () => {
|
||||
invariant(dragging, "Can only flush responders while dragging");
|
||||
asyncMarshal.flush();
|
||||
};
|
||||
const drop = (result) => {
|
||||
invariant(dragging, "Cannot fire onDragEnd when there is no matching onDragStart");
|
||||
dragging = null;
|
||||
// not adding to frame marshal - we want this to be done in the same render pass
|
||||
// we also want the consumers reorder logic to be in the same render pass
|
||||
withTimings("onDragEnd", () => execute(getResponders().onDragEnd, result, announce, messagePreset.onDragEnd));
|
||||
};
|
||||
|
||||
// A non user initiated cancel
|
||||
const abort = () => {
|
||||
// aborting can happen defensively
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
const result = {
|
||||
...getDragStart(dragging.lastCritical, dragging.mode),
|
||||
combine: null,
|
||||
destination: null,
|
||||
reason: "CANCEL"
|
||||
};
|
||||
drop(result);
|
||||
};
|
||||
return {
|
||||
beforeCapture,
|
||||
beforeStart,
|
||||
start,
|
||||
update,
|
||||
flush,
|
||||
drop,
|
||||
abort
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import getPublisher from "./publisher";
|
||||
|
||||
export default (getResponders, announce) => {
|
||||
const publisher = getPublisher(getResponders, announce);
|
||||
return (store) => (next) => (action) => {
|
||||
if (action.type === "BEFORE_INITIAL_CAPTURE") {
|
||||
publisher.beforeCapture(action.payload.draggableId, action.payload.movementMode);
|
||||
return;
|
||||
}
|
||||
if (action.type === "INITIAL_PUBLISH") {
|
||||
const critical = action.payload.critical;
|
||||
publisher.beforeStart(critical, action.payload.movementMode);
|
||||
next(action);
|
||||
publisher.start(critical, action.payload.movementMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag end
|
||||
if (action.type === "DROP_COMPLETE") {
|
||||
// it is important that we use the result and not the last impact
|
||||
// the last impact might be different to the result for visual reasons
|
||||
const result = action.payload.completed.result;
|
||||
// flushing all pending responders before snapshots are updated
|
||||
publisher.flush();
|
||||
next(action);
|
||||
publisher.drop(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// All other responders can fire after we have updated our connected components
|
||||
next(action);
|
||||
|
||||
// Drag state resetting - need to check if
|
||||
// we should fire a onDragEnd responder
|
||||
if (action.type === "FLUSH") {
|
||||
publisher.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// ## Perform drag updates
|
||||
// impact of action has already been reduced
|
||||
|
||||
const state = store.getState();
|
||||
if (state.phase === "DRAGGING") {
|
||||
publisher.update(state.critical, state.impact);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { moveByWindowScroll } from "../action-creators";
|
||||
import getScrollListener from "../../view/scroll-listener";
|
||||
|
||||
// TODO: this is taken from auto-scroll. Let's make it a util
|
||||
const shouldEnd = (action) =>
|
||||
action.type === "DROP_COMPLETE" || action.type === "DROP_ANIMATE" || action.type === "FLUSH";
|
||||
export default (store) => {
|
||||
const listener = getScrollListener({
|
||||
onWindowScroll: (newScroll) => {
|
||||
store.dispatch(
|
||||
moveByWindowScroll({
|
||||
newScroll
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
return (next) => (action) => {
|
||||
if (!listener.isActive() && action.type === "INITIAL_PUBLISH") {
|
||||
listener.start();
|
||||
}
|
||||
if (listener.isActive() && shouldEnd(action)) {
|
||||
listener.stop();
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export default (marshal) => () => (next) => (action) => {
|
||||
if (action.type === "INITIAL_PUBLISH") {
|
||||
marshal.dragging();
|
||||
}
|
||||
if (action.type === "DROP_ANIMATE") {
|
||||
marshal.dropping(action.payload.completed.result.reason);
|
||||
}
|
||||
|
||||
// this will clear any styles immediately before a reorder
|
||||
if (action.type === "FLUSH" || action.type === "DROP_COMPLETE") {
|
||||
marshal.resting();
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import getDraggablesInsideDroppable from "../../get-draggables-inside-droppable";
|
||||
import { warning } from "../../../dev-warning";
|
||||
|
||||
function checkIndexes(insideDestination) {
|
||||
// no point running if there are 1 or less items
|
||||
if (insideDestination.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const indexes = insideDestination.map((d) => d.descriptor.index);
|
||||
const errors = {};
|
||||
for (let i = 1; i < indexes.length; i++) {
|
||||
const current = indexes[i];
|
||||
const previous = indexes[i - 1];
|
||||
|
||||
// this will be an error if:
|
||||
// 1. index is not consecutive
|
||||
// 2. index is duplicated (which is true if #1 is not passed)
|
||||
if (current !== previous + 1) {
|
||||
errors[current] = true;
|
||||
}
|
||||
}
|
||||
if (!Object.keys(errors).length) {
|
||||
return;
|
||||
}
|
||||
const formatted = indexes
|
||||
.map((index) => {
|
||||
const hasError = Boolean(errors[index]);
|
||||
return hasError ? `[🔥${index}]` : `${index}`;
|
||||
})
|
||||
.join(", ");
|
||||
warning(`
|
||||
Detected non-consecutive <Draggable /> indexes.
|
||||
|
||||
(This can cause unexpected bugs)
|
||||
|
||||
${formatted}
|
||||
`);
|
||||
}
|
||||
|
||||
export default function validateDimensions(critical, dimensions) {
|
||||
// wrapping entire block for better minification
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const insideDestination = getDraggablesInsideDroppable(critical.droppable.id, dimensions.draggables);
|
||||
checkIndexes(insideDestination);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import moveToNextPlace from "./move-to-next-place";
|
||||
import moveCrossAxis from "./move-cross-axis";
|
||||
import whatIsDraggedOver from "../droppable/what-is-dragged-over";
|
||||
|
||||
const getDroppableOver = (impact, droppables) => {
|
||||
const id = whatIsDraggedOver(impact);
|
||||
return id ? droppables[id] : null;
|
||||
};
|
||||
export default ({ state, type }) => {
|
||||
const isActuallyOver = getDroppableOver(state.impact, state.dimensions.droppables);
|
||||
const isMainAxisMovementAllowed = Boolean(isActuallyOver);
|
||||
const home = state.dimensions.droppables[state.critical.droppable.id];
|
||||
// use home when not actually over a droppable (can happen when move is disabled)
|
||||
const isOver = isActuallyOver || home;
|
||||
const direction = isOver.axis.direction;
|
||||
const isMovingOnMainAxis =
|
||||
(direction === "vertical" && (type === "MOVE_UP" || type === "MOVE_DOWN")) ||
|
||||
(direction === "horizontal" && (type === "MOVE_LEFT" || type === "MOVE_RIGHT"));
|
||||
|
||||
// This movement is not permitted right now
|
||||
if (isMovingOnMainAxis && !isMainAxisMovementAllowed) {
|
||||
return null;
|
||||
}
|
||||
const isMovingForward = type === "MOVE_DOWN" || type === "MOVE_RIGHT";
|
||||
const draggable = state.dimensions.draggables[state.critical.draggable.id];
|
||||
const previousPageBorderBoxCenter = state.current.page.borderBoxCenter;
|
||||
const { draggables, droppables } = state.dimensions;
|
||||
return isMovingOnMainAxis
|
||||
? moveToNextPlace({
|
||||
isMovingForward,
|
||||
previousPageBorderBoxCenter,
|
||||
draggable,
|
||||
destination: isOver,
|
||||
draggables,
|
||||
viewport: state.viewport,
|
||||
previousClientSelection: state.current.client.selection,
|
||||
previousImpact: state.impact,
|
||||
afterCritical: state.afterCritical
|
||||
})
|
||||
: moveCrossAxis({
|
||||
isMovingForward,
|
||||
previousPageBorderBoxCenter,
|
||||
draggable,
|
||||
isOver,
|
||||
draggables,
|
||||
droppables,
|
||||
viewport: state.viewport,
|
||||
afterCritical: state.afterCritical
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { invariant } from "../../../invariant";
|
||||
import { closest } from "../../position";
|
||||
import isWithin from "../../is-within";
|
||||
import { getCorners } from "../../spacing";
|
||||
import isPartiallyVisibleThroughFrame from "../../visibility/is-partially-visible-through-frame";
|
||||
import { toDroppableList } from "../../dimension-structures";
|
||||
|
||||
const getKnownActive = (droppable) => {
|
||||
const rect = droppable.subject.active;
|
||||
invariant(rect, "Cannot get clipped area from droppable");
|
||||
return rect;
|
||||
};
|
||||
export default ({ isMovingForward, pageBorderBoxCenter, source, droppables, viewport }) => {
|
||||
const active = source.subject.active;
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
const axis = source.axis;
|
||||
const isBetweenSourceClipped = isWithin(active[axis.start], active[axis.end]);
|
||||
const candidates = toDroppableList(droppables)
|
||||
// Remove the source droppable from the list
|
||||
.filter((droppable) => droppable !== source)
|
||||
// Remove any options that are not enabled
|
||||
.filter((droppable) => droppable.isEnabled)
|
||||
// Remove any droppables that do not have a visible subject
|
||||
.filter((droppable) => Boolean(droppable.subject.active))
|
||||
// Remove any that are not visible in the window
|
||||
.filter((droppable) => isPartiallyVisibleThroughFrame(viewport.frame)(getKnownActive(droppable)))
|
||||
.filter((droppable) => {
|
||||
const activeOfTarget = getKnownActive(droppable);
|
||||
|
||||
// is the target in front of the source on the cross axis?
|
||||
if (isMovingForward) {
|
||||
return active[axis.crossAxisEnd] < activeOfTarget[axis.crossAxisEnd];
|
||||
}
|
||||
// is the target behind the source on the cross axis?
|
||||
return activeOfTarget[axis.crossAxisStart] < active[axis.crossAxisStart];
|
||||
})
|
||||
// Must have some overlap on the main axis
|
||||
.filter((droppable) => {
|
||||
const activeOfTarget = getKnownActive(droppable);
|
||||
const isBetweenDestinationClipped = isWithin(activeOfTarget[axis.start], activeOfTarget[axis.end]);
|
||||
return (
|
||||
isBetweenSourceClipped(activeOfTarget[axis.start]) ||
|
||||
isBetweenSourceClipped(activeOfTarget[axis.end]) ||
|
||||
isBetweenDestinationClipped(active[axis.start]) ||
|
||||
isBetweenDestinationClipped(active[axis.end])
|
||||
);
|
||||
})
|
||||
// Sort on the cross axis
|
||||
.sort((a, b) => {
|
||||
const first = getKnownActive(a)[axis.crossAxisStart];
|
||||
const second = getKnownActive(b)[axis.crossAxisStart];
|
||||
if (isMovingForward) {
|
||||
return first - second;
|
||||
}
|
||||
return second - first;
|
||||
})
|
||||
// Find the droppables that have the same cross axis value as the first item
|
||||
.filter(
|
||||
(droppable, index, array) =>
|
||||
getKnownActive(droppable)[axis.crossAxisStart] === getKnownActive(array[0])[axis.crossAxisStart]
|
||||
);
|
||||
|
||||
// no possible candidates
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// only one result - all done!
|
||||
if (candidates.length === 1) {
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
// At this point we have a number of candidates that
|
||||
// all have the same axis.crossAxisStart value.
|
||||
|
||||
// Check to see if the center position is within the size of a Droppable on the main axis
|
||||
const contains = candidates.filter((droppable) => {
|
||||
const isWithinDroppable = isWithin(getKnownActive(droppable)[axis.start], getKnownActive(droppable)[axis.end]);
|
||||
return isWithinDroppable(pageBorderBoxCenter[axis.line]);
|
||||
});
|
||||
if (contains.length === 1) {
|
||||
return contains[0];
|
||||
}
|
||||
|
||||
// The center point of the draggable falls on the boundary between two droppables
|
||||
if (contains.length > 1) {
|
||||
// sort on the main axis and choose the first
|
||||
return contains.sort((a, b) => getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start])[0];
|
||||
}
|
||||
|
||||
// The center is not contained within any droppable
|
||||
// 1. Find the candidate that has the closest corner
|
||||
// 2. If there is a tie - choose the one that is first on the main axis
|
||||
return candidates.sort((a, b) => {
|
||||
const first = closest(pageBorderBoxCenter, getCorners(getKnownActive(a)));
|
||||
const second = closest(pageBorderBoxCenter, getCorners(getKnownActive(b)));
|
||||
|
||||
// if the distances are not equal - choose the shortest
|
||||
if (first !== second) {
|
||||
return first - second;
|
||||
}
|
||||
|
||||
// They both have the same distance -
|
||||
// choose the one that is first on the main axis
|
||||
return getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start];
|
||||
})[0];
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { distance } from "../../position";
|
||||
import { isTotallyVisible } from "../../visibility/is-visible";
|
||||
import withDroppableDisplacement from "../../with-scroll-change/with-droppable-displacement";
|
||||
import { getCurrentPageBorderBox, getCurrentPageBorderBoxCenter } from "./without-starting-displacement";
|
||||
|
||||
export default ({ pageBorderBoxCenter, viewport, destination, insideDestination, afterCritical }) => {
|
||||
const sorted = insideDestination
|
||||
.filter((draggable) =>
|
||||
// Allowing movement to draggables that are not visible in the viewport
|
||||
// but must be visible in the droppable
|
||||
// We can improve this, but this limitation is easier for now
|
||||
isTotallyVisible({
|
||||
target: getCurrentPageBorderBox(draggable, afterCritical),
|
||||
destination,
|
||||
viewport: viewport.frame,
|
||||
withDroppableDisplacement: true
|
||||
})
|
||||
)
|
||||
.sort((a, b) => {
|
||||
// Need to consider the change in scroll in the destination
|
||||
const distanceToA = distance(
|
||||
pageBorderBoxCenter,
|
||||
withDroppableDisplacement(destination, getCurrentPageBorderBoxCenter(a, afterCritical))
|
||||
);
|
||||
const distanceToB = distance(
|
||||
pageBorderBoxCenter,
|
||||
withDroppableDisplacement(destination, getCurrentPageBorderBoxCenter(b, afterCritical))
|
||||
);
|
||||
|
||||
// if a is closer - return a
|
||||
if (distanceToA < distanceToB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if b is closer - return b
|
||||
if (distanceToB < distanceToA) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if the distance to a and b are the same:
|
||||
// return the one with the lower index (it will be higher on the main axis)
|
||||
return a.descriptor.index - b.descriptor.index;
|
||||
});
|
||||
return sorted[0] || null;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user