main 09672e398ad8 cached
28 files
69.5 KB
19.4k tokens
94 symbols
1 requests
Download .txt
Repository: StreakYC/react-draggable-list
Branch: main
Commit: 09672e398ad8
Files: 28
Total size: 69.5 KB

Directory structure:
gitextract_v5ra7eiw/

├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .flowconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── node.js.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── example/
│   ├── Example.tsx
│   ├── index.html
│   ├── main.css
│   └── main.tsx
├── index.js.flow
├── jest.config.js
├── package.json
├── src/
│   ├── MoveContainer.tsx
│   ├── OnUpdate.tsx
│   ├── TemplateContainer.tsx
│   ├── index.test.tsx
│   └── index.tsx
├── test-types/
│   └── typescript.tsx
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-transform-runtime",
    [
      "add-module-exports",
      {
        "addDefaultProperty": true
      }
    ]
  ]
}


================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

indent_style = space
indent_size = 2


================================================
FILE: .eslintignore
================================================
/example/dist
/dist
*.js.flow


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  env: {
    browser: true,
    jest: true,
    node: true,
    es6: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
  ],
  rules: {
    'react/no-children-prop': ['off'],

    '@typescript-eslint/no-non-null-assertion': ['off'],
    '@typescript-eslint/no-explicit-any': ['off'],
    '@typescript-eslint/explicit-function-return-type': ['off'],
    '@typescript-eslint/no-unused-vars': [
      'error',
      { argsIgnorePattern: '[iI]gnored' },
    ],
    '@typescript-eslint/ban-types': ['off'],
    '@typescript-eslint/explicit-module-boundary-types': ['off'],

    indent: ['off'],
    'react/jsx-indent': ['error', 2],
    'react/jsx-indent-props': ['error', 2],
    'linebreak-style': ['error', 'unix'],
    quotes: ['off'],
    semi: ['error', 'always'],
    'no-var': ['error'],
    'brace-style': ['error'],
    'array-bracket-spacing': ['error', 'never'],
    'block-spacing': ['error', 'always'],
    'no-spaced-func': ['error'],
    'no-whitespace-before-property': ['error'],
    'space-before-blocks': ['error', 'always'],
    'keyword-spacing': ['error'],

    // We use Typescript for this
    'react/prop-types': ['off'],
  },
  settings: {
    react: {
      version: '16.6',
    },
  },
};


================================================
FILE: .flowconfig
================================================
[ignore]
.*/node_modules/.*/\(test\|lib\|example\|samplejson\)/.*\.json
<PROJECT_ROOT>/dist/

[include]

[libs]

[options]


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/workflows/node.js.yml
================================================
name: Node.js CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci --force
      - run: npm test


================================================
FILE: .gitignore
================================================
*~
.DS_Store
/node_modules
npm-debug.log
/dist
/example/dist
/.cache


================================================
FILE: .prettierignore
================================================
/example/dist
/dist
.cache


================================================
FILE: .prettierrc
================================================
{
  "singleQuote": true
}


================================================
FILE: .vscode/settings.json
================================================
{
  "javascript.validate.enable": false,
  "flow.useNPMPackagedFlow": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}


================================================
FILE: CHANGELOG.md
================================================
## 4.2.1 (2024-05-23)

