├── website ├── src │ ├── styles │ │ ├── font.scss │ │ ├── custom-antd.scss │ │ ├── space.scss │ │ ├── globals.scss │ │ ├── text.scss │ │ ├── reset.scss │ │ └── color.scss │ ├── vite-env.d.ts │ ├── main.tsx │ └── App.tsx ├── README.md ├── .prettierignore ├── .eslintignore ├── tsconfig.node.json ├── .prettierrc ├── .eslintrc.json ├── .gitignore ├── .editorconfig ├── index.html ├── tsconfig.json ├── vite.config.ts ├── public │ └── vite.svg └── package.json ├── FUNDING.yml ├── pnpm-workspace.yaml ├── .vscode └── settings.json ├── .prettierrc.js ├── src ├── SortableContainer │ ├── defaultGetHelperDimensions.tsx │ ├── defaultShouldCancelStart.tsx │ ├── props.tsx │ └── index.tsx ├── SortableHandle │ └── index.tsx ├── Manager │ └── index.tsx ├── AutoScroller │ └── index.tsx ├── SortableElement │ └── index.tsx ├── index.tsx └── utils.tsx ├── tsconfig.json ├── .eslintrc.js ├── turbo.json ├── .gitignore ├── LICENSE.md ├── package.json └── README.md /website/src/styles/font.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hunghg255 2 | -------------------------------------------------------------------------------- /website/src/styles/custom-antd.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'website' 3 | - '.' 4 | - 'test' 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /website/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const process; 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | trailingComma: 'all', 6 | printWidth: 120, 7 | }; 8 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | # Install package 7 | npm install 8 | 9 | # Run 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | node_modules 6 | 7 | # Ignore all HTML files: 8 | *.html 9 | 10 | .github 11 | 12 | .next 13 | 14 | .swc 15 | -------------------------------------------------------------------------------- /src/SortableContainer/defaultGetHelperDimensions.tsx: -------------------------------------------------------------------------------- 1 | export default function defaultGetHelperDimensions({ node }) { 2 | return { 3 | height: node.offsetHeight, 4 | width: node.offsetWidth, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es2018", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "lib": ["es2015", "dom"] 8 | }, 9 | "include": ["src"], 10 | } 11 | -------------------------------------------------------------------------------- /website/.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | node_modules 6 | 7 | # Ignore all HTML files: 8 | *.html 9 | 10 | .github 11 | 12 | .next 13 | 14 | .swc 15 | 16 | next.config.js 17 | 18 | next-i18next.config.js 19 | -------------------------------------------------------------------------------- /website/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /website/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | import './styles/globals.scss'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ['custom'], 5 | settings: { 6 | next: { 7 | rootDir: ['apps/*/'], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**"] 7 | }, 8 | "dev": { 9 | "cache": false 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "jsxBracketSameLine": false, 7 | "endOfLine": "auto", 8 | "jsxSingleQuote": true, 9 | "trailingComma": "all", 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "react/react-in-jsx-scope": "off", 5 | "react/display-name": "off", 6 | "react/prop-types": "off", 7 | "react/jsx-key": "error", 8 | "no-console": 1, 9 | "no-unused-vars": "off", 10 | "@typescript-eslint/no-unused-vars": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /website/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /website/src/styles/space.scss: -------------------------------------------------------------------------------- 1 | .my-auto { 2 | margin-top: auto; 3 | margin-bottom: auto; 4 | } 5 | .mr-1 { 6 | margin-right: 1em; 7 | } 8 | .mr-1 { 9 | margin-right: 1em; 10 | } 11 | .text_center { 12 | text-align: center; 13 | } 14 | .text-between { 15 | display: flex; 16 | justify-content: space-between; 17 | } 18 | 19 | .mb-16 { 20 | margin: 0 0 16px; 21 | } 22 | 23 | .mb-12 { 24 | margin: 0 0 12px; 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | dist 3 | 4 | 5 | # dependencies 6 | node_modules 7 | .pnp 8 | .pnp.js 9 | 10 | # testing 11 | coverage 12 | 13 | # next.js 14 | .next/ 15 | out/ 16 | build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # turbo 35 | .turbo 36 | /test-results/ 37 | /playwright-report/ 38 | /playwright/.cache/ 39 | -------------------------------------------------------------------------------- /src/SortableContainer/defaultShouldCancelStart.tsx: -------------------------------------------------------------------------------- 1 | import { NodeType, closest } from '../utils.js'; 2 | 3 | export default function defaultShouldCancelStart(event) { 4 | // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option` 5 | const interactiveElements = [NodeType.Input, NodeType.Textarea, NodeType.Select, NodeType.Option, NodeType.Button]; 6 | 7 | if (interactiveElements.indexOf(event.target.tagName) !== -1) { 8 | // Return true to cancel sorting 9 | return true; 10 | } 11 | 12 | if (closest(event.target, el => el.contentEditable === 'true')) { 13 | return true; 14 | } 15 | 16 | return false; 17 | } 18 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Reactjs Table Dnd 9 | 10 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /website/src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | // @import '~antd/dist/antd.css'; 2 | @import './reset.scss'; 3 | @import './font.scss'; 4 | @import './color.scss'; 5 | @import './space.scss'; 6 | @import './text.scss'; 7 | @import './custom-antd.scss'; 8 | 9 | *, 10 | html, 11 | body { 12 | padding: 0; 13 | margin: 0; 14 | box-sizing: border-box; 15 | font-family: sans-serif; 16 | font-size: 14px; 17 | line-height: normal; 18 | text-rendering: optimizeSpeed; 19 | } 20 | 21 | main { 22 | max-width: 768px; 23 | margin: 0 auto; 24 | padding: 32px; 25 | 26 | h2 { 27 | font-size: 32px; 28 | text-align: center; 29 | margin: 0 0 12px; 30 | } 31 | } 32 | 33 | .item { 34 | margin-bottom: 8px; 35 | padding: 10px; 36 | background-color: #ccc; 37 | } 38 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "types": ["node"], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunghg255 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/SortableHandle/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import invariant from 'invariant'; 4 | 5 | import { provideDisplayName } from '../utils.js'; 6 | 7 | export default function sortableHandle(WrappedComponent, config = { withRef: false }) { 8 | return class WithSortableHandle extends React.Component { 9 | static displayName = provideDisplayName('sortableHandle', WrappedComponent); 10 | 11 | componentDidMount() { 12 | const node = findDOMNode(this) as any; 13 | node.sortableHandle = true; 14 | } 15 | 16 | getWrappedInstance() { 17 | invariant( 18 | config.withRef, 19 | 'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableHandle() call', 20 | ); 21 | return this.wrappedInstance.current; 22 | } 23 | 24 | wrappedInstance = React.createRef(); 25 | 26 | render() { 27 | const ref = config.withRef ? this.wrappedInstance : null; 28 | 29 | return ; 30 | } 31 | }; 32 | } 33 | 34 | export function isSortableHandle(node) { 35 | return node.sortableHandle != null; 36 | } 37 | -------------------------------------------------------------------------------- /src/Manager/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | export default class Manager { 3 | refs = {}; 4 | 5 | add(collection, ref) { 6 | if (!this.refs[collection]) { 7 | this.refs[collection] = []; 8 | } 9 | 10 | this.refs[collection].push(ref); 11 | } 12 | 13 | remove(collection, ref) { 14 | const index = this.getIndex(collection, ref); 15 | 16 | if (index !== -1) { 17 | this.refs[collection].splice(index, 1); 18 | } 19 | } 20 | 21 | isActive() { 22 | return this.active; 23 | } 24 | 25 | getActive() { 26 | return this.refs[this.active.collection].find( 27 | // eslint-disable-next-line eqeqeq 28 | ({ node }) => node.sortableInfo.index == this.active.index, 29 | ); 30 | } 31 | 32 | getIndex(collection, ref) { 33 | return this.refs[collection].indexOf(ref); 34 | } 35 | 36 | getOrderedRefs(collection = this.active.collection) { 37 | return this.refs[collection].sort(sortByIndex); 38 | } 39 | } 40 | 41 | function sortByIndex( 42 | { 43 | node: { 44 | sortableInfo: { index: index1 }, 45 | }, 46 | }, 47 | { 48 | node: { 49 | sortableInfo: { index: index2 }, 50 | }, 51 | }, 52 | ) { 53 | return index1 - index2; 54 | } 55 | -------------------------------------------------------------------------------- /website/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import EnvironmentPlugin from 'vite-plugin-environment'; 4 | import checker from 'vite-plugin-checker'; 5 | import * as path from 'path'; 6 | import { antdDayjs } from 'antd-dayjs-vite-plugin'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ mode }) => { 10 | const isDev = mode !== 'production'; 11 | const isAnalyze = mode === 'analyze'; 12 | 13 | return { 14 | plugins: [ 15 | react(), 16 | antdDayjs(), 17 | EnvironmentPlugin('all'), 18 | // resolve({ "react-codemirror2": ` 19 | // const UnControlled = {}; 20 | // export { 21 | // UnControlled, 22 | // }` 23 | // } 24 | checker({ 25 | typescript: true, 26 | }), 27 | ], 28 | css: { 29 | devSourcemap: isDev, 30 | }, 31 | optimizeDeps: { 32 | include: ['react'], 33 | }, 34 | build: { 35 | commonjsOptions: { 36 | include: [/node_modules/], 37 | }, 38 | sourcemap: isAnalyze, 39 | }, 40 | resolve: { 41 | alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], 42 | }, 43 | esbuild: { 44 | sourcemap: isDev, 45 | }, 46 | server: { 47 | port: 3000, 48 | }, 49 | preview: { 50 | port: 3000, 51 | }, 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /website/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/styles/text.scss: -------------------------------------------------------------------------------- 1 | .h1-bold { 2 | font-weight: 600; 3 | font-size: 40px; 4 | line-height: 60px; 5 | } 6 | .h1-regular { 7 | font-weight: 400; 8 | font-size: 40px; 9 | line-height: 60px; 10 | } 11 | .h2-bold { 12 | font-weight: 600; 13 | font-size: 32px; 14 | line-height: 48px; 15 | } 16 | .h2-regular { 17 | font-weight: 400; 18 | font-size: 32px; 19 | line-height: 48px; 20 | } 21 | .h3-bold { 22 | font-weight: 700; 23 | font-size: 24px; 24 | line-height: 36px; 25 | 26 | @media (max-width: 768px) { 27 | font-size: 16px; 28 | line-height: 24px; 29 | } 30 | } 31 | .h3-regular { 32 | font-weight: 400; 33 | font-size: 24px; 34 | line-height: 36px; 35 | } 36 | .body-bold { 37 | font-weight: 600; 38 | font-size: 20px; 39 | line-height: 32px; 40 | } 41 | .body-regular { 42 | font-weight: 400; 43 | font-size: 20px; 44 | line-height: 32px; 45 | } 46 | .body-2-bold { 47 | font-weight: 700; 48 | font-size: 16px; 49 | line-height: 24px; 50 | } 51 | .body-2-regular { 52 | font-weight: 400; 53 | font-size: 16px; 54 | line-height: 24px; 55 | } 56 | .body-3-bold { 57 | font-weight: 700; 58 | font-size: 14px; 59 | line-height: 24px; 60 | } 61 | .body-3-regular { 62 | font-weight: 400; 63 | font-size: 14px; 64 | line-height: 24px; 65 | } 66 | .body-4-bold { 67 | font-weight: 600; 68 | font-size: 12px; 69 | line-height: 16px; 70 | } 71 | .body-4-regular { 72 | font-weight: 400; 73 | font-size: 12px; 74 | line-height: 16px; 75 | } 76 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-table-dnd-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "npm run lint && tsc && vite build", 9 | "preview": "vite preview", 10 | "start": "npm run build && vite preview", 11 | "lint": "eslint --ext .ts,.tsx src --color", 12 | "format": "prettier --write \"./src/**/*.{ts,tsx,json}\"", 13 | "analyze": "npm run lint && tsc && vite build --mode=analyze && source-map-explorer 'dist/assets/*.js'" 14 | }, 15 | "dependencies": { 16 | "@ant-design/icons": "^5.3.7", 17 | "@uiw/react-github-corners": "^1.5.15", 18 | "antd": "^5.2.0", 19 | "antd-dayjs-vite-plugin": "^1.2.2", 20 | "dayjs": "^1.11.7", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-helmet": "^6.1.0", 24 | "reactjs-table-dnd": "workspace:*" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^18.11.11", 28 | "@types/react": "^18.0.24", 29 | "@types/react-dom": "^18.0.8", 30 | "@types/react-helmet": "^6.1.6", 31 | "@typescript-eslint/eslint-plugin": "^5.48.1", 32 | "@typescript-eslint/parser": "^5.48.1", 33 | "@vitejs/plugin-react": "^2.2.0", 34 | "eslint": "^8.31.0", 35 | "eslint-config-react-app": "^7.0.1", 36 | "prettier": "^2.8.2", 37 | "sass": "^1.57.1", 38 | "source-map-explorer": "^2.5.3", 39 | "typescript": "^4.6.4", 40 | "vite": "^3.2.3", 41 | "vite-plugin-checker": "^0.5.1", 42 | "vite-plugin-environment": "^1.1.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-table-dnd", 3 | "version": "1.0.5", 4 | "description": "A Sort component for Reactjs and antd ✨", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "exports": { 12 | "import": { 13 | "types": "./dist/index.d.mts", 14 | "default": "./dist/index.mjs" 15 | }, 16 | "require": { 17 | "types": "./dist/index.d.ts", 18 | "default": "./dist/index.js" 19 | } 20 | }, 21 | "scripts": { 22 | "type-check": "tsc --noEmit", 23 | "build": "bunchee --minify", 24 | "dev": "bunchee --watch", 25 | "dev:website": "turbo run dev --filter=website...", 26 | "format": "prettier --write ." 27 | }, 28 | "keywords": [ 29 | "react", 30 | "ant-design", 31 | "antd", 32 | "table", 33 | "sort" 34 | ], 35 | "author": "hunghg255 ", 36 | "license": "MIT", 37 | "homepage": "https://reactjs-table-dnd.vercel.app/", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/hunghg255/reactjs-table-dnd.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/hunghg255/reactjs-table-dnd/issues" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "20.5.7", 47 | "@types/react": "18.2.55", 48 | "@types/react-dom": "18.2.18", 49 | "bunchee": "^4.4.6", 50 | "eslint": "^7.32.0", 51 | "prettier": "^2.5.1", 52 | "react": "^18.2.0", 53 | "react-dom": "^18.2.0", 54 | "turbo": "1.6", 55 | "typescript": "5.2.2" 56 | }, 57 | "peerDependencies": { 58 | "react": "^16.8 || ^17.0 || ^18.0", 59 | "react-dom": "^16.8 || ^17.0 || ^18.0" 60 | }, 61 | "packageManager": "pnpm@6.32.11", 62 | "dependencies": { 63 | "invariant": "^2.2.4", 64 | "prop-types": "^15.8.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /website/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | vertical-align: baseline; 92 | } 93 | 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | 109 | body { 110 | line-height: 1; 111 | } 112 | 113 | ol, 114 | ul { 115 | list-style: none; 116 | } 117 | 118 | blockquote, 119 | q { 120 | quotes: none; 121 | } 122 | 123 | blockquote:before, 124 | blockquote:after, 125 | q:before, 126 | q:after { 127 | content: ""; 128 | content: none; 129 | } 130 | 131 | table { 132 | border-collapse: collapse; 133 | border-spacing: 0; 134 | } 135 | a, 136 | a:hover, 137 | a:active, 138 | a:focus { 139 | color: inherit; 140 | } 141 | 142 | img { 143 | width: 100%; 144 | height: auto; 145 | } 146 | -------------------------------------------------------------------------------- /src/AutoScroller/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | export default class AutoScroller { 3 | constructor(container, onScrollCallback) { 4 | this.container = container; 5 | this.onScrollCallback = onScrollCallback; 6 | } 7 | 8 | clear() { 9 | if (this.interval == null) { 10 | return; 11 | } 12 | 13 | clearInterval(this.interval); 14 | this.interval = null; 15 | } 16 | 17 | update({ translate, minTranslate, maxTranslate, width, height }) { 18 | const direction = { 19 | x: 0, 20 | y: 0, 21 | }; 22 | const speed = { 23 | x: 1, 24 | y: 1, 25 | }; 26 | const acceleration = { 27 | x: 10, 28 | y: 10, 29 | }; 30 | 31 | const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } = this.container; 32 | 33 | const isTop = scrollTop === 0; 34 | const isBottom = scrollHeight - scrollTop - clientHeight === 0; 35 | const isLeft = scrollLeft === 0; 36 | const isRight = scrollWidth - scrollLeft - clientWidth === 0; 37 | 38 | if (translate.y >= maxTranslate.y - height / 2 && !isBottom) { 39 | // Scroll Down 40 | direction.y = 1; 41 | speed.y = acceleration.y * Math.abs((maxTranslate.y - height / 2 - translate.y) / height); 42 | } else if (translate.x >= maxTranslate.x - width / 2 && !isRight) { 43 | // Scroll Right 44 | direction.x = 1; 45 | speed.x = acceleration.x * Math.abs((maxTranslate.x - width / 2 - translate.x) / width); 46 | } else if (translate.y <= minTranslate.y + height / 2 && !isTop) { 47 | // Scroll Up 48 | direction.y = -1; 49 | speed.y = acceleration.y * Math.abs((translate.y - height / 2 - minTranslate.y) / height); 50 | } else if (translate.x <= minTranslate.x + width / 2 && !isLeft) { 51 | // Scroll Left 52 | direction.x = -1; 53 | speed.x = acceleration.x * Math.abs((translate.x - width / 2 - minTranslate.x) / width); 54 | } 55 | 56 | if (this.interval) { 57 | this.clear(); 58 | this.isAutoScrolling = false; 59 | } 60 | 61 | if (direction.x !== 0 || direction.y !== 0) { 62 | this.interval = setInterval(() => { 63 | this.isAutoScrolling = true; 64 | const offset = { 65 | left: speed.x * direction.x, 66 | top: speed.y * direction.y, 67 | }; 68 | this.container.scrollTop += offset.top; 69 | this.container.scrollLeft += offset.left; 70 | 71 | this.onScrollCallback(offset); 72 | }, 5); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/SortableElement/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import * as React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { findDOMNode } from 'react-dom'; 5 | import invariant from 'invariant'; 6 | import { SortableContext } from '../SortableContainer'; 7 | 8 | import { provideDisplayName, omit } from '../utils.js'; 9 | 10 | const propTypes = { 11 | index: PropTypes.number.isRequired, 12 | collection: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 13 | disabled: PropTypes.bool, 14 | }; 15 | 16 | const omittedProps = Object.keys(propTypes); 17 | 18 | export default function sortableElement(WrappedComponent, config = { withRef: false }) { 19 | return class WithSortableElement extends React.Component { 20 | static displayName = provideDisplayName('sortableElement', WrappedComponent); 21 | 22 | static contextType = SortableContext; 23 | 24 | static propTypes = propTypes; 25 | 26 | static defaultProps = { 27 | collection: 0, 28 | }; 29 | node: any; 30 | 31 | componentDidMount() { 32 | this.register(); 33 | } 34 | 35 | componentDidUpdate(prevProps) { 36 | if (this.node) { 37 | if (prevProps.index !== this.props.index) { 38 | this.node.sortableInfo.index = this.props.index; 39 | } 40 | 41 | if (prevProps.disabled !== this.props.disabled) { 42 | this.node.sortableInfo.disabled = this.props.disabled; 43 | } 44 | } 45 | 46 | if (prevProps.collection !== this.props.collection) { 47 | this.unregister(prevProps.collection); 48 | this.register(); 49 | } 50 | } 51 | 52 | componentWillUnmount() { 53 | this.unregister(); 54 | } 55 | 56 | register() { 57 | const { collection, disabled, index } = this.props as any; 58 | const node = findDOMNode(this) as any; 59 | 60 | node.sortableInfo = { 61 | collection, 62 | disabled, 63 | index, 64 | manager: this.context.manager, 65 | }; 66 | 67 | this.node = node; 68 | this.ref = { node }; 69 | 70 | this.context.manager.add(collection, this.ref); 71 | } 72 | 73 | unregister(collection = this.props.collection) { 74 | this.context.manager.remove(collection, this.ref); 75 | } 76 | 77 | getWrappedInstance() { 78 | invariant( 79 | config.withRef, 80 | 'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableElement() call', 81 | ); 82 | return this.wrappedInstance.current; 83 | } 84 | 85 | wrappedInstance = React.createRef(); 86 | 87 | render() { 88 | const ref = config.withRef ? this.wrappedInstance : null; 89 | 90 | return ; 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/SortableContainer/props.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import invariant from 'invariant'; 3 | 4 | import { KEYCODE } from '../utils.js'; 5 | import defaultGetHelperDimensions from './defaultGetHelperDimensions.js'; 6 | import defaultShouldCancelStart from './defaultShouldCancelStart.js'; 7 | 8 | export const propTypes = { 9 | axis: PropTypes.oneOf(['x', 'y', 'xy']), 10 | contentWindow: PropTypes.any, 11 | disableAutoscroll: PropTypes.bool, 12 | distance: PropTypes.number, 13 | getContainer: PropTypes.func, 14 | getHelperDimensions: PropTypes.func, 15 | helperClass: PropTypes.string, 16 | helperContainer: PropTypes.oneOfType([ 17 | PropTypes.func, 18 | typeof HTMLElement === 'undefined' ? PropTypes.any : PropTypes.instanceOf(HTMLElement), 19 | ]), 20 | hideSortableGhost: PropTypes.bool, 21 | keyboardSortingTransitionDuration: PropTypes.number, 22 | lockAxis: PropTypes.string, 23 | lockOffset: PropTypes.oneOfType([ 24 | PropTypes.number, 25 | PropTypes.string, 26 | PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), 27 | ]), 28 | lockToContainerEdges: PropTypes.bool, 29 | onSortEnd: PropTypes.func, 30 | onSortMove: PropTypes.func, 31 | onSortOver: PropTypes.func, 32 | onSortStart: PropTypes.func, 33 | pressDelay: PropTypes.number, 34 | pressThreshold: PropTypes.number, 35 | keyCodes: PropTypes.shape({ 36 | lift: PropTypes.arrayOf(PropTypes.number), 37 | drop: PropTypes.arrayOf(PropTypes.number), 38 | cancel: PropTypes.arrayOf(PropTypes.number), 39 | up: PropTypes.arrayOf(PropTypes.number), 40 | down: PropTypes.arrayOf(PropTypes.number), 41 | }), 42 | shouldCancelStart: PropTypes.func, 43 | transitionDuration: PropTypes.number, 44 | updateBeforeSortStart: PropTypes.func, 45 | useDragHandle: PropTypes.bool, 46 | useWindowAsScrollContainer: PropTypes.bool, 47 | }; 48 | 49 | export const defaultKeyCodes = { 50 | lift: [KEYCODE.SPACE], 51 | drop: [KEYCODE.SPACE], 52 | cancel: [KEYCODE.ESC], 53 | up: [KEYCODE.UP, KEYCODE.LEFT], 54 | down: [KEYCODE.DOWN, KEYCODE.RIGHT], 55 | }; 56 | 57 | export const defaultProps = { 58 | axis: 'y', 59 | disableAutoscroll: false, 60 | distance: 0, 61 | getHelperDimensions: defaultGetHelperDimensions, 62 | hideSortableGhost: true, 63 | lockOffset: '50%', 64 | lockToContainerEdges: false, 65 | pressDelay: 0, 66 | pressThreshold: 5, 67 | keyCodes: defaultKeyCodes, 68 | shouldCancelStart: defaultShouldCancelStart, 69 | transitionDuration: 300, 70 | useWindowAsScrollContainer: false, 71 | }; 72 | 73 | export const omittedProps = Object.keys(propTypes); 74 | 75 | export function validateProps(props) { 76 | invariant( 77 | !(props.distance && props.pressDelay), 78 | 'Attempted to set both `pressDelay` and `distance` on SortableContainer, you may only use one or the other, not both at the same time.', 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /website/src/styles/color.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --white: #ffffff; 3 | --black: #000000; 4 | 5 | --primary-1: #00a652; 6 | --primary-2: #33b875; 7 | --primary-3: #66ca97; 8 | --primary-4: #99dbba; 9 | --primary-5: #d9f2e5; 10 | 11 | --secondary-1: #cd3a64; 12 | --secondary-2: #f05984; 13 | --secondary-3: #f69bb5; 14 | --secondary-4: #f9bdce; 15 | --secondary-5: #fde6ed; 16 | 17 | --neutral-1: #2f2f2f; 18 | --neutral-2: #494949; 19 | --neutral-3: #7b7b7b; 20 | --neutral-4: #b8b8b8; 21 | --neutral-5: #d9d9d9; 22 | --neutral-6: #f6f6f6; 23 | --neutral-7: #ffffff; 24 | 25 | --danger-1: #f41515; 26 | --danger-2: #ff3b3b; 27 | --danger-3: #ff6d6d; 28 | --danger-4: #ffa5a5; 29 | --danger-5: #ffe9e9; 30 | 31 | --link-1: #0066ff; 32 | --link-2: #3e7bfa; 33 | --link-3: #719efb; 34 | --link-4: #a9c4fc; 35 | --link-5: #d6e3ff; 36 | } 37 | 38 | .primary-1 { 39 | color: var(--primary-1); 40 | } 41 | .primary-2 { 42 | color: var(--primary-2); 43 | } 44 | .primary-3 { 45 | color: var(--primary-3); 46 | } 47 | .primary-4 { 48 | color: var(--primary-4); 49 | } 50 | .primary-5 { 51 | color: var(--primary-5); 52 | } 53 | 54 | .secondary-1 { 55 | color: var(--secondary-1); 56 | } 57 | .secondary-2 { 58 | color: var(--secondary-2); 59 | } 60 | .secondary-3 { 61 | color: var(--secondary-3); 62 | } 63 | .secondary-4 { 64 | color: var(--secondary-4); 65 | } 66 | .secondary-5 { 67 | color: var(--secondary-5); 68 | } 69 | 70 | .danger-1 { 71 | color: var(--danger-1); 72 | } 73 | .danger-2 { 74 | color: var(--danger-2); 75 | } 76 | .danger-3 { 77 | color: var(--danger-3); 78 | } 79 | .danger-4 { 80 | color: var(--danger-4); 81 | } 82 | .danger-5 { 83 | color: var(--danger-5); 84 | } 85 | 86 | .neutral-1 { 87 | color: var(--neutral-1); 88 | } 89 | 90 | .neutral-2 { 91 | color: var(--neutral-2); 92 | } 93 | 94 | .neutral-3 { 95 | color: var(--neutral-3); 96 | } 97 | 98 | .neutral-4 { 99 | color: var(--neutral-4); 100 | } 101 | .neutral-5 { 102 | color: var(--neutral-5); 103 | } 104 | .neutral-6 { 105 | color: var(--neutral-6); 106 | } 107 | .neutral-7 { 108 | color: var(--neutral-7); 109 | } 110 | 111 | .link-1 { 112 | color: var(--link-1); 113 | } 114 | .link-2 { 115 | color: var(--link-2); 116 | } 117 | .link-3 { 118 | color: var(--link-3); 119 | } 120 | .link-4 { 121 | color: var(--link-4); 122 | } 123 | .link-5 { 124 | color: var(--link-5); 125 | } 126 | 127 | .cwhite { 128 | color: var(--white); 129 | } 130 | 131 | .cblack { 132 | color: var(--black); 133 | } 134 | 135 | .bg-white { 136 | background-color: var(--white); 137 | } 138 | 139 | .bg-gray-4 { 140 | background-color: var(--neutral-4); 141 | } 142 | 143 | .danger { 144 | color: var(--danger-1); 145 | } 146 | 147 | .link { 148 | color: var(--link-1); 149 | } 150 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export { default as SortableContainer } from './SortableContainer'; 4 | export { default as SortableElement } from './SortableElement'; 5 | export { default as SortableHandle } from './SortableHandle'; 6 | 7 | export { default as sortableContainer } from './SortableContainer'; 8 | export { default as sortableElement } from './SortableElement'; 9 | export { default as sortableHandle } from './SortableHandle'; 10 | 11 | export { arrayMove } from './utils.js'; 12 | 13 | export type Axis = 'x' | 'y' | 'xy'; 14 | 15 | export type Offset = number | string; 16 | 17 | export interface SortStart { 18 | node: Element; 19 | index: number; 20 | collection: Offset; 21 | isKeySorting: boolean; 22 | nodes: HTMLElement[]; 23 | helper: HTMLElement; 24 | } 25 | 26 | export interface SortOver { 27 | index: number; 28 | oldIndex: number; 29 | newIndex: number; 30 | collection: Offset; 31 | isKeySorting: boolean; 32 | nodes: HTMLElement[]; 33 | helper: HTMLElement; 34 | } 35 | 36 | export interface SortEnd { 37 | oldIndex: number; 38 | newIndex: number; 39 | collection: Offset; 40 | isKeySorting: boolean; 41 | nodes: HTMLElement[]; 42 | } 43 | 44 | export type SortEvent = React.MouseEvent | React.TouchEvent; 45 | 46 | export type SortEventWithTag = SortEvent & { 47 | target: { 48 | tagName: string; 49 | }; 50 | }; 51 | 52 | export type SortStartHandler = (sort: SortStart, event: SortEvent) => void; 53 | 54 | export type SortMoveHandler = (event: SortEvent) => void; 55 | 56 | export type SortEndHandler = (sort: SortEnd, event: SortEvent) => void; 57 | 58 | export type SortOverHandler = (sort: SortOver, event: SortEvent) => void; 59 | 60 | export type ContainerGetter = (element: React.ReactElement) => HTMLElement | Promise; 61 | 62 | export type HelperContainerGetter = () => HTMLElement; 63 | 64 | export interface Dimensions { 65 | width: number; 66 | height: number; 67 | } 68 | 69 | export interface SortableContainerProps { 70 | axis?: Axis; 71 | lockAxis?: Axis; 72 | helperClass?: string; 73 | transitionDuration?: number; 74 | keyboardSortingTransitionDuration?: number; 75 | keyCodes?: { 76 | lift?: number[]; 77 | drop?: number[]; 78 | cancel?: number[]; 79 | up?: number[]; 80 | down?: number[]; 81 | }; 82 | pressDelay?: number; 83 | pressThreshold?: number; 84 | distance?: number; 85 | shouldCancelStart?: (event: SortEvent | SortEventWithTag) => boolean; 86 | updateBeforeSortStart?: SortStartHandler; 87 | onSortStart?: SortStartHandler; 88 | onSortMove?: SortMoveHandler; 89 | onSortEnd?: SortEndHandler; 90 | onSortOver?: SortOverHandler; 91 | useDragHandle?: boolean; 92 | useWindowAsScrollContainer?: boolean; 93 | hideSortableGhost?: boolean; 94 | lockToContainerEdges?: boolean; 95 | lockOffset?: Offset | [Offset, Offset]; 96 | getContainer?: ContainerGetter; 97 | getHelperDimensions?: (sort: SortStart) => Dimensions; 98 | helperContainer?: HTMLElement | HelperContainerGetter; 99 | disableAutoscroll?: boolean; 100 | } 101 | 102 | export interface SortableElementProps { 103 | index: number; 104 | collection?: Offset; 105 | disabled?: boolean; 106 | } 107 | 108 | export interface Config { 109 | withRef: boolean; 110 | } 111 | 112 | export type WrappedComponentFactory

