Initial commit: MVP+ wish board with FastAPI + Tailwind
This commit is contained in:
commit
cd945a6128
2277 files changed
+843095
No files matched your search
+15
@@ -0,0 +1,15 @@
|
||||
Copyright 2021 Gabriel Santerre
|
||||
|
||||
Copyright 2019 Atlassian Pty Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/2182637/53611918-54c1ff80-3c24-11e9-9917-66ac3cef513d.png" alt="react beautiful dnd logo" />
|
||||
</p>
|
||||
<h1 align="center">@hello-pangea/dnd</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Beautiful** and **accessible** drag and drop for lists with [`React`](https://facebook.github.io/react/)
|
||||
|
||||
[](https://circleci.com/gh/hello-pangea/dnd/tree/main)
|
||||
[](https://www.npmjs.com/package/@hello-pangea/dnd)
|
||||
[](https://discord.gg/zKhPpmvCEv)
|
||||
[](https://conventionalcommits.org)
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
|
||||

|
||||
|
||||
[Play with this example if you want!](https://dnd.hellopangea.com/?path=/story/examples-board--simple)
|
||||
|
||||
</div>
|
||||
|
||||
## Core characteristics
|
||||
|
||||
- Beautiful and [natural movement](/docs/about/animations.md) of items 💐
|
||||
- [Accessible](/docs/about/accessibility.md): powerful keyboard and screen reader support ♿️
|
||||
- [Extremely performant](/docs/support/media.md) 🚀
|
||||
- Clean and powerful api which is simple to get started with
|
||||
- Plays extremely well with standard browser interactions
|
||||
- [Unopinionated styling](/docs/guides/preset-styles.md)
|
||||
- No creation of additional wrapper dom nodes - flexbox and focus management friendly!
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#hello-pangea/dnd&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=hello-pangea/dnd&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=hello-pangea/dnd&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hello-pangea/dnd&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Get started 👩🏫
|
||||
|
||||
Alex Reardon has created [a free course on `egghead.io` 🥚](https://egghead.io/courses/beautiful-and-accessible-drag-and-drop-with-react-beautiful-dnd) (using [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)) to help you get started with `@hello-pangea/dnd` as quickly as possible.
|
||||
|
||||
[](https://egghead.io/courses/beautiful-and-accessible-drag-and-drop-with-react-beautiful-dnd)
|
||||
|
||||
## Currently supported feature set ✅
|
||||
|
||||
- Vertical lists ↕
|
||||
- Horizontal lists ↔
|
||||
- Movement between lists (▤ ↔ ▤)
|
||||
- [Virtual list support 👾](/docs/patterns/virtual-lists.md) - unlocking 10,000 items @ 60fps
|
||||
- [Combining items](/docs/guides/combining.md)
|
||||
- Mouse 🐭, keyboard 🎹♿️ and touch 👉📱 (mobile, tablet and so on) support
|
||||
- [Multi drag support](/docs/patterns/multi-drag.md)
|
||||
- Incredible screen reader support ♿️ - we provide an amazing experience for english screen readers out of the box 📦. We also provide complete customisation control and internationalisation support for those who need it 💖
|
||||
- [Conditional dragging](/docs/api/draggable.md#optional-props) and [conditional dropping](/docs/api/droppable.md#conditionally-dropping)
|
||||
- Multiple independent lists on the one page
|
||||
- Flexible item sizes - the draggable items can have different heights (vertical lists) or widths (horizontal lists)
|
||||
- [Add and remove items during a drag](/docs/guides/changes-while-dragging.md)
|
||||
- Compatible with semantic `<table>` reordering - [table pattern](/docs/patterns/tables.md)
|
||||
- [Auto scrolling](/docs/guides/auto-scrolling.md) - automatically scroll containers and the window as required during a drag (even with keyboard 🔥)
|
||||
- Custom drag handles - you can drag a whole item by just a part of it
|
||||
- Able to move the dragging item to another element while dragging (clone, portal) - [Reparenting your `<Draggable />`](/docs/guides/reparenting.md)
|
||||
- [Create scripted drag and drop experiences 🎮](/docs/sensors/sensor-api.md)
|
||||
- Allows extensions to support for [any input type you like 🕹](/docs/sensors/sensor-api.md)
|
||||
- 🌲 Tree support through the [`@atlaskit/tree`](https://atlaskit.atlassian.com/packages/confluence/tree) package
|
||||
- A `<Droppable />` list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent)
|
||||
- Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list
|
||||
- Server side rendering (SSR) compatible
|
||||
- Plays well with [nested interactive elements](/docs/api/draggable.md#interactive-child-elements-within-a-draggable-) by default
|
||||
|
||||
## Motivation 🤔
|
||||
|
||||
`@hello-pangea/dnd` exists to create beautiful drag and drop for lists that anyone can use - even people who cannot see. For a good overview of the history and motivations of the project you can take a look at these external resources:
|
||||
|
||||
- 📖 [Rethinking drag and drop](https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b)
|
||||
- 🎧 [React podcast: fast, accessible and beautiful drag and drop](https://reactpodcast.simplecast.fm/17)
|
||||
|
||||
## Not for everyone ✌️
|
||||
|
||||
There are a lot of libraries out there that allow for drag and drop interactions within React. Most notable of these is the amazing [`react-dnd`](https://github.com/react-dnd/react-dnd). It does an incredible job at providing a great set of drag and drop primitives which work especially well with the [wildly inconsistent](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html) html5 drag and drop feature. `@hello-pangea/dnd` is a higher level abstraction specifically built for lists (vertical, horizontal, movement between lists, nested lists and so on). Within that subset of functionality `@hello-pangea/dnd` offers a powerful, natural and beautiful drag and drop experience. However, it does not provide the breadth of functionality offered by `react-dnd`. One shortcoming is that grid layouts are not supported (yet). So `@hello-pangea/dnd` might not be for you depending on what your use case is.
|
||||
|
||||
## Documentation 📖
|
||||
|
||||
### About 👋
|
||||
|
||||
- [Installation](/docs/about/installation.md)
|
||||
- [Examples and samples](/docs/about/examples.md)
|
||||
- [Get started](https://egghead.io/courses/beautiful-and-accessible-drag-and-drop-with-react-beautiful-dnd) (This is using [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd))
|
||||
- [Design principles](/docs/about/design-principles.md)
|
||||
- [Animations](/docs/about/animations.md)
|
||||
- [Accessibility](/docs/about/accessibility.md)
|
||||
- [Browser support](/docs/about/browser-support.md)
|
||||
|
||||
### Sensors 🔉
|
||||
|
||||
> The ways in which somebody can start and control a drag
|
||||
|
||||
- [Mouse dragging 🐭](/docs/sensors/mouse.md)
|
||||
- [Touch dragging 👉📱](/docs/sensors/touch.md)
|
||||
- [Keyboard dragging 🎹♿️](/docs/sensors/keyboard.md)
|
||||
- [Create your own sensor](/docs/sensors/sensor-api.md) (allows for any input type as well as scripted experiences)
|
||||
|
||||
### API 🏋️
|
||||
|
||||

|
||||
|
||||
- [`<DragDropContext />`](/docs/api/drag-drop-context.md) - _Wraps the part of your application you want to have drag and drop enabled for_
|
||||
- [`<Droppable />`](/docs/api/droppable.md) - _An area that can be dropped into. Contains `<Draggable />`s_
|
||||
- [`<Draggable />`](/docs/api/draggable.md) - _What can be dragged around_
|
||||
|
||||
### Guides 🗺
|
||||
|
||||
- [`<DragDropContext />` responders](/docs/guides/responders.md) - _`onDragStart`, `onDragUpdate`, `onDragEnd` and `onBeforeDragStart`_
|
||||
- [Combining `<Draggable />`s](/docs/guides/combining.md)
|
||||
- [Common setup issues](/docs/guides/common-setup-issues.md)
|
||||
- [Using `innerRef`](/docs/guides/using-inner-ref.md)
|
||||
- [Setup problem detection and error recovery](/docs/guides/setup-problem-detection-and-error-recovery.md)
|
||||
- [Rules for `draggableId` and `droppableId`s](/docs/guides/identifiers.md)
|
||||
- [Browser focus retention](/docs/guides/browser-focus.md)
|
||||
- [Customising or skipping the drop animation](/docs/guides/drop-animation.md)
|
||||
- [Auto scrolling](/docs/guides/auto-scrolling.md)
|
||||
- [Controlling the screen reader](/docs/guides/screen-reader.md)
|
||||
- [Use the html5 `doctype`](/docs/guides/doctype.md)
|
||||
- [`TypeScript`: type information](/docs/guides/types.md)
|
||||
- [Dragging `<svg>`s](/docs/guides/dragging-svgs.md)
|
||||
- [Avoiding image flickering](/docs/guides/avoiding-image-flickering.md)
|
||||
- [Non-visible preset styles](/docs/guides/preset-styles.md)
|
||||
- [How we detect scroll containers](/docs/guides/how-we-detect-scroll-containers.md)
|
||||
- [How we use dom events](/docs/guides/how-we-use-dom-events.md) - _Useful if you need to build on top of `@hello-pangea/dnd`_
|
||||
- [Adding `<Draggable />`s during a drag (11.x behaviour)](/docs/guides/changes-while-dragging.md) - _⚠️ Advanced_
|
||||
- [Setting up Content Security Policy](/docs/guides/content-security-policy.md)
|
||||
|
||||
### Patterns 👷
|
||||
|
||||
- [Virtual lists 👾](/docs/patterns/virtual-lists.md)
|
||||
- [Multi drag](/docs/patterns/multi-drag.md)
|
||||
- [Tables](/docs/patterns/tables.md)
|
||||
- [Reparenting a `<Draggable />`](/docs/guides/reparenting.md) - _Using our cloning API or your own portal_
|
||||
|
||||
### Support 👩⚕️
|
||||
|
||||
- [Engineering health](/docs/support/engineering-health.md)
|
||||
- [Community and addons](/docs/support/community-and-addons.md)
|
||||
- [Release notes and changelog](https://github.com/hello-pangea/dnd/releases)
|
||||
- [Upgrading](/docs/support/upgrading.md)
|
||||
- [Road map](https://github.com/hello-pangea/dnd/issues)
|
||||
- [Media](/docs/support/media.md)
|
||||
|
||||
## Creator ✍️
|
||||
|
||||
Alex Reardon [@alexandereardon](https://twitter.com/alexandereardon)
|
||||
|
||||
> Alex is no longer personally maintaning this project. The other wonderful maintainers are carrying this project forward.
|
||||
|
||||
## Maintainers 🛠️
|
||||
|
||||
- Gabriel Santerre [@100terres](https://github.com/100terres)
|
||||
- Reece Carolan [@Xhale1](https://github.com/Xhale1)
|
||||
- Many [@Atlassian](https://twitter.com/Atlassian)'s have contributed to the original [`react-beautiful-dnd`](https://github.com/atlassian/react-beautiful-dnd). _Atlassian is no longer involved with this project._
|
||||
|
||||
## Collaborators 🤝
|
||||
|
||||
- Bogdan Chadkin [@IAmTrySound](https://twitter.com/IAmTrySound)
|
||||
|
||||
## Thanks 🤗
|
||||
|
||||
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
|
||||
|
||||
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"name": "@hello-pangea/dnd",
|
||||
"version": "18.0.1",
|
||||
"private": false,
|
||||
"description": "Beautiful and accessible drag and drop for lists with React",
|
||||
"author": "Alex Reardon <areardon@atlassian.com>",
|
||||
"maintainers": [
|
||||
"Gabriel Santerre <gab@100terres.com>",
|
||||
"Reece Carolan <reece@hellopangea.com>"
|
||||
],
|
||||
"keywords": [
|
||||
"drag and drop",
|
||||
"dnd",
|
||||
"sortable",
|
||||
"reorder",
|
||||
"reorderable",
|
||||
"react",
|
||||
"react.js",
|
||||
"natural",
|
||||
"beautiful",
|
||||
"accessible"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hello-pangea/dnd.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hello-pangea/dnd/issues"
|
||||
},
|
||||
"main": "dist/dnd.cjs.js",
|
||||
"module": "dist/dnd.esm.js",
|
||||
"types": "dist/dnd.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"/dist",
|
||||
"/src"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"config": {
|
||||
"prettier_target": "*.{js,jsx,ts,tsx,md,json} src/**/*.{js,jsx,ts,tsx,md,json} test/**/*.{js,jsx,ts,tsx,md,json} docs/**/*.{js,jsx,ts,tsx,md,json} stories/**/*.{js,jsx,ts,tsx,md,json} cypress/**/*.{js,jsx,ts,tsx,md,json} csp-server/**/*.{js,jsx,ts,tsx,md,json}"
|
||||
},
|
||||
"scripts": {
|
||||
"commit": "cz",
|
||||
"chromatic": "chromatic --project-token=f92123f238de",
|
||||
"prepare": "husky",
|
||||
"release": "release-it",
|
||||
"release:test": "release-it --dry-run",
|
||||
"test:accessibility": "lighthouse http://localhost:9002/iframe.html?id=examples-single-vertical-list--basic --no-enable-error-reporting --config-path=lighthouse.config.js --chrome-flags='--headless' --output=json --output=html --output-path=./test-reports/lighthouse/a11y.json && node a11y-audit-parse.js",
|
||||
"test": "jest --config ./jest.config.ts",
|
||||
"test:react-18": "cross-env REACT_MAJOR_VERSION=18 pnpm test",
|
||||
"test:react-19": "cross-env REACT_MAJOR_VERSION=19 pnpm test",
|
||||
"test:browser": "cypress open",
|
||||
"test:browser:ci": "cypress run",
|
||||
"test:coverage": "pnpm test --coverage --coveragePathIgnorePatterns=/debug",
|
||||
"validate": "pnpm prettier:check && pnpm lint:eslint && pnpm lint:css && pnpm typecheck",
|
||||
"prettier:check": "pnpm prettier --debug-check $npm_package_config_prettier_target",
|
||||
"prettier:write": "pnpm prettier --write $npm_package_config_prettier_target",
|
||||
"lint:eslint": "pnpm eslint \"./**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:css": "stylelint \"stories/**/*.{js,jsx,ts,tsx}\"",
|
||||
"typecheck": "pnpm typecheck:lib && pnpm typecheck:test && pnpm typecheck:storybook",
|
||||
"typecheck:lib": "pnpm tsc --project tsconfig.json",
|
||||
"typecheck:storybook": "pnpm tsc --project stories/tsconfig.json && pnpm tsc --project .storybook/tsconfig.json",
|
||||
"typecheck:test": "pnpm tsc --project test/tsconfig.json && pnpm tsc --project csp-server/tsconfig.json && pnpm tsc --project cypress/tsconfig.json",
|
||||
"bundle-size:check": "cross-env SNAPSHOT=match pnpm bundle-size:update",
|
||||
"bundle-size:update": "pnpm build:clean && pnpm build:dist && pnpm build:clean",
|
||||
"build": "pnpm build:clean && pnpm build:dist",
|
||||
"build:clean": "rimraf dist",
|
||||
"build:dist": "cross-env NODE_ENV=production rollup -c",
|
||||
"storybook": "storybook dev -p 9002",
|
||||
"build-storybook": "storybook build -c .storybook -o site",
|
||||
"prepublishOnly": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"css-box-model": "^1.2.1",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atlaskit/css-reset": "6.11.5",
|
||||
"@atlaskit/theme": "13.1.0",
|
||||
"@babel/core": "7.26.8",
|
||||
"@babel/eslint-parser": "7.26.8",
|
||||
"@babel/plugin-transform-class-properties": "7.25.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "7.26.3",
|
||||
"@babel/plugin-transform-object-assign": "7.25.9",
|
||||
"@babel/plugin-transform-private-methods": "7.25.9",
|
||||
"@babel/plugin-transform-private-property-in-object": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.8",
|
||||
"@babel/preset-env": "7.26.8",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@commitlint/cli": "19.7.1",
|
||||
"@commitlint/config-conventional": "19.7.1",
|
||||
"@commitlint/cz-commitlint": "19.6.1",
|
||||
"@emotion/eslint-plugin": "11.12.0",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@jest/environment": "29.7.0",
|
||||
"@release-it/conventional-changelog": "10.0.0",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.8",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.3.1",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@rollup/plugin-strip": "3.0.4",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@storybook/addon-docs": "8.5.3",
|
||||
"@storybook/addon-essentials": "8.5.3",
|
||||
"@storybook/addon-storysource": "8.5.3",
|
||||
"@storybook/addon-webpack5-compiler-swc": "1.0.6",
|
||||
"@storybook/manager-api": "8.5.3",
|
||||
"@storybook/react": "8.5.3",
|
||||
"@storybook/react-webpack5": "8.5.3",
|
||||
"@storybook/theming": "8.5.3",
|
||||
"@swc/core": "1.10.15",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/jest-axe": "3.5.9",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/lodash.isequal": "4.5.8",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/raf-schd": "4.0.3",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-dev-expression": "0.2.3",
|
||||
"babel-plugin-module-resolver": "5.0.2",
|
||||
"browserslist": "4.24.4",
|
||||
"commitizen": "4.3.1",
|
||||
"cross-env": "7.0.3",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.17.0",
|
||||
"dotenv": "16.4.7",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"eslint-plugin-cypress": "2.15.2",
|
||||
"eslint-plugin-es5": "1.5.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-n": "17.15.1",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-storybook": "0.11.2",
|
||||
"express": "4.21.2",
|
||||
"fast-glob": "3.3.3",
|
||||
"fs-extra": "11.3.0",
|
||||
"husky": "9.1.7",
|
||||
"jest": "29.7.0",
|
||||
"jest-axe": "9.0.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"jest-watch-typeahead": "2.2.2",
|
||||
"jsdom": "25.0.1",
|
||||
"lighthouse": "12.3.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"memory-fs": "0.5.0",
|
||||
"postcss-styled-syntax": "0.7.1",
|
||||
"prettier": "3.5.0",
|
||||
"raf-stub": "3.0.0",
|
||||
"react": "19.0.0",
|
||||
"react-18": "npm:react@18.3.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom-18": "npm:react-dom@18.3.1",
|
||||
"react-window": "1.8.11",
|
||||
"release-it": "18.1.2",
|
||||
"require-from-string": "2.0.2",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "4.34.6",
|
||||
"rollup-plugin-dts": "6.1.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"storybook": "8.5.3",
|
||||
"styled-components": "6.1.15",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-recommended": "14.0.1",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.7.3",
|
||||
"wait-on": "8.0.2",
|
||||
"webpack": "5.97.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"jest-junit": {
|
||||
"output": "test-reports/junit/js-test-results.xml"
|
||||
},
|
||||
"packageManager": "pnpm@10.2.1"
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
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: number): string => {
|
||||
const timing = `${duration}s ${curves.drop}`;
|
||||
return `transform ${timing}, opacity ${timing}`;
|
||||
},
|
||||
outOfTheWay: `transform ${outOfTheWayTiming}`,
|
||||
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`,
|
||||
};
|
||||
|
||||
const moveTo = (offset: Position): string | undefined =>
|
||||
isEqual(offset, origin)
|
||||
? undefined
|
||||
: `translate(${offset.x}px, ${offset.y}px)`;
|
||||
|
||||
export const transforms = {
|
||||
moveTo,
|
||||
drop: (offset: Position, isCombining: boolean): string | undefined => {
|
||||
const translate: string | undefined = moveTo(offset);
|
||||
if (!translate) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// only transforming the translate
|
||||
if (!isCombining) {
|
||||
return translate;
|
||||
}
|
||||
|
||||
// when dropping while combining we also update the scale
|
||||
return `${translate} scale(${combine.scale.drop})`;
|
||||
},
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Original author: Alex Reardon
|
||||
* License: MIT
|
||||
* Repo: https://github.com/alexreardon/memoize-one
|
||||
*/
|
||||
|
||||
function isEqual(first: unknown, second: unknown): boolean {
|
||||
if (first === second) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case for NaN (NaN !== NaN)
|
||||
if (Number.isNaN(first) && Number.isNaN(second)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function areInputsEqual(
|
||||
newInputs: readonly unknown[],
|
||||
lastInputs: readonly unknown[],
|
||||
): boolean {
|
||||
// no checks needed if the inputs length has changed
|
||||
if (newInputs.length !== lastInputs.length) {
|
||||
return false;
|
||||
}
|
||||
// Using for loop for speed. It generally performs better than array.every
|
||||
// https://github.com/alexreardon/memoize-one/pull/59
|
||||
for (let i = 0; i < newInputs.length; i++) {
|
||||
if (!isEqual(newInputs[i], lastInputs[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { Action } from '../../state/store-types';
|
||||
|
||||
interface Bucket {
|
||||
[key: string]: number[];
|
||||
}
|
||||
|
||||
const average = (values: number[]): number => {
|
||||
const sum: number = values.reduce(
|
||||
(previous: number, current: number) => previous + current,
|
||||
0,
|
||||
);
|
||||
return sum / values.length;
|
||||
};
|
||||
|
||||
export default (groupSize: number) => {
|
||||
console.log('Starting average action timer middleware');
|
||||
console.log(`Will take an average every ${groupSize} actions`);
|
||||
const bucket: Bucket = {};
|
||||
|
||||
return () =>
|
||||
(next: (a: Action) => unknown) =>
|
||||
(action: Action): any => {
|
||||
const start: number = performance.now();
|
||||
|
||||
const result: unknown = next(action);
|
||||
|
||||
const end: number = performance.now();
|
||||
|
||||
const duration: number = 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;
|
||||
};
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable no-console */
|
||||
import * as timings from '../timings';
|
||||
import type { Action } from '../../state/store-types';
|
||||
|
||||
export default () =>
|
||||
(next: (a: Action) => unknown) =>
|
||||
(action: Action): any => {
|
||||
timings.forceEnable();
|
||||
const key = `redux action: ${action.type}`;
|
||||
timings.start(key);
|
||||
|
||||
const result: unknown = next(action);
|
||||
|
||||
timings.finish(key);
|
||||
|
||||
return result;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { Action, Store } from '../../state/store-types';
|
||||
|
||||
type Mode = 'verbose' | 'light';
|
||||
|
||||
export default (mode: Mode = 'verbose') =>
|
||||
(store: Store) =>
|
||||
(next: (a: Action) => unknown) =>
|
||||
(action: Action): any => {
|
||||
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: unknown = next(action);
|
||||
|
||||
console.log('state after', store.getState());
|
||||
console.groupEnd();
|
||||
|
||||
return result;
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { Action } from '../../state/store-types';
|
||||
|
||||
export default () =>
|
||||
(next: (a: Action) => unknown) =>
|
||||
(action: Action): any => {
|
||||
const title = `👾 redux (action): ${action.type}`;
|
||||
const startMark = `${action.type}:start`;
|
||||
const endMark = `${action.type}:end`;
|
||||
|
||||
performance.mark(startMark);
|
||||
const result: unknown = next(action);
|
||||
performance.mark(endMark);
|
||||
|
||||
performance.measure(title, startMark, endMark);
|
||||
|
||||
return result;
|
||||
};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
interface Records {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
const records: Records = {};
|
||||
let isEnabled = false;
|
||||
|
||||
const isTimingsEnabled = (): boolean => isEnabled;
|
||||
|
||||
export const forceEnable = () => {
|
||||
isEnabled = true;
|
||||
};
|
||||
|
||||
// Debug: uncomment to enable
|
||||
// forceEnable();
|
||||
|
||||
export const start = (key: string) => {
|
||||
// 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: number = performance.now();
|
||||
|
||||
records[key] = now;
|
||||
}
|
||||
};
|
||||
|
||||
interface Style {
|
||||
textColor: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export const finish = (key: string) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now: number = performance.now();
|
||||
|
||||
const previous: number | null = records[key];
|
||||
|
||||
if (!previous) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('cannot finish timing as no previous time found', key);
|
||||
return;
|
||||
}
|
||||
|
||||
const result: number = now - previous;
|
||||
const rounded: string = result.toFixed(2);
|
||||
|
||||
const style: 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;',
|
||||
);
|
||||
}
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
const isProduction: boolean = 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: string): string =>
|
||||
value.replace(spacesAndTabs, ' ').replace(lineStartWithSpaces, '').trim();
|
||||
|
||||
const getDevMessage = (message: string): string =>
|
||||
clean(`
|
||||
%c@hello-pangea/dnd
|
||||
|
||||
%c${clean(message)}
|
||||
|
||||
%c👷 This is a development only message. It will be removed in production builds.
|
||||
`);
|
||||
|
||||
export const getFormattedMessage = (message: string): string[] => [
|
||||
getDevMessage(message),
|
||||
// title (green400)
|
||||
'color: #00C584; font-size: 1.2em; font-weight: bold;',
|
||||
// message
|
||||
'line-height: 1.5',
|
||||
// footer (purple300)
|
||||
'color: #723874;',
|
||||
];
|
||||
|
||||
const isDisabledFlag = '__@hello-pangea/dnd-disable-dev-warnings';
|
||||
|
||||
export function log(type: 'error' | 'warn', message: string): void {
|
||||
// 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
@@ -0,0 +1,5 @@
|
||||
export function noop(): void {}
|
||||
|
||||
export function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
declare type AnimationFrameID = number;
|
||||
declare type TimeoutID = number;
|
||||
declare type IntervalID = number;
|
||||
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: 'development' | 'production';
|
||||
}
|
||||
|
||||
interface Process {
|
||||
env: ProcessEnv;
|
||||
}
|
||||
|
||||
declare const process: Process;
|
||||
|
||||
declare interface Window {
|
||||
'__@hello-pangea/dnd-disable-dev-warnings'?: boolean;
|
||||
}
|
||||
|
||||
// From the test folder we explicitly set typescript to load node.
|
||||
// From the stories folder @storybook/react load NodeJS types.
|
||||
// See: https://github.com/storybookjs/storybook/issues/12969#issue-733867804
|
||||
// Loading node can causes issues similar to what is mention below
|
||||
// See: https://github.com/Microsoft/TypeScript/issues/842
|
||||
// In order to fix the problem we override some node types
|
||||
declare namespace NodeJS {
|
||||
declare type Timeout = TimeoutID;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Public types
|
||||
export type {
|
||||
Announce,
|
||||
BeforeCapture,
|
||||
Direction,
|
||||
DraggableId,
|
||||
DraggableRubric,
|
||||
DragStart,
|
||||
DragUpdate,
|
||||
DraggableLocation,
|
||||
DroppableId,
|
||||
DropResult,
|
||||
FluidDragActions,
|
||||
Id,
|
||||
MovementMode,
|
||||
OnBeforeCaptureResponder,
|
||||
OnBeforeDragStartResponder,
|
||||
OnDragEndResponder,
|
||||
OnDragStartResponder,
|
||||
OnDragUpdateResponder,
|
||||
PreDragActions,
|
||||
ResponderProvided,
|
||||
SensorAPI,
|
||||
Sensor,
|
||||
SnapDragActions,
|
||||
TypeId,
|
||||
TryGetLock,
|
||||
TryGetLockOptions,
|
||||
} from './types';
|
||||
|
||||
// DragDropContext
|
||||
export { default as DragDropContext } from './view/drag-drop-context';
|
||||
export type { DragDropContextProps } from './view/drag-drop-context';
|
||||
|
||||
// Draggable
|
||||
export { default as Draggable } from './view/draggable';
|
||||
export type {
|
||||
DraggableChildrenFn,
|
||||
DraggableProps,
|
||||
DraggableProvided,
|
||||
DraggableProvidedDraggableProps,
|
||||
DraggableProvidedDragHandleProps,
|
||||
DraggableStateSnapshot,
|
||||
DraggableStyle,
|
||||
DraggingStyle,
|
||||
DropAnimation,
|
||||
NotDraggingStyle,
|
||||
} from './view/draggable/draggable-types';
|
||||
|
||||
// Droppable
|
||||
export { default as Droppable } from './view/droppable';
|
||||
export type {
|
||||
DroppableProps,
|
||||
DroppableProvided,
|
||||
DroppableProvidedProps,
|
||||
DroppableStateSnapshot,
|
||||
} from './view/droppable/droppable-types';
|
||||
|
||||
// Sensors
|
||||
export {
|
||||
useMouseSensor,
|
||||
useTouchSensor,
|
||||
useKeyboardSensor,
|
||||
} from './view/use-sensor-marshal';
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const isProduction: boolean = process.env.NODE_ENV === 'production';
|
||||
const prefix = 'Invariant failed';
|
||||
|
||||
export class RbdInvariant extends Error {}
|
||||
|
||||
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: false, message?: string): never;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function invariant(condition: any, message?: string): asserts condition;
|
||||
export function invariant(
|
||||
condition: unknown,
|
||||
message?: string,
|
||||
): asserts condition {
|
||||
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 || ''}`);
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends (...args: any) => any
|
||||
? T[P] | undefined
|
||||
: T[P] extends object
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Original author: Alex Reardon
|
||||
* License: MIT
|
||||
* Repo: https://github.com/alexreardon/memoize-one
|
||||
* Description: A memoization library which only remembers the latest invocation.
|
||||
*/
|
||||
|
||||
import areInputsEqual from './are-inputs-equal';
|
||||
|
||||
export type EqualityFn<TFunc extends (...args: any[]) => any> = (
|
||||
newArgs: Parameters<TFunc>,
|
||||
lastArgs: Parameters<TFunc>,
|
||||
) => boolean;
|
||||
|
||||
export interface MemoizedFn<TFunc extends (this: any, ...args: any[]) => any> {
|
||||
clear: () => void;
|
||||
(
|
||||
this: ThisParameterType<TFunc>,
|
||||
...args: Parameters<TFunc>
|
||||
): ReturnType<TFunc>;
|
||||
}
|
||||
|
||||
// internal type
|
||||
interface Cache<TFunc extends (this: any, ...args: any[]) => any> {
|
||||
lastThis: ThisParameterType<TFunc>;
|
||||
lastArgs: Parameters<TFunc>;
|
||||
lastResult: ReturnType<TFunc>;
|
||||
}
|
||||
|
||||
export function memoizeOne<TFunc extends (this: any, ...newArgs: any[]) => any>(
|
||||
resultFn: TFunc,
|
||||
isEqual: EqualityFn<TFunc> = areInputsEqual,
|
||||
): MemoizedFn<TFunc> {
|
||||
let cache: Cache<TFunc> | null = null;
|
||||
|
||||
// breaking cache when context (this) or arguments change
|
||||
function memoized(
|
||||
this: ThisParameterType<TFunc>,
|
||||
...newArgs: Parameters<TFunc>
|
||||
): ReturnType<TFunc> {
|
||||
if (cache && cache.lastThis === this && isEqual(newArgs, cache.lastArgs)) {
|
||||
return cache.lastResult;
|
||||
}
|
||||
|
||||
// Throwing during an assignment aborts the assignment: https://codepen.io/alexreardon/pen/RYKoaz
|
||||
// Doing the lastResult assignment first so that if it throws
|
||||
// the cache will not be overwritten
|
||||
const lastResult = resultFn.apply(this, newArgs);
|
||||
cache = {
|
||||
lastResult,
|
||||
lastArgs: newArgs,
|
||||
lastThis: this,
|
||||
};
|
||||
|
||||
return lastResult;
|
||||
}
|
||||
|
||||
// Adding the ability to clear the cache of a memoized function
|
||||
memoized.clear = function clear() {
|
||||
cache = null;
|
||||
};
|
||||
|
||||
return memoized;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export function querySelectorAll(
|
||||
parentNode: ParentNode,
|
||||
selector: string,
|
||||
): HTMLElement[] {
|
||||
return Array.from(parentNode.querySelectorAll(selector));
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
DraggableId,
|
||||
DragStart,
|
||||
DragUpdate,
|
||||
DropResult,
|
||||
DraggableLocation,
|
||||
Combine,
|
||||
} from './types';
|
||||
|
||||
export interface MessagePreset {
|
||||
dragHandleUsageInstructions: string;
|
||||
onDragStart: (start: DragStart) => string;
|
||||
onDragUpdate: (update: DragUpdate) => string;
|
||||
onDragEnd: (result: DropResult) => string;
|
||||
}
|
||||
|
||||
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: number): number => 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: DragStart): string => `
|
||||
You have lifted an item in position ${position(start.source.index)}
|
||||
`;
|
||||
|
||||
const withLocation = (
|
||||
source: DraggableLocation,
|
||||
destination: DraggableLocation,
|
||||
) => {
|
||||
const isInHomeList: boolean = source.droppableId === destination.droppableId;
|
||||
|
||||
const startPosition: number = position(source.index);
|
||||
const endPosition: number = 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: DraggableId,
|
||||
source: DraggableLocation,
|
||||
combine: Combine,
|
||||
): string => {
|
||||
const inHomeList: boolean = 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: DragUpdate): string => {
|
||||
const location: DraggableLocation | null = update.destination;
|
||||
if (location) {
|
||||
return withLocation(update.source, location);
|
||||
}
|
||||
|
||||
const combine: Combine | null = 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: DraggableLocation): string => `
|
||||
The item has returned to its starting position
|
||||
of ${position(source.index)}
|
||||
`;
|
||||
|
||||
const onDragEnd = (result: DropResult): string => {
|
||||
if (result.reason === 'CANCEL') {
|
||||
return `
|
||||
Movement cancelled.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
}
|
||||
|
||||
const location: DraggableLocation | null = result.destination;
|
||||
const combine: Combine | null = 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: MessagePreset = {
|
||||
dragHandleUsageInstructions,
|
||||
onDragStart,
|
||||
onDragUpdate,
|
||||
onDragEnd,
|
||||
};
|
||||
|
||||
export default preset;
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
Critical,
|
||||
DraggableId,
|
||||
DroppableId,
|
||||
CompletedDrag,
|
||||
MovementMode,
|
||||
Viewport,
|
||||
DimensionMap,
|
||||
DropReason,
|
||||
Published,
|
||||
} from '../types';
|
||||
|
||||
type ActionTypes = Action['type'];
|
||||
|
||||
type ExtractAction<TActionType extends ActionTypes> = Extract<
|
||||
Action,
|
||||
{ type: TActionType }
|
||||
>;
|
||||
|
||||
export function guard<TActionType extends ActionTypes>(
|
||||
action: unknown,
|
||||
predicate: TActionType,
|
||||
): action is ExtractAction<TActionType> {
|
||||
return (
|
||||
action instanceof Object && 'type' in action && action.type === predicate
|
||||
);
|
||||
}
|
||||
|
||||
export interface BeforeInitialCaptureArgs {
|
||||
draggableId: DraggableId;
|
||||
movementMode: MovementMode;
|
||||
}
|
||||
|
||||
export interface BeforeInitialCaptureAction {
|
||||
type: 'BEFORE_INITIAL_CAPTURE';
|
||||
payload: BeforeInitialCaptureArgs;
|
||||
}
|
||||
|
||||
export type BeforeInitialCaptureActionCreator = typeof beforeInitialCapture;
|
||||
|
||||
export const beforeInitialCapture = (
|
||||
args: BeforeInitialCaptureArgs,
|
||||
): BeforeInitialCaptureAction => ({
|
||||
type: 'BEFORE_INITIAL_CAPTURE',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface LiftArgs {
|
||||
// lifting with DraggableId rather than descriptor
|
||||
// as the descriptor might change after a drop is flushed
|
||||
id: DraggableId;
|
||||
clientSelection: Position;
|
||||
movementMode: MovementMode;
|
||||
}
|
||||
|
||||
export interface LiftAction {
|
||||
type: 'LIFT';
|
||||
payload: LiftArgs;
|
||||
}
|
||||
|
||||
export type LiftActionCreator = typeof lift;
|
||||
|
||||
export const lift = (args: LiftArgs): LiftAction => ({
|
||||
type: 'LIFT',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface InitialPublishArgs {
|
||||
critical: Critical;
|
||||
dimensions: DimensionMap;
|
||||
clientSelection: Position;
|
||||
viewport: Viewport;
|
||||
movementMode: MovementMode;
|
||||
}
|
||||
|
||||
export interface InitialPublishAction {
|
||||
type: 'INITIAL_PUBLISH';
|
||||
payload: InitialPublishArgs;
|
||||
}
|
||||
|
||||
export type InitialPublishActionCreator = typeof initialPublish;
|
||||
|
||||
export const initialPublish = (
|
||||
args: InitialPublishArgs,
|
||||
): InitialPublishAction => ({
|
||||
type: 'INITIAL_PUBLISH',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface PublishWhileDraggingAction {
|
||||
type: 'PUBLISH_WHILE_DRAGGING';
|
||||
payload: Published;
|
||||
}
|
||||
|
||||
export type PublishWhileDraggingArgs = Published;
|
||||
|
||||
export type PublishWhileDraggingActionCreator = typeof publishWhileDragging;
|
||||
|
||||
export const publishWhileDragging = (
|
||||
args: PublishWhileDraggingArgs,
|
||||
): PublishWhileDraggingAction => ({
|
||||
type: 'PUBLISH_WHILE_DRAGGING',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface CollectionStartingAction {
|
||||
type: 'COLLECTION_STARTING';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type CollectionStartingActionCreator = typeof collectionStarting;
|
||||
|
||||
export const collectionStarting = (): CollectionStartingAction => ({
|
||||
type: 'COLLECTION_STARTING',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface UpdateDroppableScrollArgs {
|
||||
id: DroppableId;
|
||||
newScroll: Position;
|
||||
}
|
||||
|
||||
export interface UpdateDroppableScrollAction {
|
||||
type: 'UPDATE_DROPPABLE_SCROLL';
|
||||
payload: UpdateDroppableScrollArgs;
|
||||
}
|
||||
|
||||
export type UpdateDroppableScrollActionCreator = typeof updateDroppableScroll;
|
||||
|
||||
export const updateDroppableScroll = (
|
||||
args: UpdateDroppableScrollArgs,
|
||||
): UpdateDroppableScrollAction => ({
|
||||
type: 'UPDATE_DROPPABLE_SCROLL',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface UpdateDroppableIsEnabledArgs {
|
||||
id: DroppableId;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateDroppableIsEnabledAction {
|
||||
type: 'UPDATE_DROPPABLE_IS_ENABLED';
|
||||
payload: UpdateDroppableIsEnabledArgs;
|
||||
}
|
||||
|
||||
export type UpdateDroppableIsEnabledActionCreator =
|
||||
typeof updateDroppableIsEnabled;
|
||||
|
||||
export const updateDroppableIsEnabled = (
|
||||
args: UpdateDroppableIsEnabledArgs,
|
||||
): UpdateDroppableIsEnabledAction => ({
|
||||
type: 'UPDATE_DROPPABLE_IS_ENABLED',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface UpdateDroppableIsCombineEnabledArgs {
|
||||
id: DroppableId;
|
||||
isCombineEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateDroppableIsCombineEnabledAction {
|
||||
type: 'UPDATE_DROPPABLE_IS_COMBINE_ENABLED';
|
||||
payload: UpdateDroppableIsCombineEnabledArgs;
|
||||
}
|
||||
|
||||
export type UpdateDroppableIsCombineEnabledActionCreator =
|
||||
typeof updateDroppableIsCombineEnabled;
|
||||
|
||||
export const updateDroppableIsCombineEnabled = (
|
||||
args: UpdateDroppableIsCombineEnabledArgs,
|
||||
): UpdateDroppableIsCombineEnabledAction => ({
|
||||
type: 'UPDATE_DROPPABLE_IS_COMBINE_ENABLED',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface MoveArgs {
|
||||
client: Position;
|
||||
}
|
||||
|
||||
export interface MoveAction {
|
||||
type: 'MOVE';
|
||||
payload: MoveArgs;
|
||||
}
|
||||
|
||||
export type MoveActionCreator = typeof move;
|
||||
|
||||
export const move = (args: MoveArgs): MoveAction => ({
|
||||
type: 'MOVE',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
interface MoveByWindowScrollArgs {
|
||||
newScroll: Position;
|
||||
}
|
||||
|
||||
export interface MoveByWindowScrollAction {
|
||||
type: 'MOVE_BY_WINDOW_SCROLL';
|
||||
payload: MoveByWindowScrollArgs;
|
||||
}
|
||||
|
||||
export type MoveByWindowScrollActionCreator = typeof moveByWindowScroll;
|
||||
|
||||
export const moveByWindowScroll = (
|
||||
args: MoveByWindowScrollArgs,
|
||||
): MoveByWindowScrollAction => ({
|
||||
type: 'MOVE_BY_WINDOW_SCROLL',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface UpdateViewportMaxScrollArgs {
|
||||
maxScroll: Position;
|
||||
}
|
||||
|
||||
export interface UpdateViewportMaxScrollAction {
|
||||
type: 'UPDATE_VIEWPORT_MAX_SCROLL';
|
||||
payload: UpdateViewportMaxScrollArgs;
|
||||
}
|
||||
|
||||
export type UpdateViewportMaxScrollActionCreator =
|
||||
typeof updateViewportMaxScroll;
|
||||
|
||||
export const updateViewportMaxScroll = (
|
||||
args: UpdateViewportMaxScrollArgs,
|
||||
): UpdateViewportMaxScrollAction => ({
|
||||
type: 'UPDATE_VIEWPORT_MAX_SCROLL',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface MoveUpAction {
|
||||
type: 'MOVE_UP';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type MoveUpActionCreator = typeof moveUp;
|
||||
|
||||
export const moveUp = (): MoveUpAction => ({
|
||||
type: 'MOVE_UP',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface MoveDownAction {
|
||||
type: 'MOVE_DOWN';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type MoveDownActionCreator = typeof moveDown;
|
||||
|
||||
export const moveDown = (): MoveDownAction => ({
|
||||
type: 'MOVE_DOWN',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface MoveRightAction {
|
||||
type: 'MOVE_RIGHT';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type MoveRightActionCreator = typeof moveRight;
|
||||
|
||||
export const moveRight = (): MoveRightAction => ({
|
||||
type: 'MOVE_RIGHT',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface MoveLeftAction {
|
||||
type: 'MOVE_LEFT';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type MoveLeftActionCreator = typeof moveLeft;
|
||||
|
||||
export const moveLeft = (): MoveLeftAction => ({
|
||||
type: 'MOVE_LEFT',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface FlushAction {
|
||||
type: 'FLUSH';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type FlushActionCreator = typeof flush;
|
||||
|
||||
export const flush = (): FlushAction => ({
|
||||
type: 'FLUSH',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export interface AnimateDropArgs {
|
||||
completed: CompletedDrag;
|
||||
newHomeClientOffset: Position;
|
||||
dropDuration: number;
|
||||
}
|
||||
|
||||
export interface DropAnimateAction {
|
||||
type: 'DROP_ANIMATE';
|
||||
payload: AnimateDropArgs;
|
||||
}
|
||||
|
||||
export type AnimateDropActionCreator = typeof animateDrop;
|
||||
|
||||
export const animateDrop = (args: AnimateDropArgs): DropAnimateAction => ({
|
||||
type: 'DROP_ANIMATE',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface DropCompleteArgs {
|
||||
completed: CompletedDrag;
|
||||
}
|
||||
|
||||
export interface DropCompleteAction {
|
||||
type: 'DROP_COMPLETE';
|
||||
payload: DropCompleteArgs;
|
||||
}
|
||||
|
||||
export type CompleteDropActionCreator = typeof completeDrop;
|
||||
|
||||
export const completeDrop = (args: DropCompleteArgs): DropCompleteAction => ({
|
||||
type: 'DROP_COMPLETE',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
interface DropArgs {
|
||||
reason: DropReason;
|
||||
}
|
||||
|
||||
export interface DropAction {
|
||||
type: 'DROP';
|
||||
payload: DropArgs;
|
||||
}
|
||||
|
||||
export type DropActionCreator = typeof drop;
|
||||
|
||||
export const drop = (args: DropArgs): DropAction => ({
|
||||
type: 'DROP',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export type CancelActionCreator = typeof cancel;
|
||||
|
||||
export const cancel = () => drop({ reason: 'CANCEL' });
|
||||
|
||||
export interface DropPendingAction {
|
||||
type: 'DROP_PENDING';
|
||||
payload: DropArgs;
|
||||
}
|
||||
|
||||
export type DropPendingActionCreator = typeof dropPending;
|
||||
|
||||
export const dropPending = (args: DropArgs): DropPendingAction => ({
|
||||
type: 'DROP_PENDING',
|
||||
payload: args,
|
||||
});
|
||||
|
||||
export interface DropAnimationFinishedAction {
|
||||
type: 'DROP_ANIMATION_FINISHED';
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type DropAnimationFinishedActionCreator = typeof dropAnimationFinished;
|
||||
|
||||
export const dropAnimationFinished = (): DropAnimationFinishedAction => ({
|
||||
type: 'DROP_ANIMATION_FINISHED',
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export type Action =
|
||||
| BeforeInitialCaptureAction
|
||||
| LiftAction
|
||||
| InitialPublishAction
|
||||
| PublishWhileDraggingAction
|
||||
| CollectionStartingAction
|
||||
| UpdateDroppableScrollAction
|
||||
| UpdateDroppableIsEnabledAction
|
||||
| UpdateDroppableIsCombineEnabledAction
|
||||
| MoveByWindowScrollAction // | PostJumpScrollAction
|
||||
// | PostSnapDestinationChangeAction
|
||||
| UpdateViewportMaxScrollAction
|
||||
| MoveAction
|
||||
| MoveUpAction
|
||||
| MoveDownAction
|
||||
| MoveRightAction
|
||||
| MoveLeftAction
|
||||
| DropPendingAction
|
||||
| DropAction
|
||||
| DropAnimateAction
|
||||
| DropAnimationFinishedAction
|
||||
| DropCompleteAction
|
||||
| FlushAction;
|
||||
|
||||
export type ActionCreator =
|
||||
| BeforeInitialCaptureActionCreator
|
||||
| LiftActionCreator
|
||||
| InitialPublishActionCreator
|
||||
| PublishWhileDraggingActionCreator
|
||||
| CollectionStartingActionCreator
|
||||
| UpdateDroppableScrollActionCreator
|
||||
| UpdateDroppableIsEnabledActionCreator
|
||||
| UpdateDroppableIsCombineEnabledActionCreator
|
||||
| MoveActionCreator
|
||||
| MoveByWindowScrollActionCreator
|
||||
| UpdateViewportMaxScrollActionCreator
|
||||
| MoveUpActionCreator
|
||||
| MoveDownActionCreator
|
||||
| MoveRightActionCreator
|
||||
| MoveLeftActionCreator
|
||||
| FlushActionCreator
|
||||
| AnimateDropActionCreator
|
||||
| CompleteDropActionCreator
|
||||
| DropActionCreator
|
||||
| CancelActionCreator
|
||||
| DropPendingActionCreator
|
||||
| DropAnimationFinishedActionCreator;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { State, DraggingState } from '../../types';
|
||||
|
||||
export interface AutoScroller {
|
||||
start: (state: DraggingState) => void;
|
||||
stop: () => void;
|
||||
scroll: (state: State) => void;
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { add, apply, isEqual, origin } from '../position';
|
||||
import type { DroppableDimension, Viewport, Scrollable } from '../../types';
|
||||
|
||||
interface CanPartiallyScrollArgs {
|
||||
max: Position;
|
||||
current: Position;
|
||||
change: Position;
|
||||
}
|
||||
|
||||
const smallestSigned = apply((value: number) => {
|
||||
if (value === 0) {
|
||||
return 0;
|
||||
}
|
||||
return value > 0 ? 1 : -1;
|
||||
});
|
||||
|
||||
interface GetRemainderArgs {
|
||||
current: Position;
|
||||
max: Position;
|
||||
change: Position;
|
||||
}
|
||||
|
||||
// We need to figure out how much of the movement
|
||||
// cannot be done with a scroll
|
||||
export const getOverlap = (() => {
|
||||
const getRemainder = (target: number, max: number): number => {
|
||||
if (target < 0) {
|
||||
return target;
|
||||
}
|
||||
if (target > max) {
|
||||
return target - max;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return ({ current, max, change }: GetRemainderArgs): Position | null => {
|
||||
const targetScroll: Position = add(current, change);
|
||||
|
||||
const overlap: Position = {
|
||||
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,
|
||||
}: CanPartiallyScrollArgs): boolean => {
|
||||
// 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: Position = {
|
||||
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: Position = smallestSigned(change);
|
||||
|
||||
const overlap: Position | null = 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: Viewport,
|
||||
change: Position,
|
||||
): boolean =>
|
||||
canPartiallyScroll({
|
||||
current: viewport.scroll.current,
|
||||
max: viewport.scroll.max,
|
||||
change,
|
||||
});
|
||||
|
||||
export const getWindowOverlap = (
|
||||
viewport: Viewport,
|
||||
change: Position,
|
||||
): Position | null => {
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const max: Position = viewport.scroll.max;
|
||||
const current: Position = viewport.scroll.current;
|
||||
|
||||
return getOverlap({
|
||||
current,
|
||||
max,
|
||||
change,
|
||||
});
|
||||
};
|
||||
|
||||
export const canScrollDroppable = (
|
||||
droppable: DroppableDimension,
|
||||
change: Position,
|
||||
): boolean => {
|
||||
const frame: Scrollable | null = 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: DroppableDimension,
|
||||
change: Position,
|
||||
): Position | null => {
|
||||
const frame: Scrollable | null = droppable.frame;
|
||||
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getOverlap({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change,
|
||||
});
|
||||
};
|
||||
node_modules/@hello-pangea/dnd/src/state/auto-scroller/fluid-scroller/auto-scroller-options-types.ts
Generated
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
import { RecursivePartial } from '../../../lib-types';
|
||||
|
||||
/**
|
||||
* Customize autoScroller behavior
|
||||
*/
|
||||
export interface AutoScrollerOptions {
|
||||
/**
|
||||
* Percentage distance from edge of container at which to start auto scrolling.
|
||||
* ex. 0.1 or 0.9
|
||||
*/
|
||||
startFromPercentage: number;
|
||||
|
||||
/**
|
||||
* Percentage distance from edge of container at which max scroll speed is achieved.
|
||||
* Should be less than startFromPercentage
|
||||
*/
|
||||
maxScrollAtPercentage: number;
|
||||
|
||||
/**
|
||||
* Maximum pixels to scroll per frame
|
||||
*/
|
||||
maxPixelScroll: number;
|
||||
|
||||
/**
|
||||
* 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: number) => number;
|
||||
|
||||
durationDampening: {
|
||||
/**
|
||||
* How long to dampen the speed of an auto scroll from the start of a drag in milliseconds
|
||||
*/
|
||||
stopDampeningAt: number;
|
||||
|
||||
/**
|
||||
* When to start accelerating the reduction of duration dampening in milliseconds
|
||||
*/
|
||||
accelerateAt: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not autoscroll should be turned off entirely
|
||||
*/
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export type PartialAutoScrollerOptions = RecursivePartial<AutoScrollerOptions>;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { AutoScrollerOptions } from './auto-scroller-options-types';
|
||||
|
||||
// default autoScroll configuration options
|
||||
export const defaultAutoScrollerOptions: AutoScrollerOptions = {
|
||||
startFromPercentage: 0.25,
|
||||
maxScrollAtPercentage: 0.05,
|
||||
maxPixelScroll: 28,
|
||||
ease: (percentage: number): number => percentage ** 2,
|
||||
durationDampening: {
|
||||
stopDampeningAt: 1200,
|
||||
accelerateAt: 360,
|
||||
},
|
||||
disabled: false,
|
||||
};
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { memoizeOne } from '../../../memoize-one';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
DroppableDimensionMap,
|
||||
DroppableId,
|
||||
} from '../../../types';
|
||||
import { invariant } from '../../../invariant';
|
||||
import isPositionInFrame from '../../visibility/is-position-in-frame';
|
||||
import { toDroppableList } from '../../dimension-structures';
|
||||
|
||||
const getScrollableDroppables = memoizeOne(
|
||||
(droppables: DroppableDimensionMap): DroppableDimension[] =>
|
||||
toDroppableList(droppables).filter(
|
||||
(droppable: DroppableDimension): boolean => {
|
||||
// exclude disabled droppables
|
||||
if (!droppable.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only want droppables that are scrollable
|
||||
if (!droppable.frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const getScrollableDroppableOver = (
|
||||
target: Position,
|
||||
droppables: DroppableDimensionMap,
|
||||
): DroppableDimension | null => {
|
||||
const maybe =
|
||||
getScrollableDroppables(droppables).find(
|
||||
(droppable: DroppableDimension): boolean => {
|
||||
invariant(droppable.frame, 'Invalid result');
|
||||
|
||||
return isPositionInFrame(droppable.frame.pageMarginBox)(target);
|
||||
},
|
||||
) || null;
|
||||
|
||||
return maybe;
|
||||
};
|
||||
|
||||
interface Api {
|
||||
center: Position;
|
||||
destination: DroppableId | null;
|
||||
droppables: DroppableDimensionMap;
|
||||
}
|
||||
|
||||
export default ({
|
||||
center,
|
||||
destination,
|
||||
droppables,
|
||||
}: Api): DroppableDimension | null => {
|
||||
// We need to scroll the best droppable frame we can so that the
|
||||
// placeholder buffer logic works correctly
|
||||
|
||||
if (destination) {
|
||||
const dimension: DroppableDimension = 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: DroppableDimension | null = getScrollableDroppableOver(
|
||||
center,
|
||||
droppables,
|
||||
);
|
||||
|
||||
return dimension;
|
||||
};
|
||||
node_modules/@hello-pangea/dnd/src/state/auto-scroller/fluid-scroller/get-droppable-scroll-change.ts
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
import type { Position, Rect } from 'css-box-model';
|
||||
import type { Scrollable, DroppableDimension } from '../../../types';
|
||||
import getScroll from './get-scroll';
|
||||
import { canScrollDroppable } from '../can-scroll';
|
||||
import { AutoScrollerOptions } from './auto-scroller-options-types';
|
||||
|
||||
interface Args {
|
||||
droppable: DroppableDimension;
|
||||
subject: Rect;
|
||||
center: Position;
|
||||
dragStartTime: number;
|
||||
shouldUseTimeDampening: boolean;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
droppable,
|
||||
subject,
|
||||
center,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): Position | null => {
|
||||
// We know this has a closestScrollable
|
||||
const frame: Scrollable | null = droppable.frame;
|
||||
|
||||
// this should never happen - just being safe
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scroll: Position | null = getScroll({
|
||||
dragStartTime,
|
||||
container: frame.pageMarginBox,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
return scroll && canScrollDroppable(droppable, scroll) ? scroll : null;
|
||||
};
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { warning } from '../../../dev-warning';
|
||||
|
||||
interface Args {
|
||||
startOfRange: number;
|
||||
endOfRange: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export default ({ startOfRange, endOfRange, current }: Args): number => {
|
||||
const range: number = 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: number = current - startOfRange;
|
||||
const percentage: number = currentInRange / range;
|
||||
return percentage;
|
||||
};
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import type { Rect, Position } from 'css-box-model';
|
||||
|
||||
interface Args {
|
||||
container: Rect;
|
||||
subject: Rect;
|
||||
proposedScroll: Position;
|
||||
}
|
||||
|
||||
export default ({
|
||||
container,
|
||||
subject,
|
||||
proposedScroll,
|
||||
}: Args): Position | null => {
|
||||
const isTooBigVertically: boolean = subject.height > container.height;
|
||||
const isTooBigHorizontally: boolean = 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,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import getPercentage from '../../get-percentage';
|
||||
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
|
||||
import minScroll from './min-scroll';
|
||||
|
||||
export default (
|
||||
proposedScroll: number,
|
||||
dragStartTime: number,
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions,
|
||||
): number => {
|
||||
const autoScrollerOptions = getAutoScrollerOptions();
|
||||
|
||||
const accelerateAt: number =
|
||||
autoScrollerOptions.durationDampening.accelerateAt;
|
||||
const stopAt: number = autoScrollerOptions.durationDampening.stopDampeningAt;
|
||||
|
||||
const startOfRange: number = dragStartTime;
|
||||
const endOfRange: number = stopAt;
|
||||
const now: number = Date.now();
|
||||
const runTime: number = 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: number = getPercentage({
|
||||
startOfRange: accelerateAt,
|
||||
endOfRange,
|
||||
current: runTime,
|
||||
});
|
||||
|
||||
const scroll: number =
|
||||
proposedScroll *
|
||||
autoScrollerOptions.ease(betweenAccelerateAtAndStopAtPercentage);
|
||||
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
import type { Rect } from 'css-box-model';
|
||||
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
|
||||
import type { Axis } from '../../../../../types';
|
||||
import { defaultAutoScrollerOptions } from '../../config';
|
||||
|
||||
// all in pixels
|
||||
export interface DistanceThresholds {
|
||||
startScrollingFrom: number;
|
||||
maxScrollValueAt: number;
|
||||
}
|
||||
|
||||
// converts the percentages in the config into actual pixel values
|
||||
export default (
|
||||
container: Rect,
|
||||
axis: Axis,
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions = () =>
|
||||
defaultAutoScrollerOptions,
|
||||
): DistanceThresholds => {
|
||||
const autoScrollerOptions = getAutoScrollerOptions();
|
||||
|
||||
const startScrollingFrom: number =
|
||||
container[axis.size] * autoScrollerOptions.startFromPercentage;
|
||||
const maxScrollValueAt: number =
|
||||
container[axis.size] * autoScrollerOptions.maxScrollAtPercentage;
|
||||
|
||||
const thresholds: DistanceThresholds = {
|
||||
startScrollingFrom,
|
||||
maxScrollValueAt,
|
||||
};
|
||||
|
||||
return thresholds;
|
||||
};
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
import type { DistanceThresholds } from './get-distance-thresholds';
|
||||
import getPercentage from '../../get-percentage';
|
||||
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
|
||||
import minScroll from './min-scroll';
|
||||
import { defaultAutoScrollerOptions } from '../../config';
|
||||
|
||||
export default (
|
||||
distanceToEdge: number,
|
||||
thresholds: DistanceThresholds,
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions = () =>
|
||||
defaultAutoScrollerOptions,
|
||||
): number => {
|
||||
const autoScrollerOptions = getAutoScrollerOptions();
|
||||
|
||||
/*
|
||||
// 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 autoScrollerOptions.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: number = getPercentage({
|
||||
startOfRange: thresholds.maxScrollValueAt,
|
||||
endOfRange: thresholds.startScrollingFrom,
|
||||
current: distanceToEdge,
|
||||
});
|
||||
|
||||
const percentageFromStartScrollingFrom: number =
|
||||
1 - percentageFromMaxScrollValueAt;
|
||||
|
||||
const scroll: number =
|
||||
autoScrollerOptions.maxPixelScroll *
|
||||
autoScrollerOptions.ease(percentageFromStartScrollingFrom);
|
||||
|
||||
// scroll will always be a positive integer
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import type { DistanceThresholds } from './get-distance-thresholds';
|
||||
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
|
||||
import getValueFromDistance from './get-value-from-distance';
|
||||
import dampenValueByTime from './dampen-value-by-time';
|
||||
import minScroll from './min-scroll';
|
||||
|
||||
interface Args {
|
||||
distanceToEdge: number;
|
||||
thresholds: DistanceThresholds;
|
||||
dragStartTime: number;
|
||||
shouldUseTimeDampening: boolean;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
distanceToEdge,
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): number => {
|
||||
const scroll: number = getValueFromDistance(
|
||||
distanceToEdge,
|
||||
thresholds,
|
||||
getAutoScrollerOptions,
|
||||
);
|
||||
|
||||
// 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, getAutoScrollerOptions),
|
||||
minScroll,
|
||||
);
|
||||
};
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
import type { Rect, Spacing } from 'css-box-model';
|
||||
import getDistanceThresholds from './get-distance-thresholds';
|
||||
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
|
||||
import type { DistanceThresholds } from './get-distance-thresholds';
|
||||
import type { Axis } from '../../../../../types';
|
||||
import getValue from './get-value';
|
||||
|
||||
interface GetOnAxisArgs {
|
||||
container: Rect;
|
||||
distanceToEdges: Spacing;
|
||||
dragStartTime: number;
|
||||
axis: Axis;
|
||||
shouldUseTimeDampening: boolean;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
}: GetOnAxisArgs): number => {
|
||||
const thresholds: DistanceThresholds = getDistanceThresholds(
|
||||
container,
|
||||
axis,
|
||||
getAutoScrollerOptions,
|
||||
);
|
||||
const isCloserToEnd: boolean =
|
||||
distanceToEdges[axis.end] < distanceToEdges[axis.start];
|
||||
|
||||
if (isCloserToEnd) {
|
||||
return getValue({
|
||||
distanceToEdge: distanceToEdges[axis.end],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
-1 *
|
||||
getValue({
|
||||
distanceToEdge: distanceToEdges[axis.start],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
})
|
||||
);
|
||||
};
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// A scroll event will only be triggered when there is a value of at least 1px change
|
||||
export default 1;
|
||||
Generated
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
import type { Position, Rect, Spacing } from 'css-box-model';
|
||||
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';
|
||||
import { AutoScrollerOptions } from '../auto-scroller-options-types';
|
||||
|
||||
// will replace -0 and replace with +0
|
||||
const clean = apply((value: number) => (value === 0 ? 0 : value));
|
||||
|
||||
interface Args {
|
||||
dragStartTime: number;
|
||||
container: Rect;
|
||||
subject: Rect;
|
||||
center: Position;
|
||||
shouldUseTimeDampening: boolean;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
dragStartTime,
|
||||
container,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): Position | null => {
|
||||
// get distance to each edge
|
||||
const distanceToEdges: Spacing = {
|
||||
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: number = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: vertical,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
const x: number = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: horizontal,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
const required: Position = 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: Position | null = adjustForSizeLimits({
|
||||
container,
|
||||
subject,
|
||||
proposedScroll: required,
|
||||
});
|
||||
|
||||
if (!limited) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isEqual(limited, origin) ? null : limited;
|
||||
};
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import type { Position, Rect } from 'css-box-model';
|
||||
import type { Viewport } from '../../../types';
|
||||
import getScroll from './get-scroll';
|
||||
import { canScrollWindow } from '../can-scroll';
|
||||
import { AutoScrollerOptions } from './auto-scroller-options-types';
|
||||
|
||||
interface Args {
|
||||
viewport: Viewport;
|
||||
subject: Rect;
|
||||
center: Position;
|
||||
dragStartTime: number;
|
||||
shouldUseTimeDampening: boolean;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
viewport,
|
||||
subject,
|
||||
center,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): Position | null => {
|
||||
const scroll: Position | null = getScroll({
|
||||
dragStartTime,
|
||||
container: viewport.frame,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
return scroll && canScrollWindow(viewport, scroll) ? scroll : null;
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import rafSchd from 'raf-schd';
|
||||
import type { Position } from 'css-box-model';
|
||||
import type { DraggingState, DroppableId } from '../../../types';
|
||||
import scroll from './scroll';
|
||||
import { invariant } from '../../../invariant';
|
||||
import * as timings from '../../../debug/timings';
|
||||
import { AutoScrollerOptions } from './auto-scroller-options-types';
|
||||
import { defaultAutoScrollerOptions } from './config';
|
||||
|
||||
export interface PublicArgs {
|
||||
scrollWindow: (change: Position) => void;
|
||||
scrollDroppable: (id: DroppableId, change: Position) => void;
|
||||
getAutoScrollerOptions?: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export interface FluidScroller {
|
||||
scroll: (state: DraggingState) => void;
|
||||
start: (state: DraggingState) => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
interface WhileDragging {
|
||||
dragStartTime: number;
|
||||
shouldUseTimeDampening: boolean;
|
||||
}
|
||||
|
||||
export default ({
|
||||
scrollWindow,
|
||||
scrollDroppable,
|
||||
getAutoScrollerOptions = () => defaultAutoScrollerOptions,
|
||||
}: PublicArgs): FluidScroller => {
|
||||
const scheduleWindowScroll = rafSchd(scrollWindow);
|
||||
const scheduleDroppableScroll = rafSchd(scrollDroppable);
|
||||
let dragging: WhileDragging | null = null;
|
||||
|
||||
const tryScroll = (state: DraggingState): void => {
|
||||
invariant(dragging, 'Cannot fluid scroll if not dragging');
|
||||
const { shouldUseTimeDampening, dragStartTime } = dragging;
|
||||
|
||||
scroll({
|
||||
state,
|
||||
scrollWindow: scheduleWindowScroll,
|
||||
scrollDroppable: scheduleDroppableScroll,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const start = (state: DraggingState) => {
|
||||
timings.start('starting fluid scroller');
|
||||
invariant(!dragging, 'Cannot start auto scrolling when already started');
|
||||
const dragStartTime: number = Date.now();
|
||||
|
||||
let wasScrollNeeded = false;
|
||||
const fakeScrollCallback = () => {
|
||||
wasScrollNeeded = true;
|
||||
};
|
||||
scroll({
|
||||
state,
|
||||
dragStartTime: 0,
|
||||
shouldUseTimeDampening: false,
|
||||
scrollWindow: fakeScrollCallback,
|
||||
scrollDroppable: fakeScrollCallback,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { Position, Rect } from 'css-box-model';
|
||||
import type {
|
||||
DraggingState,
|
||||
DroppableId,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
Viewport,
|
||||
} from '../../../types';
|
||||
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';
|
||||
import { AutoScrollerOptions } from './auto-scroller-options-types';
|
||||
|
||||
interface Args {
|
||||
state: DraggingState;
|
||||
dragStartTime: number;
|
||||
shouldUseTimeDampening: boolean;
|
||||
scrollWindow: (scroll: Position) => void;
|
||||
scrollDroppable: (id: DroppableId, scroll: Position) => void;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
state,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening,
|
||||
scrollWindow,
|
||||
scrollDroppable,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): void => {
|
||||
const center: Position = state.current.page.borderBoxCenter;
|
||||
const draggable: DraggableDimension =
|
||||
state.dimensions.draggables[state.critical.draggable.id];
|
||||
const subject: Rect = draggable.page.marginBox;
|
||||
// 1. Can we scroll the viewport?
|
||||
if (state.isWindowScrollAllowed) {
|
||||
const viewport: Viewport = state.viewport;
|
||||
const change: Position | null = getWindowScrollChange({
|
||||
dragStartTime,
|
||||
viewport,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
if (change) {
|
||||
scrollWindow(change);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const droppable: DroppableDimension | null = getBestScrollableDroppable({
|
||||
center,
|
||||
destination: whatIsDraggedOver(state.impact),
|
||||
droppables: state.dimensions.droppables,
|
||||
});
|
||||
|
||||
if (!droppable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const change: Position | null = getDroppableScrollChange({
|
||||
dragStartTime,
|
||||
droppable,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
if (change) {
|
||||
scrollDroppable(droppable.descriptor.id, change);
|
||||
}
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import createFluidScroller from './fluid-scroller';
|
||||
import type { FluidScroller } from './fluid-scroller';
|
||||
import createJumpScroller from './jump-scroller';
|
||||
import type { JumpScroller } from './jump-scroller';
|
||||
import type { AutoScroller } from './auto-scroller-types';
|
||||
import type { DroppableId, State } from '../../types';
|
||||
import type { MoveArgs } from '../action-creators';
|
||||
import { AutoScrollerOptions } from './fluid-scroller/auto-scroller-options-types';
|
||||
|
||||
export interface Args {
|
||||
scrollWindow: (offset: Position) => void;
|
||||
scrollDroppable: (id: DroppableId, change: Position) => void;
|
||||
move: (args: MoveArgs) => unknown;
|
||||
getAutoScrollerOptions: () => AutoScrollerOptions;
|
||||
}
|
||||
|
||||
export default ({
|
||||
scrollDroppable,
|
||||
scrollWindow,
|
||||
move,
|
||||
getAutoScrollerOptions,
|
||||
}: Args): AutoScroller => {
|
||||
const fluidScroller: FluidScroller = createFluidScroller({
|
||||
scrollWindow,
|
||||
scrollDroppable,
|
||||
getAutoScrollerOptions,
|
||||
});
|
||||
|
||||
const jumpScroll: JumpScroller = createJumpScroller({
|
||||
move,
|
||||
scrollWindow,
|
||||
scrollDroppable,
|
||||
});
|
||||
|
||||
const scroll = (state: State) => {
|
||||
const autoScrollerOptions = getAutoScrollerOptions();
|
||||
|
||||
// Only allowing auto scrolling in the DRAGGING phase
|
||||
// and when autoScroll is not disabled
|
||||
if (autoScrollerOptions.disabled || state.phase !== 'DRAGGING') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.movementMode === 'FLUID') {
|
||||
fluidScroller.scroll(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.scrollJumpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
jumpScroll(state);
|
||||
};
|
||||
|
||||
const scroller: AutoScroller = {
|
||||
scroll,
|
||||
start: fluidScroller.start,
|
||||
stop: fluidScroller.stop,
|
||||
};
|
||||
|
||||
return scroller;
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
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';
|
||||
import type { MoveArgs } from '../action-creators';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
Viewport,
|
||||
DraggingState,
|
||||
DroppableId,
|
||||
} from '../../types';
|
||||
|
||||
interface Args {
|
||||
scrollDroppable: (id: DroppableId, change: Position) => void;
|
||||
scrollWindow: (offset: Position) => void;
|
||||
move: (args: MoveArgs) => unknown;
|
||||
}
|
||||
|
||||
export type JumpScroller = (state: DraggingState) => void;
|
||||
|
||||
type Remainder = Position;
|
||||
|
||||
export default ({
|
||||
move,
|
||||
scrollDroppable,
|
||||
scrollWindow,
|
||||
}: Args): JumpScroller => {
|
||||
const moveByOffset = (state: DraggingState, offset: Position) => {
|
||||
const client: Position = add(state.current.client.selection, offset);
|
||||
move({ client });
|
||||
};
|
||||
|
||||
const scrollDroppableAsMuchAsItCan = (
|
||||
droppable: DroppableDimension,
|
||||
change: Position,
|
||||
): Remainder | null => {
|
||||
// Droppable cannot absorb any of the scroll
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const overlap: Position | null = 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: Position = subtract(change, overlap);
|
||||
scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll);
|
||||
|
||||
const remainder: Position = subtract(change, whatTheDroppableCanScroll);
|
||||
return remainder;
|
||||
};
|
||||
|
||||
const scrollWindowAsMuchAsItCan = (
|
||||
isWindowScrollAllowed: boolean,
|
||||
viewport: Viewport,
|
||||
change: Position,
|
||||
): Position | null => {
|
||||
if (!isWindowScrollAllowed) {
|
||||
return change;
|
||||
}
|
||||
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
// window cannot absorb any of the scroll
|
||||
return change;
|
||||
}
|
||||
|
||||
const overlap: Position | null = 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: Position = subtract(change, overlap);
|
||||
scrollWindow(whatTheWindowCanScroll);
|
||||
|
||||
const remainder: Position = subtract(change, whatTheWindowCanScroll);
|
||||
return remainder;
|
||||
};
|
||||
|
||||
const jumpScroller: JumpScroller = (state: DraggingState) => {
|
||||
const request: Position | null = state.scrollJumpRequest;
|
||||
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destination: DroppableId | null = 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: Position | null = scrollDroppableAsMuchAsItCan(
|
||||
state.dimensions.droppables[destination],
|
||||
request,
|
||||
);
|
||||
|
||||
// droppable absorbed the entire scroll
|
||||
if (!droppableRemainder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport: Viewport = state.viewport;
|
||||
const windowRemainder: Position | null = 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;
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import type { HorizontalAxis, VerticalAxis } from '../types';
|
||||
|
||||
export const vertical: VerticalAxis = {
|
||||
direction: 'vertical',
|
||||
line: 'y',
|
||||
crossAxisLine: 'x',
|
||||
start: 'top',
|
||||
end: 'bottom',
|
||||
size: 'height',
|
||||
crossAxisStart: 'left',
|
||||
crossAxisEnd: 'right',
|
||||
crossAxisSize: 'width',
|
||||
};
|
||||
|
||||
export const horizontal: HorizontalAxis = {
|
||||
direction: 'horizontal',
|
||||
line: 'x',
|
||||
crossAxisLine: 'y',
|
||||
start: 'left',
|
||||
end: 'right',
|
||||
size: 'width',
|
||||
crossAxisStart: 'top',
|
||||
crossAxisEnd: 'bottom',
|
||||
crossAxisSize: 'height',
|
||||
};
|
||||
Generated
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
import type {
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DragImpact,
|
||||
DisplacementGroups,
|
||||
Viewport,
|
||||
DisplacedBy,
|
||||
} from '../../types';
|
||||
import removeDraggableFromList from '../remove-draggable-from-list';
|
||||
import isHomeOf from '../droppable/is-home-of';
|
||||
import { emptyGroups } from '../no-impact';
|
||||
import getDisplacementGroups from '../get-displacement-groups';
|
||||
|
||||
interface Args {
|
||||
draggable: DraggableDimension;
|
||||
insideDestination: DraggableDimension[];
|
||||
destination: DroppableDimension;
|
||||
viewport: Viewport;
|
||||
displacedBy: DisplacedBy;
|
||||
last: DisplacementGroups;
|
||||
index: number | null;
|
||||
forceShouldAnimate?: boolean;
|
||||
}
|
||||
|
||||
function getIndexOfLastItem(
|
||||
draggables: DraggableDimension[],
|
||||
options: {
|
||||
inHomeList: boolean;
|
||||
},
|
||||
): number {
|
||||
if (!draggables.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const indexOfLastItem: number =
|
||||
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;
|
||||
}
|
||||
|
||||
interface GoAtEndArgs {
|
||||
insideDestination: DraggableDimension[];
|
||||
inHomeList: boolean;
|
||||
displacedBy: DisplacedBy;
|
||||
destination: DroppableDimension;
|
||||
}
|
||||
|
||||
function goAtEnd({
|
||||
insideDestination,
|
||||
inHomeList,
|
||||
displacedBy,
|
||||
destination,
|
||||
}: GoAtEndArgs): DragImpact {
|
||||
const newIndex: number = 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,
|
||||
}: Args): DragImpact {
|
||||
const inHomeList: boolean = 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 = insideDestination.find(
|
||||
(item: DraggableDimension) => item.descriptor.index === index,
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
return goAtEnd({
|
||||
insideDestination,
|
||||
inHomeList,
|
||||
displacedBy,
|
||||
destination,
|
||||
});
|
||||
}
|
||||
const withoutDragging: DraggableDimension[] = removeDraggableFromList(
|
||||
draggable,
|
||||
insideDestination,
|
||||
);
|
||||
|
||||
const sliceFrom: number = insideDestination.indexOf(match);
|
||||
const impacted: DraggableDimension[] = withoutDragging.slice(sliceFrom);
|
||||
|
||||
const displaced: DisplacementGroups = getDisplacementGroups({
|
||||
afterDragging: impacted,
|
||||
destination,
|
||||
displacedBy,
|
||||
last,
|
||||
viewport: viewport.frame,
|
||||
forceShouldAnimate,
|
||||
});
|
||||
|
||||
return {
|
||||
displaced,
|
||||
displacedBy,
|
||||
at: {
|
||||
type: 'REORDER',
|
||||
destination: {
|
||||
droppableId: destination.descriptor.id,
|
||||
index,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { State, DraggableId } from '../types';
|
||||
|
||||
export default (state: State, id: DraggableId): boolean => {
|
||||
// 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';
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { applyMiddleware, createStore, compose, StoreEnhancer } 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';
|
||||
import type { DimensionMarshal } from './dimension-marshal/dimension-marshal-types';
|
||||
import type { FocusMarshal } from '../view/use-focus-marshal/focus-marshal-types';
|
||||
import type { StyleMarshal } from '../view/use-style-marshal/style-marshal-types';
|
||||
import type { AutoScroller } from './auto-scroller/auto-scroller-types';
|
||||
import type { Responders, Announce, State } from '../types';
|
||||
import type { Store, Dispatch } from './store-types';
|
||||
|
||||
// For more config
|
||||
// See: https://github.com/reduxjs/redux-devtools/blob/main/packages/redux-devtools-extension/src/index.ts#L3
|
||||
interface Config {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ReduxDevtoolsExtensionCompose {
|
||||
(config: Config): (...funcs: StoreEnhancer[]) => StoreEnhancer;
|
||||
(...funcs: StoreEnhancer[]): StoreEnhancer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: ReduxDevtoolsExtensionCompose;
|
||||
}
|
||||
}
|
||||
|
||||
// See: https://github.com/reduxjs/redux-devtools/blob/main/packages/redux-devtools-extension/src/index.ts#L219-L222
|
||||
const composeEnhancers =
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
typeof window !== 'undefined' &&
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||
name: '@hello-pangea/dnd',
|
||||
})
|
||||
: compose;
|
||||
|
||||
interface Args {
|
||||
dimensionMarshal: DimensionMarshal;
|
||||
focusMarshal: FocusMarshal;
|
||||
styleMarshal: StyleMarshal;
|
||||
getResponders: () => Responders;
|
||||
announce: Announce;
|
||||
autoScroller: AutoScroller;
|
||||
}
|
||||
|
||||
export default ({
|
||||
dimensionMarshal,
|
||||
focusMarshal,
|
||||
styleMarshal,
|
||||
getResponders,
|
||||
announce,
|
||||
autoScroller,
|
||||
}: Args): Store =>
|
||||
createStore(
|
||||
reducer,
|
||||
composeEnhancers(
|
||||
applyMiddleware<Dispatch, State>(
|
||||
// ## 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { DraggableId, LiftEffect } from '../types';
|
||||
|
||||
export default function didStartAfterCritical(
|
||||
draggableId: DraggableId,
|
||||
afterCritical: LiftEffect,
|
||||
): boolean {
|
||||
return Boolean(afterCritical.effected[draggableId]);
|
||||
}
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
CollectionStartingActionCreator,
|
||||
PublishWhileDraggingActionCreator,
|
||||
UpdateDroppableScrollActionCreator,
|
||||
UpdateDroppableIsEnabledActionCreator,
|
||||
UpdateDroppableIsCombineEnabledActionCreator,
|
||||
} from '../action-creators';
|
||||
import type {
|
||||
DroppableId,
|
||||
Critical,
|
||||
DimensionMap,
|
||||
LiftRequest,
|
||||
Viewport,
|
||||
} from '../../types';
|
||||
|
||||
export interface StartPublishingResult {
|
||||
critical: Critical;
|
||||
dimensions: DimensionMap;
|
||||
viewport: Viewport;
|
||||
}
|
||||
|
||||
export interface DimensionMarshal {
|
||||
// it is possible for a droppable to change whether it is enabled during a drag
|
||||
updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void;
|
||||
// it is also possible to update whether combining is enabled
|
||||
updateDroppableIsCombineEnabled: (
|
||||
id: DroppableId,
|
||||
isEnabled: boolean,
|
||||
) => void;
|
||||
updateDroppableScroll: (id: DroppableId, newScroll: Position) => void;
|
||||
scrollDroppable: (id: DroppableId, change: Position) => void;
|
||||
// Entry
|
||||
startPublishing: (request: LiftRequest) => StartPublishingResult;
|
||||
stopPublishing: () => void;
|
||||
}
|
||||
|
||||
export interface Callbacks {
|
||||
collectionStarting: CollectionStartingActionCreator;
|
||||
publishWhileDragging: PublishWhileDraggingActionCreator;
|
||||
updateDroppableScroll: UpdateDroppableScrollActionCreator;
|
||||
updateDroppableIsEnabled: UpdateDroppableIsEnabledActionCreator;
|
||||
updateDroppableIsCombineEnabled: UpdateDroppableIsCombineEnabledActionCreator;
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { invariant } from '../../invariant';
|
||||
import type {
|
||||
DimensionMarshal,
|
||||
Callbacks,
|
||||
StartPublishingResult,
|
||||
} from './dimension-marshal-types';
|
||||
import createPublisher from './while-dragging-publisher';
|
||||
import type { WhileDraggingPublisher } from './while-dragging-publisher';
|
||||
import getInitialPublish from './get-initial-publish';
|
||||
import type {
|
||||
Registry,
|
||||
DroppableEntry,
|
||||
DraggableEntry,
|
||||
Subscriber,
|
||||
Unsubscribe,
|
||||
RegistryEvent,
|
||||
} from '../registry/registry-types';
|
||||
import type {
|
||||
DroppableId,
|
||||
DroppableDescriptor,
|
||||
LiftRequest,
|
||||
Critical,
|
||||
DraggableDescriptor,
|
||||
} from '../../types';
|
||||
import { warning } from '../../dev-warning';
|
||||
|
||||
interface Collection {
|
||||
critical: Critical;
|
||||
unsubscribe: Unsubscribe;
|
||||
}
|
||||
|
||||
function shouldPublishUpdate(
|
||||
registry: Registry,
|
||||
dragging: DraggableDescriptor,
|
||||
entry: DraggableEntry,
|
||||
): boolean {
|
||||
// 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: DroppableEntry = 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/hello-pangea/dnd/blob/main/docs/patterns/virtual-lists.md
|
||||
`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default (registry: Registry, callbacks: Callbacks) => {
|
||||
let collection: Collection | null = null;
|
||||
|
||||
const publisher: WhileDraggingPublisher = createPublisher({
|
||||
callbacks: {
|
||||
publish: callbacks.publishWhileDragging,
|
||||
collectionStarting: callbacks.collectionStarting,
|
||||
},
|
||||
registry,
|
||||
});
|
||||
|
||||
const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => {
|
||||
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: DroppableId,
|
||||
isCombineEnabled: boolean,
|
||||
) => {
|
||||
// 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: DroppableId, newScroll: Position) => {
|
||||
// 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: DroppableId, change: Position) => {
|
||||
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: DroppableDescriptor = collection.critical.droppable;
|
||||
registry.droppable
|
||||
.getAllByType(home.type)
|
||||
.forEach((entry: DroppableEntry) => entry.callbacks.dragStopped());
|
||||
|
||||
// Unsubscribe from registry updates
|
||||
collection.unsubscribe();
|
||||
// Finally - clear our collection
|
||||
collection = null;
|
||||
};
|
||||
|
||||
const subscriber: Subscriber = (event: RegistryEvent) => {
|
||||
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: DraggableDescriptor = 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: LiftRequest): StartPublishingResult => {
|
||||
invariant(
|
||||
!collection,
|
||||
'Cannot start capturing critical dimensions as there is already a collection',
|
||||
);
|
||||
const entry: DraggableEntry = registry.draggable.getById(
|
||||
request.draggableId,
|
||||
);
|
||||
const home: DroppableEntry = registry.droppable.getById(
|
||||
entry.descriptor.droppableId,
|
||||
);
|
||||
|
||||
const critical: Critical = {
|
||||
draggable: entry.descriptor,
|
||||
droppable: home.descriptor,
|
||||
};
|
||||
|
||||
const unsubscribe = registry.subscribe(subscriber);
|
||||
|
||||
collection = {
|
||||
critical,
|
||||
unsubscribe,
|
||||
};
|
||||
|
||||
return getInitialPublish({
|
||||
critical,
|
||||
registry,
|
||||
scrollOptions: request.scrollOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const marshal: DimensionMarshal = {
|
||||
// Droppable changes
|
||||
updateDroppableIsEnabled,
|
||||
updateDroppableIsCombineEnabled,
|
||||
scrollDroppable,
|
||||
updateDroppableScroll,
|
||||
|
||||
// Entry
|
||||
startPublishing,
|
||||
stopPublishing,
|
||||
};
|
||||
|
||||
return marshal;
|
||||
};
|
||||
Generated
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import * as timings from '../../debug/timings';
|
||||
import type { StartPublishingResult } from './dimension-marshal-types';
|
||||
import type {
|
||||
Registry,
|
||||
DraggableEntry,
|
||||
DroppableEntry,
|
||||
} from '../registry/registry-types';
|
||||
import { toDraggableMap, toDroppableMap } from '../dimension-structures';
|
||||
import type {
|
||||
DroppableDescriptor,
|
||||
DroppableDimension,
|
||||
DraggableDimension,
|
||||
DimensionMap,
|
||||
ScrollOptions,
|
||||
Critical,
|
||||
Viewport,
|
||||
} from '../../types';
|
||||
import getViewport from '../../view/window/get-viewport';
|
||||
|
||||
interface Args {
|
||||
critical: Critical;
|
||||
scrollOptions: ScrollOptions;
|
||||
registry: Registry;
|
||||
}
|
||||
|
||||
export default ({
|
||||
critical,
|
||||
scrollOptions,
|
||||
registry,
|
||||
}: Args): StartPublishingResult => {
|
||||
const timingKey = 'Initial collection from DOM';
|
||||
timings.start(timingKey);
|
||||
const viewport: Viewport = getViewport();
|
||||
const windowScroll: Position = viewport.scroll.current;
|
||||
|
||||
const home: DroppableDescriptor = critical.droppable;
|
||||
|
||||
const droppables: DroppableDimension[] = registry.droppable
|
||||
.getAllByType(home.type)
|
||||
.map(
|
||||
(entry: DroppableEntry): DroppableDimension =>
|
||||
entry.callbacks.getDimensionAndWatchScroll(windowScroll, scrollOptions),
|
||||
);
|
||||
|
||||
const draggables: DraggableDimension[] = registry.draggable
|
||||
.getAllByType(critical.draggable.type)
|
||||
.map(
|
||||
(entry: DraggableEntry): DraggableDimension =>
|
||||
entry.getDimension(windowScroll),
|
||||
);
|
||||
|
||||
const dimensions: DimensionMap = {
|
||||
draggables: toDraggableMap(draggables),
|
||||
droppables: toDroppableMap(droppables),
|
||||
};
|
||||
|
||||
timings.finish(timingKey);
|
||||
|
||||
const result: StartPublishingResult = {
|
||||
dimensions,
|
||||
critical,
|
||||
viewport,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
Generated
Vendored
+145
@@ -0,0 +1,145 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
DraggableId,
|
||||
DroppableId,
|
||||
DraggableDescriptor,
|
||||
Published,
|
||||
DraggableDimension,
|
||||
DroppablePublish,
|
||||
DroppableIdMap,
|
||||
DraggableIdMap,
|
||||
} from '../../types';
|
||||
import type {
|
||||
DroppableEntry,
|
||||
Registry,
|
||||
DraggableEntry,
|
||||
DraggableEntryMap,
|
||||
} from '../registry/registry-types';
|
||||
import * as timings from '../../debug/timings';
|
||||
import { origin } from '../position';
|
||||
|
||||
export interface WhileDraggingPublisher {
|
||||
add: (entry: DraggableEntry) => void;
|
||||
remove: (entry: DraggableEntry) => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
interface Staging {
|
||||
additions: DraggableEntryMap;
|
||||
removals: DraggableIdMap;
|
||||
modified: DroppableIdMap;
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
publish: (args: Published) => unknown;
|
||||
collectionStarting: () => unknown;
|
||||
}
|
||||
|
||||
interface Args {
|
||||
registry: Registry;
|
||||
callbacks: Callbacks;
|
||||
}
|
||||
|
||||
const clean = (): Staging => ({
|
||||
additions: {},
|
||||
removals: {},
|
||||
modified: {},
|
||||
});
|
||||
|
||||
const timingKey = 'Publish collection from DOM';
|
||||
|
||||
export default function createPublisher({
|
||||
registry,
|
||||
callbacks,
|
||||
}: Args): WhileDraggingPublisher {
|
||||
let staging: Staging = clean();
|
||||
let frameId: AnimationFrameID | null = null;
|
||||
|
||||
const collect = () => {
|
||||
if (frameId) {
|
||||
return;
|
||||
}
|
||||
|
||||
callbacks.collectionStarting();
|
||||
frameId = requestAnimationFrame(() => {
|
||||
frameId = null;
|
||||
timings.start(timingKey);
|
||||
|
||||
const { additions, removals, modified } = staging;
|
||||
|
||||
const added: DraggableDimension[] = Object.keys(additions)
|
||||
.map(
|
||||
// Using the origin as the window scroll. This will be adjusted when processing the published values
|
||||
(id: DraggableId): DraggableDimension =>
|
||||
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: DraggableDimension, b: DraggableDimension): number =>
|
||||
a.descriptor.index - b.descriptor.index,
|
||||
);
|
||||
|
||||
const updated: DroppablePublish[] = Object.keys(modified).map(
|
||||
(id: DroppableId) => {
|
||||
const entry: DroppableEntry = registry.droppable.getById(id);
|
||||
|
||||
const scroll: Position = entry.callbacks.getScrollWhileDragging();
|
||||
return {
|
||||
droppableId: id,
|
||||
scroll,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const result: Published = {
|
||||
additions: added,
|
||||
removals: Object.keys(removals),
|
||||
modified: updated,
|
||||
};
|
||||
|
||||
staging = clean();
|
||||
|
||||
timings.finish(timingKey);
|
||||
callbacks.publish(result);
|
||||
});
|
||||
};
|
||||
|
||||
const add = (entry: DraggableEntry) => {
|
||||
const id: DraggableId = 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: DraggableEntry) => {
|
||||
const descriptor: DraggableDescriptor = 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,
|
||||
};
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { memoizeOne } from '../memoize-one';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
DroppableDimensionMap,
|
||||
DraggableDimension,
|
||||
DraggableDimensionMap,
|
||||
} from '../types';
|
||||
|
||||
export const toDroppableMap = memoizeOne((droppables: DroppableDimension[]) =>
|
||||
droppables.reduce<DroppableDimensionMap>((previous, current) => {
|
||||
previous[current.descriptor.id] = current;
|
||||
return previous;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
export const toDraggableMap = memoizeOne((draggables: DraggableDimension[]) =>
|
||||
draggables.reduce<DraggableDimensionMap>((previous, current) => {
|
||||
previous[current.descriptor.id] = current;
|
||||
return previous;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
export const toDroppableList = memoizeOne(
|
||||
(droppables: DroppableDimensionMap): DroppableDimension[] =>
|
||||
Object.values(droppables),
|
||||
);
|
||||
|
||||
export const toDraggableList = memoizeOne(
|
||||
(draggables: DraggableDimensionMap): DraggableDimension[] =>
|
||||
Object.values(draggables),
|
||||
);
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import type { BoxModel, Position } from 'css-box-model';
|
||||
import type {
|
||||
Axis,
|
||||
DroppableDimension,
|
||||
DroppableDescriptor,
|
||||
Scrollable,
|
||||
DroppableSubject,
|
||||
ScrollSize,
|
||||
} from '../../types';
|
||||
import { vertical, horizontal } from '../axis';
|
||||
import { origin } from '../position';
|
||||
import getMaxScroll from '../get-max-scroll';
|
||||
import getSubject from './util/get-subject';
|
||||
|
||||
export interface Closest {
|
||||
client: BoxModel;
|
||||
page: BoxModel;
|
||||
scroll: Position;
|
||||
scrollSize: ScrollSize;
|
||||
shouldClipSubject: boolean;
|
||||
}
|
||||
|
||||
interface Args {
|
||||
descriptor: DroppableDescriptor;
|
||||
isEnabled: boolean;
|
||||
isCombineEnabled: boolean;
|
||||
isFixedOnPage: boolean;
|
||||
direction: 'vertical' | 'horizontal';
|
||||
client: BoxModel;
|
||||
// is null when in a fixed container
|
||||
page: BoxModel;
|
||||
closest?: Closest | null;
|
||||
}
|
||||
|
||||
export default ({
|
||||
descriptor,
|
||||
isEnabled,
|
||||
isCombineEnabled,
|
||||
isFixedOnPage,
|
||||
direction,
|
||||
client,
|
||||
page,
|
||||
closest,
|
||||
}: Args): DroppableDimension => {
|
||||
const frame: Scrollable | null = (() => {
|
||||
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: Position = 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: Axis = direction === 'vertical' ? vertical : horizontal;
|
||||
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page,
|
||||
withPlaceholder: null,
|
||||
axis,
|
||||
frame,
|
||||
});
|
||||
|
||||
const dimension: DroppableDimension = {
|
||||
descriptor,
|
||||
isCombineEnabled,
|
||||
isFixedOnPage,
|
||||
axis,
|
||||
isEnabled,
|
||||
client,
|
||||
page,
|
||||
frame,
|
||||
subject,
|
||||
};
|
||||
|
||||
return dimension;
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { DraggableDimension, DroppableDimension } from '../../types';
|
||||
|
||||
export default (
|
||||
draggable: DraggableDimension,
|
||||
destination: DroppableDimension,
|
||||
): boolean => draggable.descriptor.droppableId === destination.descriptor.id;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { invariant } from '../../invariant';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
Scrollable,
|
||||
DroppableSubject,
|
||||
} from '../../types';
|
||||
import { negate, subtract } from '../position';
|
||||
import getSubject from './util/get-subject';
|
||||
|
||||
export default (
|
||||
droppable: DroppableDimension,
|
||||
newScroll: Position,
|
||||
): DroppableDimension => {
|
||||
invariant(droppable.frame);
|
||||
const scrollable: Scrollable = droppable.frame;
|
||||
|
||||
const scrollDiff: Position = subtract(newScroll, scrollable.scroll.initial);
|
||||
// a positive scroll difference leads to a negative displacement
|
||||
// (scrolling down pulls an item upwards)
|
||||
const scrollDisplacement: Position = 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 = {
|
||||
...scrollable,
|
||||
scroll: {
|
||||
initial: scrollable.scroll.initial,
|
||||
current: newScroll,
|
||||
diff: {
|
||||
value: scrollDiff,
|
||||
displacement: scrollDisplacement,
|
||||
},
|
||||
// TODO: rename 'softMax?'
|
||||
max: scrollable.scroll.max,
|
||||
},
|
||||
};
|
||||
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: droppable.subject.withPlaceholder,
|
||||
axis: droppable.axis,
|
||||
frame,
|
||||
});
|
||||
const result: DroppableDimension = {
|
||||
...droppable,
|
||||
frame,
|
||||
subject,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { DraggableDescriptor, DragImpact } from '../../types';
|
||||
import whatIsDraggedOver from './what-is-dragged-over';
|
||||
|
||||
// use placeholder if dragged over
|
||||
export default (descriptor: DraggableDescriptor, impact: DragImpact): boolean =>
|
||||
whatIsDraggedOver(impact) === descriptor.droppableId;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { getRect } from 'css-box-model';
|
||||
|
||||
import type { Rect, Spacing } from 'css-box-model';
|
||||
|
||||
export default (frame: Spacing, subject: Spacing): Rect | null => {
|
||||
const result: Rect = 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;
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { getRect } from 'css-box-model';
|
||||
import type { Rect, Spacing, BoxModel } from 'css-box-model';
|
||||
import type {
|
||||
Axis,
|
||||
Scrollable,
|
||||
DroppableSubject,
|
||||
PlaceholderInSubject,
|
||||
} from '../../../types';
|
||||
import executeClip from './clip';
|
||||
import { offsetByPosition } from '../../spacing';
|
||||
|
||||
const scroll = (target: Spacing, frame?: Scrollable | null): Spacing => {
|
||||
if (!frame) {
|
||||
return target;
|
||||
}
|
||||
|
||||
return offsetByPosition(target, frame.scroll.diff.displacement);
|
||||
};
|
||||
|
||||
const increase = (
|
||||
target: Spacing,
|
||||
axis: Axis,
|
||||
withPlaceholder?: PlaceholderInSubject | null,
|
||||
): Spacing => {
|
||||
if (withPlaceholder && withPlaceholder.increasedBy) {
|
||||
return {
|
||||
...target,
|
||||
[axis.end]: target[axis.end] + withPlaceholder.increasedBy[axis.line],
|
||||
};
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const clip = (target: Spacing, frame?: Scrollable | null): Rect | null => {
|
||||
if (frame && frame.shouldClipSubject) {
|
||||
return executeClip(frame.pageMarginBox, target);
|
||||
}
|
||||
return getRect(target);
|
||||
};
|
||||
|
||||
interface Args {
|
||||
page: BoxModel;
|
||||
withPlaceholder: PlaceholderInSubject | null;
|
||||
axis: Axis;
|
||||
frame: Scrollable | null;
|
||||
}
|
||||
|
||||
export default ({
|
||||
page,
|
||||
withPlaceholder,
|
||||
axis,
|
||||
frame,
|
||||
}: Args): DroppableSubject => {
|
||||
const scrolled: Spacing = scroll(page.marginBox, frame);
|
||||
const increased: Spacing = increase(scrolled, axis, withPlaceholder);
|
||||
const clipped: Rect | null = clip(increased, frame);
|
||||
|
||||
return {
|
||||
page,
|
||||
withPlaceholder,
|
||||
active: clipped,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import type { DroppableId, DropResult } from '../../types';
|
||||
|
||||
export default (result: DropResult): DroppableId | null => {
|
||||
const { combine, destination } = result;
|
||||
|
||||
if (destination) {
|
||||
return destination.droppableId;
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
return combine.droppableId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { ImpactLocation, DroppableId, DragImpact } from '../../types';
|
||||
|
||||
export default (impact: DragImpact): DroppableId | null => {
|
||||
const at: ImpactLocation | null = impact.at;
|
||||
|
||||
if (!at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (at.type === 'REORDER') {
|
||||
return at.destination.droppableId;
|
||||
}
|
||||
|
||||
return at.combine.droppableId;
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { invariant } from '../../invariant';
|
||||
import type {
|
||||
Axis,
|
||||
DroppableDimension,
|
||||
DraggableDimension,
|
||||
DraggableDimensionMap,
|
||||
Scrollable,
|
||||
DroppableSubject,
|
||||
PlaceholderInSubject,
|
||||
} from '../../types';
|
||||
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: DroppableDimension,
|
||||
placeholderSize: Position,
|
||||
draggables: DraggableDimensionMap,
|
||||
): Position | null => {
|
||||
const axis: 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: number = droppable.subject.page.contentBox[axis.size];
|
||||
const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable(
|
||||
droppable.descriptor.id,
|
||||
draggables,
|
||||
);
|
||||
const spaceUsed: number = insideDroppable.reduce(
|
||||
(sum: number, dimension: DraggableDimension): number =>
|
||||
sum + dimension.client.marginBox[axis.size],
|
||||
0,
|
||||
);
|
||||
const requiredSpace: number = spaceUsed + placeholderSize[axis.line];
|
||||
const needsToGrowBy: number = requiredSpace - availableSpace;
|
||||
|
||||
// nothing to do here
|
||||
if (needsToGrowBy <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return patch(axis.line, needsToGrowBy);
|
||||
};
|
||||
|
||||
const withMaxScroll = (frame: Scrollable, max: Position): Scrollable => ({
|
||||
...frame,
|
||||
|
||||
scroll: {
|
||||
...frame.scroll,
|
||||
max,
|
||||
},
|
||||
});
|
||||
|
||||
export const addPlaceholder = (
|
||||
droppable: DroppableDimension,
|
||||
draggable: DraggableDimension,
|
||||
draggables: DraggableDimensionMap,
|
||||
): DroppableDimension => {
|
||||
const frame: Scrollable | null = 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: Position = getDisplacedBy(
|
||||
droppable.axis,
|
||||
draggable.displaceBy,
|
||||
).point;
|
||||
|
||||
const requiredGrowth: Position | null = getRequiredGrowthForPlaceholder(
|
||||
droppable,
|
||||
placeholderSize,
|
||||
draggables,
|
||||
);
|
||||
|
||||
const added: PlaceholderInSubject = {
|
||||
placeholderSize,
|
||||
increasedBy: requiredGrowth,
|
||||
oldFrameMaxScroll: droppable.frame ? droppable.frame.scroll.max : null,
|
||||
};
|
||||
|
||||
if (!frame) {
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: added,
|
||||
axis: droppable.axis,
|
||||
frame: droppable.frame,
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
|
||||
const maxScroll: Position = requiredGrowth
|
||||
? add(frame.scroll.max, requiredGrowth)
|
||||
: frame.scroll.max;
|
||||
|
||||
const newFrame: Scrollable = withMaxScroll(frame, maxScroll);
|
||||
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
withPlaceholder: added,
|
||||
axis: droppable.axis,
|
||||
frame: newFrame,
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
frame: newFrame,
|
||||
};
|
||||
};
|
||||
|
||||
export const removePlaceholder = (
|
||||
droppable: DroppableDimension,
|
||||
): DroppableDimension => {
|
||||
const added: PlaceholderInSubject | null = droppable.subject.withPlaceholder;
|
||||
invariant(
|
||||
added,
|
||||
'Cannot remove placeholder form subject when there was none',
|
||||
);
|
||||
|
||||
const frame: Scrollable | null = droppable.frame;
|
||||
|
||||
if (!frame) {
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
axis: droppable.axis,
|
||||
frame: null,
|
||||
// cleared
|
||||
withPlaceholder: null,
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
|
||||
const oldMaxScroll: Position | null = added.oldFrameMaxScroll;
|
||||
invariant(
|
||||
oldMaxScroll,
|
||||
'Expected droppable with frame to have old max frame scroll when removing placeholder',
|
||||
);
|
||||
|
||||
const newFrame: Scrollable = withMaxScroll(frame, oldMaxScroll);
|
||||
|
||||
const subject: DroppableSubject = getSubject({
|
||||
page: droppable.subject.page,
|
||||
axis: droppable.axis,
|
||||
frame: newFrame,
|
||||
// cleared
|
||||
withPlaceholder: null,
|
||||
});
|
||||
return {
|
||||
...droppable,
|
||||
subject,
|
||||
frame: newFrame,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type { Viewport, DraggableDimension } from '../../../types';
|
||||
import { add, subtract } from '../../position';
|
||||
import withViewportDisplacement from '../../with-scroll-change/with-viewport-displacement';
|
||||
|
||||
interface Args {
|
||||
pageBorderBoxCenter: Position;
|
||||
draggable: DraggableDimension;
|
||||
viewport: Viewport;
|
||||
}
|
||||
|
||||
export default ({
|
||||
pageBorderBoxCenter,
|
||||
draggable,
|
||||
viewport,
|
||||
}: Args): Position => {
|
||||
const withoutPageScrollChange: Position = withViewportDisplacement(
|
||||
viewport,
|
||||
pageBorderBoxCenter,
|
||||
);
|
||||
|
||||
const offset: Position = subtract(
|
||||
withoutPageScrollChange,
|
||||
draggable.page.borderBox.center,
|
||||
);
|
||||
|
||||
return add(draggable.client.borderBox.center, offset);
|
||||
};
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
Viewport,
|
||||
DragImpact,
|
||||
DraggableDimension,
|
||||
DraggableDimensionMap,
|
||||
LiftEffect,
|
||||
} from '../../../types';
|
||||
import getPageBorderBoxCenterFromImpact from '../get-page-border-box-center';
|
||||
import getClientFromPageBorderBoxCenter from './get-client-from-page-border-box-center';
|
||||
|
||||
interface Args {
|
||||
impact: DragImpact;
|
||||
draggable: DraggableDimension;
|
||||
droppable: DroppableDimension;
|
||||
draggables: DraggableDimensionMap;
|
||||
viewport: Viewport;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
export default ({
|
||||
impact,
|
||||
draggable,
|
||||
droppable,
|
||||
draggables,
|
||||
viewport,
|
||||
afterCritical,
|
||||
}: Args): Position => {
|
||||
const pageBorderBoxCenter: Position = getPageBorderBoxCenterFromImpact({
|
||||
impact,
|
||||
draggable,
|
||||
draggables,
|
||||
droppable,
|
||||
afterCritical,
|
||||
});
|
||||
|
||||
return getClientFromPageBorderBoxCenter({
|
||||
pageBorderBoxCenter,
|
||||
draggable,
|
||||
viewport,
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
DragImpact,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DraggableDimensionMap,
|
||||
ImpactLocation,
|
||||
LiftEffect,
|
||||
} from '../../../types';
|
||||
import whenCombining from './when-combining';
|
||||
import whenReordering from './when-reordering';
|
||||
import withDroppableDisplacement from '../../with-scroll-change/with-droppable-displacement';
|
||||
|
||||
interface Args {
|
||||
impact: DragImpact;
|
||||
afterCritical: LiftEffect;
|
||||
draggable: DraggableDimension;
|
||||
droppable: DroppableDimension | null;
|
||||
draggables: DraggableDimensionMap;
|
||||
}
|
||||
|
||||
const getResultWithoutDroppableDisplacement = ({
|
||||
impact,
|
||||
draggable,
|
||||
droppable,
|
||||
draggables,
|
||||
afterCritical,
|
||||
}: Args): Position => {
|
||||
const original: Position = draggable.page.borderBox.center;
|
||||
const at: ImpactLocation | null = 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: Args): Position => {
|
||||
const withoutDisplacement: Position =
|
||||
getResultWithoutDroppableDisplacement(args);
|
||||
|
||||
const droppable: DroppableDimension | null = args.droppable;
|
||||
|
||||
const withDisplacement: Position = droppable
|
||||
? withDroppableDisplacement(droppable, withoutDisplacement)
|
||||
: withoutDisplacement;
|
||||
|
||||
return withDisplacement;
|
||||
};
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { invariant } from '../../../invariant';
|
||||
import type {
|
||||
DraggableDimensionMap,
|
||||
DraggableId,
|
||||
Combine,
|
||||
LiftEffect,
|
||||
DragImpact,
|
||||
} from '../../../types';
|
||||
import { add } from '../../position';
|
||||
import getCombinedItemDisplacement from '../../get-combined-item-displacement';
|
||||
import { tryGetCombine } from '../../get-impact-location';
|
||||
|
||||
interface Args {
|
||||
impact: DragImpact;
|
||||
// all draggables in the system
|
||||
draggables: DraggableDimensionMap;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
// Returns the client offset required to move an item from its
|
||||
// original client position to its final resting position
|
||||
export default ({ afterCritical, impact, draggables }: Args): Position => {
|
||||
const combine: Combine | null = tryGetCombine(impact);
|
||||
invariant(combine);
|
||||
|
||||
const combineWith: DraggableId = combine.draggableId;
|
||||
const center: Position = draggables[combineWith].page.borderBox.center;
|
||||
|
||||
const displaceBy: Position = getCombinedItemDisplacement({
|
||||
displaced: impact.displaced,
|
||||
afterCritical,
|
||||
combineWith,
|
||||
displacedBy: impact.displacedBy,
|
||||
});
|
||||
|
||||
return add(center, displaceBy);
|
||||
};
|
||||
Generated
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
import { offset } from 'css-box-model';
|
||||
import type { Position, BoxModel } from 'css-box-model';
|
||||
import type {
|
||||
Axis,
|
||||
DragImpact,
|
||||
DraggableId,
|
||||
DraggableDimension,
|
||||
DraggableDimensionMap,
|
||||
DroppableDimension,
|
||||
LiftEffect,
|
||||
} from '../../../types';
|
||||
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';
|
||||
|
||||
interface NewHomeArgs {
|
||||
impact: DragImpact;
|
||||
draggable: DraggableDimension;
|
||||
draggables: DraggableDimensionMap;
|
||||
droppable: DroppableDimension;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
// 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,
|
||||
}: NewHomeArgs): Position => {
|
||||
const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable(
|
||||
droppable.descriptor.id,
|
||||
draggables,
|
||||
);
|
||||
|
||||
const draggablePage: BoxModel = draggable.page;
|
||||
const axis: 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: DraggableId | null = displaced.all[0];
|
||||
|
||||
// go before the first displaced item
|
||||
// items can only be displaced forwards
|
||||
if (closestAfter) {
|
||||
const closest: DraggableDimension = 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: BoxModel = 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: DraggableDimension =
|
||||
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: BoxModel = 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,
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
import type { Position, BoxModel, Rect } from 'css-box-model';
|
||||
import { patch } from '../position';
|
||||
import type { Axis } from '../../types';
|
||||
|
||||
interface Args {
|
||||
axis: Axis;
|
||||
moveRelativeTo: BoxModel;
|
||||
isMoving: BoxModel;
|
||||
}
|
||||
|
||||
const distanceFromStartToBorderBoxCenter = (
|
||||
axis: Axis,
|
||||
box: BoxModel,
|
||||
): number => box.margin[axis.start] + box.borderBox[axis.size] / 2;
|
||||
|
||||
const distanceFromEndToBorderBoxCenter = (axis: Axis, box: BoxModel): number =>
|
||||
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: Axis,
|
||||
target: Rect,
|
||||
isMoving: BoxModel,
|
||||
): number =>
|
||||
target[axis.crossAxisStart] +
|
||||
isMoving.margin[axis.crossAxisStart] +
|
||||
isMoving.borderBox[axis.crossAxisSize] / 2;
|
||||
|
||||
export const goAfter = ({ axis, moveRelativeTo, isMoving }: Args): Position =>
|
||||
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 }: Args): Position =>
|
||||
patch(
|
||||
axis.line,
|
||||
// start measuring from the start of the target
|
||||
moveRelativeTo.marginBox[axis.start] -
|
||||
distanceFromEndToBorderBoxCenter(axis, isMoving),
|
||||
getCrossAxisBorderBoxCenter(axis, moveRelativeTo.marginBox, isMoving),
|
||||
);
|
||||
|
||||
interface GoIntoArgs {
|
||||
axis: Axis;
|
||||
moveInto: BoxModel;
|
||||
isMoving: BoxModel;
|
||||
}
|
||||
|
||||
// moves into the content box
|
||||
export const goIntoStart = ({
|
||||
axis,
|
||||
moveInto,
|
||||
isMoving,
|
||||
}: GoIntoArgs): Position =>
|
||||
patch(
|
||||
axis.line,
|
||||
moveInto.contentBox[axis.start] +
|
||||
distanceFromStartToBorderBoxCenter(axis, isMoving),
|
||||
getCrossAxisBorderBoxCenter(axis, moveInto.contentBox, isMoving),
|
||||
);
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
DisplacementGroups,
|
||||
LiftEffect,
|
||||
DraggableId,
|
||||
DisplacedBy,
|
||||
} from '../types';
|
||||
import { origin, negate } from './position';
|
||||
import didStartAfterCritical from './did-start-after-critical';
|
||||
|
||||
interface Args {
|
||||
displaced: DisplacementGroups;
|
||||
afterCritical: LiftEffect;
|
||||
combineWith: DraggableId;
|
||||
displacedBy: DisplacedBy;
|
||||
}
|
||||
|
||||
export default ({
|
||||
displaced,
|
||||
afterCritical,
|
||||
combineWith,
|
||||
displacedBy,
|
||||
}: Args): Position => {
|
||||
const isDisplaced = Boolean(
|
||||
displaced.visible[combineWith] || displaced.invisible[combineWith],
|
||||
);
|
||||
|
||||
if (didStartAfterCritical(combineWith, afterCritical)) {
|
||||
return isDisplaced ? origin : negate(displacedBy.point);
|
||||
}
|
||||
|
||||
return isDisplaced ? displacedBy.point : origin;
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { memoizeOne } from '../memoize-one';
|
||||
import type { Axis, DisplacedBy } from '../types';
|
||||
import { patch } from './position';
|
||||
|
||||
// TODO: memoization needed?
|
||||
export default memoizeOne(function getDisplacedBy(
|
||||
axis: Axis,
|
||||
displaceBy: Position,
|
||||
): DisplacedBy {
|
||||
const displacement: number = displaceBy[axis.line];
|
||||
return {
|
||||
value: displacement,
|
||||
point: patch(axis.line, displacement),
|
||||
};
|
||||
});
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { expand, getRect } from 'css-box-model';
|
||||
import type { Rect, Spacing } from 'css-box-model';
|
||||
import type {
|
||||
DraggableId,
|
||||
Displacement,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DisplacementGroups,
|
||||
DisplacedBy,
|
||||
} from '../types';
|
||||
import { isPartiallyVisible } from './visibility/is-visible';
|
||||
|
||||
interface Args {
|
||||
afterDragging: DraggableDimension[];
|
||||
destination: DroppableDimension;
|
||||
displacedBy: DisplacedBy;
|
||||
last: DisplacementGroups | null;
|
||||
viewport: Rect;
|
||||
forceShouldAnimate?: boolean;
|
||||
}
|
||||
|
||||
const getShouldAnimate = (
|
||||
id: DraggableId,
|
||||
last?: DisplacementGroups | null,
|
||||
forceShouldAnimate?: boolean | null,
|
||||
) => {
|
||||
// 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: Displacement | null = 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: DraggableDimension,
|
||||
displacedBy: DisplacedBy,
|
||||
): Rect {
|
||||
const marginBox: Rect = 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: Spacing = {
|
||||
// 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,
|
||||
}: Args): DisplacementGroups {
|
||||
return afterDragging.reduce(
|
||||
function process(
|
||||
groups: DisplacementGroups,
|
||||
draggable: DraggableDimension,
|
||||
): DisplacementGroups {
|
||||
const target: Rect = getTarget(draggable, displacedBy);
|
||||
const id: DraggableId = draggable.descriptor.id;
|
||||
|
||||
groups.all.push(id);
|
||||
|
||||
const isVisible: boolean = isPartiallyVisible({
|
||||
target,
|
||||
destination,
|
||||
viewport,
|
||||
withDroppableDisplacement: true,
|
||||
});
|
||||
|
||||
if (!isVisible) {
|
||||
groups.invisible[draggable.descriptor.id] = true;
|
||||
return groups;
|
||||
}
|
||||
|
||||
// item is visible
|
||||
|
||||
const shouldAnimate: boolean = getShouldAnimate(
|
||||
id,
|
||||
last,
|
||||
forceShouldAnimate,
|
||||
);
|
||||
|
||||
const displacement: Displacement = {
|
||||
draggableId: id,
|
||||
shouldAnimate,
|
||||
};
|
||||
|
||||
groups.visible[id] = displacement;
|
||||
return groups;
|
||||
},
|
||||
{
|
||||
all: [],
|
||||
visible: {},
|
||||
invisible: {},
|
||||
},
|
||||
);
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import type { Rect } from 'css-box-model';
|
||||
import type {
|
||||
DraggableId,
|
||||
Axis,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DragImpact,
|
||||
LiftEffect,
|
||||
DisplacedBy,
|
||||
} from '../../types';
|
||||
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';
|
||||
|
||||
interface Args {
|
||||
draggable: DraggableDimension;
|
||||
pageBorderBoxWithDroppableScroll: Rect;
|
||||
previousImpact: DragImpact;
|
||||
destination: DroppableDimension;
|
||||
insideDestination: DraggableDimension[];
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export const combineThresholdDivisor = 4;
|
||||
|
||||
export default ({
|
||||
draggable,
|
||||
pageBorderBoxWithDroppableScroll: targetRect,
|
||||
previousImpact,
|
||||
destination,
|
||||
insideDestination,
|
||||
afterCritical,
|
||||
}: Args): DragImpact | null => {
|
||||
if (!destination.isCombineEnabled) {
|
||||
return null;
|
||||
}
|
||||
const axis: Axis = destination.axis;
|
||||
const displacedBy: DisplacedBy = getDisplacedBy(
|
||||
destination.axis,
|
||||
draggable.displaceBy,
|
||||
);
|
||||
const displacement: number = displacedBy.value;
|
||||
|
||||
const targetStart: number = targetRect[axis.start];
|
||||
const targetEnd: number = targetRect[axis.end];
|
||||
|
||||
const withoutDragging: DraggableDimension[] = removeDraggableFromList(
|
||||
draggable,
|
||||
insideDestination,
|
||||
);
|
||||
|
||||
const combineWith = withoutDragging.find((child): boolean => {
|
||||
const id: DraggableId = child.descriptor.id;
|
||||
const childRect: Rect = child.page.borderBox;
|
||||
const childSize: number = childRect[axis.size];
|
||||
const threshold: number = childSize / combineThresholdDivisor;
|
||||
|
||||
const didStartAfterCritical: boolean = getDidStartAfterCritical(
|
||||
id,
|
||||
afterCritical,
|
||||
);
|
||||
|
||||
const isDisplaced: boolean = 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: DragImpact = {
|
||||
// no change to displacement when combining
|
||||
displacedBy,
|
||||
displaced: previousImpact.displaced,
|
||||
at: {
|
||||
type: 'COMBINE',
|
||||
combine: {
|
||||
draggableId: combineWith.descriptor.id,
|
||||
droppableId: destination.descriptor.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
return impact;
|
||||
};
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import type { Rect } from 'css-box-model';
|
||||
import type {
|
||||
DraggableId,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DragImpact,
|
||||
Axis,
|
||||
DisplacementGroups,
|
||||
Viewport,
|
||||
DisplacedBy,
|
||||
LiftEffect,
|
||||
} from '../../types';
|
||||
import getDisplacedBy from '../get-displaced-by';
|
||||
import removeDraggableFromList from '../remove-draggable-from-list';
|
||||
import isHomeOf from '../droppable/is-home-of';
|
||||
import getDidStartAfterCritical from '../did-start-after-critical';
|
||||
import calculateReorderImpact from '../calculate-drag-impact/calculate-reorder-impact';
|
||||
import getIsDisplaced from '../get-is-displaced';
|
||||
|
||||
interface Args {
|
||||
pageBorderBoxWithDroppableScroll: Rect;
|
||||
draggable: DraggableDimension;
|
||||
destination: DroppableDimension;
|
||||
insideDestination: DraggableDimension[];
|
||||
last: DisplacementGroups;
|
||||
viewport: Viewport;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
interface AtIndexArgs {
|
||||
draggable: DraggableDimension;
|
||||
closest: DraggableDimension | null;
|
||||
inHomeList: boolean;
|
||||
}
|
||||
|
||||
function atIndex({
|
||||
draggable,
|
||||
closest,
|
||||
inHomeList,
|
||||
}: AtIndexArgs): number | null {
|
||||
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,
|
||||
}: Args): DragImpact => {
|
||||
const axis: Axis = destination.axis;
|
||||
const displacedBy: DisplacedBy = getDisplacedBy(
|
||||
destination.axis,
|
||||
draggable.displaceBy,
|
||||
);
|
||||
const displacement: number = displacedBy.value;
|
||||
|
||||
const targetStart: number = targetRect[axis.start];
|
||||
const targetEnd: number = targetRect[axis.end];
|
||||
|
||||
const withoutDragging: DraggableDimension[] = removeDraggableFromList(
|
||||
draggable,
|
||||
insideDestination,
|
||||
);
|
||||
|
||||
const closest =
|
||||
withoutDragging.find((child): boolean => {
|
||||
const id: DraggableId = child.descriptor.id;
|
||||
const childCenter: number = child.page.borderBox.center[axis.line];
|
||||
|
||||
const didStartAfterCritical: boolean = getDidStartAfterCritical(
|
||||
id,
|
||||
afterCritical,
|
||||
);
|
||||
|
||||
const isDisplaced: boolean = 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;
|
||||
}) || null;
|
||||
|
||||
const newIndex: number | null = 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,
|
||||
});
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import type { Position, Rect } from 'css-box-model';
|
||||
import type {
|
||||
DroppableId,
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DraggableDimensionMap,
|
||||
DroppableDimensionMap,
|
||||
DragImpact,
|
||||
Viewport,
|
||||
LiftEffect,
|
||||
} from '../../types';
|
||||
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';
|
||||
|
||||
interface Args {
|
||||
pageOffset: Position;
|
||||
draggable: DraggableDimension;
|
||||
// all dimensions in system
|
||||
draggables: DraggableDimensionMap;
|
||||
droppables: DroppableDimensionMap;
|
||||
previousImpact: DragImpact;
|
||||
viewport: Viewport;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
export default ({
|
||||
pageOffset,
|
||||
draggable,
|
||||
draggables,
|
||||
droppables,
|
||||
previousImpact,
|
||||
viewport,
|
||||
afterCritical,
|
||||
}: Args): DragImpact => {
|
||||
const pageBorderBox: Rect = offsetRectByPosition(
|
||||
draggable.page.borderBox,
|
||||
pageOffset,
|
||||
);
|
||||
|
||||
const destinationId: DroppableId | null = 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: DroppableDimension = droppables[destinationId];
|
||||
const insideDestination: DraggableDimension[] = 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: Rect = 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,
|
||||
})
|
||||
);
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { memoizeOne } from '../memoize-one';
|
||||
import { toDraggableList } from './dimension-structures';
|
||||
import type {
|
||||
DraggableDimension,
|
||||
DroppableId,
|
||||
DraggableDimensionMap,
|
||||
} from '../types';
|
||||
|
||||
export default memoizeOne(
|
||||
(
|
||||
// using droppableId to avoid cache busted when we
|
||||
// update the droppable (such as when it scrolls)
|
||||
droppableId: DroppableId,
|
||||
draggables: DraggableDimensionMap,
|
||||
): DraggableDimension[] => {
|
||||
const result = toDraggableList(draggables)
|
||||
.filter(
|
||||
(draggable: DraggableDimension): boolean =>
|
||||
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: DraggableDimension, b: DraggableDimension): number =>
|
||||
a.descriptor.index - b.descriptor.index,
|
||||
);
|
||||
|
||||
return result;
|
||||
},
|
||||
);
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import type { Position, Rect } from 'css-box-model';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
DroppableDimensionMap,
|
||||
DroppableId,
|
||||
DraggableDimension,
|
||||
Axis,
|
||||
} from '../types';
|
||||
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: Rect, second: Rect): boolean {
|
||||
return (
|
||||
first.left < second.right &&
|
||||
first.right > second.left &&
|
||||
first.top < second.bottom &&
|
||||
first.bottom > second.top
|
||||
);
|
||||
}
|
||||
|
||||
interface Args {
|
||||
pageBorderBox: Rect;
|
||||
draggable: DraggableDimension;
|
||||
droppables: DroppableDimensionMap;
|
||||
}
|
||||
|
||||
interface WithDistance {
|
||||
distance: number;
|
||||
id: DroppableId;
|
||||
}
|
||||
|
||||
interface GetFurthestArgs {
|
||||
pageBorderBox: Rect;
|
||||
draggable: DraggableDimension;
|
||||
candidates: DroppableDimension[];
|
||||
}
|
||||
|
||||
function getFurthestAway({
|
||||
pageBorderBox,
|
||||
draggable,
|
||||
candidates,
|
||||
}: GetFurthestArgs): DroppableId | null {
|
||||
// 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: Position = draggable.page.borderBox.center;
|
||||
const sorted: WithDistance[] = candidates
|
||||
.map((candidate: DroppableDimension): WithDistance => {
|
||||
const axis: Axis = candidate.axis;
|
||||
const target: Position = 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: WithDistance, b: WithDistance) => b.distance - a.distance);
|
||||
|
||||
// just being safe
|
||||
return sorted[0] ? sorted[0].id : null;
|
||||
}
|
||||
|
||||
export default function getDroppableOver({
|
||||
pageBorderBox,
|
||||
draggable,
|
||||
droppables,
|
||||
}: Args): DroppableId | null {
|
||||
// We know at this point that some overlap has to exist
|
||||
const candidates: DroppableDimension[] = toDroppableList(droppables).filter(
|
||||
(item: DroppableDimension): boolean => {
|
||||
// Cannot be a candidate when disabled
|
||||
if (!item.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot be a candidate when there is no visible area
|
||||
const active: Rect | null = 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: Axis = item.axis;
|
||||
const childCenter: number = active.center[axis.crossAxisLine];
|
||||
const crossAxisStart: number = pageBorderBox[axis.crossAxisStart];
|
||||
const crossAxisEnd: number = pageBorderBox[axis.crossAxisEnd];
|
||||
|
||||
const isContained = isWithin(
|
||||
active[axis.crossAxisStart],
|
||||
active[axis.crossAxisEnd],
|
||||
);
|
||||
|
||||
const isStartContained: boolean = isContained(crossAxisStart);
|
||||
const isEndContained: boolean = 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,
|
||||
});
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { invariant } from '../invariant';
|
||||
|
||||
import type { DroppableDimension, Scrollable } from '../types';
|
||||
|
||||
export default (droppable: DroppableDimension): Scrollable => {
|
||||
const frame: Scrollable | null = droppable.frame;
|
||||
invariant(frame, 'Expected Droppable to have a frame');
|
||||
return frame;
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { DraggableDescriptor, DraggableLocation } from '../types';
|
||||
|
||||
export default (descriptor: DraggableDescriptor): DraggableLocation => ({
|
||||
index: descriptor.index,
|
||||
droppableId: descriptor.droppableId,
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { DragImpact, DraggableLocation, Combine } from '../types';
|
||||
|
||||
export function tryGetDestination(
|
||||
impact: DragImpact,
|
||||
): DraggableLocation | null {
|
||||
if (impact.at && impact.at.type === 'REORDER') {
|
||||
return impact.at.destination;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function tryGetCombine(impact: DragImpact): Combine | null {
|
||||
if (impact.at && impact.at.type === 'COMBINE') {
|
||||
return impact.at.combine;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import type { DisplacementGroups, DraggableId } from '../types';
|
||||
|
||||
interface Args {
|
||||
displaced: DisplacementGroups;
|
||||
id: DraggableId;
|
||||
}
|
||||
|
||||
export default function getIsDisplaced({ displaced, id }: Args): boolean {
|
||||
return Boolean(displaced.visible[id] || displaced.invisible[id]);
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { invariant } from '../invariant';
|
||||
import getHomeLocation from './get-home-location';
|
||||
import type {
|
||||
DraggableDimension,
|
||||
DroppableDimension,
|
||||
DraggableDimensionMap,
|
||||
DragImpact,
|
||||
DisplacedBy,
|
||||
Viewport,
|
||||
DraggableIdMap,
|
||||
DisplacementGroups,
|
||||
LiftEffect,
|
||||
} from '../types';
|
||||
import getDraggablesInsideDroppable from './get-draggables-inside-droppable';
|
||||
import getDisplacedBy from './get-displaced-by';
|
||||
import getDisplacementGroups from './get-displacement-groups';
|
||||
|
||||
interface Args {
|
||||
draggable: DraggableDimension;
|
||||
home: DroppableDimension;
|
||||
draggables: DraggableDimensionMap;
|
||||
viewport: Viewport;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
afterCritical: LiftEffect;
|
||||
impact: DragImpact;
|
||||
}
|
||||
|
||||
export default ({ draggable, home, draggables, viewport }: Args): Result => {
|
||||
const displacedBy: DisplacedBy = getDisplacedBy(
|
||||
home.axis,
|
||||
draggable.displaceBy,
|
||||
);
|
||||
|
||||
const insideHome: DraggableDimension[] = 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: number = insideHome.indexOf(draggable);
|
||||
invariant(rawIndex !== -1, 'Expected draggable to be inside home list');
|
||||
|
||||
const afterDragging: DraggableDimension[] = insideHome.slice(rawIndex + 1);
|
||||
const effected: DraggableIdMap = afterDragging.reduce(
|
||||
(previous: DraggableIdMap, item: DraggableDimension): DraggableIdMap => {
|
||||
previous[item.descriptor.id] = true;
|
||||
return previous;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const afterCritical: LiftEffect = {
|
||||
inVirtualList: home.descriptor.mode === 'virtual',
|
||||
displacedBy,
|
||||
effected,
|
||||
};
|
||||
|
||||
const displaced: DisplacementGroups = 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: DragImpact = {
|
||||
displaced,
|
||||
displacedBy,
|
||||
at: {
|
||||
type: 'REORDER',
|
||||
destination: getHomeLocation(draggable.descriptor),
|
||||
},
|
||||
};
|
||||
return { impact, afterCritical };
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { subtract } from './position';
|
||||
|
||||
interface Args {
|
||||
scrollHeight: number;
|
||||
scrollWidth: number;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export default ({
|
||||
scrollHeight,
|
||||
scrollWidth,
|
||||
height,
|
||||
width,
|
||||
}: Args): Position => {
|
||||
const maxScroll: Position = subtract(
|
||||
// full size
|
||||
{ x: scrollWidth, y: scrollHeight },
|
||||
// viewport size
|
||||
{ x: width, y: height },
|
||||
);
|
||||
|
||||
const adjustedMaxScroll: Position = {
|
||||
x: Math.max(0, maxScroll.x),
|
||||
y: Math.max(0, maxScroll.y),
|
||||
};
|
||||
|
||||
return adjustedMaxScroll;
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type {
|
||||
State,
|
||||
DraggingState,
|
||||
CollectingState,
|
||||
DropPendingState,
|
||||
} from '../types';
|
||||
|
||||
export default function isDragging(
|
||||
state: State,
|
||||
): state is DraggingState | CollectingState | DropPendingState {
|
||||
if (state.phase === 'IDLE' || state.phase === 'DROP_ANIMATING') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state.isDragging;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { State, StateWhenUpdatesAllowed } from '../types';
|
||||
// Using function declaration as arrow function does not play well with the %checks syntax
|
||||
export default function isMovementAllowed(
|
||||
state: State,
|
||||
): state is StateWhenUpdatesAllowed {
|
||||
return state.phase === 'DRAGGING' || state.phase === 'COLLECTING';
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// is a value between two other values
|
||||
|
||||
export default (
|
||||
lowerBound: number,
|
||||
upperBound: number,
|
||||
): ((a: number) => boolean) =>
|
||||
(value: number): boolean =>
|
||||
lowerBound <= value && value <= upperBound;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { invariant } from '../../invariant';
|
||||
import type { AutoScroller } from '../auto-scroller/auto-scroller-types';
|
||||
import type { Middleware } from '../store-types';
|
||||
import { guard } from '../action-creators';
|
||||
import type { State } from '../../types';
|
||||
|
||||
const shouldStop = (action: unknown) =>
|
||||
guard(action, 'DROP_COMPLETE') ||
|
||||
guard(action, 'DROP_ANIMATE') ||
|
||||
guard(action, 'FLUSH');
|
||||
|
||||
export default (autoScroller: AutoScroller): Middleware =>
|
||||
(store) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (shouldStop(action)) {
|
||||
autoScroller.stop();
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard(action, 'INITIAL_PUBLISH')) {
|
||||
// letting the action go first to hydrate the state
|
||||
next(action);
|
||||
const state: 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());
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { guard } from '../action-creators';
|
||||
import type { Middleware } from '../store-types';
|
||||
import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types';
|
||||
|
||||
export default (marshal: DimensionMarshal): Middleware =>
|
||||
() =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
// Not stopping a collection on a 'DROP' as we want a collection to continue
|
||||
if (
|
||||
// drag is finished
|
||||
guard(action, 'DROP_COMPLETE') ||
|
||||
guard(action, 'FLUSH') ||
|
||||
// no longer accepting changes once the drop has started
|
||||
guard(action, 'DROP_ANIMATE')
|
||||
) {
|
||||
marshal.stopPublishing();
|
||||
}
|
||||
|
||||
next(action);
|
||||
};
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import { invariant } from '../../../invariant';
|
||||
import { completeDrop, guard } from '../../action-creators';
|
||||
import type { State } from '../../../types';
|
||||
import type { Middleware } from '../../store-types';
|
||||
|
||||
const dropAnimationFinishMiddleware: Middleware =
|
||||
(store) => (next) => (action) => {
|
||||
if (!guard(action, 'DROP_ANIMATION_FINISHED')) {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
|
||||
const state: State = store.getState();
|
||||
invariant(
|
||||
state.phase === 'DROP_ANIMATING',
|
||||
'Cannot finish a drop animating when no drop is occurring',
|
||||
);
|
||||
store.dispatch(completeDrop({ completed: state.completed }));
|
||||
};
|
||||
|
||||
export default dropAnimationFinishMiddleware;
|
||||
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
import { dropAnimationFinished, guard } from '../../action-creators';
|
||||
import type { State } from '../../../types';
|
||||
import type { Middleware } from '../../store-types';
|
||||
import type { EventBinding } from '../../../view/event-bindings/event-types';
|
||||
import bindEvents from '../../../view/event-bindings/bind-events';
|
||||
|
||||
const dropAnimationFlushOnScrollMiddleware: Middleware = (store) => {
|
||||
let unbind: (() => void) | null = null;
|
||||
let frameId: AnimationFrameID | null = null;
|
||||
|
||||
function clear() {
|
||||
if (frameId) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = null;
|
||||
}
|
||||
|
||||
if (unbind) {
|
||||
unbind();
|
||||
unbind = null;
|
||||
}
|
||||
}
|
||||
|
||||
return (next) => (action) => {
|
||||
if (
|
||||
guard(action, 'FLUSH') ||
|
||||
guard(action, 'DROP_COMPLETE') ||
|
||||
guard(action, 'DROP_ANIMATION_FINISHED')
|
||||
) {
|
||||
clear();
|
||||
}
|
||||
|
||||
next(action);
|
||||
|
||||
if (!guard(action, 'DROP_ANIMATE')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const binding: EventBinding = {
|
||||
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: 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]);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default dropAnimationFlushOnScrollMiddleware;
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { invariant } from '../../../invariant';
|
||||
import type {
|
||||
State,
|
||||
DropReason,
|
||||
Critical,
|
||||
DraggableLocation,
|
||||
DropResult,
|
||||
CompletedDrag,
|
||||
Combine,
|
||||
DimensionMap,
|
||||
DraggableDimension,
|
||||
} from '../../../types';
|
||||
import type { Middleware } from '../../store-types';
|
||||
import {
|
||||
animateDrop,
|
||||
completeDrop,
|
||||
dropPending,
|
||||
guard,
|
||||
} from '../../action-creators';
|
||||
import type { AnimateDropArgs } 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 type { Result } from './get-drop-impact';
|
||||
import { tryGetCombine, tryGetDestination } from '../../get-impact-location';
|
||||
|
||||
const dropMiddleware: Middleware =
|
||||
({ getState, dispatch }) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (!guard(action, 'DROP')) {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
|
||||
const state: State = getState();
|
||||
const reason: DropReason = 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: boolean =
|
||||
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: Critical = state.critical;
|
||||
const dimensions: DimensionMap = state.dimensions;
|
||||
const draggable: DraggableDimension =
|
||||
dimensions.draggables[state.critical.draggable.id];
|
||||
// Only keeping impact when doing a user drop - otherwise we are cancelling
|
||||
|
||||
const { impact, didDropInsideDroppable }: Result = 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: DraggableLocation | null = didDropInsideDroppable
|
||||
? tryGetDestination(impact)
|
||||
: null;
|
||||
const combine: Combine | null = didDropInsideDroppable
|
||||
? tryGetCombine(impact)
|
||||
: null;
|
||||
|
||||
const source: DraggableLocation = {
|
||||
index: critical.draggable.index,
|
||||
droppableId: critical.droppable.id,
|
||||
};
|
||||
|
||||
const result: DropResult = {
|
||||
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: Position = getNewHomeClientOffset({
|
||||
impact,
|
||||
draggable,
|
||||
dimensions,
|
||||
viewport: state.viewport,
|
||||
afterCritical: state.afterCritical,
|
||||
});
|
||||
|
||||
const completed: CompletedDrag = {
|
||||
critical: state.critical,
|
||||
afterCritical: state.afterCritical,
|
||||
result,
|
||||
impact,
|
||||
};
|
||||
|
||||
const isAnimationRequired: boolean =
|
||||
// 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: number = getDropDuration({
|
||||
current: state.current.client.offset,
|
||||
destination: newHomeClientOffset,
|
||||
reason,
|
||||
});
|
||||
|
||||
const args: AnimateDropArgs = {
|
||||
newHomeClientOffset,
|
||||
dropDuration,
|
||||
completed,
|
||||
};
|
||||
|
||||
dispatch(animateDrop(args));
|
||||
};
|
||||
|
||||
export default dropMiddleware;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import { distance as getDistance } from '../../position';
|
||||
import { timings } from '../../../animation';
|
||||
import type { DropReason } from '../../../types';
|
||||
|
||||
interface GetDropDurationArgs {
|
||||
current: Position;
|
||||
destination: Position;
|
||||
reason: DropReason;
|
||||
}
|
||||
|
||||
const { minDropTime, maxDropTime } = timings;
|
||||
const dropTimeRange: number = maxDropTime - minDropTime;
|
||||
const maxDropTimeAtDistance = 1500;
|
||||
// will bring a time lower - which makes it faster
|
||||
const cancelDropModifier = 0.6;
|
||||
|
||||
export default ({
|
||||
current,
|
||||
destination,
|
||||
reason,
|
||||
}: GetDropDurationArgs): number => {
|
||||
const distance: number = 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: number = distance / maxDropTimeAtDistance;
|
||||
const duration: number = minDropTime + dropTimeRange * percentage;
|
||||
|
||||
const withDuration: number =
|
||||
reason === 'CANCEL' ? duration * cancelDropModifier : duration;
|
||||
// To two decimal points by converting to string and back
|
||||
return Number(withDuration.toFixed(2));
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
DropReason,
|
||||
DragImpact,
|
||||
Viewport,
|
||||
DroppableDimension,
|
||||
DraggableDimensionMap,
|
||||
LiftEffect,
|
||||
} from '../../../types';
|
||||
import recompute from '../../update-displacement-visibility/recompute';
|
||||
import { emptyGroups } from '../../no-impact';
|
||||
|
||||
interface Args {
|
||||
draggables: DraggableDimensionMap;
|
||||
home: DroppableDimension;
|
||||
reason: DropReason;
|
||||
lastImpact: DragImpact;
|
||||
onLiftImpact: DragImpact;
|
||||
viewport: Viewport;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
impact: DragImpact;
|
||||
didDropInsideDroppable: boolean;
|
||||
}
|
||||
|
||||
export default ({
|
||||
draggables,
|
||||
reason,
|
||||
lastImpact,
|
||||
home,
|
||||
viewport,
|
||||
onLiftImpact,
|
||||
}: Args): Result => {
|
||||
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: DragImpact = 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: DragImpact = {
|
||||
...lastImpact,
|
||||
displaced: emptyGroups,
|
||||
};
|
||||
|
||||
return {
|
||||
impact: withoutMovement,
|
||||
didDropInsideDroppable: true,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
import type { Position } from 'css-box-model';
|
||||
import type {
|
||||
DroppableDimension,
|
||||
Viewport,
|
||||
DragImpact,
|
||||
DimensionMap,
|
||||
DraggableDimension,
|
||||
DroppableId,
|
||||
LiftEffect,
|
||||
} from '../../../types';
|
||||
import whatIsDraggedOver from '../../droppable/what-is-dragged-over';
|
||||
import { subtract } from '../../position';
|
||||
import getClientBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center';
|
||||
|
||||
interface Args {
|
||||
impact: DragImpact;
|
||||
draggable: DraggableDimension;
|
||||
dimensions: DimensionMap;
|
||||
viewport: Viewport;
|
||||
afterCritical: LiftEffect;
|
||||
}
|
||||
|
||||
export default ({
|
||||
impact,
|
||||
draggable,
|
||||
dimensions,
|
||||
viewport,
|
||||
afterCritical,
|
||||
}: Args): Position => {
|
||||
const { draggables, droppables } = dimensions;
|
||||
const droppableId: DroppableId | null = whatIsDraggedOver(impact);
|
||||
const destination: DroppableDimension | null = droppableId
|
||||
? droppables[droppableId]
|
||||
: null;
|
||||
const home: DroppableDimension = droppables[draggable.descriptor.droppableId];
|
||||
|
||||
const newClientCenter: Position = 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: Position = subtract(
|
||||
newClientCenter,
|
||||
draggable.client.borderBox.center,
|
||||
);
|
||||
|
||||
return offset;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from './drop-middleware';
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import type { DropResult } from '../../types';
|
||||
import type { Middleware } from '../store-types';
|
||||
import { guard } from '../action-creators';
|
||||
import type { FocusMarshal } from '../../view/use-focus-marshal/focus-marshal-types';
|
||||
|
||||
export default (marshal: FocusMarshal): Middleware => {
|
||||
let isWatching = false;
|
||||
|
||||
return () => (next) => (action) => {
|
||||
if (guard(action, 'INITIAL_PUBLISH')) {
|
||||
isWatching = true;
|
||||
|
||||
marshal.tryRecordFocus(action.payload.critical.draggable.id);
|
||||
next(action);
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
return;
|
||||
}
|
||||
|
||||
next(action);
|
||||
|
||||
if (!isWatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard(action, 'FLUSH')) {
|
||||
isWatching = false;
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard(action, 'DROP_COMPLETE')) {
|
||||
isWatching = false;
|
||||
const result: DropResult = action.payload.completed.result;
|
||||
|
||||
// give focus to the combine target when combining
|
||||
if (result.combine) {
|
||||
marshal.tryShiftRecord(result.draggableId, result.combine.draggableId);
|
||||
}
|
||||
marshal.tryRestoreFocusRecorded();
|
||||
}
|
||||
};
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { invariant } from '../../invariant';
|
||||
import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types';
|
||||
import type { State, ScrollOptions, LiftRequest } from '../../types';
|
||||
import type { Middleware } from '../store-types';
|
||||
import {
|
||||
completeDrop,
|
||||
initialPublish,
|
||||
flush,
|
||||
beforeInitialCapture,
|
||||
guard,
|
||||
} from '../action-creators';
|
||||
import validateDimensions from './util/validate-dimensions';
|
||||
|
||||
export default (marshal: DimensionMarshal): Middleware =>
|
||||
({ getState, dispatch }) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (!guard(action, 'LIFT')) {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
const { id, clientSelection, movementMode } = action.payload;
|
||||
const initial: State = 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: ScrollOptions = {
|
||||
shouldPublishImmediately: movementMode === 'SNAP',
|
||||
};
|
||||
const request: LiftRequest = {
|
||||
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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { drop, guard } from '../action-creators';
|
||||
import type { State } from '../../types';
|
||||
import type { Middleware } from '../store-types';
|
||||
|
||||
const pendingDrop: Middleware = (store) => (next) => (action) => {
|
||||
// Always let the action go through first
|
||||
next(action);
|
||||
|
||||
if (!guard(action, '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: State = 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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export default pendingDrop;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { invariant } from '../../../invariant';
|
||||
|
||||
interface Entry {
|
||||
timerId: TimeoutID;
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export interface AsyncMarshal {
|
||||
add: (fn: () => void) => void;
|
||||
flush: () => void;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const entries: Entry[] = [];
|
||||
|
||||
const execute = (timerId: TimeoutID) => {
|
||||
const index: number = entries.findIndex(
|
||||
(item): boolean => item.timerId === timerId,
|
||||
);
|
||||
invariant(index !== -1, 'Could not find timer');
|
||||
// delete in place
|
||||
const [entry] = entries.splice(index, 1);
|
||||
entry.callback();
|
||||
};
|
||||
|
||||
const add = (fn: () => void) => {
|
||||
const timerId: TimeoutID = setTimeout(() => execute(timerId));
|
||||
const entry: Entry = {
|
||||
timerId,
|
||||
callback: fn,
|
||||
};
|
||||
entries.push(entry);
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
// nothing to flush
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shallow: Entry[] = [...entries];
|
||||
// clearing entries in case a callback adds some more callbacks
|
||||
entries.length = 0;
|
||||
|
||||
shallow.forEach((entry: Entry) => {
|
||||
clearTimeout(entry.timerId);
|
||||
entry.callback();
|
||||
});
|
||||
};
|
||||
|
||||
return { add, flush };
|
||||
};
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import type { Announce } from '../../../types';
|
||||
import { warning } from '../../../dev-warning';
|
||||
|
||||
export type ExpiringAnnounce = Announce & {
|
||||
wasCalled: () => boolean;
|
||||
};
|
||||
|
||||
export default (announce: Announce): ExpiringAnnounce => {
|
||||
let wasCalled = false;
|
||||
let isExpired = false;
|
||||
|
||||
// not allowing async announcements
|
||||
const timeoutId: TimeoutID = setTimeout(() => {
|
||||
isExpired = true;
|
||||
});
|
||||
|
||||
const result = (message: string): void => {
|
||||
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 = (): boolean => wasCalled;
|
||||
|
||||
return result;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from './responders-middleware';
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import type { Critical, DraggableLocation, Combine } from '../../../types';
|
||||
|
||||
export const areLocationsEqual = (
|
||||
first?: DraggableLocation | null,
|
||||
second?: DraggableLocation | null,
|
||||
): boolean => {
|
||||
// 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?: Combine | null,
|
||||
second?: Combine | null,
|
||||
): boolean => {
|
||||
// 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: Critical, second: Critical): boolean => {
|
||||
if (first === second) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isDraggableEqual: boolean =
|
||||
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: boolean =
|
||||
first.droppable.id === second.droppable.id &&
|
||||
first.droppable.type === second.droppable.type;
|
||||
|
||||
return isDraggableEqual && isDroppableEqual;
|
||||
};
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import { invariant } from '../../../invariant';
|
||||
import messagePreset from '../../../screen-reader-message-preset';
|
||||
import * as timings from '../../../debug/timings';
|
||||
import getExpiringAnnounce from './expiring-announce';
|
||||
import type { ExpiringAnnounce } from './expiring-announce';
|
||||
import getAsyncMarshal from './async-marshal';
|
||||
import type { AsyncMarshal } from './async-marshal';
|
||||
import type {
|
||||
DropResult,
|
||||
Responders,
|
||||
ResponderProvided,
|
||||
Critical,
|
||||
BeforeCapture,
|
||||
DragImpact,
|
||||
DraggableLocation,
|
||||
Combine,
|
||||
DragStart,
|
||||
Announce,
|
||||
DragUpdate,
|
||||
MovementMode,
|
||||
DraggableId,
|
||||
OnBeforeCaptureResponder,
|
||||
OnBeforeDragStartResponder,
|
||||
} from '../../../types';
|
||||
import { isCombineEqual, isCriticalEqual, areLocationsEqual } from './is-equal';
|
||||
import { tryGetDestination, tryGetCombine } from '../../get-impact-location';
|
||||
|
||||
const withTimings = (key: string, fn: () => void) => {
|
||||
timings.start(key);
|
||||
fn();
|
||||
timings.finish(key);
|
||||
};
|
||||
|
||||
const getDragStart = (critical: Critical, mode: MovementMode): DragStart => ({
|
||||
draggableId: critical.draggable.id,
|
||||
type: critical.droppable.type,
|
||||
|
||||
source: {
|
||||
droppableId: critical.droppable.id,
|
||||
index: critical.draggable.index,
|
||||
},
|
||||
|
||||
mode,
|
||||
});
|
||||
|
||||
type AnyResponderData = DragStart | DragUpdate | DropResult;
|
||||
|
||||
type PrimaryResponderFn<TData extends AnyResponderData> = (
|
||||
data: TData,
|
||||
provided: ResponderProvided,
|
||||
) => unknown;
|
||||
|
||||
type GetDefaultMessage<TData extends AnyResponderData> = (
|
||||
data: TData,
|
||||
) => string;
|
||||
|
||||
function execute<TData extends AnyResponderData>(
|
||||
responder: PrimaryResponderFn<TData> | undefined,
|
||||
data: TData,
|
||||
announce: Announce,
|
||||
// eslint-disable-next-line no-shadow
|
||||
getDefaultMessage: GetDefaultMessage<TData>,
|
||||
) {
|
||||
if (!responder) {
|
||||
announce(getDefaultMessage(data));
|
||||
return;
|
||||
}
|
||||
|
||||
const willExpire: ExpiringAnnounce = getExpiringAnnounce(announce);
|
||||
const provided: ResponderProvided = {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
interface WhileDragging {
|
||||
mode: MovementMode;
|
||||
lastCritical: Critical;
|
||||
lastCombine: Combine | null;
|
||||
lastLocation: DraggableLocation | null;
|
||||
}
|
||||
|
||||
export default (getResponders: () => Responders, announce: Announce) => {
|
||||
const asyncMarshal: AsyncMarshal = getAsyncMarshal();
|
||||
let dragging: WhileDragging | null = null;
|
||||
|
||||
const beforeCapture = (draggableId: DraggableId, mode: MovementMode) => {
|
||||
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: OnBeforeCaptureResponder | undefined =
|
||||
getResponders().onBeforeCapture;
|
||||
if (fn) {
|
||||
const before: BeforeCapture = {
|
||||
draggableId,
|
||||
mode,
|
||||
};
|
||||
fn(before);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const beforeStart = (critical: Critical, mode: MovementMode) => {
|
||||
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: OnBeforeDragStartResponder | undefined =
|
||||
getResponders().onBeforeDragStart;
|
||||
if (fn) {
|
||||
fn(getDragStart(critical, mode));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const start = (critical: Critical, mode: MovementMode) => {
|
||||
invariant(
|
||||
!dragging,
|
||||
'Cannot fire onBeforeDragStart as a drag start has already been published',
|
||||
);
|
||||
const data: DragStart = 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: Critical, impact: DragImpact) => {
|
||||
const location: DraggableLocation | null = tryGetDestination(impact);
|
||||
const combine: Combine | null = 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: DragUpdate = {
|
||||
...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: DropResult) => {
|
||||
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: DropResult = {
|
||||
...getDragStart(dragging.lastCritical, dragging.mode),
|
||||
combine: null,
|
||||
destination: null,
|
||||
reason: 'CANCEL',
|
||||
};
|
||||
drop(result);
|
||||
};
|
||||
|
||||
return {
|
||||
beforeCapture,
|
||||
beforeStart,
|
||||
start,
|
||||
update,
|
||||
flush,
|
||||
drop,
|
||||
abort,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import getPublisher from './publisher';
|
||||
import type {
|
||||
State,
|
||||
DropResult,
|
||||
Responders,
|
||||
Critical,
|
||||
Announce,
|
||||
} from '../../../types';
|
||||
import type { Middleware } from '../../store-types';
|
||||
import { guard } from '../../action-creators';
|
||||
|
||||
export default (
|
||||
getResponders: () => Responders,
|
||||
announce: Announce,
|
||||
): Middleware => {
|
||||
const publisher = getPublisher(
|
||||
getResponders as () => Responders,
|
||||
announce as Announce,
|
||||
);
|
||||
|
||||
return (store) => (next) => (action) => {
|
||||
if (guard(action, 'BEFORE_INITIAL_CAPTURE')) {
|
||||
publisher.beforeCapture(
|
||||
action.payload.draggableId,
|
||||
action.payload.movementMode,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard(action, 'INITIAL_PUBLISH')) {
|
||||
const critical: Critical = action.payload.critical;
|
||||
publisher.beforeStart(critical, action.payload.movementMode);
|
||||
next(action);
|
||||
publisher.start(critical, action.payload.movementMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag end
|
||||
if (guard(action, '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: DropResult = 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 (guard(action, 'FLUSH')) {
|
||||
publisher.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// ## Perform drag updates
|
||||
// impact of action has already been reduced
|
||||
|
||||
const state: State = store.getState();
|
||||
if (state.phase === 'DRAGGING') {
|
||||
publisher.update(state.critical, state.impact);
|
||||
}
|
||||
};
|
||||
};
|
||||
Loaded 100 of 2277 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user