- Reupload package to work around minor file metadata issue caused by Yarn (https://github.com/yarnpkg/yarn/issues/8109). This version contains no other changes.

## 4.2.0 (2024-02-19)

- Added `draggedItem` parameter to the `onDragStart` and `onDragEnd` callback props.

## 4.1.0 (2022-08-12)

- Implemented `onDragStart` and `onDragEnd` props. ([@JayHales](https://github.com/JayHales) in [#52](https://github.com/StreakYC/react-draggable-list/pull/52))

## 4.0.4 (2021-09-22)

- Updated peerDependencies list to mark compatibility with React 17.

## 4.0.3 (2019-06-05)

- Updated to use immutability-helper 3.0.

## 4.0.2 (2019-04-05)

- Fixed issue where the DraggableList could have an incorrect height during drag depending on the styling of its parents.

## 4.0.0 (2018-11-28)

### Breaking Changes

- React v16.6+ is now required.
- The `dragHandle` function prop was removed. Now the Template component is instead given a prop `dragHandleProps` which is an object that must be spread as props on the HTML element to be used as the drag handle.

ReactDraggableList v3:

```js
<div>
  {this.props.dragHandle(<div>drag me</div>)}
  <div>content</div>
</div>
```

ReactDraggableList v4:

```js
<div>
  <div {...this.props.dragHandleProps}>drag me</div>
  <div>content</div>
</div>
```

### Improvements

- No longer uses any deprecated APIs (lifecycle methods and ReactDOM.findDOMNode).
- Fixed bug where the `oldIndex` parameter passed to `onMoveEnd` was incorrect if the `list` prop was updated while the user was dragging an item.

## 3.7.0 (2018-11-05)

- Added TypeScript type definitions.
- Changed Flow type definitions to use `$ReadOnlyArray` where applicable. Users may need to change the type annotations on the function they give to the `onMoveEnd` prop to keep Flow's type-check passing.

## 3.6.0 (2018-08-24)

- Added `constrainDrag` prop. [#30](https://github.com/StreakYC/react-draggable-list/pull/30)

## 3.5.3 (2018-05-15)

- Updated for compatibility with Flow v0.72.

## 3.5.2 (2018-04-13)

- Improved Flow type definitions to cover the proper return type of `getItemInstance`.

## 3.5.1 (2018-04-13)

- Fixed accidental usage of `event` global variable. This didn't cause any user-visible bugs to my knowledge.

## 3.5.0 (2018-04-13)

- Flow types for DraggableList now include a type parameter representing the list item's type, enabling fuller type-checking coverage.

## 3.4.1 (2017-10-02)

- Updated package.json to mark compatibility with React v16.
- Internal: tests now use React v16.

## 3.4.0 (2017-09-11)

- Updated for compatibility with Flow v0.54.1. This made it incompatible with older versions of Flow, so I'm making this update be a semver-minor change so users still on an older Flow version can pin to the previous minor version. Because of its frequent changes, I'm not considering incompatibilities with old Flow versions as semver-major breaking changes.

## 3.3.1 (2017-07-07)

- Updated for compatibility with Flow v0.49.1.

## 3.3.0 (2017-04-25)

- Stop using the newly deprecated `React.PropTypes` and now use the separate prop-types module.

## 3.2.1 (2017-03-24)

- Fixed issue where components didn't re-render when the value of the `commonProps` prop changed. [#20](https://github.com/StreakYC/react-draggable-list/pull/20)

## 3.2.0 (2017-03-06)

- Added `autoScrollMaxSpeed` and `autoScrollRegionSize` props to DraggableList. [#15](https://github.com/StreakYC/react-draggable-list/pull/15)
- Added `commonProps` prop to DraggableList. [#18](https://github.com/StreakYC/react-draggable-list/pull/18)

## 3.1.4 (2017-03-06)

- Updated for compatibility with Flow v0.41.

## 3.1.3 (2017-01-24)

- Updated for compatibility with Flow v0.38.

## 3.1.2 (2016-10-26)

- Fixed issue with the dragged item itself being removed during the post-drag animation.

## 3.1.1 (2016-10-25)

- Fixed handling of items being removed from list during the post-drag animation.

## 3.1.0 (2016-10-25)

- Added `getItemInstance` method.

## 3.0.4 (2016-09-26)

- Fixed Flow type-checking issue when used with newer version of react-motion.

## 3.0.3 (2016-09-13)

- Updated for compatibility with Flow v0.32.

## 3.0.2 (2016-08-05)

- Updated for compatibility with Flow v0.30.

## 3.0.1 (2016-06-27)

- Fixed handling of props being changed while the user is dragging an item.
- Fixed issue where DraggableList could prevent elements from having a natural layout applied after being dragged when the container prop was not used.

## 3.0.0 (2016-04-07)

### Breaking Changes

- React v15 is now required.

## 2.1.0 (2016-03-02)

- Added `unsetZIndex` prop.

## 2.0.0 (2016-02-29)

### Breaking Changes

- If the `getDragHeight` method isn't present on the template component, the drag height now defaults to the element's natural height instead of the arbitrary height of 30px.

## 1.0.6 (2016-02-29)

- Fixed the DraggableList element changing height when the last item is grabbed.

## 1.0.5 (2016-02-25)

- Re-use dragHandle prop value given to component to reduce amount of re-renders.

## 1.0.3 (2016-02-25)

- Fixed a scroll animation glitch when DraggableList was not in a scrollable container.

## 1.0.2 (2016-02-25)

- Fixed an accuracy issue with the scroll animation on drop.

## 1.0.1 (2016-02-25)

- Fixed animation glitch if you pick up an item while it's still animating.
- Efficiency improvements: minimize amount of re-renders needed during dragging.

## 1.0.0 (2016-02-24)

Initial stable release.


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2018 Rewardly, Inc.

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
================================================
# react-draggable-list

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/StreakYC/react-draggable-list/blob/master/LICENSE.txt) [![npm version](https://img.shields.io/npm/v/react-draggable-list.svg?style=flat)](https://www.npmjs.com/package/react-draggable-list) [![Node.js CI](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml/badge.svg)](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml)

This component lets you make a user re-orderable list that animates nicely so
that the user can easily move large items:

![Example](https://streakyc.github.io/react-draggable-list/video/dragitem.gif)

The above example can be tried here:

https://streakyc.github.io/react-draggable-list/example/

You can find its code in the `example` directory. The example may be compiled
by running:

```
npm i --force
npm run example-build
# or use this to auto-rebuild on changes:
npm run example-watch
```

## DraggableList

This module exports the `DraggableList` React component, which takes the
following props:

- `list` must be an array of objects representing your list's items.
- `itemKey` must be the name of a property of the list's objects to use as a
  key to identify the objects, or it must be a function that takes an object as
  an argument and returns a key.
- `template` must be a React component used to render the list items. This must
  not be a stateless-functional component. If possible, don't pass a new
  class instance on every render. See the next section for more information
  on the template including a description of the props passed to the component.
- `onMoveEnd` may be a function which will be called when the user drags and
  drops an item to a new position in the list. The arguments to the function
  will be `(newList: Array<Object>, movedItem: Object, oldIndex: number, newIndex: number)`. A component using DraggableList should immediately store
  the newList into its state and then pass the new list (or an equivalent list)
  as the `list` prop to DraggableList.
- `container`: If the DraggableList is inside a scrollable element, then this
  property should be set to a function which returns a reference to it. When the
  user moves an item in the list, the container will be scrolled to keep the
  item in view. If the DraggableList is in no scrollable elements besides the
  page itself, then a function returning a reference to `document.body` should
  be given.
- `springConfig` is an optional object which sets the [SpringHelperConfig
  object passed to
  React-Motion](https://github.com/chenglou/react-motion/tree/85ca75c6de9ed85937d1c95646b6044a66981eee#--spring-val-number-config-springhelperconfig--opaqueconfig)
  for animations. This prop defaults to `{stiffness: 300, damping: 50}`.
- `padding` is an optional number of pixels to leave between items. Defaults to 10.
- `unsetZIndex` is an optional property that defaults to false. If set to true,
  then the z-index of all of the list items will be set to "auto" when the list
  isn't animating. This may have a small performance cost when the list starts
  and stops animating. Use this if you need to avoid having the list item create
  a stacking context when it's not being animated.
- `constrainDrag` is an option property that defaults to false. If it is set to
  true, then the y-coordinate of a dragged item will be constrained vertically to
  the bounds of the list.
- `autoScrollMaxSpeed` is an optional number that allows the scroll speed when
  the user drags to the top or bottom of the list to be overridden.
- `autoScrollRegionSize` is an optional number that allows the height of the
  region that triggers auto-scrolling when dragged onto to be overridden.
- `commonProps` is an optional value that will be passed as the `commonProps`
  prop to every template component instance.
- `onDragStart` is an optional function which is called once a list item starts
  being dragged. Receives the dragged item as an argument.
- `onDragEnd` is an optional function which is called once a list item is no longer being dragged. Receives the dragged item as an argument. It differs from `onMoveEnd` in that it's called even if the user does not reorder any items in the lists, like when an item is just picked up and then dropped.

A DraggableList instance has the following methods:

- `getItemInstance(key)` will return a reference to the mounted instance of the
  template for a given key.

## Template

The template component is passed the following props:

- `item` is an object from the list prop passed to DraggableList.
- `itemSelected` is a number from 0 to 1. It starts at 0, and quickly increases
  to 1 when the item is picked up by the user. This may be used to animate the
  item when the user picks it up or drops it.
- `anySelected` is a number from 0 to 1. It starts at 0, and quickly increases
  to 1 when any item is picked up by the user.
- `dragHandleProps` is an object which should be spread as props on the HTML
  element to be used as the drag handle. The whole item will be draggable by the
  wrapped element. See the
  [example](https://github.com/StreakYC/react-draggable-list/blob/master/example/Example.tsx)
  to see how it should be used.
- `commonProps` will be set to the same value passed as the `commonProps` prop
  to the DraggableList component.

The template component should be styled with max-height set to "100%" for best
results.

The template component will have its props updated many times quickly during
the animation, so implementing `shouldComponentUpdate` in its children is
highly recommended.

The template component may have a `getDragHeight` method which may return a
number to set the height in pixels of the item while the user is dragging it.
If the method returns null or is not present, then the drag height will be
equal to the element's natural height.

## Bundling Note

To use this module in browsers, a CommonJS bundler such as Parcel, Browserify, or
Webpack should be used.

This project relies on the javascript Map object being available globally. A
global polyfill such as [Babel's polyfill](https://babeljs.io/docs/usage/polyfill/)
is required to support [older browsers that don't implement these](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Browser_compatibility).

## Types

Both [TypeScript](https://www.typescriptlang.org/) and
[Flow](https://flowtype.org/) type definitions for this module are included!
The type definitions won't require any configuration to use.


================================================
FILE: example/Example.tsx
================================================
/* eslint-disable no-console */

import * as React from 'react';
import cx from 'classnames';
import DraggableList from '../src';

interface PlanetListItem {
  name: string;
  subtitle?: boolean;
}

interface PlanetProps {
  item: PlanetListItem;
  itemSelected: number;
  dragHandleProps: object;
}
interface PlanetState {
  value: number;
}
class PlanetItem extends React.Component<PlanetProps, PlanetState> {
  state = {
    value: 0,
  };

  _inc() {
    this.setState({
      value: this.state.value + 1,
    });
  }

  getDragHeight() {
    return this.props.item.subtitle ? 47 : 28;
  }

  render() {
    const { item, itemSelected, dragHandleProps } = this.props;
    const { value } = this.state;
    const scale = itemSelected * 0.05 + 1;
    const shadow = itemSelected * 15 + 1;
    const dragged = itemSelected !== 0;

    return (
      <div
        className={cx('item', { dragged })}
        style={{
          transform: `scale(${scale})`,
          boxShadow: `rgba(0, 0, 0, 0.3) 0px ${shadow}px ${2 * shadow}px 0px`,
        }}
      >
        <div className="dragHandle" {...dragHandleProps} />
        <h2>{item.name}</h2>
        {item.subtitle && (
          <div className="subtitle">
            This item has a subtitle visible while dragging
          </div>
        )}
        <div>
          some description here
          <br />
          this planet orbits the sun
          <br />
          this planet is mostly round
        </div>
        {item.subtitle && (
          <div>
            subtitled planets are better
            <br />
            and have longer descriptions
          </div>
        )}
        <div>
          State works and is retained during movement:{' '}
          <input type="button" value={value} onClick={() => this._inc()} />
        </div>
      </div>
    );
  }
}

type ExampleState = {
  useContainer: boolean;
  list: ReadonlyArray<PlanetListItem>;
};
export default class Example extends React.Component<{}, ExampleState> {
  private _container = React.createRef<HTMLDivElement>();

  state = {
    useContainer: false,
    list: [
      { name: 'Mercury' },
      { name: 'Venus' },
      { name: 'Earth', subtitle: true },
      { name: 'Mars' },
      { name: 'Jupiter' },
      { name: 'Saturn', subtitle: true },
      { name: 'Uranus', subtitle: true },
      { name: 'Neptune' },
    ],
  };

  private _togglePluto() {
    const noPluto = this.state.list.filter((item) => item.name !== 'Pluto');
    if (noPluto.length !== this.state.list.length) {
      this.setState({ list: noPluto });
    } else {
      this.setState({ list: this.state.list.concat([{ name: 'Pluto' }]) });
    }
  }

  private _toggleContainer() {
    this.setState({ useContainer: !this.state.useContainer });
  }

  private _onListChange(newList: ReadonlyArray<PlanetListItem>) {
    this.setState({ list: newList });
  }

  render() {
    const { useContainer } = this.state;

    return (
      <div className="main">
        <div className="intro">
          <p>
            This is a demonstration of the{' '}
            <a href="https://github.com/StreakYC/react-draggable-list">
              react-draggable-list
            </a>{' '}
            library.
          </p>
          <p>
            Each item has a drag handle visible when the user hovers over them.
            The items may have any height, and can each define their own height
            to use while being dragged.
          </p>
          <p>
            When the list is reordered, the page will be scrolled if possible to
            keep the moved item visible and on the same part of the screen.
          </p>
          <div>
            <input
              type="button"
              value="Toggle Pluto"
              onClick={() => this._togglePluto()}
            />
            <input
              type="button"
              value="Toggle Container"
              onClick={() => this._toggleContainer()}
            />
          </div>
        </div>
        <div
          className="list"
          ref={this._container}
          style={{
            overflow: useContainer ? 'auto' : '',
            height: useContainer ? '200px' : '',
            border: useContainer ? '1px solid gray' : '',
          }}
        >
          <DraggableList<PlanetListItem, void, PlanetItem>
            itemKey="name"
            template={PlanetItem}
            list={this.state.list}
            onMoveEnd={(newList) => this._onListChange(newList)}
            container={() =>
              useContainer ? this._container.current! : document.body
            }
          />
        </div>
        <footer>Footer here.</footer>
      </div>
    );
  }
}


================================================
FILE: example/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-eval'"
    />
    <title>React Draggable List Test Page</title>
    <script src="dist/main.js"></script>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <div id="main"></div>
  </body>
</html>


================================================
FILE: example/main.css
================================================
.intro,
footer {
  width: 400px;
  margin: auto;
}

.list {
  width: 430px;
  margin: 10px auto;
  padding: 15px;
}
.item {
  border: 1px solid black;
  overflow: hidden;
  transform-origin: 30% 50% 0px;
  padding-left: 20px;
  background: white;
  max-height: 100%;
}
.item .dragHandle {
  visibility: hidden;
  position: absolute;
  top: 0;
  left: 0;
  cursor: move;
  width: 20px;
  height: 16px;
  background: url('grippy.png') 30% 50% no-repeat no-repeat;
}
.item:hover .dragHandle,
.item.dragged .dragHandle {
  visibility: visible;
}
.item h2 {
  margin: 0;
}
.item .subtitle {
  font-weight: bold;
}

::-webkit-scrollbar {
  -webkit-appearance: none;
  width: 7px;
}
::-webkit-scrollbar-thumb {
  border-radius: 4px;
  background-color: rgba(0, 0, 0, 0.5);
  -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}


================================================
FILE: example/main.tsx
================================================
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Example from './Example';

const onReady = new Promise((resolve) => {
  if (document.readyState === 'complete') {
    resolve(undefined);
  } else {
    document.addEventListener('DOMContentLoaded', resolve, false);
    window.addEventListener('load', resolve, false);
  }
});

function main() {
  const mainDiv = document.getElementById('main');
  if (!mainDiv) throw new Error();
  ReactDOM.render(<Example />, mainDiv);
}

onReady.then(main).catch((e) => {
  console.error(e, e.stack); // eslint-disable-line no-console
});


================================================
FILE: index.js.flow
================================================
/* @flow */

import * as React from 'react';

export type TemplateProps<I, C> = {
  item: I,
  itemSelected: number,
  anySelected: number,
  dragHandleProps: Object,
  commonProps: C,
};

export type Props<I, C, T> = {
  itemKey: string | ((item: I) => string),
  template: Class<T>,
  list: $ReadOnlyArray<I>,
  onMoveEnd?: ?(
    newList: $ReadOnlyArray<I>,
    movedItem: I,
    oldIndex: number,
    newIndex: number
  ) => void,
  container?: ?() => ?HTMLElement,
  constrainDrag: boolean,
  springConfig: Object,
  padding: number,
  unsetZIndex: boolean,
  autoScrollMaxSpeed: number,
  autoScrollRegionSize: number,
  commonProps?: C,
};

declare export default class DraggableList<
    I,
    C = *,
    T: React.Component<$Shape<TemplateProps<I, C>>, *> = *
  >
  extends React.Component<Props<I, C, T>>
{
  getItemInstance(key: string): T;
}


================================================
FILE: jest.config.js
================================================
module.exports = {
  testEnvironment: 'jsdom',
  modulePathIgnorePatterns: ['<rootDir>/dist/']
};


================================================
FILE: package.json
================================================
{
  "name": "react-draggable-list",
  "version": "4.2.1",
  "description": "Make a list of draggable collapsible items.",
  "main": "dist/src/index.js",
  "sideEffects": false,
  "scripts": {
    "prepare": "rimraf dist && babel -s true -d dist/src/ src/ -x .ts,.tsx --ignore '**/*.test.tsx' && tsc",
    "example-build": "esbuild example/main.tsx --bundle --outdir=example/dist --public-path=.",
    "example-watch": "npm example-build --watch",
    "test": "npm run lint && flow check && jest && tsc --noEmit",
    "lint": "eslint . --ext js,jsx,ts,tsx",
    "lint-fix": "eslint . --ext js,jsx,ts,tsx --fix"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/StreakYC/react-draggable-list.git"
  },
  "keywords": [
    "react",
    "react-component",
    "animation",
    "reorder",
    "move",
    "drag-and-drop",
    "draggable"
  ],
  "files": [
    "dist",
    "index.js.flow"
  ],
  "author": "Chris Cowan <agentme49@gmail.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/StreakYC/react-draggable-list/issues"
  },
  "homepage": "https://github.com/StreakYC/react-draggable-list#readme",
  "devDependencies": {
    "@babel/cli": "^7.0.0",
    "@babel/core": "^7.0.0",
    "@babel/plugin-transform-runtime": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "@babel/preset-typescript": "^7.6.0",
    "@types/classnames": "^2.2.9",
    "@types/jest": "^29.5.12",
    "@types/react": "^17.0.24",
    "@types/react-dom": "^17.0.9",
    "@types/react-motion": "^0.0.33",
    "@typescript-eslint/eslint-plugin": "^7.0.2",
    "@typescript-eslint/parser": "^7.0.2",
    "babel-jest": "^29.7.0",
    "babel-plugin-add-module-exports": "^1.0.0",
    "classnames": "^2.2.3",
    "esbuild": "^0.19.10",
    "eslint": "^8.21.0",
    "eslint-plugin-react": "^7.4.0",
    "flow-bin": "^0.184.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "pdelay": "^2.0.0",
    "prettier": "~2.4.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "rimraf": "^5.0.5",
    "typescript": "^5.8.3"
  },
  "resolutions": {
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0"
  },
  "dependencies": {
    "@babel/runtime": "^7.0.0",
    "@types/prop-types": "^15.7.3",
    "immutability-helper": "^3.0.0",
    "prop-types": "^15.6.0",
    "react-motion": "^0.5.2",
    "react-multi-ref": "^1.0.0"
  },
  "peerDependencies": {
    "react": "^16.6.0 || ^17.0.0"
  }
}


================================================
FILE: src/MoveContainer.tsx
================================================
import * as React from 'react';
import TemplateContainer from './TemplateContainer';

export interface HeightData {
  natural: number;
  drag: number;
}

interface Props<I, C, T> {
  item: I;
  template: new (props: any, context?: any) => T;
  padding: number;
  y: number | undefined;
  itemSelected: number;
  anySelected: number;
  height: HeightData;
  zIndex: React.CSSProperties['zIndex'];
  makeDragHandleProps: (getY: () => number | undefined) => object;
  commonProps: C;
}

export default class MoveContainer<
  I,
  C,
  T extends React.Component<any, any>
> extends React.Component<Props<I, C, T>> {
  private readonly _templateContainer =
    React.createRef<TemplateContainer<I, C, T>>();
  private readonly _el = React.createRef<HTMLDivElement>();

  getDOMNode(): HTMLElement {
    return this._el.current!;
  }

  getTemplate(): T {
    return this._templateContainer.current!.getTemplate();
  }

  shouldComponentUpdate(nextProps: Props<I, C, T>): boolean {
    return (
      this.props.anySelected !== nextProps.anySelected ||
      this.props.itemSelected !== nextProps.itemSelected ||
      this.props.item !== nextProps.item ||
      this.props.template !== nextProps.template ||
      this.props.y !== nextProps.y ||
      this.props.height !== nextProps.height ||
      this.props.zIndex !== nextProps.zIndex ||
      this.props.commonProps !== nextProps.commonProps
    );
  }

  private _dragHandleProps = this.props.makeDragHandleProps(() => this.props.y);

  render() {
    const {
      item,
      y,
      padding,
      itemSelected,
      anySelected,
      height,
      zIndex,
      template,
      commonProps,
    } = this.props;

    return (
      <div
        ref={this._el}
        style={{
          position: y == null ? ('relative' as const) : ('absolute' as const),
          boxSizing: 'border-box' as const,
          left: '0px',
          right: '0px',
          top: y == null ? '0px' : `${y}px`,
          marginBottom: `${padding}px`,
          height:
            y == null
              ? 'auto'
              : `${
                  anySelected * (height.drag - height.natural) + height.natural
                }px`,
          zIndex,
        }}
      >
        <TemplateContainer
          ref={this._templateContainer}
          item={item}
          template={template}
          itemSelected={itemSelected}
          anySelected={anySelected}
          dragHandleProps={this._dragHandleProps}
          commonProps={commonProps}
        />
      </div>
    );
  }
}


================================================
FILE: src/OnUpdate.tsx
================================================
import * as React from 'react';

interface Props {
  cb: () => void;
}

export default class OnUpdate extends React.Component<Props> {
  public componentDidUpdate() {
    this.props.cb();
  }

  public render() {
    return null;
  }
}


================================================
FILE: src/TemplateContainer.tsx
================================================
import * as React from 'react';

interface Props<I, C, T> {
  item: I;
  template: new (props: any, context?: any) => T;
  itemSelected: number;
  anySelected: number;
  dragHandleProps: object;
  commonProps: C;
}

export default class TemplateContainer<
  I,
  C,
  T extends React.Component<any, any>
> extends React.Component<Props<I, C, T>> {
  private _template: T | undefined;
  private readonly _templateSetter = (cmp: any) => {
    this._template = cmp;
  };

  public shouldComponentUpdate(nextProps: Props<I, C, T>): boolean {
    return (
      this.props.anySelected !== nextProps.anySelected ||
      this.props.itemSelected !== nextProps.itemSelected ||
      this.props.item !== nextProps.item ||
      this.props.template !== nextProps.template ||
      this.props.commonProps !== nextProps.commonProps
    );
  }

  public getTemplate(): T {
    return this._template!;
  }

  public render() {
    const { item, itemSelected, anySelected, dragHandleProps, commonProps } =
      this.props;
    const Template = this.props.template;

    return (
      <Template
        ref={this._templateSetter}
        item={item}
        itemSelected={itemSelected}
        anySelected={anySelected}
        dragHandleProps={dragHandleProps}
        commonProps={commonProps}
      />
    );
  }
}


================================================
FILE: src/index.test.tsx
================================================
/**
 * @jest-environment jsdom
 */
/* eslint-disable @typescript-eslint/no-use-before-define, @typescript-eslint/no-empty-function */

import delay from 'pdelay';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as TestUtils from 'react-dom/test-utils';
import DraggableList from '../src';

interface Item {
  name: string;
  extra?: number;
}

interface TestTemplateProps {
  item: Item;
  dragHandleProps: any;
  commonProps: any;
}

class TestTemplate extends React.Component<TestTemplateProps> {
  private readonly _elRef = React.createRef<HTMLDivElement>();

  render() {
    const { item, dragHandleProps } = this.props;
    return (
      <div ref={this._elRef} className="item" {...dragHandleProps}>
        {item.name}
      </div>
    );
  }

  getName() {
    return this.props.item.name;
  }

  getDragHeight() {
    return 30;
  }

  getDOMNode(): HTMLDivElement {
    return this._elRef.current!;
  }

  shouldComponentUpdate(nextProps: TestTemplateProps) {
    return this.props.item !== nextProps.item;
  }

  componentDidMount() {
    const el = this._elRef.current!;
    Object.defineProperty(el, 'offsetHeight', {
      get: () => 115,
    });
  }
}

const springConfig = { stiffness: 1500, damping: 50 };

test('drag works', async () => {
  let _scrollTop = 0;
  const containerEl: any = {
    get scrollTop() {
      return _scrollTop;
    },
    set scrollTop(x) {
      _scrollTop = x;
    },
  };

  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  const commonProps = { a: 'foo' };

  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
        container={() => containerEl}
        commonProps={commonProps}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  expect(root.getItemInstance('grif').getName()).toBe('grif');
  expect(root.getItemInstance('grif').getDragHeight()).toBe(30);
  expect(root.getItemInstance('grif').props.commonProps).toBe(commonProps);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  expect(root.state.dragging).toBe(false);
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });
  expect(root.state.dragging).toBe(true);

  (root as any)._handleMouseMove({ pageY: 600 });
  await delay(30);

  (root as any)._handleMouseMove({ pageY: 650 });

  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);

  const reorderedList = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'caboose' },
    { name: 'grif' },
    { name: 'donut' },
  ];

  expect(onMoveEnd.mock.calls[0]).toEqual([
    reorderedList,
    { name: 'caboose' },
    0,
    4,
  ]);

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);

  expect(_scrollTop).toBe(0);
  await delay(30);
  expect(_scrollTop).toBeGreaterThan(20);
});

test('two drags work', async () => {
  let _scrollTop = 0;
  const containerEl: any = {
    get scrollTop() {
      return _scrollTop;
    },
    set scrollTop(x) {
      _scrollTop = x;
    },
  };

  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];

  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        onMoveEnd={onMoveEnd}
        template={TestTemplate}
        springConfig={springConfig}
        container={() => containerEl}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  expect(root.state.dragging).toBe(false);
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });
  expect(root.state.dragging).toBe(true);

  (root as any)._handleMouseMove({ pageY: 600 });
  await delay(30);
  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);

  const reorderedList = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'caboose' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  expect(onMoveEnd.mock.calls[0]).toEqual([
    reorderedList,
    { name: 'caboose' },
    0,
    2,
  ]);

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);

  expect(root.state.dragging).toBe(false);
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 600,
    preventDefault() {},
  });
  expect(root.state.dragging).toBe(true);
  (root as any)._handleMouseMove({ pageY: 650 });
  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(2);

  const reorderedList2 = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'caboose' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  expect(onMoveEnd.mock.calls[1]).toEqual([
    reorderedList2,
    { name: 'caboose' },
    2,
    3,
  ]);

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList2);

  expect(_scrollTop).toBe(0);
  await delay(30);
  expect(_scrollTop).toBeGreaterThan(20);
});