= (props: P) => JSX.Element; 113 | 114 | export type WrappedComponent

= React.ComponentClass

| WrappedComponentFactory

; 115 | -------------------------------------------------------------------------------- /website/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState } from 'react'; 2 | import { MenuOutlined } from '@ant-design/icons'; 3 | 4 | import { Space, Table, Tag } from 'antd'; 5 | import type { ColumnsType } from 'antd/es/table'; 6 | import { 7 | arrayMove, 8 | SortableContainer, 9 | SortableContainerProps, 10 | SortableElement, 11 | SortableHandle, 12 | SortEnd, 13 | } from 'reactjs-table-dnd'; 14 | import GitHubCorners from '@uiw/react-github-corners'; 15 | 16 | interface DataType { 17 | key: string; 18 | name: string; 19 | age: number; 20 | address: string; 21 | tags: string[]; 22 | index: number; 23 | } 24 | 25 | const columns: ColumnsType = [ 26 | { 27 | title: '', 28 | dataIndex: 'Sort', 29 | width: 30, 30 | className: 'drag-visible', 31 | render: () => , 32 | }, 33 | { 34 | title: 'Name', 35 | dataIndex: 'name', 36 | key: 'name', 37 | render: (text) => {text}, 38 | }, 39 | { 40 | title: 'Age', 41 | dataIndex: 'age', 42 | key: 'age', 43 | }, 44 | { 45 | title: 'Address', 46 | dataIndex: 'address', 47 | key: 'address', 48 | }, 49 | { 50 | title: 'Tags', 51 | key: 'tags', 52 | dataIndex: 'tags', 53 | render: (_, { tags }) => ( 54 | <> 55 | {tags.map((tag) => { 56 | let color = tag.length > 5 ? 'geekblue' : 'green'; 57 | if (tag === 'loser') { 58 | color = 'volcano'; 59 | } 60 | return ( 61 | 62 | {tag.toUpperCase()} 63 | 64 | ); 65 | })} 66 | 67 | ), 68 | }, 69 | { 70 | title: 'Action', 71 | key: 'action', 72 | render: (_, record) => ( 73 | 74 | Invite {record.name} 75 | Delete 76 | 77 | ), 78 | }, 79 | ]; 80 | 81 | const DragHandle = SortableHandle(() => ); 82 | 83 | const SortableItem = SortableElement((props: React.HTMLAttributes) => ( 84 | 85 | )); 86 | const SortableBody = SortableContainer((props: React.HTMLAttributes) => ( 87 | 88 | )); 89 | 90 | const SortableItem1 = SortableElement(({ value }: any) =>

  • {value}
  • ); 91 | 92 | const SortableList = SortableContainer(({ items }: any) => { 93 | return ( 94 |
      95 | {items.map((value: any, index: any) => ( 96 | //@ts-ignore 97 | 98 | ))} 99 |
    100 | ); 101 | }); 102 | 103 | class SortableComponent extends Component { 104 | state = { 105 | items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'], 106 | }; 107 | onSortEnd = ({ oldIndex, newIndex }: any) => { 108 | this.setState(({ items }: any) => ({ 109 | items: arrayMove(items, oldIndex, newIndex), 110 | })); 111 | }; 112 | render() { 113 | //@ts-ignore 114 | return ; 115 | } 116 | } 117 | 118 | const App = () => { 119 | const [data, setData] = useState([ 120 | { 121 | key: '1', 122 | name: 'John Brown', 123 | age: 32, 124 | address: 'New York No. 1 Lake Park', 125 | tags: ['nice', 'developer'], 126 | index: 1, 127 | }, 128 | { 129 | key: '2', 130 | name: 'Jim Green', 131 | age: 42, 132 | address: 'London No. 1 Lake Park', 133 | tags: ['loser'], 134 | index: 2, 135 | }, 136 | { 137 | key: '3', 138 | name: 'Joe Black', 139 | age: 32, 140 | address: 'Sydney No. 1 Lake Park', 141 | tags: ['cool', 'teacher'], 142 | index: 3, 143 | }, 144 | ]); 145 | 146 | const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => { 147 | if (oldIndex !== newIndex) { 148 | const newData = arrayMove(data, oldIndex, newIndex); 149 | 150 | setData(newData); 151 | } 152 | }; 153 | 154 | const DraggableContainer = (props: SortableContainerProps) => ( 155 | 162 | ); 163 | const DraggableBodyRow: React.FC = ({ ...restProps }) => { 164 | const index = data?.findIndex((x) => `${x.index}` === restProps['data-row-key']); 165 | return ; 166 | }; 167 | 168 | return ( 169 |
    170 |

    Ant Design Table

    171 | 172 | 182 | 183 | 184 | 185 |

    List Item

    186 | 187 | 188 | 189 | ); 190 | }; 191 | 192 | export default App; 193 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | import invariant from 'invariant'; 3 | 4 | export function arrayMove(array, from, to) { 5 | // Will be deprecated soon. Consumers should install 'array-move' instead 6 | // https://www.npmjs.com/package/array-move 7 | 8 | if (process.env.NODE_ENV !== 'production') { 9 | if (typeof console !== 'undefined') { 10 | // eslint-disable-next-line no-console 11 | console.warn( 12 | ' Please install the `array-move` package locally instead. https://www.npmjs.com/package/array-move', 13 | ); 14 | } 15 | } 16 | 17 | array = array.slice(); 18 | array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]); 19 | 20 | return array; 21 | } 22 | 23 | export function omit(obj, keysToOmit) { 24 | return Object.keys(obj).reduce((acc, key) => { 25 | if (keysToOmit.indexOf(key) === -1) { 26 | acc[key] = obj[key]; 27 | } 28 | 29 | return acc; 30 | }, {}); 31 | } 32 | 33 | export const events = { 34 | end: ['touchend', 'touchcancel', 'mouseup'], 35 | move: ['touchmove', 'mousemove'], 36 | start: ['touchstart', 'mousedown'], 37 | }; 38 | 39 | export const vendorPrefix = (function() { 40 | if (typeof window === 'undefined' || typeof document === 'undefined') { 41 | // Server environment 42 | return ''; 43 | } 44 | 45 | // fix for: https://bugzilla.mozilla.org/show_bug.cgi?id=548397 46 | // window.getComputedStyle() returns null inside an iframe with display: none 47 | // in this case return an array with a fake mozilla style in it. 48 | const styles = window.getComputedStyle(document.documentElement, '') || ['-moz-hidden-iframe']; 49 | const pre = (Array.prototype.slice 50 | .call(styles) 51 | .join('') 52 | .match(/-(moz|webkit|ms)-/) || 53 | //@ts-ignore 54 | (styles.OLink === '' && ['', 'o']))[1]; 55 | 56 | switch (pre) { 57 | case 'ms': 58 | return 'ms'; 59 | default: 60 | return pre && pre.length ? pre[0].toUpperCase() + pre.substr(1) : ''; 61 | } 62 | })(); 63 | 64 | export function setInlineStyles(node, styles) { 65 | Object.keys(styles).forEach(key => { 66 | node.style[key] = styles[key]; 67 | }); 68 | } 69 | 70 | export function setTranslate3d(node, translate) { 71 | node.style[`${vendorPrefix}Transform`] = translate == null ? '' : `translate3d(${translate.x}px,${translate.y}px,0)`; 72 | } 73 | 74 | export function setTransitionDuration(node, duration) { 75 | node.style[`${vendorPrefix}TransitionDuration`] = duration == null ? '' : `${duration}ms`; 76 | } 77 | 78 | export function closest(el, fn) { 79 | while (el) { 80 | if (fn(el)) { 81 | return el; 82 | } 83 | 84 | el = el.parentNode; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | export function limit(min, max, value) { 91 | return Math.max(min, Math.min(value, max)); 92 | } 93 | 94 | function getPixelValue(stringValue) { 95 | if (stringValue.substr(-2) === 'px') { 96 | return parseFloat(stringValue); 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | export function getElementMargin(element) { 103 | const style = window.getComputedStyle(element); 104 | 105 | return { 106 | bottom: getPixelValue(style.marginBottom), 107 | left: getPixelValue(style.marginLeft), 108 | right: getPixelValue(style.marginRight), 109 | top: getPixelValue(style.marginTop), 110 | }; 111 | } 112 | 113 | export function provideDisplayName(prefix, Component) { 114 | const componentName = Component.displayName || Component.name; 115 | 116 | return componentName ? `${prefix}(${componentName})` : prefix; 117 | } 118 | 119 | export function getScrollAdjustedBoundingClientRect(node, scrollDelta) { 120 | const boundingClientRect = node.getBoundingClientRect(); 121 | 122 | return { 123 | top: boundingClientRect.top + scrollDelta.top, 124 | left: boundingClientRect.left + scrollDelta.left, 125 | }; 126 | } 127 | 128 | export function getPosition(event) { 129 | if (event.touches && event.touches.length) { 130 | return { 131 | x: event.touches[0].pageX, 132 | y: event.touches[0].pageY, 133 | }; 134 | } else if (event.changedTouches && event.changedTouches.length) { 135 | return { 136 | x: event.changedTouches[0].pageX, 137 | y: event.changedTouches[0].pageY, 138 | }; 139 | } else { 140 | return { 141 | x: event.pageX, 142 | y: event.pageY, 143 | }; 144 | } 145 | } 146 | 147 | export function isTouchEvent(event) { 148 | return (event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length); 149 | } 150 | 151 | export function getEdgeOffset(node, parent, offset = { left: 0, top: 0 }) { 152 | if (!node) { 153 | return undefined; 154 | } 155 | 156 | // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested 157 | const nodeOffset = { 158 | left: offset.left + node.offsetLeft, 159 | top: offset.top + node.offsetTop, 160 | }; 161 | 162 | if (node.parentNode === parent) { 163 | return nodeOffset; 164 | } 165 | 166 | return getEdgeOffset(node.parentNode, parent, nodeOffset); 167 | } 168 | 169 | export function getTargetIndex(newIndex, prevIndex, oldIndex) { 170 | if (newIndex < oldIndex && newIndex > prevIndex) { 171 | return newIndex - 1; 172 | } else if (newIndex > oldIndex && newIndex < prevIndex) { 173 | return newIndex + 1; 174 | } else { 175 | return newIndex; 176 | } 177 | } 178 | 179 | export function getLockPixelOffset({ lockOffset, width, height }) { 180 | let offsetX = lockOffset; 181 | let offsetY = lockOffset; 182 | let unit = 'px'; 183 | 184 | if (typeof lockOffset === 'string') { 185 | const match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset); 186 | 187 | invariant( 188 | match !== null, 189 | 'lockOffset value should be a number or a string of a ' + 'number followed by "px" or "%". Given %s', 190 | lockOffset, 191 | ); 192 | 193 | offsetX = parseFloat(lockOffset); 194 | offsetY = parseFloat(lockOffset); 195 | unit = match[1]; 196 | } 197 | 198 | invariant(isFinite(offsetX) && isFinite(offsetY), 'lockOffset value should be a finite. Given %s', lockOffset); 199 | 200 | if (unit === '%') { 201 | offsetX = (offsetX * width) / 100; 202 | offsetY = (offsetY * height) / 100; 203 | } 204 | 205 | return { 206 | x: offsetX, 207 | y: offsetY, 208 | }; 209 | } 210 | 211 | export function getLockPixelOffsets({ height, width, lockOffset }) { 212 | const offsets = Array.isArray(lockOffset) ? lockOffset : [lockOffset, lockOffset]; 213 | 214 | invariant( 215 | offsets.length === 2, 216 | 'lockOffset prop of SortableContainer should be a single ' + 'value or an array of exactly two values. Given %s', 217 | lockOffset, 218 | ); 219 | 220 | const [minLockOffset, maxLockOffset] = offsets; 221 | 222 | return [ 223 | getLockPixelOffset({ height, lockOffset: minLockOffset, width }), 224 | getLockPixelOffset({ height, lockOffset: maxLockOffset, width }), 225 | ]; 226 | } 227 | 228 | function isScrollable(el) { 229 | const computedStyle = window.getComputedStyle(el); 230 | const overflowRegex = /(auto|scroll)/; 231 | const properties = ['overflow', 'overflowX', 'overflowY']; 232 | 233 | return properties.find(property => overflowRegex.test(computedStyle[property])); 234 | } 235 | 236 | export function getScrollingParent(el) { 237 | if (!(el instanceof HTMLElement)) { 238 | return null; 239 | } else if (isScrollable(el)) { 240 | return el; 241 | } else { 242 | return getScrollingParent(el.parentNode); 243 | } 244 | } 245 | 246 | export function getContainerGridGap(element) { 247 | const style = window.getComputedStyle(element); 248 | 249 | if (style.display === 'grid') { 250 | return { 251 | x: getPixelValue(style.gridColumnGap), 252 | y: getPixelValue(style.gridRowGap), 253 | }; 254 | } 255 | 256 | return { x: 0, y: 0 }; 257 | } 258 | 259 | export const KEYCODE = { 260 | TAB: 9, 261 | ESC: 27, 262 | SPACE: 32, 263 | LEFT: 37, 264 | UP: 38, 265 | RIGHT: 39, 266 | DOWN: 40, 267 | }; 268 | 269 | export const NodeType = { 270 | Anchor: 'A', 271 | Button: 'BUTTON', 272 | Canvas: 'CANVAS', 273 | Input: 'INPUT', 274 | Option: 'OPTION', 275 | Textarea: 'TEXTAREA', 276 | Select: 'SELECT', 277 | }; 278 | 279 | export function cloneNode(node) { 280 | const selector = 'input, textarea, select, canvas, [contenteditable]'; 281 | const fields = node.querySelectorAll(selector); 282 | const clonedNode = node.cloneNode(true); 283 | const clonedFields = [...clonedNode.querySelectorAll(selector)]; 284 | 285 | clonedFields.forEach((field, i) => { 286 | if (field.type !== 'file') { 287 | field.value = fields[i]?.value ?? ''; 288 | } 289 | 290 | // Fixes an issue with original radio buttons losing their value once the 291 | // clone is inserted in the DOM, as radio button `name` attributes must be unique 292 | if (field.type === 'radio' && field.name) { 293 | field.name = `__sortableClone__${field.name}`; 294 | } 295 | 296 | if (field.tagName === NodeType.Canvas && fields[i].width > 0 && fields[i].height > 0) { 297 | const destCtx = field.getContext('2d'); 298 | destCtx.drawImage(fields[i], 0, 0); 299 | } 300 | }); 301 | 302 | return clonedNode; 303 | } 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | 3 | logo 4 |

    5 | 6 |

    7 | A Sort component for Reactjs and antd ✨. 8 |

    9 | 10 |

    11 | NPM Version 12 | NPM Downloads 13 | Minizip 14 | Contributors 15 | License 16 |

    17 | 18 | ## Live demo 19 | 20 | [Live Demo](https://reactjs-table-dnd.vercel.app/) 21 | 22 | ## Installation 23 | 24 | [![NPM](https://nodei.co/npm/reactjs-table-dnd.png?compact=true)](https://nodei.co/npm/reactjs-table-dnd/) 25 | 26 | ## Example 27 | 28 | [Example](./website/) 29 | 30 | ## Install 31 | 32 | ``` 33 | npm i reactjs-table-dnd@latest 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Basic Example 39 | 40 | ```js 41 | import React, { Component } from 'react'; 42 | import { render } from 'react-dom'; 43 | import { SortableContainer, SortableElement } from 'reactjs-table-dnd'; 44 | import arrayMove from 'array-move'; 45 | 46 | const SortableItem = SortableElement(({ value }) =>
  • {value}
  • ); 47 | 48 | const SortableList = SortableContainer(({ items }) => { 49 | return ( 50 |
      51 | {items.map((value, index) => ( 52 | 53 | ))} 54 |
    55 | ); 56 | }); 57 | 58 | class SortableComponent extends Component { 59 | state = { 60 | items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'], 61 | }; 62 | onSortEnd = ({ oldIndex, newIndex }) => { 63 | this.setState(({ items }) => ({ 64 | items: arrayMove(items, oldIndex, newIndex), 65 | })); 66 | }; 67 | render() { 68 | return ; 69 | } 70 | } 71 | 72 | render(, document.getElementById('root')); 73 | ``` 74 | 75 | That's it! React Sortable does not come with any styles by default, since it's meant to enhance your existing components. 76 | 77 | ## Why should I use this? 78 | 79 | There are already a number of great Drag & Drop libraries out there (for instance, [react-dnd](https://github.com/gaearon/react-dnd/) is fantastic). If those libraries fit your needs, you should definitely give them a try first. However, most of those libraries rely on the HTML5 Drag & Drop API, which has some severe limitations. For instance, things rapidly become tricky if you need to support touch devices, if you need to lock dragging to an axis, or want to animate the nodes as they're being sorted. React Sortable HOC aims to provide a simple set of higher-order components to fill those gaps. If you're looking for a dead-simple, mobile-friendly way to add sortable functionality to your lists, then you're in the right place. 80 | 81 | ### Prop Types 82 | 83 | #### SortableContainer HOC 84 | 85 | | Property | Type | Default | Description | 86 | | :-------------------------------- | :-------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 87 | | axis | String | `y` | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy` | 88 | | lockAxis | String | | If you'd like, you can lock movement to an axis while sorting. This is not something that is possible with HTML5 Drag & Drop. Possible values: `x` or `y`. | 89 | | helperClass | String | | You can provide a class you'd like to add to the sortable helper to add some styles to it | 90 | | transitionDuration | Number | `300` | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions | 91 | | keyboardSortingTransitionDuration | Number | `transitionDuration` | The duration of the transition when the helper is shifted during keyboard sorting. Set this to `0` if you'd like to disable transitions for the keyboard sorting helper. Defaults to the value set for `transitionDuration` if undefined | 92 | | keyCodes | Array | `{`
      `lift: [32],`
      `drop: [32],`
      `cancel: [27],`
      `up: [38, 37],`
      `down: [40, 39]`
    `}` | An object containing an array of keycodes for each keyboard-accessible action. | 93 | | pressDelay | Number | `0` | If you'd like elements to only become sortable after being pressed for a certain time, change this property. A good sensible default value for mobile is `200`. Cannot be used in conjunction with the `distance` prop. | 94 | | pressThreshold | Number | `5` | Number of pixels of movement to tolerate before ignoring a press event. | 95 | | distance | Number | `0` | If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the `pressDelay` prop. | 96 | | shouldCancelStart | Function | [Function](https://github.com/clauderic/reactjs-table-dnd/blob/master/src/SortableContainer/index.js#L48) | This function is invoked before sorting begins, and can be used to programatically cancel sorting before it begins. By default, it will cancel sorting if the event target is either an `input`, `textarea`, `select`, `option`, or `button`. | 97 | | updateBeforeSortStart | Function | | This function is invoked before sorting begins. It can return a promise, allowing you to run asynchronous updates (such as `setState`) before sorting begins. `function({node, index, collection, isKeySorting}, event)` | 98 | | onSortStart | Function | | Callback that is invoked when sorting begins. `function({node, index, collection, isKeySorting}, event)` | 99 | | onSortMove | Function | | Callback that is invoked during sorting as the cursor moves. `function(event)` | 100 | | onSortOver | Function | | Callback that is invoked when moving over an item. `function({index, oldIndex, newIndex, collection, isKeySorting}, e)` | 101 | | onSortEnd | Function | | Callback that is invoked when sorting ends. `function({oldIndex, newIndex, collection, isKeySorting}, e)` | 102 | | useDragHandle | Boolean | `false` | If you're using the `SortableHandle` HOC, set this to `true` | 103 | | useWindowAsScrollContainer | Boolean | `false` | If you want, you can set the `window` as the scrolling container | 104 | | hideSortableGhost | Boolean | `true` | Whether to auto-hide the ghost element. By default, as a convenience, React Sortable List will automatically hide the element that is currently being sorted. Set this to false if you would like to apply your own styling. | 105 | | lockToContainerEdges | Boolean | `false` | You can lock movement of the sortable element to it's parent `SortableContainer` | 106 | | lockOffset | `OffsetValue`\* | [`OffsetValue`\*, `OffsetValue`\*] | `"50%"` | When`lockToContainerEdges`is set to`true`, this controls the offset distance between the sortable helper and the top/bottom edges of it's parent`SortableContainer`. Percentage values are relative to the height of the item currently being sorted. If you wish to specify different behaviours for locking to the _top_ of the container vs the _bottom_, you may also pass in an`array`(For example:`["0%", "100%"]`). | 107 | | getContainer | Function | | Optional function to return the scrollable container element. This property defaults to the `SortableContainer` element itself or (if `useWindowAsScrollContainer` is true) the window. Use this function to specify a custom container object (eg this is useful for integrating with certain 3rd party components such as `FlexTable`). This function is passed a single parameter (the `wrappedInstance` React element) and it is expected to return a DOM element. | 108 | | getHelperDimensions | Function | [Function](https://github.com/clauderic/reactjs-table-dnd/blob/master/src/SortableContainer/index.js#L74-L77) | Optional `function({node, index, collection})` that should return the computed dimensions of the SortableHelper. See [default implementation](https://github.com/clauderic/reactjs-table-dnd/blob/master/src/SortableContainer/defaultGetHelperDimensions.js) for more details | 109 | | helperContainer | HTMLElement | Function | `document.body` | By default, the cloned sortable helper is appended to the document body. Use this prop to specify a different container for the sortable clone to be appended to. Accepts an `HTMLElement` or a function returning an `HTMLElement` that will be invoked before right before sorting begins | 110 | | disableAutoscroll | Boolean | `false` | Disables autoscrolling while dragging | 111 | 112 | \* `OffsetValue` can either be a finite `Number` or a `String` made up of a number and a unit (`px` or `%`). 113 | Examples: `10` (which is the same as `"10px"`), `"50%"` 114 | 115 | #### SortableElement HOC 116 | 117 | | Property | Type | Default | Required? | Description | 118 | | :--------- | :--------------- | :------ | :-------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 119 | | index | Number | | ✓ | This is the element's sortableIndex within it's collection. This prop is required. | 120 | | collection | Number or String | `0` | | The collection the element is part of. This is useful if you have multiple groups of sortable elements within the same `SortableContainer`. [Example](http://clauderic.github.io/reactjs-table-dnd/#/basic-configuration/multiple-lists) | 121 | | disabled | Boolean | `false` | | Whether the element should be sortable or not | 122 | 123 | ### Accessibility 124 | 125 | React Sortable HOC supports keyboard sorting out of the box. To enable it, make sure your `SortableElement` or `SortableHandle` is focusable. This can be done by setting `tabIndex={0}` on the outermost HTML node rendered by the component you're enhancing with `SortableElement` or `SortableHandle`. 126 | 127 | Once an item is focused/tabbed to, pressing `SPACE` picks it up, `ArrowUp` or `ArrowLeft` moves it one place backward in the list, `ArrowDown` or `ArrowRight` moves items one place forward in the list, pressing `SPACE` again drops the item in its new position. Pressing `ESC` before the item is dropped will cancel the sort operations. 128 | 129 | ### Grid support 130 | 131 | Need to sort items in a grid? We've got you covered! Just set the `axis` prop to `xy`. Grid support is currently limited to a setup where all the cells in the grid have the same width and height, though we're working hard to get variable width support in the near future. 132 | 133 | ### Item disappearing when sorting / CSS issues 134 | 135 | Upon sorting, `reactjs-table-dnd` creates a clone of the element you are sorting (the _sortable-helper_) and appends it to the end of the `` tag. The original element will still be in-place to preserve its position in the DOM until the end of the drag (with inline-styling to make it invisible). If the _sortable-helper_ gets messed up from a CSS standpoint, consider that maybe your selectors to the draggable item are dependent on a parent element which isn't present anymore (again, since the _sortable-helper_ is at the end of the ``). This can also be a `z-index` issue, for example, when using `reactjs-table-dnd` within a Bootstrap modal, you'll need to increase the `z-index` of the SortableHelper so it is displayed on top of the modal 136 | 137 | ### Click events being swallowed 138 | 139 | By default, `reactjs-table-dnd` is triggered immediately on `mousedown`. If you'd like to prevent this behaviour, there are a number of strategies readily available. You can use the `distance` prop to set a minimum distance (in pixels) to be dragged before sorting is enabled. You can also use the `pressDelay` prop to add a delay before sorting is enabled. Alternatively 140 | 141 | ### Wrapper props not passed down to wrapped Component 142 | 143 | All props for `SortableContainer` and `SortableElement` listed above are intentionally consumed by the wrapper component and are **not** passed down to the wrapped component. To make them available pass down the desired prop again with a different name. E.g.: 144 | 145 | ```js 146 | const SortableItem = SortableElement(({ value, sortIndex }) => ( 147 |
  • 148 | {value} - #{sortIndex} 149 |
  • 150 | )); 151 | 152 | const SortableList = SortableContainer(({ items }) => { 153 | return ( 154 |
      155 | {items.map((value, index) => ( 156 | 157 | ))} 158 |
    159 | ); 160 | }); 161 | ``` 162 | 163 | ### About 164 | 165 | Buy Me A Coffee 166 | 167 | Gia Hung – [hung.hg](https://hung.thedev.id) 168 | -------------------------------------------------------------------------------- /src/SortableContainer/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import * as React from 'react'; 3 | import { findDOMNode } from 'react-dom'; 4 | import invariant from 'invariant'; 5 | 6 | import Manager from '../Manager'; 7 | import { isSortableHandle } from '../SortableHandle'; 8 | 9 | import { 10 | cloneNode, 11 | closest, 12 | events, 13 | getScrollingParent, 14 | getContainerGridGap, 15 | getEdgeOffset, 16 | getElementMargin, 17 | getLockPixelOffsets, 18 | getPosition, 19 | isTouchEvent, 20 | limit, 21 | NodeType, 22 | omit, 23 | provideDisplayName, 24 | setInlineStyles, 25 | setTransitionDuration, 26 | setTranslate3d, 27 | getTargetIndex, 28 | getScrollAdjustedBoundingClientRect, 29 | } from '../utils.js'; 30 | 31 | import AutoScroller from '../AutoScroller'; 32 | import { defaultProps, omittedProps, propTypes, validateProps, defaultKeyCodes } from './props'; 33 | 34 | export const SortableContext = React.createContext({ 35 | manager: {}, 36 | }); 37 | 38 | export default function sortableContainer(WrappedComponent, config = { withRef: false }) { 39 | return class WithSortableContainer extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | const manager = new Manager(); 43 | 44 | validateProps(props); 45 | 46 | this.manager = manager; 47 | this.wrappedInstance = React.createRef(); 48 | this.sortableContextValue = { manager }; 49 | this.events = { 50 | end: this.handleEnd, 51 | move: this.handleMove, 52 | start: this.handleStart, 53 | }; 54 | } 55 | 56 | state = {}; 57 | 58 | static displayName = provideDisplayName('sortableList', WrappedComponent); 59 | static defaultProps = defaultProps; 60 | static propTypes = propTypes; 61 | 62 | componentDidMount() { 63 | const { useWindowAsScrollContainer } = this.props; 64 | const container = this.getContainer(); 65 | 66 | Promise.resolve(container).then(containerNode => { 67 | this.container = containerNode; 68 | this.document = this.container.ownerDocument || document; 69 | 70 | /* 71 | * Set our own default rather than using defaultProps because Jest 72 | * snapshots will serialize window, causing a RangeError 73 | */ 74 | const contentWindow = this.props.contentWindow || this.document.defaultView || window; 75 | 76 | this.contentWindow = typeof contentWindow === 'function' ? contentWindow() : contentWindow; 77 | 78 | this.scrollContainer = useWindowAsScrollContainer 79 | ? this.document.scrollingElement || this.document.documentElement 80 | : getScrollingParent(this.container) || this.container; 81 | 82 | this.autoScroller = new AutoScroller(this.scrollContainer, this.onAutoScroll); 83 | 84 | Object.keys(this.events).forEach(key => 85 | events[key].forEach(eventName => this.container.addEventListener(eventName, this.events[key], false)), 86 | ); 87 | 88 | this.container.addEventListener('keydown', this.handleKeyDown); 89 | }); 90 | } 91 | 92 | componentWillUnmount() { 93 | if (this.helper && this.helper.parentNode) { 94 | this.helper.parentNode.removeChild(this.helper); 95 | } 96 | if (!this.container) { 97 | return; 98 | } 99 | 100 | Object.keys(this.events).forEach(key => 101 | events[key].forEach(eventName => this.container.removeEventListener(eventName, this.events[key])), 102 | ); 103 | this.container.removeEventListener('keydown', this.handleKeyDown); 104 | } 105 | 106 | handleStart = event => { 107 | const { distance, shouldCancelStart } = this.props; 108 | 109 | if (event.button === 2 || shouldCancelStart(event)) { 110 | return; 111 | } 112 | 113 | this.touched = true; 114 | this.position = getPosition(event); 115 | 116 | const node = closest(event.target, el => el.sortableInfo != null); 117 | 118 | if (node && node.sortableInfo && this.nodeIsChild(node) && !this.state.sorting) { 119 | const { useDragHandle } = this.props; 120 | const { index, collection, disabled } = node.sortableInfo; 121 | 122 | if (disabled) { 123 | return; 124 | } 125 | 126 | if (useDragHandle && !closest(event.target, isSortableHandle)) { 127 | return; 128 | } 129 | 130 | this.manager.active = { collection, index }; 131 | 132 | /* 133 | * Fixes a bug in Firefox where the :active state of anchor tags 134 | * prevent subsequent 'mousemove' events from being fired 135 | */ 136 | if (!isTouchEvent(event) && event.target.tagName === NodeType.Anchor) { 137 | event.preventDefault(); 138 | } 139 | 140 | if (!distance) { 141 | if (this.props.pressDelay === 0) { 142 | this.handlePress(event); 143 | } else { 144 | this.pressTimer = setTimeout(() => this.handlePress(event), this.props.pressDelay); 145 | } 146 | } 147 | } 148 | }; 149 | 150 | nodeIsChild = node => { 151 | return node.sortableInfo.manager === this.manager; 152 | }; 153 | 154 | handleMove = event => { 155 | const { distance, pressThreshold } = this.props; 156 | 157 | if (!this.state.sorting && this.touched && !this._awaitingUpdateBeforeSortStart) { 158 | const position = getPosition(event); 159 | const delta = { 160 | x: this.position.x - position.x, 161 | y: this.position.y - position.y, 162 | }; 163 | const combinedDelta = Math.abs(delta.x) + Math.abs(delta.y); 164 | 165 | this.delta = delta; 166 | 167 | if (!distance && (!pressThreshold || combinedDelta >= pressThreshold)) { 168 | clearTimeout(this.cancelTimer); 169 | this.cancelTimer = setTimeout(this.cancel, 0); 170 | } else if (distance && combinedDelta >= distance && this.manager.isActive()) { 171 | this.handlePress(event); 172 | } 173 | } 174 | }; 175 | 176 | handleEnd = () => { 177 | this.touched = false; 178 | this.cancel(); 179 | }; 180 | 181 | cancel = () => { 182 | const { distance } = this.props; 183 | const { sorting } = this.state; 184 | 185 | if (!sorting) { 186 | if (!distance) { 187 | clearTimeout(this.pressTimer); 188 | } 189 | this.manager.active = null; 190 | } 191 | }; 192 | 193 | handlePress = async event => { 194 | const active = this.manager.getActive(); 195 | 196 | if (active) { 197 | const { 198 | axis, 199 | getHelperDimensions, 200 | helperClass, 201 | hideSortableGhost, 202 | updateBeforeSortStart, 203 | onSortStart, 204 | useWindowAsScrollContainer, 205 | } = this.props; 206 | const { node, collection } = active; 207 | const { isKeySorting } = this.manager; 208 | 209 | if (typeof updateBeforeSortStart === 'function') { 210 | this._awaitingUpdateBeforeSortStart = true; 211 | 212 | try { 213 | const { index } = node.sortableInfo; 214 | await updateBeforeSortStart({ collection, index, node, isKeySorting }, event); 215 | } finally { 216 | this._awaitingUpdateBeforeSortStart = false; 217 | } 218 | } 219 | 220 | // Need to get the latest value for `index` in case it changes during `updateBeforeSortStart` 221 | const { index } = node.sortableInfo; 222 | const margin = getElementMargin(node); 223 | const gridGap = getContainerGridGap(this.container); 224 | const containerBoundingRect = this.scrollContainer.getBoundingClientRect(); 225 | const dimensions = getHelperDimensions({ index, node, collection }); 226 | 227 | this.node = node; 228 | this.margin = margin; 229 | this.gridGap = gridGap; 230 | this.width = dimensions.width; 231 | this.height = dimensions.height; 232 | this.marginOffset = { 233 | x: this.margin.left + this.margin.right + this.gridGap.x, 234 | y: Math.max(this.margin.top, this.margin.bottom, this.gridGap.y), 235 | }; 236 | this.boundingClientRect = node.getBoundingClientRect(); 237 | this.containerBoundingRect = containerBoundingRect; 238 | this.index = index; 239 | this.newIndex = index; 240 | 241 | this.axis = { 242 | x: axis.indexOf('x') >= 0, 243 | y: axis.indexOf('y') >= 0, 244 | }; 245 | this.offsetEdge = getEdgeOffset(node, this.container); 246 | 247 | if (isKeySorting) { 248 | this.initialOffset = getPosition({ 249 | ...event, 250 | pageX: this.boundingClientRect.left, 251 | pageY: this.boundingClientRect.top, 252 | }); 253 | } else { 254 | this.initialOffset = getPosition(event); 255 | } 256 | 257 | this.initialScroll = { 258 | left: this.scrollContainer.scrollLeft, 259 | top: this.scrollContainer.scrollTop, 260 | }; 261 | 262 | this.initialWindowScroll = { 263 | left: window.pageXOffset, 264 | top: window.pageYOffset, 265 | }; 266 | 267 | this.helper = this.helperContainer.appendChild(cloneNode(node)); 268 | 269 | setInlineStyles(this.helper, { 270 | boxSizing: 'border-box', 271 | height: `${this.height}px`, 272 | left: `${this.boundingClientRect.left - margin.left}px`, 273 | pointerEvents: 'none', 274 | position: 'fixed', 275 | top: `${this.boundingClientRect.top - margin.top}px`, 276 | width: `${this.width}px`, 277 | }); 278 | 279 | if (isKeySorting) { 280 | this.helper.focus(); 281 | } 282 | 283 | if (hideSortableGhost) { 284 | this.sortableGhost = node; 285 | 286 | setInlineStyles(node, { 287 | opacity: 0, 288 | visibility: 'hidden', 289 | }); 290 | } 291 | 292 | this.minTranslate = {}; 293 | this.maxTranslate = {}; 294 | 295 | if (isKeySorting) { 296 | const { 297 | top: containerTop, 298 | left: containerLeft, 299 | width: containerWidth, 300 | height: containerHeight, 301 | } = useWindowAsScrollContainer 302 | ? { 303 | top: 0, 304 | left: 0, 305 | width: this.contentWindow.innerWidth, 306 | height: this.contentWindow.innerHeight, 307 | } 308 | : this.containerBoundingRect; 309 | const containerBottom = containerTop + containerHeight; 310 | const containerRight = containerLeft + containerWidth; 311 | 312 | if (this.axis.x) { 313 | this.minTranslate.x = containerLeft - this.boundingClientRect.left; 314 | this.maxTranslate.x = containerRight - (this.boundingClientRect.left + this.width); 315 | } 316 | 317 | if (this.axis.y) { 318 | this.minTranslate.y = containerTop - this.boundingClientRect.top; 319 | this.maxTranslate.y = containerBottom - (this.boundingClientRect.top + this.height); 320 | } 321 | } else { 322 | if (this.axis.x) { 323 | this.minTranslate.x = 324 | (useWindowAsScrollContainer ? 0 : containerBoundingRect.left) - 325 | this.boundingClientRect.left - 326 | this.width / 2; 327 | this.maxTranslate.x = 328 | (useWindowAsScrollContainer 329 | ? this.contentWindow.innerWidth 330 | : containerBoundingRect.left + containerBoundingRect.width) - 331 | this.boundingClientRect.left - 332 | this.width / 2; 333 | } 334 | 335 | if (this.axis.y) { 336 | this.minTranslate.y = 337 | (useWindowAsScrollContainer ? 0 : containerBoundingRect.top) - 338 | this.boundingClientRect.top - 339 | this.height / 2; 340 | this.maxTranslate.y = 341 | (useWindowAsScrollContainer 342 | ? this.contentWindow.innerHeight 343 | : containerBoundingRect.top + containerBoundingRect.height) - 344 | this.boundingClientRect.top - 345 | this.height / 2; 346 | } 347 | } 348 | 349 | if (helperClass) { 350 | helperClass.split(' ').forEach(className => this.helper.classList.add(className)); 351 | } 352 | 353 | this.listenerNode = event.touches ? event.target : this.contentWindow; 354 | 355 | if (isKeySorting) { 356 | this.listenerNode.addEventListener('wheel', this.handleKeyEnd, true); 357 | this.listenerNode.addEventListener('mousedown', this.handleKeyEnd, true); 358 | this.listenerNode.addEventListener('keydown', this.handleKeyDown); 359 | } else { 360 | events.move.forEach(eventName => this.listenerNode.addEventListener(eventName, this.handleSortMove, false)); 361 | events.end.forEach(eventName => this.listenerNode.addEventListener(eventName, this.handleSortEnd, false)); 362 | } 363 | 364 | this.setState({ 365 | sorting: true, 366 | sortingIndex: index, 367 | }); 368 | 369 | if (onSortStart) { 370 | onSortStart( 371 | { 372 | node, 373 | index, 374 | collection, 375 | isKeySorting, 376 | nodes: this.manager.getOrderedRefs(), 377 | helper: this.helper, 378 | }, 379 | event, 380 | ); 381 | } 382 | 383 | if (isKeySorting) { 384 | // Readjust positioning in case re-rendering occurs onSortStart 385 | this.keyMove(0); 386 | } 387 | } 388 | }; 389 | 390 | handleSortMove = event => { 391 | const { onSortMove } = this.props; 392 | 393 | // Prevent scrolling on mobile 394 | if (typeof event.preventDefault === 'function' && event.cancelable) { 395 | event.preventDefault(); 396 | } 397 | 398 | this.updateHelperPosition(event); 399 | this.animateNodes(); 400 | this.autoscroll(); 401 | 402 | if (onSortMove) { 403 | onSortMove(event); 404 | } 405 | }; 406 | 407 | handleSortEnd = event => { 408 | const { hideSortableGhost, onSortEnd } = this.props; 409 | const { 410 | active: { collection }, 411 | isKeySorting, 412 | } = this.manager; 413 | const nodes = this.manager.getOrderedRefs(); 414 | 415 | // Remove the event listeners if the node is still in the DOM 416 | if (this.listenerNode) { 417 | if (isKeySorting) { 418 | this.listenerNode.removeEventListener('wheel', this.handleKeyEnd, true); 419 | this.listenerNode.removeEventListener('mousedown', this.handleKeyEnd, true); 420 | this.listenerNode.removeEventListener('keydown', this.handleKeyDown); 421 | } else { 422 | events.move.forEach(eventName => this.listenerNode.removeEventListener(eventName, this.handleSortMove)); 423 | events.end.forEach(eventName => this.listenerNode.removeEventListener(eventName, this.handleSortEnd)); 424 | } 425 | } 426 | 427 | // Remove the helper from the DOM 428 | this.helper.parentNode.removeChild(this.helper); 429 | 430 | if (hideSortableGhost && this.sortableGhost) { 431 | setInlineStyles(this.sortableGhost, { 432 | opacity: '', 433 | visibility: '', 434 | }); 435 | } 436 | 437 | for (let i = 0, len = nodes.length; i < len; i++) { 438 | const node = nodes[i]; 439 | const el = node.node; 440 | 441 | // Clear the cached offset/boundingClientRect 442 | node.edgeOffset = null; 443 | node.boundingClientRect = null; 444 | 445 | // Remove the transforms / transitions 446 | setTranslate3d(el, null); 447 | setTransitionDuration(el, null); 448 | node.translate = null; 449 | } 450 | 451 | // Stop autoscroll 452 | this.autoScroller.clear(); 453 | 454 | // Update manager state 455 | this.manager.active = null; 456 | this.manager.isKeySorting = false; 457 | 458 | this.setState({ 459 | sorting: false, 460 | sortingIndex: null, 461 | }); 462 | 463 | if (typeof onSortEnd === 'function') { 464 | onSortEnd( 465 | { 466 | collection, 467 | newIndex: this.newIndex, 468 | oldIndex: this.index, 469 | isKeySorting, 470 | nodes, 471 | }, 472 | event, 473 | ); 474 | } 475 | 476 | this.touched = false; 477 | }; 478 | 479 | updateHelperPosition(event) { 480 | const { 481 | lockAxis, 482 | lockOffset, 483 | lockToContainerEdges, 484 | transitionDuration, 485 | keyboardSortingTransitionDuration = transitionDuration, 486 | } = this.props; 487 | const { isKeySorting } = this.manager; 488 | const { ignoreTransition } = event; 489 | 490 | const offset = getPosition(event); 491 | const translate = { 492 | x: offset.x - this.initialOffset.x, 493 | y: offset.y - this.initialOffset.y, 494 | }; 495 | 496 | // Adjust for window scroll 497 | translate.y -= window.pageYOffset - this.initialWindowScroll.top; 498 | translate.x -= window.pageXOffset - this.initialWindowScroll.left; 499 | 500 | this.translate = translate; 501 | 502 | if (lockToContainerEdges) { 503 | const [minLockOffset, maxLockOffset] = getLockPixelOffsets({ 504 | height: this.height, 505 | lockOffset, 506 | width: this.width, 507 | }); 508 | const minOffset = { 509 | x: this.width / 2 - minLockOffset.x, 510 | y: this.height / 2 - minLockOffset.y, 511 | }; 512 | const maxOffset = { 513 | x: this.width / 2 - maxLockOffset.x, 514 | y: this.height / 2 - maxLockOffset.y, 515 | }; 516 | 517 | translate.x = limit(this.minTranslate.x + minOffset.x, this.maxTranslate.x - maxOffset.x, translate.x); 518 | translate.y = limit(this.minTranslate.y + minOffset.y, this.maxTranslate.y - maxOffset.y, translate.y); 519 | } 520 | 521 | if (lockAxis === 'x') { 522 | translate.y = 0; 523 | } else if (lockAxis === 'y') { 524 | translate.x = 0; 525 | } 526 | 527 | if (isKeySorting && keyboardSortingTransitionDuration && !ignoreTransition) { 528 | setTransitionDuration(this.helper, keyboardSortingTransitionDuration); 529 | } 530 | 531 | setTranslate3d(this.helper, translate); 532 | } 533 | 534 | animateNodes() { 535 | const { transitionDuration, hideSortableGhost, onSortOver } = this.props; 536 | const { containerScrollDelta, windowScrollDelta } = this; 537 | const nodes = this.manager.getOrderedRefs(); 538 | const sortingOffset = { 539 | left: this.offsetEdge.left + this.translate.x + containerScrollDelta.left, 540 | top: this.offsetEdge.top + this.translate.y + containerScrollDelta.top, 541 | }; 542 | const { isKeySorting } = this.manager; 543 | 544 | const prevIndex = this.newIndex; 545 | this.newIndex = null; 546 | 547 | for (let i = 0, len = nodes.length; i < len; i++) { 548 | const { node } = nodes[i]; 549 | const { index } = node.sortableInfo; 550 | const width = node.offsetWidth; 551 | const height = node.offsetHeight; 552 | const offset = { 553 | height: this.height > height ? height / 2 : this.height / 2, 554 | width: this.width > width ? width / 2 : this.width / 2, 555 | }; 556 | 557 | // For keyboard sorting, we want user input to dictate the position of the nodes 558 | const mustShiftBackward = isKeySorting && index > this.index && index <= prevIndex; 559 | const mustShiftForward = isKeySorting && index < this.index && index >= prevIndex; 560 | 561 | const translate = { 562 | x: 0, 563 | y: 0, 564 | }; 565 | let { edgeOffset } = nodes[i]; 566 | 567 | // If we haven't cached the node's offsetTop / offsetLeft value 568 | if (!edgeOffset) { 569 | edgeOffset = getEdgeOffset(node, this.container); 570 | nodes[i].edgeOffset = edgeOffset; 571 | // While we're at it, cache the boundingClientRect, used during keyboard sorting 572 | if (isKeySorting) { 573 | nodes[i].boundingClientRect = getScrollAdjustedBoundingClientRect(node, containerScrollDelta); 574 | } 575 | } 576 | 577 | // Get a reference to the next and previous node 578 | const nextNode = i < nodes.length - 1 && nodes[i + 1]; 579 | const prevNode = i > 0 && nodes[i - 1]; 580 | 581 | // Also cache the next node's edge offset if needed. 582 | // We need this for calculating the animation in a grid setup 583 | if (nextNode && !nextNode.edgeOffset) { 584 | nextNode.edgeOffset = getEdgeOffset(nextNode.node, this.container); 585 | if (isKeySorting) { 586 | nextNode.boundingClientRect = getScrollAdjustedBoundingClientRect(nextNode.node, containerScrollDelta); 587 | } 588 | } 589 | 590 | // If the node is the one we're currently animating, skip it 591 | if (index === this.index) { 592 | if (hideSortableGhost) { 593 | /* 594 | * With windowing libraries such as `react-virtualized`, the sortableGhost 595 | * node may change while scrolling down and then back up (or vice-versa), 596 | * so we need to update the reference to the new node just to be safe. 597 | */ 598 | this.sortableGhost = node; 599 | 600 | setInlineStyles(node, { 601 | opacity: 0, 602 | visibility: 'hidden', 603 | }); 604 | } 605 | continue; 606 | } 607 | 608 | if (transitionDuration) { 609 | setTransitionDuration(node, transitionDuration); 610 | } 611 | 612 | if (this.axis.x) { 613 | if (this.axis.y) { 614 | // Calculations for a grid setup 615 | if ( 616 | mustShiftForward || 617 | (index < this.index && 618 | ((sortingOffset.left + windowScrollDelta.left - offset.width <= edgeOffset.left && 619 | sortingOffset.top + windowScrollDelta.top <= edgeOffset.top + offset.height) || 620 | sortingOffset.top + windowScrollDelta.top + offset.height <= edgeOffset.top)) 621 | ) { 622 | // If the current node is to the left on the same row, or above the node that's being dragged 623 | // then move it to the right 624 | translate.x = this.width + this.marginOffset.x; 625 | if (edgeOffset.left + translate.x > this.containerBoundingRect.width - offset.width * 2) { 626 | // If it moves passed the right bounds, then animate it to the first position of the next row. 627 | // We just use the offset of the next node to calculate where to move, because that node's original position 628 | // is exactly where we want to go 629 | if (nextNode) { 630 | translate.x = nextNode.edgeOffset.left - edgeOffset.left; 631 | translate.y = nextNode.edgeOffset.top - edgeOffset.top; 632 | } 633 | } 634 | if (this.newIndex === null) { 635 | this.newIndex = index; 636 | } 637 | } else if ( 638 | mustShiftBackward || 639 | (index > this.index && 640 | ((sortingOffset.left + windowScrollDelta.left + offset.width >= edgeOffset.left && 641 | sortingOffset.top + windowScrollDelta.top + offset.height >= edgeOffset.top) || 642 | sortingOffset.top + windowScrollDelta.top + offset.height >= edgeOffset.top + height)) 643 | ) { 644 | // If the current node is to the right on the same row, or below the node that's being dragged 645 | // then move it to the left 646 | translate.x = -(this.width + this.marginOffset.x); 647 | if (edgeOffset.left + translate.x < this.containerBoundingRect.left + offset.width) { 648 | // If it moves passed the left bounds, then animate it to the last position of the previous row. 649 | // We just use the offset of the previous node to calculate where to move, because that node's original position 650 | // is exactly where we want to go 651 | if (prevNode) { 652 | translate.x = prevNode.edgeOffset.left - edgeOffset.left; 653 | translate.y = prevNode.edgeOffset.top - edgeOffset.top; 654 | } 655 | } 656 | this.newIndex = index; 657 | } 658 | } else { 659 | if ( 660 | mustShiftBackward || 661 | (index > this.index && sortingOffset.left + windowScrollDelta.left + offset.width >= edgeOffset.left) 662 | ) { 663 | translate.x = -(this.width + this.marginOffset.x); 664 | this.newIndex = index; 665 | } else if ( 666 | mustShiftForward || 667 | (index < this.index && sortingOffset.left + windowScrollDelta.left <= edgeOffset.left + offset.width) 668 | ) { 669 | translate.x = this.width + this.marginOffset.x; 670 | 671 | if (this.newIndex == null) { 672 | this.newIndex = index; 673 | } 674 | } 675 | } 676 | } else if (this.axis.y) { 677 | if ( 678 | mustShiftBackward || 679 | (index > this.index && sortingOffset.top + windowScrollDelta.top + offset.height >= edgeOffset.top) 680 | ) { 681 | translate.y = -(this.height + this.marginOffset.y); 682 | this.newIndex = index; 683 | } else if ( 684 | mustShiftForward || 685 | (index < this.index && sortingOffset.top + windowScrollDelta.top <= edgeOffset.top + offset.height) 686 | ) { 687 | translate.y = this.height + this.marginOffset.y; 688 | if (this.newIndex == null) { 689 | this.newIndex = index; 690 | } 691 | } 692 | } 693 | 694 | setTranslate3d(node, translate); 695 | nodes[i].translate = translate; 696 | } 697 | 698 | if (this.newIndex == null) { 699 | this.newIndex = this.index; 700 | } 701 | 702 | if (isKeySorting) { 703 | // If keyboard sorting, we want the user input to dictate index, not location of the helper 704 | this.newIndex = prevIndex; 705 | } 706 | 707 | const oldIndex = isKeySorting ? this.prevIndex : prevIndex; 708 | if (onSortOver && this.newIndex !== oldIndex) { 709 | onSortOver({ 710 | collection: this.manager.active.collection, 711 | index: this.index, 712 | newIndex: this.newIndex, 713 | oldIndex, 714 | isKeySorting, 715 | nodes, 716 | helper: this.helper, 717 | }); 718 | } 719 | } 720 | 721 | autoscroll = () => { 722 | const { disableAutoscroll } = this.props; 723 | const { isKeySorting } = this.manager; 724 | 725 | if (disableAutoscroll) { 726 | this.autoScroller.clear(); 727 | return; 728 | } 729 | 730 | if (isKeySorting) { 731 | const translate = { ...this.translate }; 732 | let scrollX = 0; 733 | let scrollY = 0; 734 | 735 | if (this.axis.x) { 736 | translate.x = Math.min(this.maxTranslate.x, Math.max(this.minTranslate.x, this.translate.x)); 737 | scrollX = this.translate.x - translate.x; 738 | } 739 | 740 | if (this.axis.y) { 741 | translate.y = Math.min(this.maxTranslate.y, Math.max(this.minTranslate.y, this.translate.y)); 742 | scrollY = this.translate.y - translate.y; 743 | } 744 | 745 | this.translate = translate; 746 | setTranslate3d(this.helper, this.translate); 747 | this.scrollContainer.scrollLeft += scrollX; 748 | this.scrollContainer.scrollTop += scrollY; 749 | 750 | return; 751 | } 752 | 753 | this.autoScroller.update({ 754 | height: this.height, 755 | maxTranslate: this.maxTranslate, 756 | minTranslate: this.minTranslate, 757 | translate: this.translate, 758 | width: this.width, 759 | }); 760 | }; 761 | 762 | onAutoScroll = offset => { 763 | this.translate.x += offset.left; 764 | this.translate.y += offset.top; 765 | 766 | this.animateNodes(); 767 | }; 768 | 769 | getWrappedInstance() { 770 | invariant( 771 | config.withRef, 772 | 'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableContainer() call', 773 | ); 774 | 775 | return this.wrappedInstance.current; 776 | } 777 | 778 | getContainer() { 779 | const { getContainer } = this.props; 780 | 781 | if (typeof getContainer !== 'function') { 782 | return findDOMNode(this); 783 | } 784 | 785 | return getContainer(config.withRef ? this.getWrappedInstance() : undefined); 786 | } 787 | 788 | handleKeyDown = event => { 789 | const { keyCode } = event; 790 | const { shouldCancelStart, keyCodes: customKeyCodes = {} } = this.props; 791 | 792 | const keyCodes = { 793 | ...defaultKeyCodes, 794 | ...customKeyCodes, 795 | }; 796 | 797 | if ( 798 | (this.manager.active && !this.manager.isKeySorting) || 799 | (!this.manager.active && 800 | (!keyCodes.lift.includes(keyCode) || shouldCancelStart(event) || !this.isValidSortingTarget(event))) 801 | ) { 802 | return; 803 | } 804 | 805 | event.stopPropagation(); 806 | event.preventDefault(); 807 | 808 | if (keyCodes.lift.includes(keyCode) && !this.manager.active) { 809 | this.keyLift(event); 810 | } else if (keyCodes.drop.includes(keyCode) && this.manager.active) { 811 | this.keyDrop(event); 812 | } else if (keyCodes.cancel.includes(keyCode)) { 813 | this.newIndex = this.manager.active.index; 814 | this.keyDrop(event); 815 | } else if (keyCodes.up.includes(keyCode)) { 816 | this.keyMove(-1); 817 | } else if (keyCodes.down.includes(keyCode)) { 818 | this.keyMove(1); 819 | } 820 | }; 821 | 822 | keyLift = event => { 823 | const { target } = event; 824 | const node = closest(target, el => el.sortableInfo != null); 825 | const { index, collection } = node.sortableInfo; 826 | 827 | this.initialFocusedNode = target; 828 | 829 | this.manager.isKeySorting = true; 830 | this.manager.active = { 831 | index, 832 | collection, 833 | }; 834 | 835 | this.handlePress(event); 836 | }; 837 | 838 | keyMove = shift => { 839 | const nodes = this.manager.getOrderedRefs(); 840 | const { index: lastIndex } = nodes[nodes.length - 1].node.sortableInfo; 841 | const newIndex = this.newIndex + shift; 842 | const prevIndex = this.newIndex; 843 | 844 | if (newIndex < 0 || newIndex > lastIndex) { 845 | return; 846 | } 847 | 848 | this.prevIndex = prevIndex; 849 | this.newIndex = newIndex; 850 | 851 | const targetIndex = getTargetIndex(this.newIndex, this.prevIndex, this.index); 852 | const target = nodes.find(({ node }) => node.sortableInfo.index === targetIndex); 853 | const { node: targetNode } = target; 854 | 855 | const scrollDelta = this.containerScrollDelta; 856 | const targetBoundingClientRect = 857 | target.boundingClientRect || getScrollAdjustedBoundingClientRect(targetNode, scrollDelta); 858 | const targetTranslate = target.translate || { x: 0, y: 0 }; 859 | 860 | const targetPosition = { 861 | top: targetBoundingClientRect.top + targetTranslate.y - scrollDelta.top, 862 | left: targetBoundingClientRect.left + targetTranslate.x - scrollDelta.left, 863 | }; 864 | 865 | const shouldAdjustForSize = prevIndex < newIndex; 866 | const sizeAdjustment = { 867 | x: shouldAdjustForSize && this.axis.x ? targetNode.offsetWidth - this.width : 0, 868 | y: shouldAdjustForSize && this.axis.y ? targetNode.offsetHeight - this.height : 0, 869 | }; 870 | 871 | this.handleSortMove({ 872 | pageX: targetPosition.left + sizeAdjustment.x, 873 | pageY: targetPosition.top + sizeAdjustment.y, 874 | ignoreTransition: shift === 0, 875 | }); 876 | }; 877 | 878 | keyDrop = event => { 879 | this.handleSortEnd(event); 880 | 881 | if (this.initialFocusedNode) { 882 | this.initialFocusedNode.focus(); 883 | } 884 | }; 885 | 886 | handleKeyEnd = event => { 887 | if (this.manager.active) { 888 | this.keyDrop(event); 889 | } 890 | }; 891 | 892 | isValidSortingTarget = event => { 893 | const { useDragHandle } = this.props; 894 | const { target } = event; 895 | const node = closest(target, el => el.sortableInfo != null); 896 | 897 | return ( 898 | node && 899 | node.sortableInfo && 900 | !node.sortableInfo.disabled && 901 | (useDragHandle ? isSortableHandle(target) : target.sortableInfo) 902 | ); 903 | }; 904 | 905 | render() { 906 | const ref = config.withRef ? this.wrappedInstance : null; 907 | 908 | return ( 909 | 910 | 911 | 912 | ); 913 | } 914 | 915 | get helperContainer() { 916 | const { helperContainer } = this.props; 917 | 918 | if (typeof helperContainer === 'function') { 919 | return helperContainer(); 920 | } 921 | 922 | return this.props.helperContainer || this.document.body; 923 | } 924 | 925 | get containerScrollDelta() { 926 | const { useWindowAsScrollContainer } = this.props; 927 | 928 | if (useWindowAsScrollContainer) { 929 | return { left: 0, top: 0 }; 930 | } 931 | 932 | return { 933 | left: this.scrollContainer.scrollLeft - this.initialScroll.left, 934 | top: this.scrollContainer.scrollTop - this.initialScroll.top, 935 | }; 936 | } 937 | 938 | get windowScrollDelta() { 939 | return { 940 | left: this.contentWindow.pageXOffset - this.initialWindowScroll.left, 941 | top: this.contentWindow.pageYOffset - this.initialWindowScroll.top, 942 | }; 943 | } 944 | }; 945 | } 946 | --------------------------------------------------------------------------------