Repository: clauderic/react-sortable-hoc
Branch: master
Commit: caf3c4f5bfb3
Files: 49
Total size: 165.4 KB
Directory structure:
gitextract_bc7ghwt0/
├── .codesandbox/
│ └── ci.json
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierrc
├── .storybook/
│ ├── config.js
│ ├── manager-head.html
│ ├── theme.js
│ └── webpack.config.js
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples/
│ ├── .eslintrc.json
│ ├── basic.js
│ ├── collections.js
│ ├── drag-handle.js
│ ├── react-infinite.js
│ ├── react-virtualized-table-columns.js
│ └── react-virtualized.js
├── package.json
├── rollup.config.js
├── src/
│ ├── .stories/
│ │ ├── Storybook.scss
│ │ ├── grouping-items/
│ │ │ ├── Item/
│ │ │ │ ├── Item.js
│ │ │ │ ├── Item.scss
│ │ │ │ └── index.js
│ │ │ ├── List/
│ │ │ │ ├── List.js
│ │ │ │ ├── List.scss
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── utils.js
│ │ ├── index.js
│ │ └── interactive-elements-stress-test/
│ │ ├── Item/
│ │ │ ├── Item.js
│ │ │ ├── Item.scss
│ │ │ └── index.js
│ │ ├── List.js
│ │ └── index.js
│ ├── AutoScroller/
│ │ └── index.js
│ ├── Manager/
│ │ └── index.js
│ ├── SortableContainer/
│ │ ├── defaultGetHelperDimensions.js
│ │ ├── defaultShouldCancelStart.js
│ │ ├── index.js
│ │ └── props.js
│ ├── SortableElement/
│ │ └── index.js
│ ├── SortableHandle/
│ │ └── index.js
│ ├── index.js
│ └── utils.js
└── types/
└── index.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .codesandbox/ci.json
================================================
{
"sandboxes": ["react", "o104x95y86"]
}
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# Markdown syntax specifies that trailing whitespaces can be meaningful,
# so let’s not trim those. e.g. 2 trailing spaces = linebreak (<br />)
# See https://daringfireball.net/projects/markdown/syntax#p
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintrc.json
================================================
{
"extends": ["plugin:shopify/react", "plugin:prettier/recommended"],
"rules": {
"no-process-env": "off",
"no-lonely-if": "off",
"no-undefined": "off",
"no-param-reassign": "off",
"no-mixed-operators": "off",
"no-misleading-character-class": "off",
"require-atomic-updates": "off",
"prefer-object-spread": "off",
"lines-around-comment": "off",
"function-paren-newline": "off",
"promise/catch-or-return": "off",
"react/forbid-prop-types": "off",
"react/jsx-filename-extension": "off",
"react/no-unused-prop-types": "off",
"react/no-string-refs": 1,
"react/no-deprecated": 1,
"shopify/binary-assignment-parens": "off",
"sort-class-members/sort-class-members": "off"
}
}
================================================
FILE: .gitignore
================================================
*.DS_Store
node_modules
dist
styles.min.css
styles.min.css.map
coverage
npm-debug.log
================================================
FILE: .npmignore
================================================
.github
.babelrc
coverage
src
test
.*
*.md
codecov.yml
.travis.yml
================================================
FILE: .prettierrc
================================================
{
"arrowParens": "always",
"bracketSpacing": false,
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: .storybook/config.js
================================================
import {addParameters, configure} from '@storybook/react';
import theme from './theme';
addParameters({
options: {
showAddonPanel: false,
theme,
},
});
function loadStories() {
require('../src/.stories/index.js');
}
configure(loadStories, module);
================================================
FILE: .storybook/manager-head.html
================================================
<link
href="https://fonts.googleapis.com/css?family=Roboto:400,500,700"
rel="stylesheet"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAC/VBMVEUAAAAUcP8ea/8Ldf8iav8gav8TcP8nZ/8rZv8xY/83Yf88Xv84YP80Yv9AXf9GWv9PVv9DXP9cUP9WU/9fT/9aUf9jTv9nTP9tSf9zR/95RP8sav92Rv99Qv+CQP+DQP9xSf+GP/+XOP+UOP+ZNv+rLv+fNP+jMv+nL/+oMP+uLf+0K//NIP+4Kf+9J/+/Jv/LIf/BJf/UHf8cbP8dbP8dbP8cbP8hav8hav8hav8hav8iav8laP8kaf8jaf8jaf8jaf8laP8maP8rZv8rZv8rZv8rZv8rZv8xY/80Zf+kvv+gu/+gu/+gu/+gu/+2zf8wYv8xY/8xY/82Yf85Y/+1u/+wuP+wuP+wuP+wuP/My/81YP83Yf83Yf88Xv88Xv88Xv88Xv88Xv9CXP9DW/9FW/9FW/9FW/9DW/9CXP9GWv9HWv9HWv9HWv9HWv9GWv9LWP9KWP9KWP9JWP9JWf9IWf9dUP9bUf9aUf9aUf9ZUv9ZUv9eUP9dUP9cUP9cUP9dUP9eUP9jTv9iTv9hT/9fT/9fT/9fT/9hTv9iTv9nTP9nTP9nTP9nTP9nTP9tSf9tSf9sRf+jw/+gu/+gu/+gu/+gu/+kxv9sRv9tSf9zR/9zR/9xQ/+2vv+yt/+yt/+yt/+yt/+4wf9xRP9zR/95RP95RP95RP95RP95RP99Q/9+Qv9/Qf+AQf+AQf9/Qf9+Qv+CQP+DQP+DQP+DQP+DQP+CQP+DQP+DQP+GP/+HPv+IPv+UOf+VOP+VOP+VOP+VOP+ZNv+YN/+YN/+YN/+YN/+ZNv+dNf+cNf+aNv+aNv+aNv+cNf+dNP+jMv+jMv+jMv+jMv+iMv+oMP+oM/+fwf+gu/+gu/+gu/+gu/+e2/+oLv+oMP+oMP+uLf+uMf+xvP+xt/+xt/+xt/+xt/+x0/+uLP+uLf+uLf+0K/+0K/+0K/+0K/+0K/+5KP+7KP+8J/+8J/+8J/+6KP+5KP+9J/++Jv++Jv++Jv+9J//DJP/CJP/CJP/DJP////88deaDAAAA/nRSTlMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGCgkETbLEnyPhs36AgdKS90iGtAT2SG6sqaqVEoK0BPZHYpqYmYYPgrQE90aEtATpnl9iY8ScY8nX1rgxARAXFRUKAREaGxwPU87b2sNGDsWiV1lYtcMX01V02hfTUkGioKGeLnHaF9NRQaKgoZ4ucdoW01d22gq4woiJzrI2qL28mywCBQYHAwEQFxYLY8nX1rgx6Z5fYmPEnPdGhLQE9kdimpiZhg+CtAT2SG6sqaqVEoK0BPdIhrQE4bN+gIHSkk2yxJ8jBgoJBCSYEg0AAAABYktHRP7SAMJTAAAAB3RJTUUH4wMMFAwxtzZCfgAAAaVJREFUOMtjYCAEGI1NTDGBiakZEzNEgbmFJVZgZc3CClZgY2tnjwU4ODqxsYMVOLtwYLGYg8PVzR0i4eHp5e2DDnz9/AMCgzjBCoJDQsPC0UFEZFR0TCwXWEFcPDcWK3i4ExKTIBLJKalpWEB6RiYvH1hBVnZOLhaQk5fPzw9WUFBYBALFJWigVEAQyUqhsvIKNFBZBQLVwmB5EdGa2rp6LKCuoRGsQKypuaW1DQto7+gEKxDv6u4RxxaT4r19YFqif8LESZOnoIOp06bPmAlWIDlr9py58+ajgwULFy1eAlYgtXTZcilpTBtkpFasBDNkV61es3YdNrB+A1iBnPzGTZu3YAGbt26DmKWguH0HGti5CwR2KzLs2bsPBPajgwNKyhDNBw8dPoIFHD56TEUFrOD4iZOnsIDTZ86qqoEVnDuvjiUINdQvXLwEkbh85eq16+jgxs1bt+/c1QQruHf/wcNH6ODxk6fPnr/QAit4+UobixXa2q/fvIVIvHv/4SMW8OnzFx1dsIKv375jBT9+6umDFRj8+v0HE/z+89fQiGDGBwMAfwKLipcveMIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDMtMTJUMjA6MTI6NDkrMDE6MDD6OZKaAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTAzLTEyVDIwOjEyOjQ5KzAxOjAwi2QqJgAAAFd6VFh0UmF3IHByb2ZpbGUgdHlwZSBpcHRjAAB4nOPyDAhxVigoyk/LzEnlUgADIwsuYwsTIxNLkxQDEyBEgDTDZAMjs1Qgy9jUyMTMxBzEB8uASKBKLgDqFxF08kI1lQAAAABJRU5ErkJggg=="
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABmFBMVEUgav8JfP8AAAAnaP8uZP85X/9BXP9jTv9qSv92Rf99Qv+eNP+lMf+xLP+4Kf+/Jv/jFP8fa/8fa/8fa/8jaf8eaP8dZ/8dZ/8gaP8kaf8oZ/8yaP+Tsf+YtP+YtP+ZtP+Ao/8sZP8uZP8+Yv+lsv+otf+otP+otf+So/85X/85X/9EW/9EV/9DV/9DV/9DV/9EWf9EW/8/Xf9JWf9IWf9IWf9OVv9SVf9SVf9XU/9cUf9bUf9bUf9jTv9gT/9cTv9bTP9bTP9bTP9cTv9gT/9qSv9qTP+esf+it/+it/+iuP+aqf9qS/92Rf93Rv+mo/+qrP+qrP+inP92Rf98Q/+AQf+BPv+APP+BPP+BPP+BPv+AQf+WOP+XN/+XN/+TOf+PO/+PO/+KPf+DQP+EP/+FP/+bNf+YM/+YMv+YMv+ZNP+bNf+fNP+lNv+jsf+jtf+jtf+jmv+lMP+lMf+xMf+vp/+vrP+vrP+vrf+wkP+yKv+xLP+7J/++I/++Iv++Iv+9JP+7KP+3Kf/AJf/AJv+/Jv/AJf/////NvskwAAAAh3RSTlMAAAAAAAAAAAAAAAAAAAAAAA4aCZOEgYKJdQaVTmBfYD2DEpJEVlVVNoITmoJ/fn2FegYRHx4uQUAtISEQDoWBfH1+g5Iffz9YV1g9lB6CRl9eRZYLfYaBgoOIiBEfHic0MyAUFAiagn9+hnsHkkRWVTaCE5VOYF9gPYMSk4SBgol1Bg4aGgk3BSnTAAAAAWJLR0SH+9kLywAAAAd0SU1FB+MDDBQMMbc2Qn4AAADJSURBVBjTYxAUQgBhBkYmBhFRMXEIEJOQlGJmYpCWkZWDAHkFRSUWJgZlFVU1CFDX0NRiZWLQ1tHV04cAA0MjNiYGYxNTM3MLCLC0srZhYGK3tbN3gABHJ2cXBiYOVzd3D08I8PL2YWDi9PXzDwiEgICgYAYmrpDQsHAIiIiMimaIiY2LT4CAxKTklFSGtPSMjEwIyMrO4WZiyM3LL4CCwqJiHiaGktKycgioqKyq5mViqKmtq4eAuobGJj4mhuaWVhhoaeMXYAIAHs1DWNndumoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDMtMTJUMjA6MTI6NDkrMDE6MDD6OZKaAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTAzLTEyVDIwOjEyOjQ5KzAxOjAwi2QqJgAAAFd6VFh0UmF3IHByb2ZpbGUgdHlwZSBpcHRjAAB4nOPyDAhxVigoyk/LzEnlUgADIwsuYwsTIxNLkxQDEyBEgDTDZAMjs1Qgy9jUyMTMxBzEB8uASKBKLgDqFxF08kI1lQAAAABJRU5ErkJggg=="
/>
<link
rel="mask-icon"
href="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy1TVkctMjAwMTA5MDQvRFREL3N2ZzEwLmR0ZCI+CjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiB3aWR0aD0iNjEyLjAwMDAwMHB0IiBoZWlnaHQ9IjYxMi4wMDAwMDBwdCIgdmlld0JveD0iMCAwIDYxMi4wMDAwMDAgNjEyLjAwMDAwMCIKIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiPgo8bWV0YWRhdGE+CkNyZWF0ZWQgYnkgcG90cmFjZSAxLjExLCB3cml0dGVuIGJ5IFBldGVyIFNlbGluZ2VyIDIwMDEtMjAxMwo8L21ldGFkYXRhPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCw2MTIuMDAwMDAwKSBzY2FsZSgwLjEwMDAwMCwtMC4xMDAwMDApIgpmaWxsPSIjMDAwMDAwIiBzdHJva2U9Im5vbmUiPgo8cGF0aCBkPSJNMjUxIDU2ODQgYy0xMDMgLTM2IC0xNzEgLTEwMCAtMjIwIC0yMDMgbC0yNiAtNTYgLTIgLTQ1MCBjLTIgLTM5OQowIC00NTYgMTUgLTUwMSAzMyAtOTkgMTAwIC0xNzcgMTkyIC0yMjIgbDU1IC0yNyAyMTk1IDAgMjE5NSAwIDU1IDI4IGM3MSAzNQoxMjMgODQgMTU5IDE1MCAzMCA1MyA1MiAxMzMgNTEgMTc3IC0xIDE0IC0xIDE4OSAtMSAzOTAgMSA0MTAgMCA0MjcgLTIzIDQ5MAotMzUgOTUgLTk1IDE2MiAtMTg2IDIwNyBsLTU1IDI4IC0yMTgwIDIgYy0xOTQ5IDIgLTIxODUgMSAtMjIyNCAtMTN6IG00MzY2Ci0yNDMgYzE4IC03IDM4IC0yOCA0OSAtNTAgMTkgLTM2IDIwIC02MiAxOSAtNDM4IDAgLTQzNyAxIC00MjkgLTYxIC00NjkgLTI0Ci0xNiAtMTgxIC0xNyAtMjE2NCAtMTcgLTE5ODQgMCAtMjE0MCAxIC0yMTY0IDE3IC02MiA0MCAtNjEgMzIgLTYyIDQ2OCAwIDQ0NAoxIDQ1MSA2MyA0ODQgMzUgMTkgOTEgMTkgMjE2NCAxOCAxNjQ2IC0xIDIxMzQgLTQgMjE1NiAtMTN6Ii8+CjxwYXRoIGQ9Ik00NDAgNTA2MyBjMCAtMjEgMCAtNTggMCAtODMgLTEgLTI1IC0xIC02MiAtMSAtODIgbDEgLTM4IDE5NzQgMApjMTA4NiAwIDE5NzcgMyAxOTc5IDggNiA4IDEwIDExMSA4IDE4NSBsLTEgNDcgLTE5ODAgMCAtMTk4MSAwIDEgLTM3eiIvPgo8cGF0aCBkPSJNMTQwMyAzODAxIGMtMjkgLTExIC02MSAtMjUgLTcxIC0zMyAtNDAgLTI4IC05MiAtNzggLTkyIC04OCAwIC01Ci00IC0xMCAtOCAtMTAgLTEyIDAgLTUwIC04OCAtNjIgLTE0MyAtMTEgLTQ5IC0xNCAtNzkyIC01IC04NzcgMTMgLTEwOCAxMDAKLTIyNyAyMDUgLTI3OCBsNTUgLTI3IDIxNzUgLTMgYzIxNDcgLTIgMjE3NSAtMiAyMjM0IDE4IDEyOSA0MyAyMjAgMTU1IDI0MgoyOTggMyAyMCA3IDM4IDkgNDAgMSAyIC0xIDE5IC01IDM4IC00IDE4IC01IDM0IC0xIDM0IDMgMCA1IDI0IDMgNTIgLTEgMjkgLTMKNjAgLTMgNjggMCA4IDAgNjIgMCAxMjAgMCA1OCAwIDExMiAxIDEyMCAwIDggMCAzNSAtMSA2MCAtMSAyNSAtMSA1MiAwIDYwIDQKODEgLTEgMjQ4IC05IDI4NSAtMjUgMTEzIC0xMTcgMjIyIC0yMjEgMjYzIGwtNTQgMjEgLTIxNzAgMCBjLTIwOTQgMCAtMjE3MiAwCi0yMjIyIC0xOHogbTQzODAgLTI0NCBjNjMgLTQxIDYyIC0zMiA2MiAtNDc2IDEgLTM5MyAwIC00MDUgLTIwIC00MzUgLTQzIC02NAoxMjUgLTYwIC0yMjExIC02MCAtMTk5MiAwIC0yMTMzIDEgLTIxNTggMTcgLTYyIDQxIC02MSAzMiAtNjEgNDcyIC0xIDQyMSAwCjQyOSA0NSA0NzAgMTIgMTAgNDAgMjEgNjMgMjQgMjMgMyA5OTAgNiAyMTQ4IDYgMTk3NCAtMSAyMTA4IC0yIDIxMzIgLTE4eiIvPgo8cGF0aCBkPSJNMzU1MyAzMTgyIGwtMTkxMyAtMiAwIC0xMjAgMCAtMTIwIDE5NzQgMCBjMTE3MCAwIDE5NzcgNCAxOTgwIDkgNgo5IDkgOTggNyAxOTAgLTEgMzggLTMgNDMgLTIxIDM4IC0xMSAtMyAtMjIgLTIgLTI1IDMgLTMgNCAtMjAgNCAtMzkgMCAtMTkgLTQKLTM4IC01IC00MyAtMSAtNCAzIC04NjkgNSAtMTkyMCAzeiIvPgo8cGF0aCBkPSJNMjQxIDE4ODAgYy03MSAtMjYgLTEzMiAtNzQgLTE3NSAtMTM4IC02NiAtMTAwIC02NyAtMTEyIC02NCAtNjA2CmwzIC00NDEgMzMgLTY3IGMzOSAtNzkgOTcgLTEzOSAxNzIgLTE3NiBsNTUgLTI3IDIxOTUgMCAyMTk1IDAgNTUgMjggYzQ5IDI1Cjg2IDU0IDEzNCAxMDggMjQgMjcgNTMgOTIgNjUgMTQ3IDEyIDU5IDE3IDgyMyA2IDg4OCAtMjAgMTExIC0xMDQgMjIyIC0yMDUKMjcyIGwtNTUgMjcgLTIxODAgMiBjLTIxMzAgMiAtMjE4MSAyIC0yMjM0IC0xN3ogbTQzODMgLTI0NCBjNjIgLTQwIDYxIC0zMgo2MiAtNDY5IDAgLTQzNyAwIC00MzcgLTU5IC00ODEgLTI2IC0xOSAtNjEgLTE5IC0yMTY2IC0xOSAtMjAwNiAwIC0yMTQxIDEKLTIxNjYgMTcgLTYyIDQwIC02MSAzNSAtNjAgNDgzIGwwIDQwOSAzMiAzNCBjMjMgMjUgNDMgMzUgNzUgMzkgMjMgMyA5OTEgNgoyMTUwIDUgMTk1MiAtMSAyMTA4IC0yIDIxMzIgLTE4eiIvPgo8cGF0aCBkPSJNNDQwIDEyMjMgYzAgLTIxIDAgLTU4IDAgLTgzIC0xIC0yNSAtMSAtNjIgLTEgLTgyIGwxIC0zOCAxOTc0IDAKYzEwODYgMCAxOTc3IDMgMTk3OSA4IDYgOCAxMCAxMTEgOCAxODUgbC0xIDQ3IC0xOTgwIDAgLTE5ODEgMCAxIC0zN3oiLz4KPC9nPgo8L3N2Zz4K"
color="#5bbad5"
/>
<style>
nav span {
font-size: 14px;
}
</style>
================================================
FILE: .storybook/theme.js
================================================
import {create} from '@storybook/theming';
export default create({
base: 'light',
colorSecondary: '#9276ff',
// UI
appBg: 'white',
appContentBg: '#f9f9f9',
appBorderColor: 'grey',
appBorderRadius: 4,
// Typography
fontBase: '"Roboto", Helvetica Neue, Helvetica, Arial, sans-serif',
fontCode: 'monospace',
// Text colors
textColor: '#364149',
textInverseColor: 'rgba(255,255,255,0.9)',
// Toolbar default and active colors
barTextColor: '#FFF',
barSelectedColor: '#FFF',
barBg: '#9276ff',
// Form colors
inputBg: 'white',
inputBorder: '#efefef',
inputTextColor: '#364149',
inputBorderRadius: 0,
brandTitle: 'React Sortable HOC',
brandUrl: 'https://github.com/clauderic/react-sortable-hoc',
brandImage:
'https://user-images.githubusercontent.com/1416436/54170652-dfd59d80-444d-11e9-9c51-658638c0454b.png',
});
================================================
FILE: .storybook/webpack.config.js
================================================
module.exports = {
module: {
rules: [
{
test: /(\.scss)$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]',
},
},
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')],
},
},
'sass-loader',
],
},
{
test: /(\.css)$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- 10
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="2.0.0"></a>
# [2.0.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.11.0...v2.0.0) (2021-03-18)
### Bug Fixes
- Fixed React strict mode warnings by upgrading to new React context API and removing legacy refs ([#624](https://github.com/clauderic/react-sortable-hoc/pull/624)). Since the new context API was introduced in React ^16.3.0, the peer dependencies had to be upgraded accordingly.
- Check if event is cancellable before calling `event.preventDefault()` [#752](https://github.com/clauderic/react-sortable-hoc/pull/752).
- Fix touch events being lost by listening to event.target on mobile [#586](https://github.com/clauderic/react-sortable-hoc/pull/586).
- Added `disableAutoscroll` prop to PropType definitions [#755](https://github.com/clauderic/react-sortable-hoc/pull/755).
### Dependencies
- Updated minimum peer dependencies for `react` and `react-dom` to ^16.3.0. Added ^17.0.0 to list of supported peer dependencies.
<a name="1.11.0"></a>
# [1.11.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.10.1...v1.11.0) (2020-01-20)
### Bug Fixes
- clear autoscroller when autoscroll is disabled ([#604](https://github.com/clauderic/react-sortable-hoc/issues/604)) ([3fd83f9](https://github.com/clauderic/react-sortable-hoc/commit/3fd83f9))
- Fix UMD path ([#611](https://github.com/clauderic/react-sortable-hoc/pull/611)) ([f61331d](https://github.com/clauderic/react-sortable-hoc/commit/f61331d))
### Features
- Add CSS Grid grid-gap support ([#657](https://github.com/clauderic/react-sortable-hoc/issues/657)) ([4efcaa2](https://github.com/clauderic/react-sortable-hoc/commit/4efcaa2))
<a name="1.10.1"></a>
## [1.10.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.10.0...v1.10.1) (2019-08-22)
### Bug Fixes
- PropType definition for keyCodes was incorrect ([eaf5070](https://github.com/clauderic/react-sortable-hoc/commit/eaf5070))
<a name="1.10.0"></a>
# [1.10.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.9.1...v1.10.0) (2019-08-22)
### Bug Fixes
- don't spread the keysToOmit parameter in omit util ([#563](https://github.com/clauderic/react-sortable-hoc/issues/563)) ([1c69772](https://github.com/clauderic/react-sortable-hoc/commit/1c69772))
- Fix broken UMD build ([#587](https://github.com/clauderic/react-sortable-hoc/issues/587)) ([6cb7750](https://github.com/clauderic/react-sortable-hoc/commit/6cb7750))
- remove browser field with umd bundle ([#541](https://github.com/clauderic/react-sortable-hoc/issues/541)) ([d3b30fd](https://github.com/clauderic/react-sortable-hoc/commit/d3b30fd)
### Features
- Add keyCodes prop to configure the keyboard shortcuts ([#588](https://github.com/clauderic/react-sortable-hoc/issues/588)) ([4c6d8dd](https://github.com/clauderic/react-sortable-hoc/commit/4c6d8dd))
<a name="1.9.1"></a>
## [1.9.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.9.0...v1.9.1) (2019-04-24)
### Bug Fixes
- do not copy canvas context if it has neither width nor height ([#530](https://github.com/clauderic/react-sortable-hoc/issues/530)) ([3808437](https://github.com/clauderic/react-sortable-hoc/commit/3808437))
- pass isKeySorting to onSortOver and updateBeforeSortStart handler props ([#531](https://github.com/clauderic/react-sortable-hoc/issues/531)) ([763fd33](https://github.com/clauderic/react-sortable-hoc/commit/763fd33)
<a name="1.9.0"></a>
# [1.9.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.3...v1.9.0) (2019-04-23)
### Bug Fixes
- issue with radio input name collision when cloning helper ([5337c97](https://github.com/clauderic/react-sortable-hoc/commit/5337c97))
### Features
- add support for keyboard sorting ([#501](https://github.com/clauderic/react-sortable-hoc/issues/501)) ([439b92f](https://github.com/clauderic/react-sortable-hoc/commit/439b92f))
- prevent sort start on contentEditable target ([d64c8cf](https://github.com/clauderic/react-sortable-hoc/commit/d64c8cf))
<a name="1.8.3"></a>
## [1.8.3](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.2...v1.8.3) (2019-03-20)
### Bug Fixes
- issue with windowAsScrollContainer and translation offsets ([0391e62](https://github.com/clauderic/react-sortable-hoc/commit/0391e62))
### Features
- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76))
- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4))
- allow helperContainer prop to be a function returning an HTMLElement ([#489](https://github.com/clauderic/react-sortable-hoc/issues/489)) ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a))
- Detect scroll container automatically ([#507](https://github.com/clauderic/react-sortable-hoc/issues/507)) ([6572921](https://github.com/clauderic/react-sortable-hoc/commit/6572921))
<a name="1.8.2"></a>
## [1.8.2](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.1...v1.8.2) (2019-03-19)
### Bug Fixes
- issue with getComputedStyle and getScrollingParent ([b104249](https://github.com/clauderic/react-sortable-hoc/commit/b104249))
<a name="1.8.1"></a>
## [1.8.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.0...v1.8.1) (2019-03-18)
### Bug Fixes
- issue with cloning canvas context of dragged items ([#512](https://github.com/clauderic/react-sortable-hoc/issues/512)) ([4df34ad](https://github.com/clauderic/react-sortable-hoc/commit/4df34ad))
<a name="1.8.0"></a>
# [1.8.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.8.0) (2019-03-18)
### Bug Fixes
- added prop-types to peerDependencies ([0e855c5](https://github.com/clauderic/react-sortable-hoc/commit/0e855c5))
- copy canvas content into cloned node ([43ad122](https://github.com/clauderic/react-sortable-hoc/commit/43ad122))
- get updated index after updateBeforeSortStart ([4471a0a](https://github.com/clauderic/react-sortable-hoc/commit/4471a0a))
- helperContainer PropType definition broke server-side rendering ([#471](https://github.com/clauderic/react-sortable-hoc/issues/471)) ([c0eef97](https://github.com/clauderic/react-sortable-hoc/commit/c0eef97))
- lock axis story should not use lockToContainerEdges ([db1d3a9](https://github.com/clauderic/react-sortable-hoc/commit/db1d3a9))
- omit disableAutoscroll prop ([#502](https://github.com/clauderic/react-sortable-hoc/issues/502)) ([e994e73](https://github.com/clauderic/react-sortable-hoc/commit/e994e73))
- omit spreading helperContainer prop ([#497](https://github.com/clauderic/react-sortable-hoc/issues/497)) ([12bafdf](https://github.com/clauderic/react-sortable-hoc/commit/12bafdf))
- overflow bug while dragging an item upwards in a grid ([1a2c87e](https://github.com/clauderic/react-sortable-hoc/commit/1a2c87e))
- replace process.env.NODE_ENV in UMD builds ([16135df](https://github.com/clauderic/react-sortable-hoc/commit/16135df))
- update helperContainer prop type definition ([#491](https://github.com/clauderic/react-sortable-hoc/issues/491)) ([fd30383](https://github.com/clauderic/react-sortable-hoc/commit/fd30383))
- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041))
- virtualized collection grid bug ([a57975c](https://github.com/clauderic/react-sortable-hoc/commit/a57975c))
### Features
- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76))
- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4))
- allow helperContainer prop to be a function returning an HTMLElement ([#489](https://github.com/clauderic/react-sortable-hoc/issues/489)) ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a))
- Detect scroll container automatically ([#507](https://github.com/clauderic/react-sortable-hoc/issues/507)) ([6572921](https://github.com/clauderic/react-sortable-hoc/commit/6572921))
<a name="1.7.1"></a>
## [1.7.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.7.1) (2019-03-06)
### Bug Fixes
- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041))
<a name="1.7.0"></a>
# [1.7.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.7.0) (2019-03-06)
### Bug Fixes
- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041))
<a name="1.6.1"></a>
## [1.6.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.6.0...v1.6.1) (2019-02-11)
### Bug Fixes
- omit disableAutoscroll prop ([#502](https://github.com/clauderic/react-sortable-hoc/issues/502)) ([e994e73](https://github.com/clauderic/react-sortable-hoc/commit/e994e73))
<a name="1.6.0"></a>
# [1.6.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.3...v1.6.0) (2019-02-07)
### Features
- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76))
<a name="1.5.4"></a>
## [1.5.4](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.3...v1.5.4) (2019-02-07)
### Bug Fixes
- overflow bug while dragging an item upwards in a grid ([1a2c87e](https://github.com/clauderic/react-sortable-hoc/commit/1a2c87e))
- virtualized collection grid bug ([a57975c](https://github.com/clauderic/react-sortable-hoc/commit/a57975c))
<a name="1.5.3"></a>
## [1.5.3](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.5.3) (2019-01-25)
### Bug Fixes
- omit spreading helperContainer prop on WrappedComponent ([#497](https://github.com/clauderic/react-sortable-hoc/issues/497)) ([12bafdf](https://github.com/clauderic/react-sortable-hoc/commit/12bafdf))
<a name="1.5.2"></a>
## [1.5.2](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.1...v1.5.2) (2019-01-22)
### Bug Fixes
- invalid helperContainer PropType definition ([#493](https://github.com/clauderic/react-sortable-hoc/issues/493)) ([dc1d18f](https://github.com/clauderic/react-sortable-hoc/commit/dc1d18f))
<a name="1.5.1"></a>
## [1.5.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.0...v1.5.1) (2019-01-22)
### Bug Fixes
- update helperContainer prop type definition ([#491](https://github.com/clauderic/react-sortable-hoc/issues/491)) ([fd30383](https://github.com/clauderic/react-sortable-hoc/commit/fd30383))
<a name="1.5.0"></a>
# [1.5.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.4.0...v1.5.0) (2019-01-22)
### Features
- allow helperContainer prop to be a function returning an HTMLElement ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a))
<a name="1.4.0"></a>
# [1.4.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.3.0...v1.4.0) (2019-01-10)
### Bug Fixes
- Fix CommonJS and UMD builds by using Rollup and Babel to generate the bundles ([#474](https://github.com/clauderic/react-sortable-hoc/issues/474))
<a name="1.3.0"></a>
# [1.3.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.2.0...v1.3.0) (2019-01-08)
### Bug Fixes
- helperContainer PropType definition broke server-side rendering ([#471](https://github.com/clauderic/react-sortable-hoc/issues/471)) ([c0eef97](https://github.com/clauderic/react-sortable-hoc/commit/c0eef97))
<a name="1.2.0"></a>
# [1.2.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.2.0) (2019-01-08)
### Features
- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4))
<a name="1.1.0"></a>
# [1.1.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.0.0...v1.1.0) (2019-01-07)
### Features
- added updateBeforeSortStart prop ([162857b](https://github.com/clauderic/react-sortable-hoc/commit/162857b))
<a name="1.0.0"></a>
# [1.0.0](https://github.com/clauderic/react-sortable-hoc/compare/v0.8.4...v1.0.0) (2019-01-07)
### BREAKING CHANGES
- The UMD release no longer includes babel-polyfill, you will need to include your own polyfills in order to support older browsers.
# 0.8.4
- Fix a bug when you use SortableHandle and distance prop [#447](https://github.com/clauderic/react-sortable-hoc/pull/447)
# 0.8.3
- Fix: TouchEvent is undefined in certain browsers, such as Safari [#382](https://github.com/clauderic/react-sortable-hoc/issues/382)
# 0.8.1
- Fix scrolling issues on mobile with anchor tag elements [#380](https://github.com/clauderic/react-sortable-hoc/pull/380)
- Update TypeScript type definition for ContainerGetter to accept Promises that return HTMLElements
# 0.8.0
- Allow `getContainer` to return a promise. This is useful when the container node is rendered by a parent component, since `componentDidMount` fires backwards (from child to parent) [#155](https://github.com/clauderic/react-sortable-hoc/pull/155/)
# 0.7.4
- Fix typo in getLockPixelOffset helper
# 0.7.3
- Fix issues with distance and pressThreshold props on mobile [#378](https://github.com/clauderic/react-sortable-hoc/pull/378)
# 0.7.2
- Fix issues with TypeScript type definitions
# 0.7.1
- Provide TypeScript type definitions out of the box [#377](https://github.com/clauderic/react-sortable-hoc/pull/377)
- Fix potential issues with calling `removeEventListeners` on `componentWillUnmount` if the container node has already unmounted [#376](https://github.com/clauderic/react-sortable-hoc/pull/376)
# 0.7.0
- [Breaking change] Removed lodash dependency. For users wishing to support Internet Explorer, a [polyfill](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill) for Array.prototype.find will be required
- Added `onSortOver` prop that gets invoked when sorting over an element [#278](https://github.com/clauderic/react-sortable-hoc/pull/278)
- Fix `useWindowAsScrollContainer` [#306](https://github.com/clauderic/react-sortable-hoc/pull/306)
# 0.6.8
Update react and react-dom peerdependency requirements for React 16+ [#283](https://github.com/clauderic/react-sortable-hoc/pull/283). Thanks [@jnsdls](https://github.com/jnsdls)!
# 0.6.7
Fixes issues with Jest Snapshot testing trying to serialize the `window` object and running out of memory [#249](https://github.com/clauderic/react-sortable-hoc/issues/249). Thanks [@cameronmcefee](https://github.com/cameronmcefee)!
# 0.6.6
Fixes an issue with Internet Explorer 11 introduced in `0.6.5` [#248](https://github.com/clauderic/react-sortable-hoc/pull/248). Thanks [@humiston](https://github.com/humiston)!
# 0.6.5
Fixes the position of the sortable helper when the page is scrolled [#213](https://github.com/clauderic/react-sortable-hoc/pull/213)
# 0.6.4
Fix: when cloning the element that is being sorted, we no longer update the value of cloned file inputs [#232](https://github.com/clauderic/react-sortable-hoc/pull/232)
# 0.6.3
Fixes issues caused by a disabled SortableElement being moved when `distance` is set to a value other than `0`
# 0.6.2
Use `prop-types` package for PropType validation for compatibility with React ^15.5
# 0.6.1
Tweak: default to `pointerEvents: none` on sortable helper, this way the underlying view can still be scrolled using the trackpad/mousewheel while sorting [#160](https://github.com/clauderic/react-sortable-hoc/pull/160)
# 0.6.0
Feature: added `pressThreshold` prop to make `pressDelay` fault tolerant [#159](https://github.com/clauderic/react-sortable-hoc/pull/159)
# 0.5.0
Tweak: `button` elements are now included in the default `shouldCancelStart` implementation [#142](https://github.com/clauderic/react-sortable-hoc/pull/142).
Fix: Omit `getHelperDimensions` before passing down props in `SortableContainer`
# 0.4.12
Fix: This release fixes some issues caused by the `onSortEnd` callback being invoked before `setState` [#82](https://github.com/clauderic/react-sortable-hoc/issues/82).
# 0.4.10
Fix: This version fixes issues with nested `SortableContainer` elements using drag handles from also dragging their parent [#112](https://github.com/clauderic/react-sortable-hoc/issues/112), #127(https://github.com/clauderic/react-sortable-hoc/pull/127). Thanks [@DeadHeadRussell](https://github.com/DeadHeadRussell)!
# 0.4.9
Fix: This release fixes a bug introduced in `0.4.8` caused by calling the `forEach` method directly on a NodeList, which is undefined in a number of browsers [#125](https://github.com/clauderic/react-sortable-hoc/issues/125)
# 0.4.8
Fix: Added logic to ensure that `select`, `input` and `textarea` fields in `SortableElement` always retain their `value` when the element is cloned (this happens when sorting begins) [#122](https://github.com/clauderic/react-sortable-hoc/issues/122) [#123](https://github.com/clauderic/react-sortable-hoc/pull/123). Thanks [@tomasztomys](https://github.com/tomasztomys)!
# 0.4.7
Fix: This release fixes a bug in Firefox caused by active anchor tags preventing mousemove events from being fired [#118](https://github.com/clauderic/react-sortable-hoc/issues/118)
# 0.4.5
Fix: getHelperDimensions height was not being used (Thanks [@SMenigat](https://github.com/SMenigat)!)
# 0.4.4
Tweak: cherry-picking lodash methods instead of importing the entire bundle (slipped by in a PR, thanks for pointing this out [@arackaf](https://github.com/arackaf)!)
# 0.4.3
Fixes an edge-case bug in Firefox where window.getComputedStyle() returns null inside an iframe with `display: none` [#106](https://github.com/clauderic/react-sortable-hoc/pull/106). Thanks [@funnel-mark](https://github.com/funnel-mark)!
# 0.4.2
Fixes an issue when attempting to sort items while rapidly moving the mouse. By setting an immediate timer, we move the cancel event to the tail of the timer queue, and ensure that it is fired after the pressTimer [#80](https://github.com/clauderic/react-sortable-hoc/pull/80). Thanks [@v0lkan](https://github.com/v0lkan)!
# 0.4.0
- Fix a timing issue in Chrome caused by setTimeout [#71](https://github.com/clauderic/react-sortable-hoc/pull/71)
- Private props are no longer passed down to the wrapped component [#98](https://github.com/clauderic/react-sortable-hoc/pull/98)
# 0.3.0
Added grid support for elements of equal widths / heights [#4](https://github.com/clauderic/react-sortable-hoc/issues/4) [#86](https://github.com/clauderic/react-sortable-hoc/pull/86). Huge shout-out to [@richmeij](https://github.com/richmeij) for making this happen!
# 0.2.0
Add a `getHelperDimensions` prop to control SortableHelper size [#83](https://github.com/clauderic/react-sortable-hoc/issues/83). Thanks [@nervetattoo](https://github.com/nervetattoo)!
# 0.1.1
Added `touchCancel` listener to properly handle canceled touches [#73](https://github.com/clauderic/react-sortable-hoc/pull/73)
# 0.1.0
- Force `box-sizing: border-box` on sortable helper [#67](https://github.com/clauderic/react-sortable-hoc/issues/67)
- Support changing an item's collection prop on the fly [#66](https://github.com/clauderic/react-sortable-hoc/pull/66)
# 0.0.11
Utilize babel-plugin-transform-runtime to utilize `babelHelpers` without them being required in application code [#45](https://github.com/clauderic/react-sortable-hoc/issues/45)
# 0.0.10
The `arrayMove` helper no longer mutates the array, it now returns a new array [#61](https://github.com/clauderic/react-sortable-hoc/issues/61)
# 0.0.9
Server-side rendering bugfix: safeguard against `document` being undefined [#59](https://github.com/clauderic/react-sortable-hoc/pull/59)
# 0.0.8
- Added `distance` prop ([#35](https://github.com/clauderic/react-sortable-hoc/issues/35))
- Added a `shouldCancelStart` ([#47](https://github.com/clauderic/react-sortable-hoc/issues/47), [#36](https://github.com/clauderic/react-sortable-hoc/issues/36), [#41](https://github.com/clauderic/react-sortable-hoc/issues/41)) prop to programatically cancel sorting before it begins.
- Prevent right click from causing sort start ([#46](https://github.com/clauderic/react-sortable-hoc/issues/46))
# 0.0.7
Fixes server-side rendering (window undefined) ([#39](https://github.com/clauderic/react-sortable-hoc/issues/39))
# 0.0.6
- Added support for a custom container ([#37](https://github.com/clauderic/react-sortable-hoc/issues/37))
- Fix changing disable property while receiving props ([#34](https://github.com/clauderic/react-sortable-hoc/issues/34))
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016, Claudéric Demers
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
> **Warning**
>
> This library is **no longer actively maintained**. It will continue to receive critical security updates, but there are no new features planned.
> In future versions of React, the [`findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode) method will be deprecated. This method is a critical piece of the architecture of `react-sortable-hoc`, and the library will stop working in the future when that method is removed from `react-dom`.
>
> All development efforts have been redirected towards [**@dnd-kit**](https://github.com/clauderic/dnd-kit). It provides feature parity, built with a modern and extensible architecture, supports complex use-cases and has accessibility features built-in. New consumers are strongly encouraged to adopt **@dnd-kit** instead of adopting `react-sortable-hoc`.
<a href="https://github.com/clauderic/dnd-kit"><img alt="Visit @dnd-kit github repository" src=".github/assets/dnd-kit-banner.svg" /></a>
# <img src=".github/assets/react-sortable-hoc-logo.png" width="400" alt="React Sortable HOC" />
> A set of higher-order components to turn any list into an animated, accessible and touch-friendly sortable list
[](https://www.npmjs.com/package/react-sortable-hoc)
[](https://www.npmjs.com/package/react-sortable-hoc)
[](https://github.com/clauderic/react-sortable-hoc/blob/master/LICENSE)
[](https://gitter.im/clauderic/react-sortable-hoc)

### Examples available here: <a href="#">http://clauderic.github.io/react-sortable-hoc/</a>
## Features
- **Higher Order Components** – Integrates with your existing components
- **Drag handle, auto-scrolling, locked axis, events, and more!**
- **Suuuper smooth animations** – Chasing the 60FPS dream 🌈
- **Works with virtualization libraries: [react-virtualized](https://github.com/bvaughn/react-virtualized/), [react-tiny-virtual-list](https://github.com/clauderic/react-tiny-virtual-list), [react-infinite](https://github.com/seatgeek/react-infinite), etc.**
- **Horizontal lists, vertical lists, or a grid** ↔ ↕ ⤡
- **Touch support** 👌
- **Accessible: supports keyboard sorting**
## Installation
Using [npm](https://www.npmjs.com/package/react-sortable-hoc):
$ npm install react-sortable-hoc --save
Then, using a module bundler that supports either CommonJS or ES2015 modules, such as [webpack](https://github.com/webpack/webpack):
```js
// Using an ES6 transpiler like Babel
import {SortableContainer, SortableElement} from 'react-sortable-hoc';
// Not using an ES6 transpiler
var Sortable = require('react-sortable-hoc');
var SortableContainer = Sortable.SortableContainer;
var SortableElement = Sortable.SortableElement;
```
Alternatively, an UMD build is also available:
```html
<script src="react-sortable-hoc/dist/react-sortable-hoc.umd.js"></script>
```
## Usage
### Basic Example
```js
import React, {Component} from 'react';
import {render} from 'react-dom';
import {SortableContainer, SortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
const SortableItem = SortableElement(({value}) => <li>{value}</li>);
const SortableList = SortableContainer(({items}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${value}`} index={index} value={value} />
))}
</ul>
);
});
class SortableComponent extends Component {
state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'],
};
onSortEnd = ({oldIndex, newIndex}) => {
this.setState(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
render() {
return <SortableList items={this.state.items} onSortEnd={this.onSortEnd} />;
}
}
render(<SortableComponent />, document.getElementById('root'));
```
That's it! React Sortable does not come with any styles by default, since it's meant to enhance your existing components.
More code examples are available [here](https://github.com/clauderic/react-sortable-hoc/blob/master/examples/).
## Why should I use this?
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.
### Prop Types
#### SortableContainer HOC
| Property | Type | Default | Description |
| :-------------------------------- | :-------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| axis | String | `y` | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy` |
| 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`. |
| helperClass | String | | You can provide a class you'd like to add to the sortable helper to add some styles to it |
| transitionDuration | Number | `300` | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions |
| 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 |
| keyCodes | Array<Number> | `{`<br/> `lift: [32],`<br/> `drop: [32],`<br/> `cancel: [27],`<br/> `up: [38, 37],`<br/> `down: [40, 39]`<br/>`}` | An object containing an array of keycodes for each keyboard-accessible action. |
| 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. |
| pressThreshold | Number | `5` | Number of pixels of movement to tolerate before ignoring a press event. |
| 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. |
| shouldCancelStart | Function | [Function](https://github.com/clauderic/react-sortable-hoc/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`. |
| 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)` |
| onSortStart | Function | | Callback that is invoked when sorting begins. `function({node, index, collection, isKeySorting}, event)` |
| onSortMove | Function | | Callback that is invoked during sorting as the cursor moves. `function(event)` |
| onSortOver | Function | | Callback that is invoked when moving over an item. `function({index, oldIndex, newIndex, collection, isKeySorting}, e)` |
| onSortEnd | Function | | Callback that is invoked when sorting ends. `function({oldIndex, newIndex, collection, isKeySorting}, e)` |
| useDragHandle | Boolean | `false` | If you're using the `SortableHandle` HOC, set this to `true` |
| useWindowAsScrollContainer | Boolean | `false` | If you want, you can set the `window` as the scrolling container |
| 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. |
| lockToContainerEdges | Boolean | `false` | You can lock movement of the sortable element to it's parent `SortableContainer` |
| 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%"]`). |
| 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. |
| getHelperDimensions | Function | [Function](https://github.com/clauderic/react-sortable-hoc/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/react-sortable-hoc/blob/master/src/SortableContainer/defaultGetHelperDimensions.js) for more details |
| 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 |
| disableAutoscroll | Boolean | `false` | Disables autoscrolling while dragging |
\* `OffsetValue` can either be a finite `Number` or a `String` made up of a number and a unit (`px` or `%`).
Examples: `10` (which is the same as `"10px"`), `"50%"`
#### SortableElement HOC
| Property | Type | Default | Required? | Description |
| :--------- | :--------------- | :------ | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| index | Number | | ✓ | This is the element's sortableIndex within it's collection. This prop is required. |
| 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/react-sortable-hoc/#/basic-configuration/multiple-lists) |
| disabled | Boolean | `false` | | Whether the element should be sortable or not |
## FAQ
### Running Examples
In root folder, run the following commands to launch React Storybook:
```
$ npm install
$ npm start
```
### Accessibility
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`.
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.
### Grid support
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.
### Item disappearing when sorting / CSS issues
Upon sorting, `react-sortable-hoc` creates a clone of the element you are sorting (the _sortable-helper_) and appends it to the end of the `<body>` 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 `<body>`). This can also be a `z-index` issue, for example, when using `react-sortable-hoc` within a Bootstrap modal, you'll need to increase the `z-index` of the SortableHelper so it is displayed on top of the modal (see [#87](https://github.com/clauderic/react-sortable-hoc/issues/87) for more details).
### Click events being swallowed
By default, `react-sortable-hoc` 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, you can also use the [SortableHandle](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableHandle/index.js) HOC.
### Wrapper props not passed down to wrapped Component
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.:
```js
const SortableItem = SortableElement(({value, sortIndex}) => (
<li>
{value} - #{sortIndex}
</li>
));
const SortableList = SortableContainer(({items}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
sortIndex={index}
value={value}
/>
))}
</ul>
);
});
```
## Dependencies
React Sortable HOC only depends on [invariant](https://github.com/zertosh/invariant). It has the following peerDependencies: `react`, `react-dom`
## Reporting Issues
If believe you've found an issue, please [report it](https://github.com/clauderic/react-sortable-hoc/issues) along with any relevant details to reproduce it. The easiest way to do so is to fork the `react-sortable-hoc` basic setup sandbox on [CodeSandbox](https://codesandbox.io/s/o104x95y86):
[](https://codesandbox.io/s/react-sortable-hoc-starter-o104x95y86)
## Asking for help
Please do not use the issue tracker for personal support requests. Instead, use [Gitter](https://gitter.im/clauderic/react-sortable-hoc) or StackOverflow.
## Contributions
Yes please! Feature requests / pull requests are welcome.
================================================
FILE: examples/.eslintrc.json
================================================
{
"rules": {
"import/no-unresolved": "off",
"react/prop-types": "off",
"react/no-array-index-key": "off"
}
}
================================================
FILE: examples/basic.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {sortableContainer, sortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
const SortableItem = sortableElement(({value}) => <li>{value}</li>);
const SortableContainer = sortableContainer(({children}) => {
return <ul>{children}</ul>;
});
class App extends Component {
state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'],
};
onSortEnd = ({oldIndex, newIndex}) => {
this.setState(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
render() {
const {items} = this.state;
return (
<SortableContainer onSortEnd={this.onSortEnd}>
{items.map((value, index) => (
<SortableItem key={`item-${value}`} index={index} value={value} />
))}
</SortableContainer>
);
}
}
render(<App />, document.getElementById('root'));
================================================
FILE: examples/collections.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {sortableContainer, sortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
const SortableItem = sortableElement(({value}) => <li>{value}</li>);
const SortableContainer = sortableContainer(({children}) => {
return <div>{children}</div>;
});
class App extends Component {
state = {
collections: [[0, 1, 2], [0, 1, 2, 3, 4], [0, 1, 2]],
};
onSortEnd = ({oldIndex, newIndex, collection}) => {
this.setState(({collections}) => {
const newCollections = [...collections];
newCollections[collection] = arrayMove(
collections[collection],
oldIndex,
newIndex,
);
return {collections: newCollections};
});
};
render() {
const {collections} = this.state;
return (
<SortableContainer onSortEnd={this.onSortEnd}>
{collections.map((items, index) => (
<React.Fragment key={index}>
<strong>LIST {index}</strong>
<ul>
{items.map((item, i) => (
<SortableItem
key={item}
value={`Item ${item}`}
index={i}
collection={index}
/>
))}
</ul>
</React.Fragment>
))}
</SortableContainer>
);
}
}
render(<App />, document.getElementById('root'));
================================================
FILE: examples/drag-handle.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {
sortableContainer,
sortableElement,
sortableHandle,
} from 'react-sortable-hoc';
import arrayMove from 'array-move';
const DragHandle = sortableHandle(() => <span>::</span>);
const SortableItem = sortableElement(({value}) => (
<li>
<DragHandle />
{value}
</li>
));
const SortableContainer = sortableContainer(({children}) => {
return <ul>{children}</ul>;
});
class App extends Component {
state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'],
};
onSortEnd = ({oldIndex, newIndex}) => {
this.setState(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
render() {
const {items} = this.state;
return (
<SortableContainer onSortEnd={this.onSortEnd} useDragHandle>
{items.map((value, index) => (
<SortableItem key={`item-${value}`} index={index} value={value} />
))}
</SortableContainer>
);
}
}
render(<App />, document.getElementById('root'));
================================================
FILE: examples/react-infinite.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {sortableContainer, sortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
import Infinite from 'react-infinite';
const SortableItem = sortableElement(({height, value}) => {
return <li style={{height}}>{value}</li>;
});
const SortableInfiniteList = sortableContainer(({items}) => {
return (
<Infinite
containerHeight={600}
elementHeight={items.map(({height}) => height)}
>
{items.map(({value, height}, index) => (
<SortableItem
key={`item-${value}`}
index={index}
value={value}
height={height}
/>
))}
</Infinite>
);
});
class App extends Component {
state = {
items: [
{value: 'Item 1', height: 89},
{value: 'Item 2', height: 59},
{value: 'Item 3', height: 130},
{value: 'Item 4', height: 59},
{value: 'Item 5', height: 200},
{value: 'Item 6', height: 150},
],
};
onSortEnd = ({oldIndex, newIndex}) => {
this.setState(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
render() {
const {items} = this.state;
return <SortableInfiniteList items={items} onSortEnd={this.onSortEnd} />;
}
}
render(<App />, document.getElementById('root'));
================================================
FILE: examples/react-virtualized-table-columns.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {Table, Column} from 'react-virtualized';
import {sortableContainer, sortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
import 'react-virtualized/styles.css';
const ROW_HEIGHT = 30;
const HEADER_ROW_HEIGHT = 20;
const COL_WIDTH = 100;
const SortableHeader = sortableElement(({children, ...props}) =>
React.cloneElement(children, props),
);
const SortableHeaderRowRenderer = sortableContainer(
({className, columns, style}) => (
<div className={className} role="row" style={style}>
{React.Children.map(columns, (column, index) => (
<SortableHeader index={index}>{column}</SortableHeader>
))}
</div>
),
);
class TableWithSortableColumns extends Component {
state = {
cols: [
{dataKey: 'col1', label: 'Column 1'},
{dataKey: 'col2', label: 'Column 2'},
{dataKey: 'col3', label: 'Column 3'},
],
rows: [
{col1: 'row1 col1', col2: 'row1 col2', col3: 'row1 col3'},
{col1: 'row2 col1', col2: 'row2 col2', col3: 'row2 col3'},
{col1: 'row3 col1', col2: 'row3 col2', col3: 'row3 col3'},
],
};
onSortEnd = ({oldIndex, newIndex}) => {
this.setState(({cols}) => ({
cols: arrayMove(cols, oldIndex, newIndex),
}));
};
getRow = ({index}) => {
const {rows} = this.state;
return rows[index];
};
renderHeaderRow = (params) => {
return (
<SortableHeaderRowRenderer
{...params}
axis="x"
lockAxis="x"
onSortEnd={this.onSortEnd}
/>
);
};
render() {
const {rows, cols} = this.state;
return (
<Table
width={COL_WIDTH * rows.length}
height={HEADER_ROW_HEIGHT + ROW_HEIGHT * rows.length}
headerHeight={ROW_HEIGHT}
rowHeight={ROW_HEIGHT}
rowCount={rows.length}
rowGetter={this.getRow}
headerRowRenderer={this.renderHeaderRow}
>
{cols.map((col) => (
<Column {...col} key={col.dataKey} width={COL_WIDTH} />
))}
</Table>
);
}
}
render(<TableWithSortableColumns />, document.getElementById('root'));
================================================
FILE: examples/react-virtualized.js
================================================
import React, {Component} from 'react';
import {render} from 'react-dom';
import {sortableContainer, sortableElement} from 'react-sortable-hoc';
import arrayMove from 'array-move';
import {List} from 'react-virtualized';
const SortableItem = sortableElement(({value}) => {
return <li>{value}</li>;
});
class VirtualList extends Component {
renderRow = ({index}) => {
const {items} = this.props;
const {value} = items[index];
return <SortableItem index={index} value={value} />;
};
getRowHeight = ({index}) => {
const {items} = this.props;
return items[index].height;
};
render() {
const {items, getRef} = this.props;
return (
<List
ref={getRef}
rowHeight={this.getRowHeight}
rowRenderer={this.renderRow}
rowCount={items.length}
width={400}
height={600}
/>
);
}
}
const SortableVirtualList = sortableContainer(VirtualList);
class App extends Component {
state = {
items: [
{value: 'Item 1', height: 89},
{value: 'Item 2', height: 59},
{value: 'Item 3', height: 130},
{value: 'Item 4', height: 59},
{value: 'Item 5', height: 200},
{value: 'Item 6', height: 150},
],
};
registerListRef = (listInstance) => {
this.List = listInstance;
};
onSortEnd = ({oldIndex, newIndex}) => {
if (oldIndex === newIndex) {
return;
}
const {items} = this.state;
this.setState({
items: arrayMove(items, oldIndex, newIndex),
});
// We need to inform React Virtualized that the items have changed heights
// This can either be done by imperatively calling the recomputeRowHeights and
// forceUpdate instance methods on the `List` ref, or by passing an additional prop
// to List that changes whenever the order changes to force it to re-render
this.List.recomputeRowHeights();
this.List.forceUpdate();
};
render() {
const {items} = this.state;
return (
<SortableVirtualList
getRef={this.registerListRef}
items={items}
onSortEnd={this.onSortEnd}
/>
);
}
}
render(<App />, document.getElementById('root'));
================================================
FILE: package.json
================================================
{
"name": "react-sortable-hoc",
"version": "2.0.0",
"description": "Set of higher-order components to turn any list into a sortable, touch-friendly, animated list",
"author": {
"name": "Clauderic Demers",
"email": "me@ced.io"
},
"user": "clauderic",
"homepage": "https://github.com/clauderic/react-sortable-hoc",
"source": "src/index.js",
"main": "dist/react-sortable-hoc.js",
"umd:main": "dist/react-sortable-hoc.umd.js",
"module": "dist/react-sortable-hoc.esm.js",
"jsnext:main": "dist/react-sortable-hoc.esm.js",
"types": "types/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/clauderic/react-sortable-hoc.git"
},
"bugs": {
"url": "https://github.com/clauderic/react-sortable-hoc/issues"
},
"keywords": [
"react",
"reactjs",
"react-component",
"sortable",
"sortable-list",
"list",
"sortable list",
"smooth",
"animated",
"hoc",
"higher-order",
"component"
],
"scripts": {
"start": "start-storybook -p 9001 -c .storybook",
"build": "rollup -c",
"test": "eslint src/** --ext .js --quiet",
"release": "standard-version --no-verify"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"dependencies": {
"@babel/runtime": "^7.13.0",
"invariant": "^2.2.4",
"prop-types": "^15.5.7"
},
"peerDependencies": {
"prop-types": "^15.5.7",
"react": "^16.3.0 || ^17.0.0",
"react-dom": "^16.3.0 || ^17.0.0"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.3",
"@babel/plugin-transform-runtime": "^7.2.0",
"@babel/preset-env": "^7.2.3",
"@babel/preset-react": "^7.0.0",
"@storybook/addon-options": "^5.1.11",
"@storybook/react": "^5.1.11",
"@storybook/theming": "^5.1.11",
"array-move": "^1.0.0",
"autoprefixer": "^6.3.6",
"babel-loader": "^8.0.5",
"babel-plugin-transform-async-to-promises": "^0.8.4",
"classnames": "^2.2.5",
"css-loader": "^2.1.0",
"eslint": "^6.2.1",
"eslint-config-prettier": "^6.1.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-shopify": "^30.0.1",
"extract-text-webpack-plugin": "^1.0.1",
"html-webpack-plugin": "^2.16.1",
"husky": "^3.0.4",
"lodash": "^4.12.0",
"node-sass": "^4.11.0",
"postcss": "^7.0.7",
"postcss-loader": "^3.0.0",
"prettier": "^1.18.2",
"pretty-quick": "^1.11.1",
"react": "^16.7.0",
"react-addons-pure-render-mixin": "^15.0.2",
"react-addons-shallow-compare": "^15.1.0",
"react-addons-test-utils": "^15.1.0",
"react-dom": "^16.7.0",
"react-infinite": "^0.13.0",
"react-inspector": "^3.0.2",
"react-tiny-virtual-list": "^2.0.1",
"react-virtualized": "^9.2.2",
"react-window": "^1.6.2",
"rollup": "^1.0.0",
"rollup-plugin-babel": "^4.2.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-filesize": "^6.0.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-uglify": "^6.0.0",
"sass-loader": "^7.1.0",
"standard-version": "^4.4.0",
"style-loader": "^0.23.1"
}
}
================================================
FILE: rollup.config.js
================================================
import replace from 'rollup-plugin-replace';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import filesize from 'rollup-plugin-filesize';
import {uglify} from 'rollup-plugin-uglify';
import pkg from './package.json';
const external = (id) => !id.startsWith('.') && !id.startsWith('/');
const babelConfig = (
{useESModules, targets} = {
useESModules: true,
targets: {browsers: 'last 2 versions'},
},
) => ({
comments: false,
runtimeHelpers: true,
presets: [
'@babel/preset-react',
[
'@babel/preset-env',
{
targets,
},
],
],
plugins: [
'@babel/plugin-proposal-class-properties',
['@babel/transform-runtime', {useESModules, regenerator: false}],
['babel-plugin-transform-async-to-promises', {inlineHelpers: true}],
],
exclude: 'node_modules/**',
});
const umdConfig = ({minify} = {}) => ({
input: pkg.source,
external: ['react', 'react-dom', 'prop-types'],
output: {
name: 'SortableHOC',
file: minify ? pkg["umd:main"].replace('.js', '.min.js') : pkg["umd:main"],
format: 'umd',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'prop-types': 'PropTypes',
},
},
plugins: [
resolve(),
babel(
babelConfig({
targets: {browsers: ['last 2 versions', 'safari >= 7']},
}),
),
replace({
'process.env.NODE_ENV': JSON.stringify(
minify ? 'production' : 'development',
),
}),
commonjs(),
minify ? uglify() : { },
filesize(),
],
});
const rollupConfig = [
// Browser-friendly UMD builds
umdConfig(),
umdConfig({minify: true}),
// CommonJS
{
input: pkg.source,
external,
output: [{file: pkg.main, format: 'cjs'}],
plugins: [resolve(), babel(babelConfig({useESModules: false})), filesize()],
},
// ES module
{
input: pkg.source,
external,
output: [{file: pkg.module, format: 'esm'}],
plugins: [resolve(), babel(babelConfig()), filesize()],
},
];
export default rollupConfig;
================================================
FILE: src/.stories/Storybook.scss
================================================
@import url(https://fonts.googleapis.com/css?family=Montserrat:400);
$focusedOutlineColor: #4c9ffe;
.root {
display: flex;
height: 100%;
box-sizing: border-box;
flex-direction: column;
justify-content: center;
align-items: center;
}
// Base styles
.list {
width: 400px;
height: 600px;
overflow: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid #999;
}
.item {
position: relative;
border-bottom: 1px solid #999;
cursor: grab;
touch-action: manipulation;
&.sorting {
pointer-events: none;
}
}
.containsDragHandle {
cursor: default;
}
// Stylized
.stylizedList {
position: relative;
z-index: 0;
background-color: #f3f3f3;
border: 1px solid #efefef;
border-radius: 3px;
outline: none;
}
.stylizedItem {
display: flex;
align-items: center;
width: 100%;
padding: 0 20px;
background-color: #fff;
border-bottom: 1px solid #efefef;
box-sizing: border-box;
user-select: none;
outline: none;
color: #333;
font-weight: 400;
&:focus:not(.containsDragHandle) {
text-indent: -2px;
border: 2px solid $focusedOutlineColor;
}
}
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
// Drag handle
.handleWrapper {
width: 18px;
height: 18px;
outline: none;
}
.handle {
display: block;
width: 18px;
height: 18px;
margin-right: 20px;
overflow: hidden;
> svg {
opacity: 0.3;
}
cursor: grab;
}
// Horizontal list
.horizontalList {
display: flex;
width: 600px;
height: 300px;
white-space: nowrap;
}
.horizontalItem {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 200px;
border-right: 1px solid #efefef;
border-bottom: 0;
}
// Grid
.grid {
display: grid;
height: 130 * 3px + 20px;
grid-gap: 10px;
grid-template-columns: auto auto auto auto;
width: auto;
white-space: nowrap;
border: 0;
background-color: transparent;
}
.gridItem {
width: 130px;
height: 130px;
padding: 0;
border: none;
background-color: transparent;
.wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: #fff;
transform: scale(1);
font-size: 28px;
span {
display: none;
}
}
}
.gridVariableSized {
.gridItem {
&[data-index='0'] {
width: auto !important;
height: auto !important;
grid-column-end: span 2;
grid-row-end: span 2;
}
&.sorting {
.wrapper {
transition: transform 150ms ease-in-out;
}
}
}
}
.gridItemVariableSized {
&[data-index='0'] {
.wrapper {
font-size: 56px;
}
}
}
// Nested
.category {
height: auto;
.categoryHeader {
display: flex;
flex-flow: row nowrap;
align-items: center;
padding: 10px 14px;
background: #f9f9f9;
border-bottom: 1px solid #efefef;
user-select: none;
}
.categoryList {
height: auto;
}
}
// Divider
.divider {
padding: 10px 20px;
background: #f9f9f9;
border-bottom: 1px solid #efefef;
text-transform: uppercase;
font-size: 14px;
color: #333;
}
// Helper styles
.helper {
box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2),
0 -5px 5px -5px rgba(0, 0, 0, 0.2);
cursor: grabbing;
}
.stylizedHelper {
&:not(.gridItem),
&.gridItem .wrapper {
border: 1px solid #efefef;
box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 3px;
&.horizontalItem {
cursor: col-resize;
}
&:focus {
box-shadow: 0 0px 5px 1px $focusedOutlineColor;
}
}
&.gridItem .wrapper {
transition: transform 150ms ease-in-out;
}
}
.shrinkedHelper {
height: 20px !important;
}
:global {
body {
font-family: 'Montserrat', 'Helvetica Neue', 'Helvetica', arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: rgba(#f4f5f9, 0.7);
}
html,
body,
#root {
height: 100%;
margin: 0;
}
}
================================================
FILE: src/.stories/grouping-items/Item/Item.js
================================================
import React from 'react';
import classNames from 'classnames';
import {sortableElement} from '../../../../src';
import styles from './Item.scss';
const ENTER_KEY = 13;
function Item(props) {
const {
dragging,
sorting,
onClick,
selected,
selectedItemsCount,
value,
} = props;
const shouldRenderItemCountBadge = dragging && selectedItemsCount > 1;
return (
<div
className={classNames(
styles.Item,
selected && !dragging && styles.selected,
dragging && styles.dragging,
sorting && styles.sorting,
)}
onClick={() => onClick(value)}
onKeyPress={(event) => {
if (event.which === ENTER_KEY) {
onClick(value);
}
}}
tabIndex={0}
>
Item {value}
{shouldRenderItemCountBadge ? <Badge count={selectedItemsCount} /> : null}
</div>
);
}
function Badge(props) {
return <div className={styles.Badge}>{props.count}</div>;
}
export default sortableElement(Item);
================================================
FILE: src/.stories/grouping-items/Item/Item.scss
================================================
$color: #333;
$white: #fff;
$backgroundColor: $white;
$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);
$fontWeight-regular: 400;
$fontWeight-bold: 600;
$borderRadius: 3px;
$borderWidth: 1px;
$borderColor: #efefef;
$selectedColor: $white;
$selectedBackgroundColor: rgba(216, 232, 251, 0.9);
$selectedBorderColor: #bbcee8;
$badgeColor: $white;
$badgeBackgroundColor: #f75959;
$badgeBorderColor: #da4553;
$focusedOutlineColor: #4c9ffe;
.Item {
display: flex;
align-items: center;
width: 100%;
height: 59px;
padding: 0 20px;
background-color: $backgroundColor;
border-bottom: $borderWidth solid #efefef;
box-sizing: border-box;
user-select: none;
outline: none;
color: $color;
font-weight: $fontWeight-regular;
cursor: grab;
&:last-child {
border-bottom: none;
}
&.selected {
background: $selectedBackgroundColor;
border-bottom-color: $selectedBorderColor;
&:focus {
border-bottom-color: $focusedOutlineColor;
}
}
&.sorting {
pointer-events: none;
}
&.dragging {
border-radius: $borderRadius;
border: $borderWidth solid #efefef;
box-shadow: $boxShadow;
&:focus {
box-shadow: 0 0px 5px 1px $focusedOutlineColor;
}
}
&:focus {
text-indent: -2px;
border: 2px solid $focusedOutlineColor;
}
}
.Badge {
position: absolute;
top: -8px;
right: -8px;
padding: 0.35em 0.5em;
border-radius: 0.3rem;
color: $badgeColor;
font-size: 0.8em;
font-weight: $fontWeight-bold;
background-color: $badgeBackgroundColor;
border: $borderWidth solid $badgeBorderColor;
}
================================================
FILE: src/.stories/grouping-items/Item/index.js
================================================
import Item from './Item';
export default Item;
================================================
FILE: src/.stories/grouping-items/List/List.js
================================================
import React from 'react';
import {sortableContainer} from '../../../../src';
import Item from '../Item';
import styles from './List.scss';
function List({items, isSorting, selectedItems, sortingItemKey, onItemSelect}) {
return (
<div className={styles.List}>
{items.map((value, index) => {
const isSelected = selectedItems.includes(value);
const itemIsBeingDragged = sortingItemKey === value;
return (
<Item
key={`item-${value}`}
selected={isSelected}
dragging={itemIsBeingDragged}
sorting={isSorting}
index={index}
value={value}
onClick={onItemSelect}
selectedItemsCount={selectedItems.length}
/>
);
})}
</div>
);
}
export default sortableContainer(List);
================================================
FILE: src/.stories/grouping-items/List/List.scss
================================================
$backgroundColor: #f3f3f3;
$borderColor: #efefef;
$borderWidth: 1px;
.List {
position: relative;
width: 400px;
height: 600px;
overflow: auto;
-webkit-overflow-scrolling: touch;
z-index: 0;
background-color: $backgroundColor;
border: $borderWidth solid $borderColor;
border-radius: 3px;
outline: none;
}
================================================
FILE: src/.stories/grouping-items/List/index.js
================================================
import List from './List';
export default List;
================================================
FILE: src/.stories/grouping-items/index.js
================================================
import React from 'react';
import arrayMove from 'array-move';
import {generateItems} from './utils';
import SortableList from './List';
class GroupedItems extends React.Component {
state = {
selectedItems: [],
items: generateItems(50),
};
render() {
const {items, isSorting, selectedItems, sortingItemKey} = this.state;
return (
<SortableList
items={items.filter(this.filterItems)}
isSorting={isSorting}
sortingItemKey={sortingItemKey}
selectedItems={selectedItems}
onItemSelect={this.handleItemSelect}
shouldCancelStart={this.handleShouldCancelStart}
updateBeforeSortStart={this.handleUpdateBeforeSortStart}
onSortStart={this.handleSortStart}
onSortEnd={this.handleSortEnd}
distance={3}
/>
);
}
filterItems = (value) => {
const {selectedItems, sortingItemKey, isSorting} = this.state;
// Do not hide the ghost of the element currently being sorted
if (sortingItemKey === value) {
return true;
}
// Hide the other items that are selected
if (isSorting && selectedItems.includes(value)) {
return false;
}
// Do not hide any other items
return true;
};
handleUpdateBeforeSortStart = ({index}) => {
return new Promise((resolve) =>
this.setState(
({items}) => ({
sortingItemKey: items[index],
isSorting: true,
}),
resolve,
),
);
};
handleSortStart() {
document.body.style.cursor = 'grabbing';
}
handleSortEnd = ({oldIndex, newIndex}) => {
const {selectedItems} = this.state;
let newItems;
if (selectedItems.length) {
const items = this.state.items.filter(
(value) => !selectedItems.includes(value),
);
newItems = [
...items.slice(0, newIndex),
...selectedItems,
...items.slice(newIndex, items.length),
];
} else {
newItems = arrayMove(this.state.items, oldIndex, newIndex);
}
this.setState({
items: newItems,
isSorting: false,
sortingItemKey: null,
selectedItems: [],
});
document.body.style.cursor = '';
};
handleItemSelect = (item) => {
this.setState(({selectedItems}) => {
if (selectedItems.includes(item)) {
return {
selectedItems: selectedItems.filter((value) => value !== item),
};
}
return {
selectedItems: [...selectedItems, item],
};
});
};
handleShouldCancelStart = (event) => {
const {items, selectedItems} = this.state;
const item = items[event.target.sortableInfo.index];
// Never cancel start if there are no selected items
if (!selectedItems.length) {
return false;
}
// If there are selected items, we want to cancel sorting
// from starting when dragging elements that are not selected
return !selectedItems.includes(item);
};
}
export default GroupedItems;
================================================
FILE: src/.stories/grouping-items/utils.js
================================================
export function generateItems(length) {
return Array.from(Array(length), (_, index) => index.toString());
}
================================================
FILE: src/.stories/index.js
================================================
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import {storiesOf} from '@storybook/react';
import style from './Storybook.scss';
import {SortableContainer, SortableElement, SortableHandle} from '../index';
import arrayMove from 'array-move';
import VirtualList from 'react-tiny-virtual-list';
import {FixedSizeList, VariableSizeList} from 'react-window';
import {defaultTableRowRenderer, Column, Table, List} from 'react-virtualized';
import '!style-loader!css-loader!react-virtualized/styles.css';
import Infinite from 'react-infinite';
import range from 'lodash/range';
import random from 'lodash/random';
import classNames from 'classnames';
import GroupedItems from './grouping-items';
import InteractiveElements from './interactive-elements-stress-test';
function getItems(count, height) {
var heights = [65, 110, 140, 65, 90, 65];
return range(count).map((value) => {
return {
value,
height: height == null ? heights[random(0, heights.length - 1)] : height,
};
});
}
const Handle = SortableHandle(({tabIndex}) => (
<div className={style.handle} tabIndex={tabIndex}>
<svg viewBox="0 0 50 50">
<path
d="M 0 7.5 L 0 12.5 L 50 12.5 L 50 7.5 L 0 7.5 z M 0 22.5 L 0 27.5 L 50 27.5 L 50 22.5 L 0 22.5 z M 0 37.5 L 0 42.5 L 50 42.5 L 50 37.5 L 0 37.5 z"
color="#000"
/>
</svg>
</div>
));
const Item = SortableElement(
({
tabbable,
className,
isDisabled,
height,
style: propStyle,
shouldUseDragHandle,
value,
itemIndex,
isSorting,
}) => {
const bodyTabIndex = tabbable && !shouldUseDragHandle ? 0 : -1;
const handleTabIndex = tabbable && shouldUseDragHandle ? 0 : -1;
return (
<div
className={classNames(
className,
isDisabled && style.disabled,
isSorting && style.sorting,
shouldUseDragHandle && style.containsDragHandle,
)}
style={{
height,
...propStyle,
}}
tabIndex={bodyTabIndex}
data-index={itemIndex}
>
{shouldUseDragHandle && <Handle tabIndex={handleTabIndex} />}
<div className={style.wrapper}>
<span>Item</span> {value}
</div>
</div>
);
},
);
const SortableList = SortableContainer(
({
className,
items,
disabledItems = [],
itemClass,
isSorting,
shouldUseDragHandle,
type,
}) => {
return (
<div className={className}>
{items.map(({value, height}, index) => {
const disabled = disabledItems.includes(value);
return (
<Item
tabbable
key={`item-${value}`}
disabled={disabled}
isDisabled={disabled}
className={itemClass}
index={index}
itemIndex={index}
value={value}
height={height}
shouldUseDragHandle={shouldUseDragHandle}
type={type}
isSorting={isSorting}
/>
);
})}
</div>
);
},
);
class SortableListWithCustomContainer extends React.Component {
state = {
container: null,
};
render() {
const {container} = this.state;
return (
<div id="CustomHelperContainer" ref={this.setContainerNode}>
<SortableList {...this.props} helperContainer={container} />
</div>
);
}
setContainerNode = (node) => {
this.setState({container: node});
};
}
const Category = SortableElement((props) => {
const tabIndex = props.tabbable ? 0 : -1;
return (
<div className={style.category}>
<div className={style.categoryHeader}>
<Handle tabIndex={tabIndex} />
<span>Category {props.value}</span>
</div>
<ListWrapper
component={SortableList}
className={style.categoryList}
items={getItems(3, 59)}
shouldUseDragHandle={true}
helperClass={style.stylizedHelper}
/>
</div>
);
});
class ListWrapper extends Component {
state = {
items: this.props.items,
isSorting: false,
};
static propTypes = {
items: PropTypes.array,
className: PropTypes.string,
itemClass: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
onSortStart: PropTypes.func,
onSortEnd: PropTypes.func,
component: PropTypes.func,
shouldUseDragHandle: PropTypes.bool,
disabledItems: PropTypes.arrayOf(PropTypes.string),
};
static defaultProps = {
className: classNames(style.list, style.stylizedList),
itemClass: classNames(style.item, style.stylizedItem),
width: 400,
height: 600,
};
onSortStart = (sortEvent, nativeEvent) => {
const {onSortStart} = this.props;
this.setState({isSorting: true});
document.body.style.cursor = 'grabbing';
if (onSortStart) {
onSortStart(sortEvent, nativeEvent, this.refs.component);
}
};
onSortEnd = (sortEvent, nativeEvent) => {
const {onSortEnd} = this.props;
const {oldIndex, newIndex} = sortEvent;
const {items} = this.state;
this.setState({
items: arrayMove(items, oldIndex, newIndex),
isSorting: false,
});
document.body.style.cursor = '';
if (onSortEnd) {
onSortEnd(sortEvent, nativeEvent, this.refs.component);
}
};
render() {
const Component = this.props.component;
const {items, isSorting} = this.state;
const props = {
isSorting,
items,
onSortEnd: this.onSortEnd,
onSortStart: this.onSortStart,
ref: 'component',
useDragHandle: this.props.shouldUseDragHandle,
};
return <Component {...this.props} {...props} />;
}
}
const SortableReactWindow = (Component) =>
SortableContainer(
class ReactWindowList extends React.Component {
render() {
const {className, items, itemHeight, height, width} = this.props;
return (
<Component
ref="VirtualList"
className={classNames(className, style.isSorting)}
itemSize={
itemHeight == null ? (index) => items[index].height : itemHeight
}
itemCount={items.length}
width={width}
height={height}
children={this.renderRow}
/>
);
}
renderRow = ({index, style}) => {
const {items, itemClass, isSorting} = this.props;
const {value, height} = items[index];
return (
<Item
tabbable
key={value}
index={index}
className={itemClass}
value={value}
height={height}
style={style}
isSorting={isSorting}
/>
);
};
},
{withRef: true},
);
const SortableVirtualList = SortableContainer(
({className, items, height, width, itemHeight, itemClass, isSorting}) => {
return (
<VirtualList
className={className}
itemSize={(index) => items[index].height}
estimatedItemSize={itemHeight}
renderItem={({index, style}) => {
const {value, height} = items[index];
return (
<Item
tabbable
key={value}
index={index}
className={itemClass}
value={value}
height={height}
style={style}
isSorting={isSorting}
/>
);
}}
itemCount={items.length}
width={width}
height={height}
/>
);
},
);
// Function components cannot have refs, so we'll be using a class for React Virtualized
class VirtualizedListWrapper extends Component {
render() {
const {
className,
items,
height,
width,
itemHeight,
itemClass,
isSorting,
} = this.props;
return (
<List
ref="VirtualList"
className={className}
rowHeight={({index}) => items[index].height}
estimatedRowSize={itemHeight}
rowRenderer={({index, style}) => {
const {value, height} = items[index];
return (
<Item
tabbable
key={value}
index={index}
className={itemClass}
value={value}
height={height}
style={style}
isSorting={isSorting}
/>
);
}}
rowCount={items.length}
width={width}
height={height}
/>
);
}
}
const SortableVirtualizedList = SortableContainer(VirtualizedListWrapper, {
withRef: true,
});
const SortableTable = SortableContainer(Table, {withRef: true});
const SortableRowRenderer = SortableElement(defaultTableRowRenderer);
class TableWrapper extends Component {
static propTypes = {
items: PropTypes.array,
className: PropTypes.string,
helperClass: PropTypes.string,
itemClass: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
itemHeight: PropTypes.number,
onSortEnd: PropTypes.func,
};
render() {
const {
className,
height,
helperClass,
itemClass,
itemHeight,
items,
onSortEnd,
width,
} = this.props;
return (
<SortableTable
getContainer={(wrappedInstance) =>
ReactDOM.findDOMNode(wrappedInstance.Grid)
}
gridClassName={className}
headerHeight={itemHeight}
height={height}
helperClass={helperClass}
onSortEnd={onSortEnd}
rowClassName={itemClass}
rowCount={items.length}
rowGetter={({index}) => items[index]}
rowHeight={itemHeight}
rowRenderer={(props) => <SortableRowRenderer {...props} />}
width={width}
>
<Column label="Index" dataKey="value" width={100} />
<Column label="Height" dataKey="height" width={width - 100} />
</SortableTable>
);
}
}
const SortableInfiniteList = SortableContainer(
({className, items, itemClass, isSorting}) => {
return (
<Infinite
className={className}
containerHeight={600}
elementHeight={items.map(({height}) => height)}
// for react-infinite, a larger preload is better for keyboard sorting
preloadBatchSize={Infinite.containerHeightScaleFactor(2)}
preloadAdditionalHeight={Infinite.containerHeightScaleFactor(2)}
>
{items.map(({value, height}, index) => (
<Item
tabbable
key={`item-${index}`}
className={itemClass}
index={index}
value={value}
height={height}
isSorting={isSorting}
/>
))}
</Infinite>
);
},
);
const ShrinkingSortableList = SortableContainer(
({className, isSorting, items, itemClass, shouldUseDragHandle}) => {
return (
<div className={className}>
{items.map(({value, height}, index) => (
<Item
tabbable
key={`item-${value}`}
className={itemClass}
index={index}
value={value}
height={isSorting ? 20 : height}
shouldUseDragHandle={shouldUseDragHandle}
isSorting={isSorting}
/>
))}
</div>
);
},
);
const NestedSortableList = SortableContainer(
({className, items, isSorting}) => {
return (
<div className={className}>
{items.map((value, index) => (
<Category
tabbable
key={`category-${value}`}
index={index}
value={value}
/>
))}
</div>
);
},
);
storiesOf('General | Layout / Vertical list', module)
.add('Basic setup', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50, 59)}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Variable heights', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50)}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Nested Lists', () => {
return (
<div className={style.root}>
<ListWrapper
component={NestedSortableList}
items={range(4)}
shouldUseDragHandle={true}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf('General | Layout / Horizontal list', module).add(
'Basic setup',
() => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
axis={'x'}
items={getItems(50, 300)}
helperClass={style.stylizedHelper}
className={classNames(
style.list,
style.stylizedList,
style.horizontalList,
)}
itemClass={classNames(style.stylizedItem, style.horizontalItem)}
/>
</div>
);
},
);
storiesOf('General | Layout / Grid', module)
.add('Basic setup', () => {
const transformOrigin = {
x: 0,
y: 0,
};
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
axis={'xy'}
items={getItems(10, false)}
helperClass={style.stylizedHelper}
className={classNames(style.list, style.stylizedList, style.grid)}
itemClass={classNames(style.stylizedItem, style.gridItem)}
/>
</div>
);
})
.add('Large first item', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
axis={'xy'}
items={getItems(9, false)}
helperClass={style.stylizedHelper}
className={classNames(
style.list,
style.stylizedList,
style.grid,
style.gridVariableSized,
)}
itemClass={classNames(
style.stylizedItem,
style.gridItem,
style.gridItemVariableSized,
)}
onSortStart={({node, helper}, event) => {
const nodeBoundingClientRect = node.getBoundingClientRect();
const helperWrapperNode = helper.childNodes[0];
const transformOrigin = {
x:
((event.clientX - nodeBoundingClientRect.left) /
nodeBoundingClientRect.width) *
100,
y:
((event.clientY - nodeBoundingClientRect.top) /
nodeBoundingClientRect.height) *
100,
};
helperWrapperNode.style.transformOrigin = `${transformOrigin.x}% ${transformOrigin.y}%`;
}}
onSortOver={({nodes, newIndex, index, helper}) => {
const finalNodes = arrayMove(nodes, index, newIndex);
const oldNode = nodes[index].node;
const newNode = nodes[newIndex].node;
const helperScale = newNode.offsetWidth / oldNode.offsetWidth;
const helperWrapperNode = helper.childNodes[0];
helperWrapperNode.style.transform = `scale(${helperScale})`;
finalNodes.forEach(({node}, i) => {
const oldNode = nodes[i].node;
const scale = oldNode.offsetWidth / node.offsetWidth;
const wrapperNode = node.querySelector(`.${style.wrapper}`);
wrapperNode.style.transform = `scale(${scale})`;
wrapperNode.style.transformOrigin =
newIndex > i ? '0 0' : '100% 0';
});
}}
onSortEnd={({nodes}) => {
nodes.forEach(({node}) => {
const wrapperNode = node.querySelector(`.${style.wrapper}`);
wrapperNode.style.transform = '';
});
}}
/>
</div>
);
});
storiesOf('General | Configuration / Options', module)
.add('Drag handle', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
shouldUseDragHandle={true}
items={getItems(50, 59)}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Disabled items', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(10, 59)}
helperClass={style.stylizedHelper}
disabledItems={[2, 3, 7]}
/>
</div>
);
})
.add('Press delay (200ms)', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50, 59)}
pressDelay={200}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Distance (20px)', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50, 50)}
distance={20}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Lock axis', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50)}
helperClass={style.stylizedHelper}
lockAxis={'y'}
lockOffset={['0%', '100%']}
/>
</div>
);
})
.add('Window as scroll container', () => {
return (
<ListWrapper
component={SortableList}
items={getItems(50, 59)}
className=""
useWindowAsScrollContainer={true}
helperClass={style.stylizedHelper}
/>
);
})
.add('Custom sortable helper container', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableListWithCustomContainer}
items={getItems(50, 59)}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf('General | Configuration / Customization', module)
.add('Minimal styling', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50)}
className={style.list}
itemClass={style.item}
helperClass={style.helper}
/>
</div>
);
})
.add('Transition duration', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50, 59)}
transitionDuration={450}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Disable transitions', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableList}
items={getItems(50, 59)}
transitionDuration={0}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf(
'Advanced examples | Virtualization libraries / react-tiny-virtual-list',
module,
)
.add('Basic setup', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableVirtualList}
items={getItems(500, 59)}
itemHeight={59}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Variable heights', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableVirtualList}
items={getItems(500)}
itemHeight={89}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf('Advanced examples | Virtualization libraries / react-window', module)
.add('Basic setup', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableReactWindow(FixedSizeList)}
items={getItems(500, 59)}
itemHeight={59}
helperClass={style.stylizedHelper}
onSortEnd={(_sortEvent, _nativeEvent, ref) => {
// We need to inform React Window that the order of the items has changed
const instance = ref.getWrappedInstance();
const list = instance.refs.VirtualList;
list.forceUpdate();
}}
/>
</div>
);
})
.add('Variable heights', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableReactWindow(VariableSizeList)}
items={getItems(500)}
helperClass={style.stylizedHelper}
onSortEnd={(_sortEvent, _nativeEvent, ref) => {
// We need to inform React Window that the item heights have changed
const instance = ref.getWrappedInstance();
const list = instance.refs.VirtualList;
list.resetAfterIndex(0);
}}
/>
</div>
);
});
storiesOf(
'Advanced examples | Virtualization libraries / react-virtualized',
module,
)
.add('Basic setup', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableVirtualizedList}
items={getItems(500, 59)}
itemHeight={59}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Variable heights', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableVirtualizedList}
items={getItems(500)}
itemHeight={89}
helperClass={style.stylizedHelper}
onSortEnd={(_sortEvent, _nativeEvent, ref) => {
// We need to inform React Virtualized that the item heights have changed
const instance = ref.getWrappedInstance();
const list = instance.refs.VirtualList;
list.recomputeRowHeights();
instance.forceUpdate();
}}
/>
</div>
);
})
.add('Table', () => {
return (
<div className={style.root}>
<ListWrapper
component={TableWrapper}
items={getItems(500, 50)}
itemHeight={50}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf(
'Advanced examples | Virtualization libraries / react-infinite',
module,
)
.add('Basic setup', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableInfiniteList}
items={getItems(500, 59)}
helperClass={style.stylizedHelper}
/>
</div>
);
})
.add('Variable heights', () => {
return (
<div className={style.root}>
<ListWrapper
component={SortableInfiniteList}
items={getItems(500)}
helperClass={style.stylizedHelper}
/>
</div>
);
});
storiesOf('Advanced examples | Re-rendering before sorting', module)
.add('Grouping items', () => (
<div className={style.root}>
<GroupedItems />
</div>
))
.add('Elements that shrink', () => {
const getHelperDimensions = ({node}) => ({
height: 20,
width: node.offsetWidth,
});
return (
<div className={style.root}>
<ListWrapper
component={ShrinkingSortableList}
items={getItems(50)}
helperClass={style.shrinkedHelper}
getHelperDimensions={getHelperDimensions}
/>
</div>
);
});
storiesOf('Stress Testing | Nested elements', module).add(
'Interactive elements',
() => (
<div className={style.root}>
<InteractiveElements />
</div>
),
);
================================================
FILE: src/.stories/interactive-elements-stress-test/Item/Item.js
================================================
import React from 'react';
import {sortableElement} from '../../../../src';
import styles from './Item.scss';
function Item(props) {
const {children} = props;
return (
<div className={styles.root} tabIndex={0}>
{children}
</div>
);
}
export default sortableElement(Item);
================================================
FILE: src/.stories/interactive-elements-stress-test/Item/Item.scss
================================================
$color: #333;
$white: #fff;
$backgroundColor: $white;
$padding: 20px;
$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);
$fontWeight-regular: 400;
$fontWeight-bold: 600;
$borderRadius: 3px;
$borderWidth: 1px;
$borderColor: #efefef;
$focusedOutlineColor: #4c9ffe;
.root {
display: block;
width: 250px;
padding: $padding;
background-color: $backgroundColor;
border-bottom: $borderWidth solid #efefef;
box-sizing: border-box;
user-select: none;
color: $color;
font-family: sans-serif;
font-weight: $fontWeight-regular;
> * {
display: block;
width: 100%;
font-size: 14px;
}
> input,
> textarea {
padding: 5px;
border: 1px solid #e0e0e0;
box-sizing: border-box;
}
label {
input {
margin-right: 0.5em;
}
}
&:focus {
outline: none;
padding: $padding - 2px;
padding-bottom: $padding - 1px;
border: 2px solid $focusedOutlineColor;
}
&.dragging {
&:focus {
box-shadow: 0 0px 5px 1px $focusedOutlineColor;
}
}
}
================================================
FILE: src/.stories/interactive-elements-stress-test/Item/index.js
================================================
import Item from './Item';
export default Item;
================================================
FILE: src/.stories/interactive-elements-stress-test/List.js
================================================
import React from 'react';
import {sortableContainer} from '../../../src';
import Item from './Item';
function List({items}) {
return (
<div>
{items.map(([key, children], index) => {
return (
<Item key={key} index={index}>
{children}
</Item>
);
})}
</div>
);
}
export default sortableContainer(List);
================================================
FILE: src/.stories/interactive-elements-stress-test/index.js
================================================
import React from 'react';
import arrayMove from 'array-move';
import SortableList from './List';
import ItemStyles from './Item/Item.scss';
const items = {
input: <input placeholder="Regular text input" />,
textarea: <textarea placeholder="Textarea input" />,
select: (
<select>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
),
checkbox: (
<>
<label>
<input type="checkbox" name="checkbox" />
Checkbox 1
</label>
<label>
<input type="checkbox" name="checkbox" />
Checkbox 2
</label>
</>
),
radio: (
<>
<label>
<input type="radio" name="option" />
Option 1
</label>
<label>
<input type="radio" name="option" />
Option 2
</label>
</>
),
range: <input type="range" min="1" max="100" />,
contentEditable: (
<div
contentEditable
dangerouslySetInnerHTML={{
__html: 'Lorem ipsum <strong>dolor sit</strong> amet',
}}
/>
),
};
export default class InteractiveElements extends React.Component {
state = {
items: Object.entries(items),
};
render() {
return (
<SortableList
// The distance prop isn't strictly required for this example, but it is recommended
// to set it to a low value for sortable items with nested interactive elements
// such as clickable labels for checkbox / radio inputs
distance={2}
items={this.state.items}
onSortEnd={this.onSortEnd}
helperClass={ItemStyles.dragging}
/>
);
}
onSortEnd = ({oldIndex, newIndex}) => {
if (oldIndex === newIndex) {
return;
}
this.setState(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
}
================================================
FILE: src/AutoScroller/index.js
================================================
export default class AutoScroller {
constructor(container, onScrollCallback) {
this.container = container;
this.onScrollCallback = onScrollCallback;
}
clear() {
if (this.interval == null) {
return;
}
clearInterval(this.interval);
this.interval = null;
}
update({translate, minTranslate, maxTranslate, width, height}) {
const direction = {
x: 0,
y: 0,
};
const speed = {
x: 1,
y: 1,
};
const acceleration = {
x: 10,
y: 10,
};
const {
scrollTop,
scrollLeft,
scrollHeight,
scrollWidth,
clientHeight,
clientWidth,
} = this.container;
const isTop = scrollTop === 0;
const isBottom = scrollHeight - scrollTop - clientHeight === 0;
const isLeft = scrollLeft === 0;
const isRight = scrollWidth - scrollLeft - clientWidth === 0;
if (translate.y >= maxTranslate.y - height / 2 && !isBottom) {
// Scroll Down
direction.y = 1;
speed.y =
acceleration.y *
Math.abs((maxTranslate.y - height / 2 - translate.y) / height);
} else if (translate.x >= maxTranslate.x - width / 2 && !isRight) {
// Scroll Right
direction.x = 1;
speed.x =
acceleration.x *
Math.abs((maxTranslate.x - width / 2 - translate.x) / width);
} else if (translate.y <= minTranslate.y + height / 2 && !isTop) {
// Scroll Up
direction.y = -1;
speed.y =
acceleration.y *
Math.abs((translate.y - height / 2 - minTranslate.y) / height);
} else if (translate.x <= minTranslate.x + width / 2 && !isLeft) {
// Scroll Left
direction.x = -1;
speed.x =
acceleration.x *
Math.abs((translate.x - width / 2 - minTranslate.x) / width);
}
if (this.interval) {
this.clear();
this.isAutoScrolling = false;
}
if (direction.x !== 0 || direction.y !== 0) {
this.interval = setInterval(() => {
this.isAutoScrolling = true;
const offset = {
left: speed.x * direction.x,
top: speed.y * direction.y,
};
this.container.scrollTop += offset.top;
this.container.scrollLeft += offset.left;
this.onScrollCallback(offset);
}, 5);
}
}
}
================================================
FILE: src/Manager/index.js
================================================
export default class Manager {
refs = {};
add(collection, ref) {
if (!this.refs[collection]) {
this.refs[collection] = [];
}
this.refs[collection].push(ref);
}
remove(collection, ref) {
const index = this.getIndex(collection, ref);
if (index !== -1) {
this.refs[collection].splice(index, 1);
}
}
isActive() {
return this.active;
}
getActive() {
return this.refs[this.active.collection].find(
// eslint-disable-next-line eqeqeq
({node}) => node.sortableInfo.index == this.active.index,
);
}
getIndex(collection, ref) {
return this.refs[collection].indexOf(ref);
}
getOrderedRefs(collection = this.active.collection) {
return this.refs[collection].sort(sortByIndex);
}
}
function sortByIndex(
{
node: {
sortableInfo: {index: index1},
},
},
{
node: {
sortableInfo: {index: index2},
},
},
) {
return index1 - index2;
}
================================================
FILE: src/SortableContainer/defaultGetHelperDimensions.js
================================================
export default function defaultGetHelperDimensions({node}) {
return {
height: node.offsetHeight,
width: node.offsetWidth,
};
}
================================================
FILE: src/SortableContainer/defaultShouldCancelStart.js
================================================
import {NodeType, closest} from '../utils';
export default function defaultShouldCancelStart(event) {
// Cancel sorting if the event target is an `input`, `textarea`, `select` or `option`
const interactiveElements = [
NodeType.Input,
NodeType.Textarea,
NodeType.Select,
NodeType.Option,
NodeType.Button,
];
if (interactiveElements.indexOf(event.target.tagName) !== -1) {
// Return true to cancel sorting
return true;
}
if (closest(event.target, (el) => el.contentEditable === 'true')) {
return true;
}
return false;
}
================================================
FILE: src/SortableContainer/index.js
================================================
import * as React from 'react';
import {findDOMNode} from 'react-dom';
import invariant from 'invariant';
import Manager from '../Manager';
import {isSortableHandle} from '../SortableHandle';
import {
cloneNode,
closest,
events,
getScrollingParent,
getContainerGridGap,
getEdgeOffset,
getElementMargin,
getLockPixelOffsets,
getPosition,
isTouchEvent,
limit,
NodeType,
omit,
provideDisplayName,
setInlineStyles,
setTransitionDuration,
setTranslate3d,
getTargetIndex,
getScrollAdjustedBoundingClientRect,
} from '../utils';
import AutoScroller from '../AutoScroller';
import {
defaultProps,
omittedProps,
propTypes,
validateProps,
defaultKeyCodes,
} from './props';
export const SortableContext = React.createContext({
manager: {},
});
export default function sortableContainer(
WrappedComponent,
config = {withRef: false},
) {
return class WithSortableContainer extends React.Component {
constructor(props) {
super(props);
const manager = new Manager();
validateProps(props);
this.manager = manager;
this.wrappedInstance = React.createRef();
this.sortableContextValue = {manager};
this.events = {
end: this.handleEnd,
move: this.handleMove,
start: this.handleStart,
};
}
state = {};
static displayName = provideDisplayName('sortableList', WrappedComponent);
static defaultProps = defaultProps;
static propTypes = propTypes;
componentDidMount() {
const {useWindowAsScrollContainer} = this.props;
const container = this.getContainer();
Promise.resolve(container).then((containerNode) => {
this.container = containerNode;
this.document = this.container.ownerDocument || document;
/*
* Set our own default rather than using defaultProps because Jest
* snapshots will serialize window, causing a RangeError
* https://github.com/clauderic/react-sortable-hoc/issues/249
*/
const contentWindow =
this.props.contentWindow || this.document.defaultView || window;
this.contentWindow =
typeof contentWindow === 'function' ? contentWindow() : contentWindow;
this.scrollContainer = useWindowAsScrollContainer
? this.document.scrollingElement || this.document.documentElement
: getScrollingParent(this.container) || this.container;
this.autoScroller = new AutoScroller(
this.scrollContainer,
this.onAutoScroll,
);
Object.keys(this.events).forEach((key) =>
events[key].forEach((eventName) =>
this.container.addEventListener(eventName, this.events[key], false),
),
);
this.container.addEventListener('keydown', this.handleKeyDown);
});
}
componentWillUnmount() {
if (this.helper && this.helper.parentNode) {
this.helper.parentNode.removeChild(this.helper);
}
if (!this.container) {
return;
}
Object.keys(this.events).forEach((key) =>
events[key].forEach((eventName) =>
this.container.removeEventListener(eventName, this.events[key]),
),
);
this.container.removeEventListener('keydown', this.handleKeyDown);
}
handleStart = (event) => {
const {distance, shouldCancelStart} = this.props;
if (event.button === 2 || shouldCancelStart(event)) {
return;
}
this.touched = true;
this.position = getPosition(event);
const node = closest(event.target, (el) => el.sortableInfo != null);
if (
node &&
node.sortableInfo &&
this.nodeIsChild(node) &&
!this.state.sorting
) {
const {useDragHandle} = this.props;
const {index, collection, disabled} = node.sortableInfo;
if (disabled) {
return;
}
if (useDragHandle && !closest(event.target, isSortableHandle)) {
return;
}
this.manager.active = {collection, index};
/*
* Fixes a bug in Firefox where the :active state of anchor tags
* prevent subsequent 'mousemove' events from being fired
* (see https://github.com/clauderic/react-sortable-hoc/issues/118)
*/
if (!isTouchEvent(event) && event.target.tagName === NodeType.Anchor) {
event.preventDefault();
}
if (!distance) {
if (this.props.pressDelay === 0) {
this.handlePress(event);
} else {
this.pressTimer = setTimeout(
() => this.handlePress(event),
this.props.pressDelay,
);
}
}
}
};
nodeIsChild = (node) => {
return node.sortableInfo.manager === this.manager;
};
handleMove = (event) => {
const {distance, pressThreshold} = this.props;
if (
!this.state.sorting &&
this.touched &&
!this._awaitingUpdateBeforeSortStart
) {
const position = getPosition(event);
const delta = {
x: this.position.x - position.x,
y: this.position.y - position.y,
};
const combinedDelta = Math.abs(delta.x) + Math.abs(delta.y);
this.delta = delta;
if (!distance && (!pressThreshold || combinedDelta >= pressThreshold)) {
clearTimeout(this.cancelTimer);
this.cancelTimer = setTimeout(this.cancel, 0);
} else if (
distance &&
combinedDelta >= distance &&
this.manager.isActive()
) {
this.handlePress(event);
}
}
};
handleEnd = () => {
this.touched = false;
this.cancel();
};
cancel = () => {
const {distance} = this.props;
const {sorting} = this.state;
if (!sorting) {
if (!distance) {
clearTimeout(this.pressTimer);
}
this.manager.active = null;
}
};
handlePress = async (event) => {
const active = this.manager.getActive();
if (active) {
const {
axis,
getHelperDimensions,
helperClass,
hideSortableGhost,
updateBeforeSortStart,
onSortStart,
useWindowAsScrollContainer,
} = this.props;
const {node, collection} = active;
const {isKeySorting} = this.manager;
if (typeof updateBeforeSortStart === 'function') {
this._awaitingUpdateBeforeSortStart = true;
try {
const {index} = node.sortableInfo;
await updateBeforeSortStart(
{collection, index, node, isKeySorting},
event,
);
} finally {
this._awaitingUpdateBeforeSortStart = false;
}
}
// Need to get the latest value for `index` in case it changes during `updateBeforeSortStart`
const {index} = node.sortableInfo;
const margin = getElementMargin(node);
const gridGap = getContainerGridGap(this.container);
const containerBoundingRect = this.scrollContainer.getBoundingClientRect();
const dimensions = getHelperDimensions({index, node, collection});
this.node = node;
this.margin = margin;
this.gridGap = gridGap;
this.width = dimensions.width;
this.height = dimensions.height;
this.marginOffset = {
x: this.margin.left + this.margin.right + this.gridGap.x,
y: Math.max(this.margin.top, this.margin.bottom, this.gridGap.y),
};
this.boundingClientRect = node.getBoundingClientRect();
this.containerBoundingRect = containerBoundingRect;
this.index = index;
this.newIndex = index;
this.axis = {
x: axis.indexOf('x') >= 0,
y: axis.indexOf('y') >= 0,
};
this.offsetEdge = getEdgeOffset(node, this.container);
if (isKeySorting) {
this.initialOffset = getPosition({
...event,
pageX: this.boundingClientRect.left,
pageY: this.boundingClientRect.top,
});
} else {
this.initialOffset = getPosition(event);
}
this.initialScroll = {
left: this.scrollContainer.scrollLeft,
top: this.scrollContainer.scrollTop,
};
this.initialWindowScroll = {
left: window.pageXOffset,
top: window.pageYOffset,
};
this.helper = this.helperContainer.appendChild(cloneNode(node));
setInlineStyles(this.helper, {
boxSizing: 'border-box',
height: `${this.height}px`,
left: `${this.boundingClientRect.left - margin.left}px`,
pointerEvents: 'none',
position: 'fixed',
top: `${this.boundingClientRect.top - margin.top}px`,
width: `${this.width}px`,
});
if (isKeySorting) {
this.helper.focus();
}
if (hideSortableGhost) {
this.sortableGhost = node;
setInlineStyles(node, {
opacity: 0,
visibility: 'hidden',
});
}
this.minTranslate = {};
this.maxTranslate = {};
if (isKeySorting) {
const {
top: containerTop,
left: containerLeft,
width: containerWidth,
height: containerHeight,
} = useWindowAsScrollContainer
? {
top: 0,
left: 0,
width: this.contentWindow.innerWidth,
height: this.contentWindow.innerHeight,
}
: this.containerBoundingRect;
const containerBottom = containerTop + containerHeight;
const containerRight = containerLeft + containerWidth;
if (this.axis.x) {
this.minTranslate.x = containerLeft - this.boundingClientRect.left;
this.maxTranslate.x =
containerRight - (this.boundingClientRect.left + this.width);
}
if (this.axis.y) {
this.minTranslate.y = containerTop - this.boundingClientRect.top;
this.maxTranslate.y =
containerBottom - (this.boundingClientRect.top + this.height);
}
} else {
if (this.axis.x) {
this.minTranslate.x =
(useWindowAsScrollContainer ? 0 : containerBoundingRect.left) -
this.boundingClientRect.left -
this.width / 2;
this.maxTranslate.x =
(useWindowAsScrollContainer
? this.contentWindow.innerWidth
: containerBoundingRect.left + containerBoundingRect.width) -
this.boundingClientRect.left -
this.width / 2;
}
if (this.axis.y) {
this.minTranslate.y =
(useWindowAsScrollContainer ? 0 : containerBoundingRect.top) -
this.boundingClientRect.top -
this.height / 2;
this.maxTranslate.y =
(useWindowAsScrollContainer
? this.contentWindow.innerHeight
: containerBoundingRect.top + containerBoundingRect.height) -
this.boundingClientRect.top -
this.height / 2;
}
}
if (helperClass) {
helperClass
.split(' ')
.forEach((className) => this.helper.classList.add(className));
}
this.listenerNode = event.touches ? event.target : this.contentWindow;
if (isKeySorting) {
this.listenerNode.addEventListener('wheel', this.handleKeyEnd, true);
this.listenerNode.addEventListener(
'mousedown',
this.handleKeyEnd,
true,
);
this.listenerNode.addEventListener('keydown', this.handleKeyDown);
} else {
events.move.forEach((eventName) =>
this.listenerNode.addEventListener(
eventName,
this.handleSortMove,
false,
),
);
events.end.forEach((eventName) =>
this.listenerNode.addEventListener(
eventName,
this.handleSortEnd,
false,
),
);
}
this.setState({
sorting: true,
sortingIndex: index,
});
if (onSortStart) {
onSortStart(
{
node,
index,
collection,
isKeySorting,
nodes: this.manager.getOrderedRefs(),
helper: this.helper,
},
event,
);
}
if (isKeySorting) {
// Readjust positioning in case re-rendering occurs onSortStart
this.keyMove(0);
}
}
};
handleSortMove = (event) => {
const {onSortMove} = this.props;
// Prevent scrolling on mobile
if (typeof event.preventDefault === 'function' && event.cancelable) {
event.preventDefault();
}
this.updateHelperPosition(event);
this.animateNodes();
this.autoscroll();
if (onSortMove) {
onSortMove(event);
}
};
handleSortEnd = (event) => {
const {hideSortableGhost, onSortEnd} = this.props;
const {
active: {collection},
isKeySorting,
} = this.manager;
const nodes = this.manager.getOrderedRefs();
// Remove the event listeners if the node is still in the DOM
if (this.listenerNode) {
if (isKeySorting) {
this.listenerNode.removeEventListener(
'wheel',
this.handleKeyEnd,
true,
);
this.listenerNode.removeEventListener(
'mousedown',
this.handleKeyEnd,
true,
);
this.listenerNode.removeEventListener('keydown', this.handleKeyDown);
} else {
events.move.forEach((eventName) =>
this.listenerNode.removeEventListener(
eventName,
this.handleSortMove,
),
);
events.end.forEach((eventName) =>
this.listenerNode.removeEventListener(
eventName,
this.handleSortEnd,
),
);
}
}
// Remove the helper from the DOM
this.helper.parentNode.removeChild(this.helper);
if (hideSortableGhost && this.sortableGhost) {
setInlineStyles(this.sortableGhost, {
opacity: '',
visibility: '',
});
}
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
const el = node.node;
// Clear the cached offset/boundingClientRect
node.edgeOffset = null;
node.boundingClientRect = null;
// Remove the transforms / transitions
setTranslate3d(el, null);
setTransitionDuration(el, null);
node.translate = null;
}
// Stop autoscroll
this.autoScroller.clear();
// Update manager state
this.manager.active = null;
this.manager.isKeySorting = false;
this.setState({
sorting: false,
sortingIndex: null,
});
if (typeof onSortEnd === 'function') {
onSortEnd(
{
collection,
newIndex: this.newIndex,
oldIndex: this.index,
isKeySorting,
nodes,
},
event,
);
}
this.touched = false;
};
updateHelperPosition(event) {
const {
lockAxis,
lockOffset,
lockToContainerEdges,
transitionDuration,
keyboardSortingTransitionDuration = transitionDuration,
} = this.props;
const {isKeySorting} = this.manager;
const {ignoreTransition} = event;
const offset = getPosition(event);
const translate = {
x: offset.x - this.initialOffset.x,
y: offset.y - this.initialOffset.y,
};
// Adjust for window scroll
translate.y -= window.pageYOffset - this.initialWindowScroll.top;
translate.x -= window.pageXOffset - this.initialWindowScroll.left;
this.translate = translate;
if (lockToContainerEdges) {
const [minLockOffset, maxLockOffset] = getLockPixelOffsets({
height: this.height,
lockOffset,
width: this.width,
});
const minOffset = {
x: this.width / 2 - minLockOffset.x,
y: this.height / 2 - minLockOffset.y,
};
const maxOffset = {
x: this.width / 2 - maxLockOffset.x,
y: this.height / 2 - maxLockOffset.y,
};
translate.x = limit(
this.minTranslate.x + minOffset.x,
this.maxTranslate.x - maxOffset.x,
translate.x,
);
translate.y = limit(
this.minTranslate.y + minOffset.y,
this.maxTranslate.y - maxOffset.y,
translate.y,
);
}
if (lockAxis === 'x') {
translate.y = 0;
} else if (lockAxis === 'y') {
translate.x = 0;
}
if (
isKeySorting &&
keyboardSortingTransitionDuration &&
!ignoreTransition
) {
setTransitionDuration(this.helper, keyboardSortingTransitionDuration);
}
setTranslate3d(this.helper, translate);
}
animateNodes() {
const {transitionDuration, hideSortableGhost, onSortOver} = this.props;
const {containerScrollDelta, windowScrollDelta} = this;
const nodes = this.manager.getOrderedRefs();
const sortingOffset = {
left:
this.offsetEdge.left + this.translate.x + containerScrollDelta.left,
top: this.offsetEdge.top + this.translate.y + containerScrollDelta.top,
};
const {isKeySorting} = this.manager;
const prevIndex = this.newIndex;
this.newIndex = null;
for (let i = 0, len = nodes.length; i < len; i++) {
const {node} = nodes[i];
const {index} = node.sortableInfo;
const width = node.offsetWidth;
const height = node.offsetHeight;
const offset = {
height: this.height > height ? height / 2 : this.height / 2,
width: this.width > width ? width / 2 : this.width / 2,
};
// For keyboard sorting, we want user input to dictate the position of the nodes
const mustShiftBackward =
isKeySorting && (index > this.index && index <= prevIndex);
const mustShiftForward =
isKeySorting && (index < this.index && index >= prevIndex);
const translate = {
x: 0,
y: 0,
};
let {edgeOffset} = nodes[i];
// If we haven't cached the node's offsetTop / offsetLeft value
if (!edgeOffset) {
edgeOffset = getEdgeOffset(node, this.container);
nodes[i].edgeOffset = edgeOffset;
// While we're at it, cache the boundingClientRect, used during keyboard sorting
if (isKeySorting) {
nodes[i].boundingClientRect = getScrollAdjustedBoundingClientRect(
node,
containerScrollDelta,
);
}
}
// Get a reference to the next and previous node
const nextNode = i < nodes.length - 1 && nodes[i + 1];
const prevNode = i > 0 && nodes[i - 1];
// Also cache the next node's edge offset if needed.
// We need this for calculating the animation in a grid setup
if (nextNode && !nextNode.edgeOffset) {
nextNode.edgeOffset = getEdgeOffset(nextNode.node, this.container);
if (isKeySorting) {
nextNode.boundingClientRect = getScrollAdjustedBoundingClientRect(
nextNode.node,
containerScrollDelta,
);
}
}
// If the node is the one we're currently animating, skip it
if (index === this.index) {
if (hideSortableGhost) {
/*
* With windowing libraries such as `react-virtualized`, the sortableGhost
* node may change while scrolling down and then back up (or vice-versa),
* so we need to update the reference to the new node just to be safe.
*/
this.sortableGhost = node;
setInlineStyles(node, {
opacity: 0,
visibility: 'hidden',
});
}
continue;
}
if (transitionDuration) {
setTransitionDuration(node, transitionDuration);
}
if (this.axis.x) {
if (this.axis.y) {
// Calculations for a grid setup
if (
mustShiftForward ||
(index < this.index &&
((sortingOffset.left + windowScrollDelta.left - offset.width <=
edgeOffset.left &&
sortingOffset.top + windowScrollDelta.top <=
edgeOffset.top + offset.height) ||
sortingOffset.top + windowScrollDelta.top + offset.height <=
edgeOffset.top))
) {
// If the current node is to the left on the same row, or above the node that's being dragged
// then move it to the right
translate.x = this.width + this.marginOffset.x;
if (
edgeOffset.left + translate.x >
this.containerBoundingRect.width - offset.width * 2
) {
// If it moves passed the right bounds, then animate it to the first position of the next row.
// We just use the offset of the next node to calculate where to move, because that node's original position
// is exactly where we want to go
if (nextNode) {
translate.x = nextNode.edgeOffset.left - edgeOffset.left;
translate.y = nextNode.edgeOffset.top - edgeOffset.top;
}
}
if (this.newIndex === null) {
this.newIndex = index;
}
} else if (
mustShiftBackward ||
(index > this.index &&
((sortingOffset.left + windowScrollDelta.left + offset.width >=
edgeOffset.left &&
sortingOffset.top + windowScrollDelta.top + offset.height >=
edgeOffset.top) ||
sortingOffset.top + windowScrollDelta.top + offset.height >=
edgeOffset.top + height))
) {
// If the current node is to the right on the same row, or below the node that's being dragged
// then move it to the left
translate.x = -(this.width + this.marginOffset.x);
if (
edgeOffset.left + translate.x <
this.containerBoundingRect.left + offset.width
) {
// If it moves passed the left bounds, then animate it to the last position of the previous row.
// We just use the offset of the previous node to calculate where to move, because that node's original position
// is exactly where we want to go
if (prevNode) {
translate.x = prevNode.edgeOffset.left - edgeOffset.left;
translate.y = prevNode.edgeOffset.top - edgeOffset.top;
}
}
this.newIndex = index;
}
} else {
if (
mustShiftBackward ||
(index > this.index &&
sortingOffset.left + windowScrollDelta.left + offset.width >=
edgeOffset.left)
) {
translate.x = -(this.width + this.marginOffset.x);
this.newIndex = index;
} else if (
mustShiftForward ||
(index < this.index &&
sortingOffset.left + windowScrollDelta.left <=
edgeOffset.left + offset.width)
) {
translate.x = this.width + this.marginOffset.x;
if (this.newIndex == null) {
this.newIndex = index;
}
}
}
} else if (this.axis.y) {
if (
mustShiftBackward ||
(index > this.index &&
sortingOffset.top + windowScrollDelta.top + offset.height >=
edgeOffset.top)
) {
translate.y = -(this.height + this.marginOffset.y);
this.newIndex = index;
} else if (
mustShiftForward ||
(index < this.index &&
sortingOffset.top + windowScrollDelta.top <=
edgeOffset.top + offset.height)
) {
translate.y = this.height + this.marginOffset.y;
if (this.newIndex == null) {
this.newIndex = index;
}
}
}
setTranslate3d(node, translate);
nodes[i].translate = translate;
}
if (this.newIndex == null) {
this.newIndex = this.index;
}
if (isKeySorting) {
// If keyboard sorting, we want the user input to dictate index, not location of the helper
this.newIndex = prevIndex;
}
const oldIndex = isKeySorting ? this.prevIndex : prevIndex;
if (onSortOver && this.newIndex !== oldIndex) {
onSortOver({
collection: this.manager.active.collection,
index: this.index,
newIndex: this.newIndex,
oldIndex,
isKeySorting,
nodes,
helper: this.helper,
});
}
}
autoscroll = () => {
const {disableAutoscroll} = this.props;
const {isKeySorting} = this.manager;
if (disableAutoscroll) {
this.autoScroller.clear();
return;
}
if (isKeySorting) {
const translate = {...this.translate};
let scrollX = 0;
let scrollY = 0;
if (this.axis.x) {
translate.x = Math.min(
this.maxTranslate.x,
Math.max(this.minTranslate.x, this.translate.x),
);
scrollX = this.translate.x - translate.x;
}
if (this.axis.y) {
translate.y = Math.min(
this.maxTranslate.y,
Math.max(this.minTranslate.y, this.translate.y),
);
scrollY = this.translate.y - translate.y;
}
this.translate = translate;
setTranslate3d(this.helper, this.translate);
this.scrollContainer.scrollLeft += scrollX;
this.scrollContainer.scrollTop += scrollY;
return;
}
this.autoScroller.update({
height: this.height,
maxTranslate: this.maxTranslate,
minTranslate: this.minTranslate,
translate: this.translate,
width: this.width,
});
};
onAutoScroll = (offset) => {
this.translate.x += offset.left;
this.translate.y += offset.top;
this.animateNodes();
};
getWrappedInstance() {
invariant(
config.withRef,
'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableContainer() call',
);
return this.wrappedInstance.current;
}
getContainer() {
const {getContainer} = this.props;
if (typeof getContainer !== 'function') {
return findDOMNode(this);
}
return getContainer(
config.withRef ? this.getWrappedInstance() : undefined,
);
}
handleKeyDown = (event) => {
const {keyCode} = event;
const {shouldCancelStart, keyCodes: customKeyCodes = {}} = this.props;
const keyCodes = {
...defaultKeyCodes,
...customKeyCodes,
};
if (
(this.manager.active && !this.manager.isKeySorting) ||
(!this.manager.active &&
(!keyCodes.lift.includes(keyCode) ||
shouldCancelStart(event) ||
!this.isValidSortingTarget(event)))
) {
return;
}
event.stopPropagation();
event.preventDefault();
if (keyCodes.lift.includes(keyCode) && !this.manager.active) {
this.keyLift(event);
} else if (keyCodes.drop.includes(keyCode) && this.manager.active) {
this.keyDrop(event);
} else if (keyCodes.cancel.includes(keyCode)) {
this.newIndex = this.manager.active.index;
this.keyDrop(event);
} else if (keyCodes.up.includes(keyCode)) {
this.keyMove(-1);
} else if (keyCodes.down.includes(keyCode)) {
this.keyMove(1);
}
};
keyLift = (event) => {
const {target} = event;
const node = closest(target, (el) => el.sortableInfo != null);
const {index, collection} = node.sortableInfo;
this.initialFocusedNode = target;
this.manager.isKeySorting = true;
this.manager.active = {
index,
collection,
};
this.handlePress(event);
};
keyMove = (shift) => {
const nodes = this.manager.getOrderedRefs();
const {index: lastIndex} = nodes[nodes.length - 1].node.sortableInfo;
const newIndex = this.newIndex + shift;
const prevIndex = this.newIndex;
if (newIndex < 0 || newIndex > lastIndex) {
return;
}
this.prevIndex = prevIndex;
this.newIndex = newIndex;
const targetIndex = getTargetIndex(
this.newIndex,
this.prevIndex,
this.index,
);
const target = nodes.find(
({node}) => node.sortableInfo.index === targetIndex,
);
const {node: targetNode} = target;
const scrollDelta = this.containerScrollDelta;
const targetBoundingClientRect =
target.boundingClientRect ||
getScrollAdjustedBoundingClientRect(targetNode, scrollDelta);
const targetTranslate = target.translate || {x: 0, y: 0};
const targetPosition = {
top: targetBoundingClientRect.top + targetTranslate.y - scrollDelta.top,
left:
targetBoundingClientRect.left + targetTranslate.x - scrollDelta.left,
};
const shouldAdjustForSize = prevIndex < newIndex;
const sizeAdjustment = {
x:
shouldAdjustForSize && this.axis.x
? targetNode.offsetWidth - this.width
: 0,
y:
shouldAdjustForSize && this.axis.y
? targetNode.offsetHeight - this.height
: 0,
};
this.handleSortMove({
pageX: targetPosition.left + sizeAdjustment.x,
pageY: targetPosition.top + sizeAdjustment.y,
ignoreTransition: shift === 0,
});
};
keyDrop = (event) => {
this.handleSortEnd(event);
if (this.initialFocusedNode) {
this.initialFocusedNode.focus();
}
};
handleKeyEnd = (event) => {
if (this.manager.active) {
this.keyDrop(event);
}
};
isValidSortingTarget = (event) => {
const {useDragHandle} = this.props;
const {target} = event;
const node = closest(target, (el) => el.sortableInfo != null);
return (
node &&
node.sortableInfo &&
!node.sortableInfo.disabled &&
(useDragHandle ? isSortableHandle(target) : target.sortableInfo)
);
};
render() {
const ref = config.withRef ? this.wrappedInstance : null;
return (
<SortableContext.Provider value={this.sortableContextValue}>
<WrappedComponent ref={ref} {...omit(this.props, omittedProps)} />
</SortableContext.Provider>
);
}
get helperContainer() {
const {helperContainer} = this.props;
if (typeof helperContainer === 'function') {
return helperContainer();
}
return this.props.helperContainer || this.document.body;
}
get containerScrollDelta() {
const {useWindowAsScrollContainer} = this.props;
if (useWindowAsScrollContainer) {
return {left: 0, top: 0};
}
return {
left: this.scrollContainer.scrollLeft - this.initialScroll.left,
top: this.scrollContainer.scrollTop - this.initialScroll.top,
};
}
get windowScrollDelta() {
return {
left: this.contentWindow.pageXOffset - this.initialWindowScroll.left,
top: this.contentWindow.pageYOffset - this.initialWindowScroll.top,
};
}
};
}
================================================
FILE: src/SortableContainer/props.js
================================================
import PropTypes from 'prop-types';
import invariant from 'invariant';
import {KEYCODE} from '../utils';
import defaultGetHelperDimensions from './defaultGetHelperDimensions';
import defaultShouldCancelStart from './defaultShouldCancelStart';
export const propTypes = {
axis: PropTypes.oneOf(['x', 'y', 'xy']),
contentWindow: PropTypes.any,
disableAutoscroll: PropTypes.bool,
distance: PropTypes.number,
getContainer: PropTypes.func,
getHelperDimensions: PropTypes.func,
helperClass: PropTypes.string,
helperContainer: PropTypes.oneOfType([
PropTypes.func,
typeof HTMLElement === 'undefined'
? PropTypes.any
: PropTypes.instanceOf(HTMLElement),
]),
hideSortableGhost: PropTypes.bool,
keyboardSortingTransitionDuration: PropTypes.number,
lockAxis: PropTypes.string,
lockOffset: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
),
]),
lockToContainerEdges: PropTypes.bool,
onSortEnd: PropTypes.func,
onSortMove: PropTypes.func,
onSortOver: PropTypes.func,
onSortStart: PropTypes.func,
pressDelay: PropTypes.number,
pressThreshold: PropTypes.number,
keyCodes: PropTypes.shape({
lift: PropTypes.arrayOf(PropTypes.number),
drop: PropTypes.arrayOf(PropTypes.number),
cancel: PropTypes.arrayOf(PropTypes.number),
up: PropTypes.arrayOf(PropTypes.number),
down: PropTypes.arrayOf(PropTypes.number),
}),
shouldCancelStart: PropTypes.func,
transitionDuration: PropTypes.number,
updateBeforeSortStart: PropTypes.func,
useDragHandle: PropTypes.bool,
useWindowAsScrollContainer: PropTypes.bool,
};
export const defaultKeyCodes = {
lift: [KEYCODE.SPACE],
drop: [KEYCODE.SPACE],
cancel: [KEYCODE.ESC],
up: [KEYCODE.UP, KEYCODE.LEFT],
down: [KEYCODE.DOWN, KEYCODE.RIGHT],
};
export const defaultProps = {
axis: 'y',
disableAutoscroll: false,
distance: 0,
getHelperDimensions: defaultGetHelperDimensions,
hideSortableGhost: true,
lockOffset: '50%',
lockToContainerEdges: false,
pressDelay: 0,
pressThreshold: 5,
keyCodes: defaultKeyCodes,
shouldCancelStart: defaultShouldCancelStart,
transitionDuration: 300,
useWindowAsScrollContainer: false,
};
export const omittedProps = Object.keys(propTypes);
export function validateProps(props) {
invariant(
!(props.distance && props.pressDelay),
'Attempted to set both `pressDelay` and `distance` on SortableContainer, you may only use one or the other, not both at the same time.',
);
}
================================================
FILE: src/SortableElement/index.js
================================================
import * as React from 'react';
import PropTypes from 'prop-types';
import {findDOMNode} from 'react-dom';
import invariant from 'invariant';
import {SortableContext} from '../SortableContainer';
import {provideDisplayName, omit} from '../utils';
const propTypes = {
index: PropTypes.number.isRequired,
collection: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.bool,
};
const omittedProps = Object.keys(propTypes);
export default function sortableElement(
WrappedComponent,
config = {withRef: false},
) {
return class WithSortableElement extends React.Component {
static displayName = provideDisplayName(
'sortableElement',
WrappedComponent,
);
static contextType = SortableContext;
static propTypes = propTypes;
static defaultProps = {
collection: 0,
};
componentDidMount() {
this.register();
}
componentDidUpdate(prevProps) {
if (this.node) {
if (prevProps.index !== this.props.index) {
this.node.sortableInfo.index = this.props.index;
}
if (prevProps.disabled !== this.props.disabled) {
this.node.sortableInfo.disabled = this.props.disabled;
}
}
if (prevProps.collection !== this.props.collection) {
this.unregister(prevProps.collection);
this.register();
}
}
componentWillUnmount() {
this.unregister();
}
register() {
const {collection, disabled, index} = this.props;
const node = findDOMNode(this);
node.sortableInfo = {
collection,
disabled,
index,
manager: this.context.manager,
};
this.node = node;
this.ref = {node};
this.context.manager.add(collection, this.ref);
}
unregister(collection = this.props.collection) {
this.context.manager.remove(collection, this.ref);
}
getWrappedInstance() {
invariant(
config.withRef,
'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableElement() call',
);
return this.wrappedInstance.current;
}
wrappedInstance = React.createRef();
render() {
const ref = config.withRef ? this.wrappedInstance : null;
return <WrappedComponent ref={ref} {...omit(this.props, omittedProps)} />;
}
};
}
================================================
FILE: src/SortableHandle/index.js
================================================
import * as React from 'react';
import {findDOMNode} from 'react-dom';
import invariant from 'invariant';
import {provideDisplayName} from '../utils';
export default function sortableHandle(
WrappedComponent,
config = {withRef: false},
) {
return class WithSortableHandle extends React.Component {
static displayName = provideDisplayName('sortableHandle', WrappedComponent);
componentDidMount() {
const node = findDOMNode(this);
node.sortableHandle = true;
}
getWrappedInstance() {
invariant(
config.withRef,
'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableHandle() call',
);
return this.wrappedInstance.current;
}
wrappedInstance = React.createRef();
render() {
const ref = config.withRef ? this.wrappedInstance : null;
return <WrappedComponent ref={ref} {...this.props} />;
}
};
}
export function isSortableHandle(node) {
return node.sortableHandle != null;
}
================================================
FILE: src/index.js
================================================
export {default as SortableContainer} from './SortableContainer';
export {default as SortableElement} from './SortableElement';
export {default as SortableHandle} from './SortableHandle';
export {default as sortableContainer} from './SortableContainer';
export {default as sortableElement} from './SortableElement';
export {default as sortableHandle} from './SortableHandle';
export {arrayMove} from './utils';
================================================
FILE: src/utils.js
================================================
/* global process */
import invariant from 'invariant';
export function arrayMove(array, from, to) {
// Will be deprecated soon. Consumers should install 'array-move' instead
// https://www.npmjs.com/package/array-move
if (process.env.NODE_ENV !== 'production') {
if (typeof console !== 'undefined') {
// eslint-disable-next-line no-console
console.warn(
"Deprecation warning: arrayMove will no longer be exported by 'react-sortable-hoc' in the next major release. Please install the `array-move` package locally instead. https://www.npmjs.com/package/array-move",
);
}
}
array = array.slice();
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
return array;
}
export function omit(obj, keysToOmit) {
return Object.keys(obj).reduce((acc, key) => {
if (keysToOmit.indexOf(key) === -1) {
acc[key] = obj[key];
}
return acc;
}, {});
}
export const events = {
end: ['touchend', 'touchcancel', 'mouseup'],
move: ['touchmove', 'mousemove'],
start: ['touchstart', 'mousedown'],
};
export const vendorPrefix = (function() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
// Server environment
return '';
}
// fix for: https://bugzilla.mozilla.org/show_bug.cgi?id=548397
// window.getComputedStyle() returns null inside an iframe with display: none
// in this case return an array with a fake mozilla style in it.
const styles = window.getComputedStyle(document.documentElement, '') || [
'-moz-hidden-iframe',
];
const pre = (Array.prototype.slice
.call(styles)
.join('')
.match(/-(moz|webkit|ms)-/) ||
(styles.OLink === '' && ['', 'o']))[1];
switch (pre) {
case 'ms':
return 'ms';
default:
return pre && pre.length ? pre[0].toUpperCase() + pre.substr(1) : '';
}
})();
export function setInlineStyles(node, styles) {
Object.keys(styles).forEach((key) => {
node.style[key] = styles[key];
});
}
export function setTranslate3d(node, translate) {
node.style[`${vendorPrefix}Transform`] =
translate == null ? '' : `translate3d(${translate.x}px,${translate.y}px,0)`;
}
export function setTransitionDuration(node, duration) {
node.style[`${vendorPrefix}TransitionDuration`] =
duration == null ? '' : `${duration}ms`;
}
export function closest(el, fn) {
while (el) {
if (fn(el)) {
return el;
}
el = el.parentNode;
}
return null;
}
export function limit(min, max, value) {
return Math.max(min, Math.min(value, max));
}
function getPixelValue(stringValue) {
if (stringValue.substr(-2) === 'px') {
return parseFloat(stringValue);
}
return 0;
}
export function getElementMargin(element) {
const style = window.getComputedStyle(element);
return {
bottom: getPixelValue(style.marginBottom),
left: getPixelValue(style.marginLeft),
right: getPixelValue(style.marginRight),
top: getPixelValue(style.marginTop),
};
}
export function provideDisplayName(prefix, Component) {
const componentName = Component.displayName || Component.name;
return componentName ? `${prefix}(${componentName})` : prefix;
}
export function getScrollAdjustedBoundingClientRect(node, scrollDelta) {
const boundingClientRect = node.getBoundingClientRect();
return {
top: boundingClientRect.top + scrollDelta.top,
left: boundingClientRect.left + scrollDelta.left,
};
}
export function getPosition(event) {
if (event.touches && event.touches.length) {
return {
x: event.touches[0].pageX,
y: event.touches[0].pageY,
};
} else if (event.changedTouches && event.changedTouches.length) {
return {
x: event.changedTouches[0].pageX,
y: event.changedTouches[0].pageY,
};
} else {
return {
x: event.pageX,
y: event.pageY,
};
}
}
export function isTouchEvent(event) {
return (
(event.touches && event.touches.length) ||
(event.changedTouches && event.changedTouches.length)
);
}
export function getEdgeOffset(node, parent, offset = {left: 0, top: 0}) {
if (!node) {
return undefined;
}
// Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested
const nodeOffset = {
left: offset.left + node.offsetLeft,
top: offset.top + node.offsetTop,
};
if (node.parentNode === parent) {
return nodeOffset;
}
return getEdgeOffset(node.parentNode, parent, nodeOffset);
}
export function getTargetIndex(newIndex, prevIndex, oldIndex) {
if (newIndex < oldIndex && newIndex > prevIndex) {
return newIndex - 1;
} else if (newIndex > oldIndex && newIndex < prevIndex) {
return newIndex + 1;
} else {
return newIndex;
}
}
export function getLockPixelOffset({lockOffset, width, height}) {
let offsetX = lockOffset;
let offsetY = lockOffset;
let unit = 'px';
if (typeof lockOffset === 'string') {
const match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset);
invariant(
match !== null,
'lockOffset value should be a number or a string of a ' +
'number followed by "px" or "%". Given %s',
lockOffset,
);
offsetX = parseFloat(lockOffset);
offsetY = parseFloat(lockOffset);
unit = match[1];
}
invariant(
isFinite(offsetX) && isFinite(offsetY),
'lockOffset value should be a finite. Given %s',
lockOffset,
);
if (unit === '%') {
offsetX = (offsetX * width) / 100;
offsetY = (offsetY * height) / 100;
}
return {
x: offsetX,
y: offsetY,
};
}
export function getLockPixelOffsets({height, width, lockOffset}) {
const offsets = Array.isArray(lockOffset)
? lockOffset
: [lockOffset, lockOffset];
invariant(
offsets.length === 2,
'lockOffset prop of SortableContainer should be a single ' +
'value or an array of exactly two values. Given %s',
lockOffset,
);
const [minLockOffset, maxLockOffset] = offsets;
return [
getLockPixelOffset({height, lockOffset: minLockOffset, width}),
getLockPixelOffset({height, lockOffset: maxLockOffset, width}),
];
}
function isScrollable(el) {
const computedStyle = window.getComputedStyle(el);
const overflowRegex = /(auto|scroll)/;
const properties = ['overflow', 'overflowX', 'overflowY'];
return properties.find((property) =>
overflowRegex.test(computedStyle[property]),
);
}
export function getScrollingParent(el) {
if (!(el instanceof HTMLElement)) {
return null;
} else if (isScrollable(el)) {
return el;
} else {
return getScrollingParent(el.parentNode);
}
}
export function getContainerGridGap(element) {
const style = window.getComputedStyle(element);
if (style.display === 'grid') {
return {
x: getPixelValue(style.gridColumnGap),
y: getPixelValue(style.gridRowGap),
};
}
return {x: 0, y: 0};
}
export const KEYCODE = {
TAB: 9,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
};
export const NodeType = {
Anchor: 'A',
Button: 'BUTTON',
Canvas: 'CANVAS',
Input: 'INPUT',
Option: 'OPTION',
Textarea: 'TEXTAREA',
Select: 'SELECT',
};
export function cloneNode(node) {
const selector = 'input, textarea, select, canvas, [contenteditable]';
const fields = node.querySelectorAll(selector);
const clonedNode = node.cloneNode(true);
const clonedFields = [...clonedNode.querySelectorAll(selector)];
clonedFields.forEach((field, i) => {
if (field.type !== 'file') {
field.value = fields[i].value;
}
// Fixes an issue with original radio buttons losing their value once the
// clone is inserted in the DOM, as radio button `name` attributes must be unique
if (field.type === 'radio' && field.name) {
field.name = `__sortableClone__${field.name}`;
}
if (
field.tagName === NodeType.Canvas &&
fields[i].width > 0 &&
fields[i].height > 0
) {
const destCtx = field.getContext('2d');
destCtx.drawImage(fields[i], 0, 0);
}
});
return clonedNode;
}
================================================
FILE: types/index.d.ts
================================================
import * as React from 'react';
export type Axis = 'x' | 'y' | 'xy';
export type Offset = number | string;
export interface SortStart {
node: Element;
index: number;
collection: Offset;
isKeySorting: boolean;
nodes: HTMLElement[];
helper: HTMLElement;
}
export interface SortOver {
index: number;
oldIndex: number;
newIndex: number;
collection: Offset;
isKeySorting: boolean;
nodes: HTMLElement[];
helper: HTMLElement;
}
export interface SortEnd {
oldIndex: number;
newIndex: number;
collection: Offset;
isKeySorting: boolean;
nodes: HTMLElement[];
}
export type SortEvent = React.MouseEvent<any> | React.TouchEvent<any>;
export type SortEventWithTag = SortEvent & {
target: {
tagName: string;
};
};
export type SortStartHandler = (sort: SortStart, event: SortEvent) => void;
export type SortMoveHandler = (event: SortEvent) => void;
export type SortEndHandler = (sort: SortEnd, event: SortEvent) => void;
export type SortOverHandler = (sort: SortOver, event: SortEvent) => void;
export type ContainerGetter = (
element: React.ReactElement<any>,
) => HTMLElement | Promise<HTMLElement>;
export type HelperContainerGetter = () => HTMLElement;
export interface Dimensions {
width: number;
height: number;
}
export interface SortableContainerProps {
axis?: Axis;
lockAxis?: Axis;
helperClass?: string;
transitionDuration?: number;
keyboardSortingTransitionDuration?: number;
keyCodes?: {
lift?: number[];
drop?: number[];
cancel?: number[];
up?: number[];
down?: number[];
};
pressDelay?: number;
pressThreshold?: number;
distance?: number;
shouldCancelStart?: (event: SortEvent | SortEventWithTag) => boolean;
updateBeforeSortStart?: SortStartHandler;
onSortStart?: SortStartHandler;
onSortMove?: SortMoveHandler;
onSortEnd?: SortEndHandler;
onSortOver?: SortOverHandler;
useDragHandle?: boolean;
useWindowAsScrollContainer?: boolean;
hideSortableGhost?: boolean;
lockToContainerEdges?: boolean;
lockOffset?: Offset | [Offset, Offset];
getContainer?: ContainerGetter;
getHelperDimensions?: (sort: SortStart) => Dimensions;
helperContainer?: HTMLElement | HelperContainerGetter;
disableAutoscroll?: boolean;
}
export interface SortableElementProps {
index: number;
collection?: Offset;
disabled?: boolean;
}
export interface Config {
withRef: boolean;
}
export type WrappedComponentFactory<P> = (props: P) => JSX.Element;
export type WrappedComponent<P> =
| React.ComponentClass<P>
| React.SFC<P>
| WrappedComponentFactory<P>;
export function SortableContainer<P>(
wrappedComponent: WrappedComponent<P>,
config?: Config,
): React.ComponentClass<P & SortableContainerProps>;
export function SortableElement<P>(
wrappedComponent: WrappedComponent<P>,
config?: Config,
): React.ComponentClass<P & SortableElementProps>;
export function SortableHandle<P>(
wrappedComponent: WrappedComponent<P>,
config?: Config,
): React.ComponentClass<P>;
export function arrayMove<T>(
collection: T[],
previousIndex: number,
newIndex: number,
): T[];
gitextract_bc7ghwt0/
├── .codesandbox/
│ └── ci.json
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierrc
├── .storybook/
│ ├── config.js
│ ├── manager-head.html
│ ├── theme.js
│ └── webpack.config.js
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples/
│ ├── .eslintrc.json
│ ├── basic.js
│ ├── collections.js
│ ├── drag-handle.js
│ ├── react-infinite.js
│ ├── react-virtualized-table-columns.js
│ └── react-virtualized.js
├── package.json
├── rollup.config.js
├── src/
│ ├── .stories/
│ │ ├── Storybook.scss
│ │ ├── grouping-items/
│ │ │ ├── Item/
│ │ │ │ ├── Item.js
│ │ │ │ ├── Item.scss
│ │ │ │ └── index.js
│ │ │ ├── List/
│ │ │ │ ├── List.js
│ │ │ │ ├── List.scss
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── utils.js
│ │ ├── index.js
│ │ └── interactive-elements-stress-test/
│ │ ├── Item/
│ │ │ ├── Item.js
│ │ │ ├── Item.scss
│ │ │ └── index.js
│ │ ├── List.js
│ │ └── index.js
│ ├── AutoScroller/
│ │ └── index.js
│ ├── Manager/
│ │ └── index.js
│ ├── SortableContainer/
│ │ ├── defaultGetHelperDimensions.js
│ │ ├── defaultShouldCancelStart.js
│ │ ├── index.js
│ │ └── props.js
│ ├── SortableElement/
│ │ └── index.js
│ ├── SortableHandle/
│ │ └── index.js
│ ├── index.js
│ └── utils.js
└── types/
└── index.d.ts
SYMBOL INDEX (100 symbols across 25 files)
FILE: .storybook/config.js
function loadStories (line 11) | function loadStories() {
FILE: examples/basic.js
class App (line 12) | class App extends Component {
method render (line 23) | render() {
FILE: examples/collections.js
class App (line 12) | class App extends Component {
method render (line 31) | render() {
FILE: examples/drag-handle.js
class App (line 23) | class App extends Component {
method render (line 34) | render() {
FILE: examples/react-infinite.js
class App (line 29) | class App extends Component {
method render (line 47) | render() {
FILE: examples/react-virtualized-table-columns.js
constant ROW_HEIGHT (line 8) | const ROW_HEIGHT = 30;
constant HEADER_ROW_HEIGHT (line 9) | const HEADER_ROW_HEIGHT = 20;
constant COL_WIDTH (line 10) | const COL_WIDTH = 100;
class TableWithSortableColumns (line 26) | class TableWithSortableColumns extends Component {
method render (line 62) | render() {
FILE: examples/react-virtualized.js
class VirtualList (line 11) | class VirtualList extends Component {
method render (line 24) | render() {
class App (line 42) | class App extends Component {
method render (line 77) | render() {
FILE: src/.stories/grouping-items/Item/Item.js
constant ENTER_KEY (line 7) | const ENTER_KEY = 13;
function Item (line 9) | function Item(props) {
function Badge (line 42) | function Badge(props) {
FILE: src/.stories/grouping-items/List/List.js
function List (line 8) | function List({items, isSorting, selectedItems, sortingItemKey, onItemSe...
FILE: src/.stories/grouping-items/index.js
class GroupedItems (line 7) | class GroupedItems extends React.Component {
method render (line 13) | render() {
method handleSortStart (line 61) | handleSortStart() {
FILE: src/.stories/grouping-items/utils.js
function generateItems (line 1) | function generateItems(length) {
FILE: src/.stories/index.js
function getItems (line 20) | function getItems(count, height) {
class SortableListWithCustomContainer (line 117) | class SortableListWithCustomContainer extends React.Component {
method render (line 122) | render() {
class ListWrapper (line 157) | class ListWrapper extends Component {
method render (line 211) | render() {
method render (line 230) | render() {
class VirtualizedListWrapper (line 300) | class VirtualizedListWrapper extends Component {
method render (line 301) | render() {
class TableWrapper (line 346) | class TableWrapper extends Component {
method render (line 357) | render() {
FILE: src/.stories/interactive-elements-stress-test/Item/Item.js
function Item (line 6) | function Item(props) {
FILE: src/.stories/interactive-elements-stress-test/List.js
function List (line 6) | function List({items}) {
FILE: src/.stories/interactive-elements-stress-test/index.js
class InteractiveElements (line 52) | class InteractiveElements extends React.Component {
method render (line 57) | render() {
FILE: src/AutoScroller/index.js
class AutoScroller (line 1) | class AutoScroller {
method constructor (line 2) | constructor(container, onScrollCallback) {
method clear (line 7) | clear() {
method update (line 16) | update({translate, minTranslate, maxTranslate, width, height}) {
FILE: src/Manager/index.js
class Manager (line 1) | class Manager {
method add (line 4) | add(collection, ref) {
method remove (line 12) | remove(collection, ref) {
method isActive (line 20) | isActive() {
method getActive (line 24) | getActive() {
method getIndex (line 31) | getIndex(collection, ref) {
method getOrderedRefs (line 35) | getOrderedRefs(collection = this.active.collection) {
function sortByIndex (line 40) | function sortByIndex(
FILE: src/SortableContainer/defaultGetHelperDimensions.js
function defaultGetHelperDimensions (line 1) | function defaultGetHelperDimensions({node}) {
FILE: src/SortableContainer/defaultShouldCancelStart.js
function defaultShouldCancelStart (line 3) | function defaultShouldCancelStart(event) {
FILE: src/SortableContainer/index.js
function sortableContainer (line 43) | function sortableContainer(
FILE: src/SortableContainer/props.js
function validateProps (line 79) | function validateProps(props) {
FILE: src/SortableElement/index.js
function sortableElement (line 17) | function sortableElement(
FILE: src/SortableHandle/index.js
function sortableHandle (line 7) | function sortableHandle(
function isSortableHandle (line 37) | function isSortableHandle(node) {
FILE: src/utils.js
function arrayMove (line 4) | function arrayMove(array, from, to) {
function omit (line 23) | function omit(obj, keysToOmit) {
function setInlineStyles (line 65) | function setInlineStyles(node, styles) {
function setTranslate3d (line 71) | function setTranslate3d(node, translate) {
function setTransitionDuration (line 76) | function setTransitionDuration(node, duration) {
function closest (line 81) | function closest(el, fn) {
function limit (line 93) | function limit(min, max, value) {
function getPixelValue (line 97) | function getPixelValue(stringValue) {
function getElementMargin (line 105) | function getElementMargin(element) {
function provideDisplayName (line 116) | function provideDisplayName(prefix, Component) {
function getScrollAdjustedBoundingClientRect (line 122) | function getScrollAdjustedBoundingClientRect(node, scrollDelta) {
function getPosition (line 131) | function getPosition(event) {
function isTouchEvent (line 150) | function isTouchEvent(event) {
function getEdgeOffset (line 157) | function getEdgeOffset(node, parent, offset = {left: 0, top: 0}) {
function getTargetIndex (line 175) | function getTargetIndex(newIndex, prevIndex, oldIndex) {
function getLockPixelOffset (line 185) | function getLockPixelOffset({lockOffset, width, height}) {
function getLockPixelOffsets (line 222) | function getLockPixelOffsets({height, width, lockOffset}) {
function isScrollable (line 242) | function isScrollable(el) {
function getScrollingParent (line 252) | function getScrollingParent(el) {
function getContainerGridGap (line 262) | function getContainerGridGap(element) {
constant KEYCODE (line 275) | const KEYCODE = {
function cloneNode (line 295) | function cloneNode(node) {
FILE: types/index.d.ts
type Axis (line 3) | type Axis = 'x' | 'y' | 'xy';
type Offset (line 5) | type Offset = number | string;
type SortStart (line 7) | interface SortStart {
type SortOver (line 16) | interface SortOver {
type SortEnd (line 26) | interface SortEnd {
type SortEvent (line 34) | type SortEvent = React.MouseEvent<any> | React.TouchEvent<any>;
type SortEventWithTag (line 36) | type SortEventWithTag = SortEvent & {
type SortStartHandler (line 42) | type SortStartHandler = (sort: SortStart, event: SortEvent) => void;
type SortMoveHandler (line 44) | type SortMoveHandler = (event: SortEvent) => void;
type SortEndHandler (line 46) | type SortEndHandler = (sort: SortEnd, event: SortEvent) => void;
type SortOverHandler (line 48) | type SortOverHandler = (sort: SortOver, event: SortEvent) => void;
type ContainerGetter (line 50) | type ContainerGetter = (
type HelperContainerGetter (line 54) | type HelperContainerGetter = () => HTMLElement;
type Dimensions (line 56) | interface Dimensions {
type SortableContainerProps (line 61) | interface SortableContainerProps {
type SortableElementProps (line 94) | interface SortableElementProps {
type Config (line 100) | interface Config {
type WrappedComponentFactory (line 104) | type WrappedComponentFactory<P> = (props: P) => JSX.Element;
type WrappedComponent (line 106) | type WrappedComponent<P> =
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (178K chars).
[
{
"path": ".codesandbox/ci.json",
"chars": 43,
"preview": "{\n \"sandboxes\": [\"react\", \"o104x95y86\"]\n}\n"
},
{
"path": ".editorconfig",
"chars": 394,
"preview": "# editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntri"
},
{
"path": ".eslintrc.json",
"chars": 746,
"preview": "{\n \"extends\": [\"plugin:shopify/react\", \"plugin:prettier/recommended\"],\n \"rules\": {\n \"no-process-env\": \"off\",\n \"n"
},
{
"path": ".gitignore",
"chars": 86,
"preview": "*.DS_Store\nnode_modules\ndist\nstyles.min.css\nstyles.min.css.map\ncoverage\nnpm-debug.log\n"
},
{
"path": ".npmignore",
"chars": 67,
"preview": ".github\n.babelrc\ncoverage\nsrc\ntest\n.*\n*.md\ncodecov.yml\n.travis.yml\n"
},
{
"path": ".prettierrc",
"chars": 106,
"preview": "{\n \"arrowParens\": \"always\",\n \"bracketSpacing\": false,\n \"singleQuote\": true,\n \"trailingComma\": \"all\"\n}\n"
},
{
"path": ".storybook/config.js",
"chars": 265,
"preview": "import {addParameters, configure} from '@storybook/react';\nimport theme from './theme';\n\naddParameters({\n options: {\n "
},
{
"path": ".storybook/manager-head.html",
"chars": 7870,
"preview": "<link\n href=\"https://fonts.googleapis.com/css?family=Roboto:400,500,700\"\n rel=\"stylesheet\"\n/>\n<link\n rel=\"icon\"\n typ"
},
{
"path": ".storybook/theme.js",
"chars": 870,
"preview": "import {create} from '@storybook/theming';\n\nexport default create({\n base: 'light',\n\n colorSecondary: '#9276ff',\n\n //"
},
{
"path": ".storybook/webpack.config.js",
"chars": 594,
"preview": "module.exports = {\n module: {\n rules: [\n {\n test: /(\\.scss)$/,\n use: [\n 'style-loader',\n"
},
{
"path": ".travis.yml",
"chars": 34,
"preview": "language: node_js\nnode_js:\n - 10\n"
},
{
"path": "CHANGELOG.md",
"chars": 20721,
"preview": "# Change Log\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github"
},
{
"path": "LICENSE",
"chars": 1084,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016, Claudéric Demers\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "README.md",
"chars": 29846,
"preview": "> **Warning**\n>\n> This library is **no longer actively maintained**. It will continue to receive critical security updat"
},
{
"path": "examples/.eslintrc.json",
"chars": 125,
"preview": "{\n \"rules\": {\n \"import/no-unresolved\": \"off\",\n \"react/prop-types\": \"off\",\n \"react/no-array-index-key\": \"off\"\n "
},
{
"path": "examples/basic.js",
"chars": 943,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {sortableContainer, sortableElement} fr"
},
{
"path": "examples/collections.js",
"chars": 1433,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {sortableContainer, sortableElement} fr"
},
{
"path": "examples/drag-handle.js",
"chars": 1074,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {\n sortableContainer,\n sortableElemen"
},
{
"path": "examples/react-infinite.js",
"chars": 1343,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {sortableContainer, sortableElement} fr"
},
{
"path": "examples/react-virtualized-table-columns.js",
"chars": 2178,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {Table, Column} from 'react-virtualized"
},
{
"path": "examples/react-virtualized.js",
"chars": 2171,
"preview": "import React, {Component} from 'react';\nimport {render} from 'react-dom';\nimport {sortableContainer, sortableElement} fr"
},
{
"path": "package.json",
"chars": 3218,
"preview": "{\n \"name\": \"react-sortable-hoc\",\n \"version\": \"2.0.0\",\n \"description\": \"Set of higher-order components to turn any lis"
},
{
"path": "rollup.config.js",
"chars": 2107,
"preview": "import replace from 'rollup-plugin-replace';\nimport resolve from 'rollup-plugin-node-resolve';\nimport commonjs from 'rol"
},
{
"path": "src/.stories/Storybook.scss",
"chars": 4013,
"preview": "@import url(https://fonts.googleapis.com/css?family=Montserrat:400);\n\n$focusedOutlineColor: #4c9ffe;\n\n.root {\n display:"
},
{
"path": "src/.stories/grouping-items/Item/Item.js",
"chars": 1005,
"preview": "import React from 'react';\nimport classNames from 'classnames';\nimport {sortableElement} from '../../../../src';\n\nimport"
},
{
"path": "src/.stories/grouping-items/Item/Item.scss",
"chars": 1587,
"preview": "$color: #333;\n$white: #fff;\n$backgroundColor: $white;\n\n$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);\n\n$fontWeight-regul"
},
{
"path": "src/.stories/grouping-items/Item/index.js",
"chars": 49,
"preview": "import Item from './Item';\n\nexport default Item;\n"
},
{
"path": "src/.stories/grouping-items/List/List.js",
"chars": 835,
"preview": "import React from 'react';\nimport {sortableContainer} from '../../../../src';\n\nimport Item from '../Item';\n\nimport style"
},
{
"path": "src/.stories/grouping-items/List/List.scss",
"chars": 324,
"preview": "$backgroundColor: #f3f3f3;\n$borderColor: #efefef;\n$borderWidth: 1px;\n\n.List {\n position: relative;\n width: 400px;\n he"
},
{
"path": "src/.stories/grouping-items/List/index.js",
"chars": 49,
"preview": "import List from './List';\n\nexport default List;\n"
},
{
"path": "src/.stories/grouping-items/index.js",
"chars": 2975,
"preview": "import React from 'react';\nimport arrayMove from 'array-move';\nimport {generateItems} from './utils';\n\nimport SortableLi"
},
{
"path": "src/.stories/grouping-items/utils.js",
"chars": 110,
"preview": "export function generateItems(length) {\n return Array.from(Array(length), (_, index) => index.toString());\n}\n"
},
{
"path": "src/.stories/index.js",
"chars": 23581,
"preview": "import React, {Component} from 'react';\nimport PropTypes from 'prop-types';\nimport ReactDOM from 'react-dom';\nimport {st"
},
{
"path": "src/.stories/interactive-elements-stress-test/Item/Item.js",
"chars": 296,
"preview": "import React from 'react';\nimport {sortableElement} from '../../../../src';\n\nimport styles from './Item.scss';\n\nfunction"
},
{
"path": "src/.stories/interactive-elements-stress-test/Item/Item.scss",
"chars": 1019,
"preview": "$color: #333;\n$white: #fff;\n$backgroundColor: $white;\n\n$padding: 20px;\n\n$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);\n\n"
},
{
"path": "src/.stories/interactive-elements-stress-test/Item/index.js",
"chars": 49,
"preview": "import Item from './Item';\n\nexport default Item;\n"
},
{
"path": "src/.stories/interactive-elements-stress-test/List.js",
"chars": 376,
"preview": "import React from 'react';\nimport {sortableContainer} from '../../../src';\n\nimport Item from './Item';\n\nfunction List({i"
},
{
"path": "src/.stories/interactive-elements-stress-test/index.js",
"chars": 1833,
"preview": "import React from 'react';\nimport arrayMove from 'array-move';\n\nimport SortableList from './List';\nimport ItemStyles fro"
},
{
"path": "src/AutoScroller/index.js",
"chars": 2292,
"preview": "export default class AutoScroller {\n constructor(container, onScrollCallback) {\n this.container = container;\n thi"
},
{
"path": "src/Manager/index.js",
"chars": 956,
"preview": "export default class Manager {\n refs = {};\n\n add(collection, ref) {\n if (!this.refs[collection]) {\n this.refs["
},
{
"path": "src/SortableContainer/defaultGetHelperDimensions.js",
"chars": 139,
"preview": "export default function defaultGetHelperDimensions({node}) {\n return {\n height: node.offsetHeight,\n width: node.o"
},
{
"path": "src/SortableContainer/defaultShouldCancelStart.js",
"chars": 570,
"preview": "import {NodeType, closest} from '../utils';\n\nexport default function defaultShouldCancelStart(event) {\n // Cancel sorti"
},
{
"path": "src/SortableContainer/index.js",
"chars": 32513,
"preview": "import * as React from 'react';\nimport {findDOMNode} from 'react-dom';\nimport invariant from 'invariant';\n\nimport Manage"
},
{
"path": "src/SortableContainer/props.js",
"chars": 2571,
"preview": "import PropTypes from 'prop-types';\nimport invariant from 'invariant';\n\nimport {KEYCODE} from '../utils';\nimport default"
},
{
"path": "src/SortableElement/index.js",
"chars": 2382,
"preview": "import * as React from 'react';\nimport PropTypes from 'prop-types';\nimport {findDOMNode} from 'react-dom';\nimport invari"
},
{
"path": "src/SortableHandle/index.js",
"chars": 1029,
"preview": "import * as React from 'react';\nimport {findDOMNode} from 'react-dom';\nimport invariant from 'invariant';\n\nimport {provi"
},
{
"path": "src/index.js",
"chars": 413,
"preview": "export {default as SortableContainer} from './SortableContainer';\nexport {default as SortableElement} from './SortableEl"
},
{
"path": "src/utils.js",
"chars": 8020,
"preview": "/* global process */\nimport invariant from 'invariant';\n\nexport function arrayMove(array, from, to) {\n // Will be depre"
},
{
"path": "types/index.d.ts",
"chars": 3104,
"preview": "import * as React from 'react';\n\nexport type Axis = 'x' | 'y' | 'xy';\n\nexport type Offset = number | string;\n\nexport int"
}
]
About this extraction
This page contains the full source code of the clauderic/react-sortable-hoc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (165.4 KB), approximately 43.6k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.