test('props reordered during drag works', () => {
  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });

  list = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'caboose', extra: 1 },
    { name: 'donut' },
  ];
  render();

  (root as any)._handleMouseMove({ pageY: 450 });
  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);

  const reorderedList = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'caboose', extra: 1 },
    { name: 'grif' },
    { name: 'donut' },
  ];
  expect(onMoveEnd.mock.calls[0]).toEqual([
    reorderedList,
    { name: 'caboose', extra: 1 },
    5,
    4,
  ]);

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);
});

test('item removed during drag works', () => {
  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });

  list = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif', extra: 2 },
    { name: 'donut' },
  ];
  render();

  (root as any)._handleMouseMove({ pageY: 650 });
  const reorderedList = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif', extra: 2 },
    { name: 'donut' },
  ];
  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);

  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);
});

test('item removed before drag end works', async () => {
  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });
  (root as any)._handleMouseMove({ pageY: 650 });
  await delay(100);

  // eslint-disable-next-line require-atomic-updates
  list = [
    { name: 'caboose', extra: 3 },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif', extra: 2 },
  ];
  render();
  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);

  const reorderedList = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'caboose', extra: 3 },
    { name: 'grif', extra: 2 },
  ];
  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(reorderedList);
});

test('dragged item removed after drag during animation works', () => {
  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });
  (root as any)._handleMouseMove({ pageY: 650 });

  expect(root.state.dragging).toBe(true);
  expect(onMoveEnd).toHaveBeenCalledTimes(0);
  (root as any)._handleMouseUp();
  expect(root.state.dragging).toBe(false);
  expect(onMoveEnd).toHaveBeenCalledTimes(1);

  const listMinusOne = [
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];
  list = listMinusOne;
  render();

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(listMinusOne);
});

test('list is shown with correct positions after being fully changed during animation', async () => {
  let list: Item[] = [
    { name: 'caboose' },
    { name: 'tucker' },
    { name: 'church' },
    { name: 'simmons' },
    { name: 'sarge' },
    { name: 'grif' },
    { name: 'donut' },
  ];

  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;
  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });

  await delay(100);

  (root as any)._handleMouseUp();
  await delay(1);

  expect(
    root.getItemInstance('caboose').getDOMNode().parentElement!.style.position
  ).toBe('absolute');

  list = [{ name: 'lopez' }, { name: "o'malley" }];
  render();
  while (
    root.getItemInstance('lopez').getDOMNode().parentElement!.style.position ===
    'absolute'
  ) {
    await delay(10);
  }
  expect(
    root.getItemInstance('lopez').getDOMNode().parentElement!.style.position
  ).toBe('relative');
});

test('updating commonProps works', () => {
  let list: Item[] = [{ name: 'caboose' }, { name: 'donut' }];
  let commonProps: any = { a: 5 };
  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
        commonProps={commonProps}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.item
    )
  ).toEqual(list);
  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.commonProps
    )
  ).toEqual(list.map(() => ({ a: 5 })));

  commonProps = { b: 6 };
  render();

  expect(
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate).map(
      (e) => e.props.commonProps
    )
  ).toEqual(list.map(() => ({ b: 6 })));
});

test('onDragEnd and onDragStart callbacks are correctly called', () => {
  let _scrollTop = 0;
  const containerEl: any = {
    get scrollTop() {
      return _scrollTop;
    },
    set scrollTop(x) {
      _scrollTop = x;
    },
  };

  let list: Item[] = [
    { name: 'alice' },
    { name: 'bob' },
    { name: 'charlie' },
    { name: 'deb' },
    { name: 'ethan' },
  ];

  const div = document.createElement('div');

  const onMoveEnd = jest.fn((newList) => {
    list = newList;
    render();
  });

  const onDragStart = jest.fn(() => {

  });

  const onDragEnd = jest.fn(() => {

  });

  const rootRef = React.createRef<DraggableList<Item, any, TestTemplate>>();
  function render() {
    ReactDOM.render(
      <DraggableList
        ref={rootRef}
        itemKey="name"
        list={list}
        template={TestTemplate}
        onMoveEnd={onMoveEnd}
        springConfig={springConfig}
        container={() => containerEl}
        onDragStart={onDragStart}
        onDragEnd={onDragEnd}
      />,
      div
    );
  }
  render();
  const root = rootRef.current!;

  const renderedHandles: Array<TestTemplate> =
    TestUtils.scryRenderedComponentsWithType(root, TestTemplate) as any;

  expect(onDragStart).toHaveBeenCalledTimes(0);

  renderedHandles[0].props.dragHandleProps.onMouseDown({
    pageY: 500,
    preventDefault() {},
  });

  expect(onDragStart).toHaveBeenCalledTimes(1);
  expect(onDragStart).toHaveBeenLastCalledWith({name: 'alice'});
  expect(onDragEnd).toHaveBeenCalledTimes(0);

  (root as any)._handleMouseMove({ pageY: 600 });

  (root as any)._handleMouseUp();

  expect(onDragEnd).toHaveBeenCalledTimes(1);
  expect(onDragEnd).toHaveBeenLastCalledWith({name: 'alice'});

});


================================================
FILE: src/index.tsx
================================================
/* eslint react/prop-types: "error" */

import * as React from 'react';
import * as PropTypes from 'prop-types';
import { Motion, spring, OpaqueConfig } from 'react-motion';
import update from 'immutability-helper';
import MultiRef from 'react-multi-ref';
import OnUpdate from './OnUpdate';
import MoveContainer, { HeightData } from './MoveContainer';

const DEFAULT_HEIGHT: HeightData = { natural: 200, drag: 30 };

function getScrollSpeed(distance: number, speed: number, size: number): number {
  // If distance is zero, then the result is the max speed. Otherwise,
  // the result tapers toward zero as it gets closer to the opposite
  // edge of the region.
  return Math.round(speed - (speed / size) * distance);
}

interface Drag {
  itemKey: string;

  // The y position of the dragged item when the drag started. This will be
  // equal to the initial mouseY value. The items not being dragged will be
  // positioned so that the dragged item's original position lines up with
  // startY.
  startY: number;

  // The y-position that corresponds to the mouse's current location. The
  // dragged item will be rendered with this as its y-position.
  mouseY: number;

  // This is the difference between the raw mouse y value and startY. For
  // example, if a user clicks the drag handle at a point 5 px below the item's
  // top, then mouseOffset will be set to 5. As the user moves their mouse, we
  // update mouseY to be the raw mouse y value minus mouseOffset.
  mouseOffset: number;
}

export interface TemplateProps<I, C> {
  item: I;
  itemSelected: number;
  anySelected: number;
  dragHandleProps: object;
  commonProps: C;
}

export interface Props<I, C, T> {
  itemKey: string | ((item: I) => string);
  template: new (props: any, context?: any) => T;
  list: ReadonlyArray<I>;
  onMoveEnd?: (
    newList: ReadonlyArray<I>,
    movedItem: I,
    oldIndex: number,
    newIndex: number
  ) => void;
  container?: () => HTMLElement | null | undefined;
  constrainDrag?: boolean;
  springConfig?: object;
  padding?: number;
  unsetZIndex?: boolean;
  autoScrollMaxSpeed?: number;
  autoScrollRegionSize?: number;
  commonProps?: C;
  onDragStart?: (draggedItem: I) => void;
  onDragEnd?: (draggedItem: I) => void;
}
interface State {
  useAbsolutePositioning: boolean;
  dragging: boolean;
  lastDrag: Drag | null;
  heights: { [key: string]: HeightData } | null;
}
export default class DraggableList<
  I,
  C,
  T extends React.Component<Partial<TemplateProps<I, C>>>
> extends React.Component<Props<I, C, T>, State> {
  public static propTypes = {
    itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
    template: PropTypes.func,
    list: PropTypes.array.isRequired,
    onMoveEnd: PropTypes.func,
    container: PropTypes.func,
    springConfig: PropTypes.object,
    constrainDrag: PropTypes.bool,
    padding: PropTypes.number,
    unsetZIndex: PropTypes.bool,
    autoScrollMaxSpeed: PropTypes.number.isRequired,
    autoScrollRegionSize: PropTypes.number.isRequired,
    commonProps: PropTypes.object,
  };
  public static defaultProps: Partial<Props<any, any, any>> = {
    springConfig: { stiffness: 300, damping: 50 },
    padding: 10,
    unsetZIndex: false,
    constrainDrag: false,
    autoScrollMaxSpeed: 15,
    autoScrollRegionSize: 30,
  };
  private readonly _itemRefs: MultiRef<string, MoveContainer<I, any, T>> =
    new MultiRef();
  private _autoScrollerTimer: any;

  private _listRef = React.createRef<HTMLDivElement>();

  public state: State = {
    useAbsolutePositioning: false,
    dragging: false,
    lastDrag: null,
    heights: null,
  };

  public getItemInstance(key: string): T {
    const ref = this._itemRefs.map.get(key);
    if (!ref) throw new Error('key not found');
    return ref.getTemplate();
  }

  public static getDerivedStateFromProps<I, C, T>(
    newProps: Props<I, C, T>,
    state: State
  ): Partial<State> | null {
    const { list } = newProps;
    const { lastDrag } = state;

    if (lastDrag) {
      const keyFn = DraggableList._getKeyFn<I>(newProps.itemKey);

      try {
        DraggableList._getIndexOfItemWithKey<I>(keyFn, list, lastDrag.itemKey);
      } catch (err) {
        // If the dragged item was removed from the list, this block will get hit.
        // Cancel the drag.
        return { dragging: false, lastDrag: null };
      }
    }

    return null;
  }

  public componentWillUnmount() {
    this._handleMouseUp();
  }

  private _handleTouchStart(
    itemKey: string,
    pressY: number | undefined,
    event: TouchEvent | React.TouchEvent
  ) {
    event.stopPropagation();
    this._handleStartDrag(itemKey, pressY, event.touches[0].pageY);
  }

  private _handleMouseDown(
    itemKey: string,
    pressY: number | undefined,
    event: MouseEvent | React.MouseEvent
  ) {
    event.preventDefault();
    this._handleStartDrag(itemKey, pressY, event.pageY);
  }

  private _handleStartDrag(
    itemKey: string,
    pressY: number | undefined,
    pageY: number
  ) {
    if (document.documentElement)
      document.documentElement.style.cursor = 'move';
    window.addEventListener('mouseup', this._handleMouseUp);
    window.addEventListener('touchend', this._handleMouseUp);
    window.addEventListener('touchmove', this._handleTouchMove);
    window.addEventListener('mousemove', this._handleMouseMove);

    const keyFn = this._getKeyFn();

    if (this.props.onDragStart) {
      const {list} = this.props;
      const draggedItem = list[DraggableList._getIndexOfItemWithKey(keyFn, list, itemKey)];
      this.props.onDragStart(draggedItem);
    }

    // If an element has focus while we drag around the parent, some browsers
    // try to scroll the parent element to keep the focused element in view.
    // Prevent that by unfocusing the element if it is focused.
    {
      const listEl = this._listRef.current;
      if (!listEl) throw new Error('Should not happen');
      if (
        listEl.contains &&
        document.activeElement &&
        listEl.contains(document.activeElement) &&
        document.activeElement instanceof HTMLElement
      ) {
        document.activeElement.blur();
      }
    }

    let newHeights = null;
    if (this.state.heights == null) {
      const _newHeights: { [key: string]: HeightData } = Object.create(null);

      this.props.list.forEach((item) => {
        const key = keyFn(item);
        const containerRef = this._itemRefs.map.get(key);
        const refEl = containerRef
          ? containerRef.getDOMNode().firstElementChild
          : null;
        const ref = containerRef ? containerRef.getTemplate() : null;
        const natural =
          refEl instanceof HTMLElement
            ? refEl.offsetHeight
            : DEFAULT_HEIGHT.natural;
        const drag =
          (ref &&
            typeof (ref as any).getDragHeight === 'function' &&
            (ref as any).getDragHeight()) ||
          natural;

        _newHeights[key] = { natural, drag };
      });

      newHeights = _newHeights;
    }

    // Need to re-render once before we start dragging so that the `y` values
    // are set using the correct state.heights and then can animate from there.

    const afterHeights = () => {
      const itemIndex = DraggableList._getIndexOfItemWithKey(keyFn, this.props.list, itemKey);

      // pressY will be non-null if the list is currently animating (because the
      // clicked item has its `y` prop set). pressY will be null if the list is
      // not currently animating (because the clicked item will be at its
      // natural position, which is calculatable using _getDistance).
      const startY =
        pressY == null ? this._getDistance(0, itemIndex, false) : pressY;

      const containerEl = this._getContainer();
      const containerScroll =
        !containerEl || containerEl === document.body
          ? 0
          : containerEl.scrollTop;

      this.setState({
        useAbsolutePositioning: true,
        dragging: true,
        lastDrag: {
          itemKey,
          startY,
          mouseY: startY,
          mouseOffset: pageY - startY + containerScroll,
        },
      });
    };

    if (newHeights) {
      this.setState({ heights: newHeights }, afterHeights);
    } else {
      afterHeights();
    }
  }

  private _handleTouchMove = (e: TouchEvent) => {
    e.preventDefault();
    this._handleMouseMove(e.touches[0]);
  };

  private _handleMouseMove = ({
    pageY,
    clientY,
  }: MouseEvent | Touch | { pageY: number; clientY: number }) => {
    const { list } = this.props;
    const autoScrollMaxSpeed = this.props.autoScrollMaxSpeed!;
    const autoScrollRegionSize = this.props.autoScrollRegionSize!;
    const { dragging, lastDrag } = this.state;
    if (!dragging || !lastDrag) return;

    const containerEl = this._getContainer();
    const dragVisualIndex = this._getDragVisualIndex();
    const keyFn = this._getKeyFn();

    clearInterval(this._autoScrollerTimer);

    // If the user has the mouse near the top or bottom of the container and
    // not at the end of the list, then autoscroll.
    if (dragVisualIndex !== 0 && dragVisualIndex !== list.length - 1) {
      let scrollSpeed = 0;

      const containerRect =
        containerEl &&
        containerEl !== document.body &&
        containerEl.getBoundingClientRect
          ? containerEl.getBoundingClientRect()
          : { top: 0, bottom: Infinity };

      // Get the lowest of the screen top and the container top.
      const top = Math.max(0, containerRect.top);

      const distanceFromTop = clientY - top;
      if (distanceFromTop > 0 && distanceFromTop < autoScrollRegionSize) {
        scrollSpeed =
          -1 *
          getScrollSpeed(
            distanceFromTop,
            autoScrollMaxSpeed,
            autoScrollRegionSize
          );
      } else {
        // Get the lowest of the screen bottom and the container bottom.
        const bottom = Math.min(window.innerHeight, containerRect.bottom);
        const distanceFromBottom = bottom - clientY;
        if (
          distanceFromBottom > 0 &&
          distanceFromBottom < autoScrollRegionSize
        ) {
          scrollSpeed = getScrollSpeed(
            distanceFromBottom,
            autoScrollMaxSpeed,
            autoScrollRegionSize
          );
        }
      }

      if (scrollSpeed !== 0) {
        this._scrollContainer(scrollSpeed);
        this._autoScrollerTimer = setTimeout(() => {
          this._handleMouseMove({
            pageY: pageY + (containerEl === document.body ? scrollSpeed : 0),
            clientY,
          });
        }, 16);
      }
    }

    const containerScroll =
      !containerEl || containerEl === document.body ? 0 : containerEl.scrollTop;
    let mouseY = pageY - lastDrag.mouseOffset + containerScroll;
    if (this.props.constrainDrag) {
      const visualList = this._getVisualListDuringDrag();

      mouseY = Math.max(
        mouseY,
        this._getDistanceFromTopDuringDrag(
          lastDrag,
          keyFn(visualList[0]),
          visualList
        )
      );
      mouseY = Math.min(
        mouseY,
        this._getDistanceFromTopDuringDrag(
          lastDrag,
          keyFn(visualList[visualList.length - 1]),
          visualList
        )
      );
    }

    this.setState({ lastDrag: { ...lastDrag, mouseY } });
  };

  private _handleMouseUp = () => {
    clearInterval(this._autoScrollerTimer);
    window.removeEventListener('mouseup', this._handleMouseUp);
    window.removeEventListener('touchend', this._handleMouseUp);
    window.removeEventListener('touchmove', this._handleTouchMove);
    window.removeEventListener('mousemove', this._handleMouseMove);

    if (document.documentElement) document.documentElement.style.cursor = '';
    this._lastScrollDelta = 0;

    const { list, onMoveEnd, onDragEnd } = this.props;
    const { dragging, lastDrag } = this.state;

    if (dragging && lastDrag && onMoveEnd) {
      const dragIndex = this._getDragListIndex();
      const newIndex = this._getDragVisualIndex();

      if (onDragEnd) {
        onDragEnd(list[dragIndex]);
      }

      if (dragIndex !== newIndex) {
        const newList = this._getVisualListDuringDrag();

        onMoveEnd(newList, list[dragIndex], dragIndex, newIndex);
      }

      this.setState({ dragging: false });
    }
  };

  private _scrollContainer(delta: number) {
    const containerEl = this._getContainer();
    if (!containerEl) return;
    if (window.scrollBy && containerEl === document.body) {
      window.scrollBy(0, delta);
    } else {
      containerEl.scrollTop += delta;
    }
  }

  private _lastScrollDelta = 0;
  private _adjustScrollAtEnd(delta: number) {
    const frameDelta = Math.round(delta - this._lastScrollDelta);
    this._scrollContainer(frameDelta);
    this._lastScrollDelta += frameDelta;
  }

  private static _getIndexOfItemWithKey<I>(
    keyFn: (item: I) => string,
    list: ReadonlyArray<I>,
    itemKey: string
  ): number {
    for (let i = 0, len = list.length; i < len; i++) {
      if (keyFn(list[i]) === itemKey) {
        return i;
      }
    }
    throw new Error('Failed to find drag index');
  }

  private _getDragListIndex(): number {
    const { list } = this.props;
    const { lastDrag } = this.state;
    if (!lastDrag) {
      throw new Error('No drag happened');
    }
    const keyFn = this._getKeyFn();
    return DraggableList._getIndexOfItemWithKey(keyFn, list, lastDrag.itemKey);
  }

  private _getDragVisualIndex(): number {
    const { list } = this.props;
    const padding = this.props.padding!;
    const { dragging, lastDrag } = this.state;
    if (!dragging || !lastDrag) throw new Error('Should not happen');

    const dragListIndex = this._getDragListIndex();

    const { mouseY, startY } = lastDrag;

    const movementFromNatural = mouseY - startY;
    // 1 down, -1 up, 0 neither
    const direction =
      movementFromNatural > 0 ? 1 : movementFromNatural < 0 ? -1 : 0;
    let newIndex = dragListIndex;
    if (direction !== 0) {
      const keyFn = this._getKeyFn();
      let reach = Math.abs(movementFromNatural);
      for (
        let i = dragListIndex + direction;
        i < list.length && i >= 0;
        i += direction
      ) {
        const iDragHeight = this._getItemHeight(keyFn(list[i])).drag;
        if (reach < iDragHeight / 2 + padding) break;
        reach -= iDragHeight + padding;
        newIndex = i;
      }
    }

    return newIndex;
  }

  private _getVisualListDuringDrag(): ReadonlyArray<I> {
    const { list } = this.props;
    const { dragging, lastDrag } = this.state;
    if (!dragging || !lastDrag)
      throw new Error(
        'Should not happen: _getVisualListDuringDrag called outside of drag'
      );

    const dragListIndex = this._getDragListIndex();
    const dragVisualIndex = this._getDragVisualIndex();

    return update(list, {
      $splice: [
        [dragListIndex, 1],
        [dragVisualIndex, 0, list[dragListIndex]],
      ],
    });
  }

  private _getItemHeight(key: string): HeightData {
    return this.state.heights != null && key in this.state.heights
      ? this.state.heights[key]
      : DEFAULT_HEIGHT;
  }

  // Get the distance between the tops of two items in the list.
  // Does not consider how the dragged item may be rendered in a different position
  // unless you pass in the re-ordered list as a parameter.
  private _getDistance(
    start: number,
    end: number,
    dragging: boolean,
    list: ReadonlyArray<I> = this.props.list
  ): number {
    if (end < start) {
      return -this._getDistance(end, start, dragging, list);
    }

    const padding = this.props.padding!;
    const keyFn = this._getKeyFn();
    let distance = 0;
    for (let i = start; i < end; i++) {
      const height = this._getItemHeight(keyFn(list[i]));
      distance += (dragging ? height.drag : height.natural) + padding;
    }
    return distance;
  }

  private _getDistanceFromTopDuringDrag(
    lastDrag: Drag,
    itemKey: string,
    visualList: ReadonlyArray<I>
  ): number {
    const keyFn = this._getKeyFn();
    const index = DraggableList._getIndexOfItemWithKey(
      keyFn,
      visualList,
      itemKey
    );
    const dragListIndex = this._getDragListIndex();
    const dragVisualIndex = DraggableList._getIndexOfItemWithKey(
      keyFn,
      visualList,
      lastDrag.itemKey
    );

    let offset = 0;
    if (dragVisualIndex < dragListIndex) {
      const dragItemHeight = this._getItemHeight(lastDrag.itemKey);
      const newCenterHeight = this._getItemHeight(
        keyFn(visualList[dragListIndex])
      );
      offset = dragItemHeight.drag - newCenterHeight.drag;
    }

    return (
      lastDrag.startY +
      offset +
      this._getDistance(dragListIndex, index, true, visualList)
    );
  }

  private _getContainer(): HTMLElement | null | undefined {
    const { container } = this.props;
    return container ? container() : null;
  }

  private static _getKeyFn<I>(
    itemKey: string | ((item: I) => string)
  ): (item: I) => string {
    return typeof itemKey === 'function' ? itemKey : (x) => (x as any)[itemKey];
  }

  private _getKeyFn(): (item: I) => string {
    return DraggableList._getKeyFn<I>(this.props.itemKey);
  }

  render() {
    const padding = this.props.padding!;
    const {
      list,
      springConfig,
      container,
      template,
      unsetZIndex,
      commonProps,
    } = this.props;
    const { dragging, lastDrag, useAbsolutePositioning } = this.state;

    const keyFn = this._getKeyFn();
    const anySelected = spring(dragging ? 1 : 0, springConfig);

    const visualList = dragging ? this._getVisualListDuringDrag() : list;

    const children = list.map((item, i) => {
      const key = keyFn(item);
      const selectedStyle =
        dragging && lastDrag && lastDrag.itemKey === key
          ? {
              itemSelected: spring(1, springConfig),
              y: lastDrag.mouseY,
            }
          : {
              itemSelected: spring(0, springConfig),
              y: (useAbsolutePositioning ? spring : (x: number) => x)(
                dragging && lastDrag
                  ? this._getDistanceFromTopDuringDrag(
                      lastDrag,
                      key,
                      visualList
                    )
                  : this._getDistance(0, i, false),
                springConfig
              ),
            };
      const style = {
        anySelected,
        ...selectedStyle,
      };
      const makeDragHandleProps = (getY: () => number | undefined): object => ({
        onMouseDown: (e: React.MouseEvent) =>
          this._handleMouseDown(key, getY(), e),
        onTouchStart: (e: React.TouchEvent) =>
          this._handleTouchStart(key, getY(), e),
      });
      const height = this._getItemHeight(key);
      return (
        <Motion
          style={style}
          key={key}
          children={({ itemSelected, anySelected, y }) => (
            <MoveContainer
              ref={this._itemRefs.ref(key)}
              y={useAbsolutePositioning ? y : undefined}
              template={template}
              padding={padding}
              item={item}
              itemSelected={itemSelected}
              anySelected={anySelected}
              height={height}
              zIndex={
                unsetZIndex && !useAbsolutePositioning
                  ? 'auto'
                  : lastDrag && lastDrag.itemKey === key
                  ? list.length
                  : i
              }
              makeDragHandleProps={makeDragHandleProps}
              commonProps={commonProps}
            />
          )}
        />
      );
    });

    let adjustScroll: number | OpaqueConfig = 0;
    if (!dragging && lastDrag && useAbsolutePositioning) {
      const dragListIndex = this._getDragListIndex();
      adjustScroll = spring(
        this._getDistance(0, dragListIndex, false) - lastDrag.mouseY,
        springConfig
      );
    }

    let heightReserverHeight = 0;
    let heightReserverMarginBottom = 0;
    if (list.length) {
      heightReserverMarginBottom = padding;
      if (useAbsolutePositioning) {
        heightReserverHeight =
          this._getDistance(0, list.length, false) - padding;
      }
    }
    return (
      <div style={{ position: 'relative' }} ref={this._listRef}>
        <Motion
          style={{ adjustScroll, anySelected }}
          onRest={() => {
            if (!dragging) {
              this.setState({
                heights: null,
                useAbsolutePositioning: false,
              });
            }
          }}
          children={({ adjustScroll }) => (
            <div
              style={{
                display: useAbsolutePositioning ? 'block' : 'none',
                height: `${heightReserverHeight}px`,
                marginBottom: `${heightReserverMarginBottom}px`,
              }}
            >
              {container && (
                <OnUpdate
                  cb={() => {
                    if (!dragging && lastDrag && useAbsolutePositioning) {
                      this._adjustScrollAtEnd(adjustScroll);
                    }
                  }}
                />
              )}
            </div>
          )}
        />
        {children}
      </div>
    );
  }
}


================================================
FILE: test-types/typescript.tsx
================================================
import * as React from 'react';
import DraggableList from '../src';

// This file isn't meant to be executed. It's just a test for the type definitions.

interface Item {
  a: number;
  b: string;
}

interface MyTempProps {
  item: Item;
  itemSelected: number;
  // purposefully omit anySelected here
  dragHandleProps: object;
}
interface MyTempState {
  foo: number;
}
class MyTemp extends React.Component<MyTempProps, MyTempState> {
  render() {
    return <div {...this.props.dragHandleProps} />;
  }
}

const list: Array<Item> = [{ a: 123, b: 'xyz' }];
const x = (
  <DraggableList<Item, void, MyTemp>
    itemKey="foo"
    list={list}
    template={MyTemp}
    onMoveEnd={(ignoredNewList) => {
      // ignore
    }}
  />
);
x;
const renderedDL: DraggableList<Item, void, MyTemp> = null as any;
const renderedItem = renderedDL.getItemInstance('foo');
renderedItem as MyTemp;


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": ".",
    "declaration": true,
    "strict": true,
    "jsx": "react",
    "module": "commonjs",
    "target": "ES2017",
    "moduleResolution": "node",
    "moduleDetection": "force"
  }
}
Download .txt
gitextract_v5ra7eiw/

├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .flowconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── node.js.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── example/
│   ├── Example.tsx
│   ├── index.html
│   ├── main.css
│   └── main.tsx
├── index.js.flow
├── jest.config.js
├── package.json
├── src/
│   ├── MoveContainer.tsx
│   ├── OnUpdate.tsx
│   ├── TemplateContainer.tsx
│   ├── index.test.tsx
│   └── index.tsx
├── test-types/
│   └── typescript.tsx
└── tsconfig.json
Download .txt
SYMBOL INDEX (94 symbols across 8 files)

FILE: example/Example.tsx
  type PlanetListItem (line 7) | interface PlanetListItem {
  type PlanetProps (line 12) | interface PlanetProps {
  type PlanetState (line 17) | interface PlanetState {
  class PlanetItem (line 20) | class PlanetItem extends React.Component<PlanetProps, PlanetState> {
    method _inc (line 25) | _inc() {
    method getDragHeight (line 31) | getDragHeight() {
    method render (line 35) | render() {
  type ExampleState (line 80) | type ExampleState = {
  class Example (line 84) | class Example extends React.Component<{}, ExampleState> {
    method _togglePluto (line 101) | private _togglePluto() {
    method _toggleContainer (line 110) | private _toggleContainer() {
    method _onListChange (line 114) | private _onListChange(newList: ReadonlyArray<PlanetListItem>) {
    method render (line 118) | render() {

FILE: example/main.tsx
  function main (line 14) | function main() {

FILE: src/MoveContainer.tsx
  type HeightData (line 4) | interface HeightData {
  type Props (line 9) | interface Props<I, C, T> {
  class MoveContainer (line 22) | class MoveContainer<
    method getDOMNode (line 31) | getDOMNode(): HTMLElement {
    method getTemplate (line 35) | getTemplate(): T {
    method shouldComponentUpdate (line 39) | shouldComponentUpdate(nextProps: Props<I, C, T>): boolean {
    method render (line 54) | render() {

FILE: src/OnUpdate.tsx
  type Props (line 3) | interface Props {
  class OnUpdate (line 7) | class OnUpdate extends React.Component<Props> {
    method componentDidUpdate (line 8) | public componentDidUpdate() {
    method render (line 12) | public render() {

FILE: src/TemplateContainer.tsx
  type Props (line 3) | interface Props<I, C, T> {
  class TemplateContainer (line 12) | class TemplateContainer<
    method shouldComponentUpdate (line 22) | public shouldComponentUpdate(nextProps: Props<I, C, T>): boolean {
    method getTemplate (line 32) | public getTemplate(): T {
    method render (line 36) | public render() {

FILE: src/index.test.tsx
  type Item (line 12) | interface Item {
  type TestTemplateProps (line 17) | interface TestTemplateProps {
  class TestTemplate (line 23) | class TestTemplate extends React.Component<TestTemplateProps> {
    method render (line 26) | render() {
    method getName (line 35) | getName() {
    method getDragHeight (line 39) | getDragHeight() {
    method getDOMNode (line 43) | getDOMNode(): HTMLDivElement {
    method shouldComponentUpdate (line 47) | shouldComponentUpdate(nextProps: TestTemplateProps) {
    method componentDidMount (line 51) | componentDidMount() {
  method scrollTop (line 64) | get scrollTop() {
  method scrollTop (line 67) | set scrollTop(x) {
  function render (line 91) | function render() {
  method preventDefault (line 124) | preventDefault() {}
  method scrollTop (line 170) | get scrollTop() {
  method scrollTop (line 173) | set scrollTop(x) {
  function render (line 196) | function render() {
  method preventDefault (line 224) | preventDefault() {}
  method preventDefault (line 261) | preventDefault() {}
  function render (line 316) | function render() {
  method preventDefault (line 342) | preventDefault() {}
  function render (line 404) | function render() {
  method preventDefault (line 430) | preventDefault() {}
  function render (line 489) | function render() {
  method preventDefault (line 515) | preventDefault() {}
  function render (line 569) | function render() {
  method preventDefault (line 595) | preventDefault() {}
  function render (line 642) | function render() {
  method preventDefault (line 662) | preventDefault() {}
  function render (line 698) | function render() {
  method scrollTop (line 739) | get scrollTop() {
  method scrollTop (line 742) | set scrollTop(x) {
  function render (line 771) | function render() {
  method preventDefault (line 797) | preventDefault() {}

FILE: src/index.tsx
  constant DEFAULT_HEIGHT (line 11) | const DEFAULT_HEIGHT: HeightData = { natural: 200, drag: 30 };
  function getScrollSpeed (line 13) | function getScrollSpeed(distance: number, speed: number, size: number): ...
  type Drag (line 20) | interface Drag {
  type TemplateProps (line 40) | interface TemplateProps<I, C> {
  type Props (line 48) | interface Props<I, C, T> {
  type State (line 69) | interface State {
  class DraggableList (line 75) | class DraggableList<
    method getItemInstance (line 115) | public getItemInstance(key: string): T {
    method getDerivedStateFromProps (line 121) | public static getDerivedStateFromProps<I, C, T>(
    method componentWillUnmount (line 143) | public componentWillUnmount() {
    method _handleTouchStart (line 147) | private _handleTouchStart(
    method _handleMouseDown (line 156) | private _handleMouseDown(
    method _handleStartDrag (line 165) | private _handleStartDrag(
    method _scrollContainer (line 396) | private _scrollContainer(delta: number) {
    method _adjustScrollAtEnd (line 407) | private _adjustScrollAtEnd(delta: number) {
    method _getIndexOfItemWithKey (line 413) | private static _getIndexOfItemWithKey<I>(
    method _getDragListIndex (line 426) | private _getDragListIndex(): number {
    method _getDragVisualIndex (line 436) | private _getDragVisualIndex(): number {
    method _getVisualListDuringDrag (line 469) | private _getVisualListDuringDrag(): ReadonlyArray<I> {
    method _getItemHeight (line 488) | private _getItemHeight(key: string): HeightData {
    method _getDistance (line 497) | private _getDistance(
    method _getDistanceFromTopDuringDrag (line 517) | private _getDistanceFromTopDuringDrag(
    method _getContainer (line 551) | private _getContainer(): HTMLElement | null | undefined {
    method _getKeyFn (line 556) | private static _getKeyFn<I>(
    method _getKeyFn (line 562) | private _getKeyFn(): (item: I) => string {
    method render (line 566) | render() {

FILE: test-types/typescript.tsx
  type Item (line 6) | interface Item {
  type MyTempProps (line 11) | interface MyTempProps {
  type MyTempState (line 17) | interface MyTempState {
  class MyTemp (line 20) | class MyTemp extends React.Component<MyTempProps, MyTempState> {
    method render (line 21) | render() {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (75K chars).
[
  {
    "path": ".babelrc",
    "chars": 256,
    "preview": "{\n  \"presets\": [\n    \"@babel/preset-env\",\n    \"@babel/preset-react\",\n    \"@babel/preset-typescript\"\n  ],\n  \"plugins\": [\n"
  },
  {
    "path": ".editorconfig",
    "chars": 132,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\nindent_style = space\ninde"
  },
  {
    "path": ".eslintignore",
    "chars": 30,
    "preview": "/example/dist\n/dist\n*.js.flow\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1360,
    "preview": "module.exports = {\n  env: {\n    browser: true,\n    jest: true,\n    node: true,\n    es6: true,\n  },\n  extends: [\n    'esl"
  },
  {
    "path": ".flowconfig",
    "chars": 123,
    "preview": "[ignore]\n.*/node_modules/.*/\\(test\\|lib\\|example\\|samplejson\\)/.*\\.json\n<PROJECT_ROOT>/dist/\n\n[include]\n\n[libs]\n\n[option"
  },
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "chars": 268,
    "preview": "name: Node.js CI\n\non: [push]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n  "
  },
  {
    "path": ".gitignore",
    "chars": 69,
    "preview": "*~\n.DS_Store\n/node_modules\nnpm-debug.log\n/dist\n/example/dist\n/.cache\n"
  },
  {
    "path": ".prettierignore",
    "chars": 27,
    "preview": "/example/dist\n/dist\n.cache\n"
  },
  {
    "path": ".prettierrc",
    "chars": 26,
    "preview": "{\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 186,
    "preview": "{\n  \"javascript.validate.enable\": false,\n  \"flow.useNPMPackagedFlow\": true,\n  \"eslint.validate\": [\n    \"javascript\",\n   "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 5472,
    "preview": "## 4.2.1 (2024-05-23)\n\n- Reupload package to work around minor file metadata issue caused by Yarn (https://github.com/ya"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1081,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Rewardly, Inc.\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 6577,
    "preview": "# react-draggable-list\n\n[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Streak"
  },
  {
    "path": "example/Example.tsx",
    "chars": 4692,
    "preview": "/* eslint-disable no-console */\n\nimport * as React from 'react';\nimport cx from 'classnames';\nimport DraggableList from "
  },
  {
    "path": "example/index.html",
    "chars": 385,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta\n      http-equiv=\"Content-Security-Policy\"\n      "
  },
  {
    "path": "example/main.css",
    "chars": 824,
    "preview": ".intro,\nfooter {\n  width: 400px;\n  margin: auto;\n}\n\n.list {\n  width: 430px;\n  margin: 10px auto;\n  padding: 15px;\n}\n.ite"
  },
  {
    "path": "example/main.tsx",
    "chars": 605,
    "preview": "import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport Example from './Example';\n\nconst onReady ="
  },
  {
    "path": "index.js.flow",
    "chars": 854,
    "preview": "/* @flow */\n\nimport * as React from 'react';\n\nexport type TemplateProps<I, C> = {\n  item: I,\n  itemSelected: number,\n  a"
  },
  {
    "path": "jest.config.js",
    "chars": 98,
    "preview": "module.exports = {\n  testEnvironment: 'jsdom',\n  modulePathIgnorePatterns: ['<rootDir>/dist/']\n};\n"
  },
  {
    "path": "package.json",
    "chars": 2469,
    "preview": "{\n  \"name\": \"react-draggable-list\",\n  \"version\": \"4.2.1\",\n  \"description\": \"Make a list of draggable collapsible items.\""
  },
  {
    "path": "src/MoveContainer.tsx",
    "chars": 2527,
    "preview": "import * as React from 'react';\nimport TemplateContainer from './TemplateContainer';\n\nexport interface HeightData {\n  na"
  },
  {
    "path": "src/OnUpdate.tsx",
    "chars": 236,
    "preview": "import * as React from 'react';\n\ninterface Props {\n  cb: () => void;\n}\n\nexport default class OnUpdate extends React.Comp"
  },
  {
    "path": "src/TemplateContainer.tsx",
    "chars": 1304,
    "preview": "import * as React from 'react';\n\ninterface Props<I, C, T> {\n  item: I;\n  template: new (props: any, context?: any) => T;"
  },
  {
    "path": "src/index.test.tsx",
    "chars": 19134,
    "preview": "/**\n * @jest-environment jsdom\n */\n/* eslint-disable @typescript-eslint/no-use-before-define, @typescript-eslint/no-empt"
  },
  {
    "path": "src/index.tsx",
    "chars": 21275,
    "preview": "/* eslint react/prop-types: \"error\" */\n\nimport * as React from 'react';\nimport * as PropTypes from 'prop-types';\nimport "
  },
  {
    "path": "test-types/typescript.tsx",
    "chars": 882,
    "preview": "import * as React from 'react';\nimport DraggableList from '../src';\n\n// This file isn't meant to be executed. It's just "
  },
  {
    "path": "tsconfig.json",
    "chars": 284,
    "preview": "{\n  \"compilerOptions\": {\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"declaration\": t"
  }
]

About this extraction

This page contains the full source code of the StreakYC/react-draggable-list GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (69.5 KB), approximately 19.4k tokens, and a symbol index with 94 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.

Copied to clipboard!