├── config
├── flow
│ ├── css.js.flow
│ └── file.js.flow
├── env.js
├── polyfills.js
├── babel.dev.js
├── babel.prod.js
├── webpack.config.umd.js
├── paths.js
├── eslint.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── src
├── types.js
├── index.js
├── decorators
│ ├── index.js
│ ├── ListPreview
│ │ ├── propTypes.js
│ │ └── index.js
│ ├── ItemPreview
│ │ ├── propTypes.js
│ │ └── index.js
│ ├── Item
│ │ ├── propTypes.js
│ │ └── index.js
│ └── List
│ │ ├── propTypes.js
│ │ └── index.js
├── demo
│ ├── App.css
│ ├── index.js
│ ├── utils
│ │ └── generateLists.js
│ ├── List.js
│ └── App.js
├── PureComponent.js
├── DragLayer
│ ├── propTypes.js
│ └── index.js
├── SortableList
│ ├── dragSpec.js
│ ├── propTypes.js
│ ├── itemCache.js
│ ├── dropSpec.js
│ └── index.js
├── SortableItem
│ ├── propTypes.js
│ ├── dragSpec.js
│ ├── index.js
│ └── dropSpec.js
├── Kanban
│ ├── __tests__
│ │ ├── __snapshots__
│ │ │ └── updateLists.test.js.snap
│ │ └── updateLists.test.js
│ ├── propTypes.js
│ ├── updateLists.js
│ └── index.js
├── propTypes.js
└── styles.css
├── .travis.yml
├── .gitignore
├── .babelrc
├── index.html
├── scripts
├── utils
│ ├── WatchMissingNodeModulesPlugin.js
│ ├── chrome.applescript
│ └── prompt.js
├── build.js
└── start.js
├── LICENSE.md
├── CHANGELOG.md
├── README.md
└── package.json
/config/flow/css.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 |
--------------------------------------------------------------------------------
/config/flow/file.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 | declare export default string;
3 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | export const ROW_TYPE = 'row';
2 | export const LIST_TYPE = 'list';
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | - "5"
5 | - "4"
6 |
7 | before_install:
8 | - npm install -g npm@3
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Kanban from './Kanban';
2 | import * as decorators from './decorators';
3 |
4 | export {
5 | decorators,
6 | Kanban as VirtualKanban
7 | };
8 |
--------------------------------------------------------------------------------
/src/decorators/index.js:
--------------------------------------------------------------------------------
1 | export Item from './Item';
2 | export ItemPreview from './ItemPreview';
3 | export List from './List';
4 | export ListPreview from './ListPreview';
5 |
--------------------------------------------------------------------------------
/src/demo/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | background-color: #21a5b8;
6 | }
7 |
8 | .KanbanWrapper {
9 | width: 100vw;
10 | height: 100vh;
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # production
7 | build
8 | dist
9 | lib
10 |
11 | # misc
12 | .DS_Store
13 | npm-debug.log
14 |
15 | # doc
16 | TODO.md
17 | RELEASE_STEPS.md
18 |
--------------------------------------------------------------------------------
/src/PureComponent.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import shallowCompare from 'react-addons-shallow-compare';
3 |
4 | export default class PureComponent extends Component {
5 | shouldComponentUpdate(nextProps, nextState) {
6 | return shallowCompare(this, nextProps, nextState);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/decorators/ListPreview/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes } from '../../propTypes';
3 |
4 | export const list = PropTypes.object.isRequired;
5 | export const listId = CustomPropTypes.id.isRequired;
6 | export const listStyle = PropTypes.object.isRequired;
7 | export const isGhost = PropTypes.bool.isRequired;
8 |
--------------------------------------------------------------------------------
/src/decorators/ItemPreview/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes } from '../../propTypes';
3 |
4 | export const row = PropTypes.object.isRequired;
5 | export const rowId = CustomPropTypes.id.isRequired;
6 | export const rowStyle = PropTypes.object.isRequired;
7 | export const containerWidth = PropTypes.number.isRequired;
8 | export const isGhost = PropTypes.bool.isRequired;
9 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["latest", "react"],
3 | "plugins": [
4 | "babel-plugin-transform-export-extensions",
5 | "babel-plugin-transform-class-properties",
6 | "babel-plugin-transform-object-rest-spread",
7 | "babel-plugin-transform-regenerator",
8 | "babel-plugin-transform-runtime",
9 | "babel-plugin-transform-react-constant-elements",
10 | "babel-plugin-transform-react-remove-prop-types",
11 | "babel-plugin-transform-remove-console"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/DragLayer/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 |
3 | export const lists = PropTypes.array;
4 | export const item = PropTypes.object;
5 | export const itemType = PropTypes.string;
6 | export const currentOffset = PropTypes.shape({
7 | x: PropTypes.number.isRequired,
8 | y: PropTypes.number.isRequire,
9 | });
10 | export const isDragging = PropTypes.bool.isRequired;
11 | export const itemPreviewComponent = PropTypes.func.isRequired;
12 | export const listPreviewComponent = PropTypes.func.isRequired;
13 |
--------------------------------------------------------------------------------
/src/decorators/Item/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes } from '../../propTypes';
3 |
4 | export const row = PropTypes.object.isRequired;
5 | export const rowId = CustomPropTypes.id.isRequired;
6 | export const listId = CustomPropTypes.id.isRequired;
7 | export const isDragging = PropTypes.bool.isRequired;
8 | export const rowStyle = PropTypes.object.isRequired;
9 | export const connectDragSource = PropTypes.func.isRequired;
10 | export const connectDropTarget = PropTypes.func.isRequired;
11 |
--------------------------------------------------------------------------------
/src/SortableList/dragSpec.js:
--------------------------------------------------------------------------------
1 | export function beginDrag(props) {
2 | const data = {
3 | list: props.list,
4 | listId: props.listId,
5 | listStyle: props.listStyle
6 | };
7 |
8 | props.dragBeginList(data);
9 |
10 | return data;
11 | }
12 |
13 | export function endDrag(props, monitor) {
14 | const { listId } = props;
15 |
16 | props.dragEndList({listId});
17 | }
18 |
19 | export function isDragging({ listId }, monitor) {
20 | const draggingListId = monitor.getItem().listId;
21 |
22 | return listId === draggingListId;
23 | }
24 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
2 | // injected into the application via DefinePlugin in Webpack configuration.
3 |
4 | var REACT_APP = /^REACT_APP_/i;
5 | var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development');
6 |
7 | module.exports = Object
8 | .keys(process.env)
9 | .filter(key => REACT_APP.test(key))
10 | .reduce((env, key) => {
11 | env['process.env.' + key] = JSON.stringify(process.env[key]);
12 | return env;
13 | }, {
14 | 'process.env.NODE_ENV': NODE_ENV
15 | });
16 |
--------------------------------------------------------------------------------
/src/decorators/List/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes, deprecate } from '../../propTypes';
3 |
4 | export const list = PropTypes.object.isRequired;
5 | export const listId = CustomPropTypes.id.isRequired;
6 | export const listStyle = PropTypes.object.isRequired;
7 | export const rows = deprecate(PropTypes.array, '`rows` is deprecated. Use `list.rows` instead');
8 | export const children = PropTypes.node;
9 | export const isDragging = PropTypes.bool.isRequired;
10 | export const connectDragSource = PropTypes.func.isRequired;
11 | export const connectDropTarget = PropTypes.func.isRequired;
12 |
--------------------------------------------------------------------------------
/config/polyfills.js:
--------------------------------------------------------------------------------
1 | if (typeof Promise === 'undefined') {
2 | // Rejection tracking prevents a common issue where React gets into an
3 | // inconsistent state due to an error, but it gets swallowed by a Promise,
4 | // and the user has no idea what causes React's erratic future behavior.
5 | require('promise/lib/rejection-tracking').enable();
6 | window.Promise = require('promise/lib/es6-extensions.js');
7 | }
8 |
9 | // fetch() polyfill for making API calls.
10 | require('whatwg-fetch');
11 |
12 | // Object.assign() is commonly used with React.
13 | // It will use the native implementation if it's present and isn't buggy.
14 | Object.assign = require('object-assign');
15 |
--------------------------------------------------------------------------------
/src/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Perf from 'react-addons-perf';
4 |
5 | import '../../lib/styles.css';
6 |
7 | import { generateLists } from './utils/generateLists';
8 |
9 | import App from './App';
10 |
11 | window.Perf = Perf;
12 |
13 | function getLists() {
14 | const lists = window.localStorage.getItem('lists');
15 |
16 | return JSON.parse(lists) || generateLists(20, 50);
17 | }
18 |
19 | function setLists(lists) {
20 | window.localStorage.setItem('lists', JSON.stringify(lists));
21 | }
22 |
23 | ReactDOM.render(
24 | ,
25 | document.getElementById('root')
26 | );
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Virtual Kanban
7 |
8 |
9 |
10 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/SortableItem/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes } from '../propTypes';
3 |
4 | export const row = PropTypes.object;
5 | export const rowId = CustomPropTypes.id.isRequired;
6 | export const listId = CustomPropTypes.id.isRequired;
7 | export const rowStyle = PropTypes.object;
8 | export const itemComponent = PropTypes.func;
9 | export const moveRow = PropTypes.func;
10 | export const dragEndRow = PropTypes.func;
11 | export const dropRow = PropTypes.func;
12 | // React DnD
13 | export const isDragging = PropTypes.bool;
14 | export const connectDropTarget = PropTypes.func;
15 | export const connectDragSource = PropTypes.func;
16 | export const connectDragPreview = PropTypes.func;
17 |
--------------------------------------------------------------------------------
/src/demo/utils/generateLists.js:
--------------------------------------------------------------------------------
1 | function generateRandom(count) {
2 | return Array.from({length: count}, (_, i) => {
3 | return {
4 | id: i,
5 | name: `${i}`,
6 | lastModified: Date.now(),
7 | };
8 | });
9 | }
10 |
11 | export function generateLists(count, rowsPerList) {
12 | let rows;
13 |
14 | console.time('rows generation');
15 | rows = generateRandom(count * rowsPerList);
16 |
17 | const lists = rows.reduce((memo, row, i) => {
18 | let group = memo[i % count];
19 |
20 | if (!group) {
21 | group = memo[i % count] = {id: i, rows: []};
22 | }
23 |
24 | group.rows.push(row);
25 |
26 | return memo;
27 | }, []);
28 |
29 | console.timeEnd('rows generation');
30 |
31 | return lists;
32 | }
33 |
--------------------------------------------------------------------------------
/src/decorators/ListPreview/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import * as propTypes from './propTypes';
4 |
5 | import PureComponent from '../../PureComponent';
6 |
7 | export default class ListPreview extends PureComponent {
8 | static propTypes = propTypes;
9 |
10 | render() {
11 | const { listId, listStyle, isGhost } = this.props;
12 | const { width, height } = listStyle;
13 |
14 | return (
15 |
16 |
17 |
18 | List {listId}
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/decorators/ItemPreview/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import * as propTypes from './propTypes';
4 |
5 | import PureComponent from '../../PureComponent';
6 |
7 | export default class ItemPreview extends PureComponent {
8 | static propTypes = propTypes;
9 |
10 | render() {
11 | // TODO: Grab a proper item width
12 | const { row, rowStyle, containerWidth: width, isGhost } = this.props;
13 | const { height } = rowStyle;
14 |
15 | return (
16 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Kanban/__tests__/__snapshots__/updateLists.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`move between lists 1`] = `"[{\\"id\\":1,\\"rows\\":[{\\"id\\":1}]},{\\"id\\":2,\\"rows\\":[{\\"id\\":2},{\\"id\\":3},{\\"id\\":4}]}]"`;
4 |
5 | exports[`move item from a list with a single element 1`] = `"[{\\"id\\":1,\\"rows\\":[{\\"id\\":1},{\\"id\\":3},{\\"id\\":2}]},{\\"id\\":2,\\"rows\\":[]}]"`;
6 |
7 | exports[`move item inside same list 1`] = `"[{\\"id\\":1,\\"rows\\":[{\\"id\\":2},{\\"id\\":1}]},{\\"id\\":2,\\"rows\\":[{\\"id\\":3},{\\"id\\":4}]}]"`;
8 |
9 | exports[`move item to an empty list 1`] = `"[{\\"id\\":1,\\"rows\\":[{\\"id\\":2}]},{\\"id\\":2,\\"rows\\":[{\\"id\\":1}]}]"`;
10 |
11 | exports[`move lists 1`] = `"[{\\"id\\":2,\\"rows\\":[{\\"id\\":3},{\\"id\\":4}]},{\\"id\\":1,\\"rows\\":[{\\"id\\":1},{\\"id\\":2}]}]"`;
12 |
--------------------------------------------------------------------------------
/src/decorators/Item/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from 'classnames';
3 |
4 | import * as propTypes from './propTypes';
5 |
6 | import PureComponent from '../../PureComponent';
7 |
8 | export default class Item extends PureComponent {
9 | static propTypes = propTypes;
10 |
11 | render() {
12 | const { row, rowStyle, connectDragSource, connectDropTarget, isDragging } = this.props;
13 |
14 | const itemContainerClass = classnames({
15 | 'ItemContainer': true,
16 | 'ItemPlaceholder': isDragging,
17 | });
18 |
19 | return connectDragSource(connectDropTarget(
20 |
27 | ));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/SortableList/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 | import { PropTypes as CustomPropTypes } from '../propTypes';
3 |
4 | export const list = PropTypes.object;
5 | export const listId = CustomPropTypes.id.isRequired;
6 | export const listStyle = PropTypes.object;
7 | export const listComponent = PropTypes.func;
8 | export const itemComponent = PropTypes.func;
9 | export const moveRow = PropTypes.func;
10 | export const moveList = PropTypes.func;
11 | export const dropRow = PropTypes.func;
12 | export const dropList = PropTypes.func;
13 | export const dragEndRow = PropTypes.func;
14 | export const overscanRowCount = PropTypes.number;
15 | export const itemCacheKey = PropTypes.func;
16 | // React DnD
17 | export const isDragging = PropTypes.bool;
18 | export const connectDropTarget = PropTypes.func;
19 | export const connectDragSource = PropTypes.func;
20 | export const connectDragPreview = PropTypes.func;
21 |
--------------------------------------------------------------------------------
/src/Kanban/propTypes.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 |
3 | export const lists = PropTypes.array;
4 | export const width = PropTypes.number;
5 | export const listWidth = PropTypes.number;
6 | export const height = PropTypes.number;
7 | export const listComponent = PropTypes.func;
8 | export const itemComponent = PropTypes.func;
9 | export const itemPreviewComponent = PropTypes.func;
10 | export const listPreviewComponent = PropTypes.func;
11 | export const onMoveRow = PropTypes.func;
12 | export const onMoveList = PropTypes.func;
13 | export const onDropRow = PropTypes.func;
14 | export const onDropList = PropTypes.func;
15 | export const onDragEndRow = PropTypes.func;
16 | export const overscanListCount = PropTypes.number;
17 | export const overscanRowCount = PropTypes.number;
18 | export const scrollToList = PropTypes.number;
19 | export const scrollToAlignment = PropTypes.string;
20 | export const itemCacheKey = PropTypes.func;
21 |
--------------------------------------------------------------------------------
/src/SortableList/itemCache.js:
--------------------------------------------------------------------------------
1 | // Cache singleton
2 | const cachedItems = new Map();
3 |
4 | export class ItemCache {
5 | constructor(items, cacheKey, store = cachedItems) {
6 | this.items = items;
7 | this.cacheKey = cacheKey;
8 | this.store = store;
9 | }
10 |
11 | clearAllRowHeights() {
12 | this.store.clear();
13 | }
14 |
15 | clearRowHeight(index) {
16 | const item = this.items[index];
17 |
18 | this.store.delete(this.cacheKey(item));
19 | }
20 |
21 | getRowHeight(index) {
22 | const item = this.items[index];
23 |
24 | return this.store.get(this.cacheKey(item));
25 | }
26 |
27 | setRowHeight(index, height) {
28 | const item = this.items[index];
29 |
30 | this.store.set(this.cacheKey(item), height);
31 | }
32 |
33 | // Not implemented
34 |
35 | clearAllColumnWidths() {}
36 | clearColumnWidth(index) {}
37 | getColumnWidth(index) {}
38 | setColumnWidth(index, width) {}
39 | }
40 |
--------------------------------------------------------------------------------
/src/SortableItem/dragSpec.js:
--------------------------------------------------------------------------------
1 | import { findDOMNode } from 'react-dom';
2 | import { width } from 'dom-helpers/query';
3 |
4 | export function beginDrag(props, _, component) {
5 | const node = findDOMNode(component);
6 | const containerWidth = node ? width(node) : 0;
7 |
8 | const data = {
9 | lists: props.lists,
10 | row: props.row,
11 | rowId: props.rowId,
12 | rowStyle: props.rowStyle,
13 | containerWidth,
14 | };
15 |
16 | props.dragBeginRow(data);
17 |
18 | return data;
19 | }
20 |
21 | export function endDrag(props, monitor) {
22 | const { rowId: itemId } = props;
23 |
24 | props.dragEndRow({itemId});
25 | }
26 |
27 | /**
28 | * Determines whether current item is being dragged or not.
29 | *
30 | * This is the logic used to display the gaps (gray items) in the list.
31 | */
32 | export function isDragging({ rowId }, monitor) {
33 | const draggingRowId = monitor.getItem().rowId;
34 |
35 | return rowId === draggingRowId;
36 | }
37 |
--------------------------------------------------------------------------------
/src/propTypes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const PropTypes = {
4 | id: React.PropTypes.oneOfType([
5 | React.PropTypes.string,
6 | React.PropTypes.number,
7 | React.PropTypes.symbol
8 | ]),
9 |
10 | decorator: React.PropTypes.func,
11 | };
12 |
13 | /**
14 | * Wraps a singular React.PropTypes.[type] with
15 | * a console.warn call that is only called if the
16 | * prop is not undefined/null and is only called
17 | * once.
18 | * @param {Object} propType React.PropType type
19 | * @param {String} message Deprecation message
20 | * @return {Function} ReactPropTypes checkType
21 | */
22 | export function deprecate(propType, message) {
23 | let warned = false;
24 | return function(...args) {
25 | const [props, propName] = args;
26 | const prop = props[propName];
27 | if (prop !== undefined && prop !== null && !warned) {
28 | warned = true;
29 | console.warn(message);
30 | }
31 | return propType.call(this, ...args);
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/scripts/utils/WatchMissingNodeModulesPlugin.js:
--------------------------------------------------------------------------------
1 | // This Webpack plugin ensures `npm install ` forces a project rebuild.
2 | // We’re not sure why this isn't Webpack's default behavior.
3 | // See https://github.com/facebookincubator/create-react-app/issues/186.
4 |
5 | function WatchMissingNodeModulesPlugin(nodeModulesPath) {
6 | this.nodeModulesPath = nodeModulesPath;
7 | }
8 |
9 | WatchMissingNodeModulesPlugin.prototype.apply = function (compiler) {
10 | compiler.plugin('emit', (compilation, callback) => {
11 | var missingDeps = compilation.missingDependencies;
12 | var nodeModulesPath = this.nodeModulesPath;
13 |
14 | // If any missing files are expected to appear in node_modules...
15 | if (missingDeps.some(file => file.indexOf(nodeModulesPath) !== -1)) {
16 | // ...tell webpack to watch node_modules recursively until they appear.
17 | compilation.contextDependencies.push(nodeModulesPath);
18 | }
19 |
20 | callback();
21 | });
22 | }
23 |
24 | module.exports = WatchMissingNodeModulesPlugin;
25 |
--------------------------------------------------------------------------------
/scripts/utils/chrome.applescript:
--------------------------------------------------------------------------------
1 | on run argv
2 | set theURL to item 1 of argv
3 |
4 | tell application "Chrome"
5 |
6 | if (count every window) = 0 then
7 | make new window
8 | end if
9 |
10 | -- Find a tab currently running the debugger
11 | set found to false
12 | set theTabIndex to -1
13 | repeat with theWindow in every window
14 | set theTabIndex to 0
15 | repeat with theTab in every tab of theWindow
16 | set theTabIndex to theTabIndex + 1
17 | if theTab's URL is theURL then
18 | set found to true
19 | exit repeat
20 | end if
21 | end repeat
22 |
23 | if found then
24 | exit repeat
25 | end if
26 | end repeat
27 |
28 | if found then
29 | tell theTab to reload
30 | set index of theWindow to 1
31 | set theWindow's active tab index to theTabIndex
32 | else
33 | tell window 1
34 | activate
35 | make new tab with properties {URL:theURL}
36 | end tell
37 | end if
38 | end tell
39 | end run
40 |
--------------------------------------------------------------------------------
/scripts/utils/prompt.js:
--------------------------------------------------------------------------------
1 | var rl = require('readline');
2 |
3 | // Convention: "no" should be the conservative choice.
4 | // If you mistype the answer, we'll always take it as a "no".
5 | // You can control the behavior on with `isYesDefault`.
6 | module.exports = function (question, isYesDefault) {
7 | if (typeof isYesDefault !== 'boolean') {
8 | throw new Error('Provide explicit boolean isYesDefault as second argument.');
9 | }
10 | return new Promise(resolve => {
11 | var rlInterface = rl.createInterface({
12 | input: process.stdin,
13 | output: process.stdout,
14 | });
15 |
16 | var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
17 | var message = question + ' ' + hint + '\n';
18 |
19 | rlInterface.question(message, function(answer) {
20 | rlInterface.close();
21 |
22 | var useDefault = answer.trim().length === 0;
23 | if (useDefault) {
24 | return resolve(isYesDefault);
25 | }
26 |
27 | var isYes = answer.match(/^(yes|y)$/i);
28 | return resolve(isYes);
29 | });
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Eduardo Lanchares
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/demo/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from 'classnames';
3 |
4 | import { decorators } from '../../src';
5 |
6 | export default class List extends decorators.List {
7 | render() {
8 | const {
9 | listId,
10 | style,
11 | connectDragSource,
12 | connectDropTarget,
13 | isDragging,
14 | children,
15 | } = this.props;
16 |
17 | let listContainerClass = classnames({
18 | 'ListContainer': true,
19 | 'ListPlaceholder': isDragging
20 | });
21 |
22 | return (
23 |
24 |
25 | {connectDragSource(
26 |
27 | List {listId}
28 |
29 | )}
30 | {connectDropTarget(
31 |
32 | {children}
33 |
34 | )}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/decorators/List/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from 'classnames';
3 |
4 | import * as propTypes from './propTypes';
5 |
6 | import PureComponent from '../../PureComponent';
7 |
8 | export default class List extends PureComponent {
9 | static propTypes = propTypes;
10 |
11 | render() {
12 | const {
13 | list,
14 | listId,
15 | listStyle,
16 | connectDragSource,
17 | connectDropTarget,
18 | isDragging,
19 | children,
20 | } = this.props;
21 |
22 | let listContainerClass = classnames({
23 | 'ListContainer': true,
24 | 'ListPlaceholder': isDragging
25 | });
26 |
27 | return (
28 |
29 |
30 | {connectDragSource(
31 |
32 | List {listId} ({list.rows.length})
33 |
34 | )}
35 | {connectDropTarget(
36 |
37 | {children}
38 |
39 | )}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/babel.dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Don't try to find .babelrc because we want to force this configuration.
3 | babelrc: false,
4 | // This is a feature of `babel-loader` for webpack (not Babel itself).
5 | // It enables caching results in OS temporary directory for faster rebuilds.
6 | cacheDirectory: true,
7 | presets: [
8 | // Latest stable ECMAScript features
9 | require.resolve('babel-preset-latest'),
10 | // JSX, Flow
11 | require.resolve('babel-preset-react')
12 | ],
13 | plugins: [
14 | // displayName for ES6 classes
15 | require.resolve('babel-plugin-transform-class-display-name'),
16 | // export Foo from './Foo'
17 | require.resolve('babel-plugin-transform-export-extensions'),
18 | // class { handleClick = () => { } }
19 | require.resolve('babel-plugin-transform-class-properties'),
20 | // { ...todo, completed: true }
21 | require.resolve('babel-plugin-transform-object-rest-spread'),
22 | // function* () { yield 42; yield 43; }
23 | [require.resolve('babel-plugin-transform-regenerator'), {
24 | // Async functions are converted to generators by babel-preset-latest
25 | async: false
26 | }],
27 | // Polyfills the runtime needed for async/await and generators
28 | [require.resolve('babel-plugin-transform-runtime'), {
29 | helpers: false,
30 | polyfill: false,
31 | regenerator: true
32 | }]
33 | ]
34 | };
35 |
--------------------------------------------------------------------------------
/config/babel.prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Don't try to find .babelrc because we want to force this configuration.
3 | babelrc: false,
4 | presets: [
5 | // Latest stable ECMAScript features
6 | require.resolve('babel-preset-latest'),
7 | // JSX, Flow
8 | require.resolve('babel-preset-react')
9 | ],
10 | plugins: [
11 | require.resolve('babel-plugin-transform-export-extensions'),
12 | // class { handleClick = () => { } }
13 | require.resolve('babel-plugin-transform-class-properties'),
14 | // { ...todo, completed: true }
15 | require.resolve('babel-plugin-transform-object-rest-spread'),
16 | // function* () { yield 42; yield 43; }
17 | [require.resolve('babel-plugin-transform-regenerator'), {
18 | // Async functions are converted to generators by babel-preset-latest
19 | async: false
20 | }],
21 | // Polyfills the runtime needed for async/await and generators
22 | [require.resolve('babel-plugin-transform-runtime'), {
23 | helpers: false,
24 | polyfill: false,
25 | regenerator: true
26 | }],
27 | // Optimization: hoist JSX that never changes out of render()
28 | require.resolve('babel-plugin-transform-react-constant-elements'),
29 | // Remove PropTypes declarations
30 | require.resolve('babel-plugin-transform-react-remove-prop-types'),
31 | // Remove console.log statements
32 | require.resolve('babel-plugin-transform-remove-console')
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/src/demo/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { AutoSizer } from 'react-virtualized';
3 |
4 | import { VirtualKanban } from '../';
5 |
6 | import './App.css';
7 |
8 | const keyGenerator = ({ id, lastModified }) => `${id}-${lastModified}`;
9 |
10 | class App extends Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | lists: props.getLists(),
16 | };
17 |
18 | setInterval(() => {
19 | this.setState((prevState) => {
20 | if (prevState.lists[0].rows.length > 0) {
21 | this._initialLists = prevState.lists;
22 | return {lists: prevState.lists.map((list) => ({...list, rows: []}))};
23 | } else {
24 | return {lists: this._initialLists.concat()};
25 | }
26 | });
27 | }, 3000);
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 | {({ width, height }) => (
35 | this.setState(() => ({lists}))}
42 | onMoveList={({ lists }) => this.setState(() => ({lists}))}
43 | />
44 | )}
45 |
46 |
47 | );
48 | }
49 | }
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/SortableItem/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DragSource, DropTarget } from 'react-dnd';
3 | import { getEmptyImage } from 'react-dnd-html5-backend';
4 |
5 | import { ROW_TYPE } from '../types';
6 | import * as dragSpec from './dragSpec';
7 | import * as dropSpec from './dropSpec';
8 | import * as propTypes from './propTypes';
9 |
10 | import PureComponent from '../PureComponent';
11 |
12 | class SortableItem extends PureComponent {
13 | static propTypes = propTypes;
14 |
15 | componentDidMount() {
16 | this.props.connectDragPreview(getEmptyImage(), {
17 | captureDraggingState: true
18 | });
19 | }
20 |
21 | render() {
22 | const {
23 | row,
24 | rowId,
25 | listId,
26 | itemComponent: DecoratedItem,
27 | isDragging,
28 | connectDragSource,
29 | connectDropTarget,
30 | rowStyle,
31 | } = this.props;
32 |
33 | return (
34 |
43 | );
44 | }
45 | }
46 |
47 | const connectDrop = DropTarget(ROW_TYPE, dropSpec, connect => ({
48 | connectDropTarget: connect.dropTarget()
49 | }))
50 |
51 |
52 | const connectDrag = DragSource(ROW_TYPE, dragSpec, (connect, monitor) => ({
53 | connectDragSource: connect.dragSource(),
54 | connectDragPreview: connect.dragPreview(),
55 | isDragging: monitor.isDragging(),
56 | }))
57 |
58 | export default connectDrop(connectDrag(SortableItem));
59 |
--------------------------------------------------------------------------------
/config/webpack.config.umd.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | var paths = require('./paths');
5 |
6 | const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
7 |
8 | const libraryName = 'ReactVirtualKanban';
9 |
10 | const plugins = [
11 | new UglifyJsPlugin({
12 | beautify: true,
13 | comments: true,
14 | mangle: false
15 | }),
16 | new ExtractTextPlugin('styles.css', {
17 | allChunks: false,
18 | beautify: true,
19 | mangle: false
20 | })
21 | ];
22 |
23 | var config = {
24 | devtool: 'source-map',
25 | entry: {
26 | 'react-virtual-kanban': path.join(paths.appSrc, 'index.js')
27 | },
28 | output: {
29 | path: paths.appDist,
30 | filename: '[name].js',
31 | library: libraryName,
32 | libraryTarget: 'umd',
33 | },
34 | // TODO: Should we declare externals?
35 | externals: {
36 | 'react': 'React',
37 | 'react-dom': 'ReactDOM',
38 | 'react-addons-shallow-compare': 'var React.addons.shallowCompare'
39 | },
40 | module: {
41 | loaders: [
42 | {
43 | test: /\.js$/,
44 | loader: 'babel',
45 | include: paths.appSrc,
46 | query: require('./babel.prod')
47 | },
48 | {
49 | test: /\.css$/,
50 | include: [paths.appSrc, paths.appNodeModules],
51 | loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss')
52 | },
53 | {
54 | test: /\.js$/,
55 | loader: 'eslint-loader',
56 | include: paths.appSrc
57 | }
58 | ]
59 | },
60 | // resolve: {
61 | // root: path.resolve('./src'),
62 | // extensions: ['', '.js']
63 | // },
64 | plugins: plugins
65 | };
66 |
67 | module.exports = config;
68 |
--------------------------------------------------------------------------------
/src/SortableList/dropSpec.js:
--------------------------------------------------------------------------------
1 | import { findDOMNode } from 'react-dom';
2 | import { width, querySelectorAll } from 'dom-helpers/query';
3 |
4 | import { LIST_TYPE, ROW_TYPE } from '../types';
5 |
6 | function calculateContainerWidth(component) {
7 | const innerScrollContainer = querySelectorAll(
8 | findDOMNode(component),
9 | '.ReactVirtualized__Grid__innerScrollContainer'
10 | )[0];
11 |
12 | if (!innerScrollContainer) return 0;
13 |
14 | return width(innerScrollContainer);
15 | }
16 |
17 | export function hover(props, monitor, component) {
18 | if (!monitor.isOver({shallow: true})) return;
19 | if (!monitor.canDrop()) return;
20 |
21 | const item = monitor.getItem();
22 | const itemType = monitor.getItemType();
23 | const { listId: dragListId } = item;
24 | const { listId: hoverListId } = props;
25 |
26 | if (dragListId === hoverListId) {
27 | return;
28 | }
29 |
30 | if (itemType === LIST_TYPE) {
31 | props.moveList({listId: dragListId}, {listId: hoverListId});
32 | return;
33 | }
34 |
35 | if (itemType === ROW_TYPE) {
36 | const dragItemId = item.rowId;
37 |
38 | item.containerWidth = calculateContainerWidth(component) || item.containerWidth;
39 |
40 | props.moveRow(
41 | {itemId: dragItemId},
42 | {listId: hoverListId}
43 | );
44 | return;
45 | }
46 | }
47 |
48 | export function canDrop(props, monitor) {
49 | const item = monitor.getItem();
50 | const itemType = monitor.getItemType();
51 |
52 | if (itemType === LIST_TYPE) {
53 | return true;
54 | }
55 |
56 | if (itemType === ROW_TYPE) {
57 | return item.listId !== props.listId;
58 | }
59 | }
60 |
61 | export function drop(props, monitor) {
62 | if (!monitor.isOver({shallow: true})) return;
63 |
64 | const { listId } = props;
65 |
66 | props.dropList({listId});
67 | }
68 |
--------------------------------------------------------------------------------
/src/SortableItem/dropSpec.js:
--------------------------------------------------------------------------------
1 | import { findDOMNode } from 'react-dom';
2 | import { width } from 'dom-helpers/query';
3 |
4 | export function hover(props, monitor, component) {
5 | const item = monitor.getItem();
6 | const { rowId: dragItemId } = item;
7 | const { rowId: hoverItemId, findItemIndex } = props;
8 |
9 | // Hovering over the same item
10 | if (dragItemId === hoverItemId) {
11 | return;
12 | }
13 |
14 | // Sometimes component may be null when it's been unmounted
15 | if (!component) {
16 | console.warn(`null component for #${dragItemId}`);
17 | return;
18 | }
19 |
20 | const dragItemIndex = findItemIndex(dragItemId);
21 | const hoverItemIndex = findItemIndex(hoverItemId);
22 |
23 | // In order to avoid swap flickering when dragging element is smaller than
24 | // dropping one, we check whether dropping middle has been reached or not.
25 |
26 | // Determine rectangle on screen
27 | const node = findDOMNode(component);
28 | const hoverBoundingRect = node.getBoundingClientRect();
29 |
30 | // Get vertical middle
31 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
32 |
33 | // Determine mouse position
34 | const clientOffset = monitor.getClientOffset();
35 |
36 | // Get pixels to the top
37 | const hoverClientY = clientOffset.y - hoverBoundingRect.top;
38 |
39 | // Dragging downwards
40 | if (dragItemIndex < hoverItemIndex && hoverClientY < hoverMiddleY) {
41 | return;
42 | }
43 |
44 | // Dragging upwards
45 | if (dragItemIndex > hoverItemIndex && hoverClientY > hoverMiddleY) {
46 | return;
47 | }
48 |
49 | item.containerWidth = width(node);
50 |
51 | props.moveRow(
52 | {itemId: dragItemId},
53 | {itemId: hoverItemId}
54 | );
55 | }
56 |
57 | export function canDrop(props, monitor) {
58 | const item = monitor.getItem();
59 |
60 | return item.rowId === props.rowId;
61 | }
62 |
63 | export function drop(props) {
64 | const { rowId: itemId } = props;
65 |
66 | props.dropRow({itemId});
67 | }
68 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | // TODO: we can split this file into several files (pre-eject, post-eject, test)
2 | // and use those instead. This way we don't need to branch here.
3 |
4 | var path = require('path');
5 |
6 | // True after ejecting, false when used as a dependency
7 | var isEjected = (
8 | path.resolve(path.join(__dirname, '..')) ===
9 | path.resolve(process.cwd())
10 | );
11 |
12 | // Are we developing create-react-app locally?
13 | var isInCreateReactAppSource = (
14 | process.argv.some(arg => arg.indexOf('--debug-template') > -1)
15 | );
16 |
17 | function resolveOwn(relativePath) {
18 | return path.resolve(__dirname, relativePath);
19 | }
20 |
21 | function resolveApp(relativePath) {
22 | return path.resolve(relativePath);
23 | }
24 |
25 | if (isInCreateReactAppSource) {
26 | // create-react-app development: we're in ./config/
27 | module.exports = {
28 | appBuild: resolveOwn('../build'),
29 | appHtml: resolveOwn('../template/index.html'),
30 | appPackageJson: resolveOwn('../package.json'),
31 | appSrc: resolveOwn('../template/src'),
32 | appNodeModules: resolveOwn('../node_modules'),
33 | ownNodeModules: resolveOwn('../node_modules')
34 | };
35 | } else if (!isEjected) {
36 | // before eject: we're in ./node_modules/react-scripts/config/
37 | module.exports = {
38 | appBuild: resolveApp('build'),
39 | appHtml: resolveApp('index.html'),
40 | appPackageJson: resolveApp('package.json'),
41 | appSrc: resolveApp('src'),
42 | appLib: resolveApp('lib'),
43 | appDist: resolveApp('dist'),
44 | appNodeModules: resolveApp('node_modules'),
45 | // this is empty with npm3 but node resolution searches higher anyway:
46 | ownNodeModules: resolveOwn('../node_modules')
47 | };
48 | } else {
49 | // after eject: we're in ./config/
50 | module.exports = {
51 | appBuild: resolveApp('build'),
52 | appHtml: resolveApp('index.html'),
53 | appPackageJson: resolveApp('package.json'),
54 | appSrc: resolveApp('src'),
55 | appLib: resolveApp('lib'),
56 | appDist: resolveApp('dist'),
57 | appNodeModules: resolveApp('node_modules'),
58 | ownNodeModules: resolveApp('node_modules')
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/DragLayer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DragLayer } from 'react-dnd';
3 |
4 | import * as ItemTypes from '../types';
5 | import * as propTypes from './propTypes';
6 |
7 | // TODO: Extract to utils dir
8 | import { findItemIndex, findListIndex } from '../Kanban/updateLists';
9 |
10 | import PureComponent from '../PureComponent';
11 |
12 | function getStyles({ currentOffset }) {
13 | if (!currentOffset) {
14 | return {
15 | display: 'none'
16 | };
17 | }
18 |
19 | const { x, y } = currentOffset;
20 | const transform = `translate(${x}px, ${y}px)`;
21 |
22 | return {
23 | transform,
24 | };
25 | }
26 |
27 | class KanbanDragLayer extends PureComponent {
28 | static propTypes = propTypes;
29 |
30 | constructor(props) {
31 | super(props);
32 |
33 | this.renderItem = this.renderItem.bind(this);
34 | }
35 |
36 | renderItem(type, item) {
37 | const {
38 | lists,
39 | itemPreviewComponent: ItemPreview,
40 | listPreviewComponent: ListPreview,
41 | } = this.props;
42 |
43 | switch (type) {
44 | case ItemTypes.ROW_TYPE:
45 | return (
46 |
53 | );
54 | case ItemTypes.LIST_TYPE:
55 | return (
56 |
62 | );
63 | default:
64 | return null;
65 | }
66 | }
67 |
68 | render() {
69 | const { item, itemType, isDragging } = this.props;
70 |
71 | if (!isDragging) {
72 | return null;
73 | }
74 |
75 | return (
76 |
77 |
78 | {this.renderItem(itemType, item)}
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | function collect(monitor) {
86 | return {
87 | item: monitor.getItem(),
88 | itemType: monitor.getItemType(),
89 | currentOffset: monitor.getSourceClientOffset(),
90 | isDragging: monitor.isDragging()
91 | };
92 | }
93 |
94 | export default DragLayer(collect)(KanbanDragLayer);
95 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ### 0.2.0
5 | Added itemCacheKey prop to be able to specify a custom key for caching Item components. This comes handy if underlaying Item data changes (like a title) and needs to be re-rendered to reflect new height.
6 |
7 | Added `scrollToList` and `scrollToAlignment` to set kanban scroll to desired list.
8 |
9 | Fixed items not being refreshing when moving lists around.
10 |
11 | Updated package.json and added some badges to the README.
12 |
13 | ### 0.1.1
14 | `onDropRow` callback is now invoked only when dropping into a valid target.
15 |
16 | Defended against null component received when hovering over an unmounted row.
17 |
18 | Remove dragDropManager prop type warning.
19 |
20 |
21 | ### 0.1.0
22 | Added `overscanListCount` and `overscanRowCount` props to be able to configure the number of extra rendered items. You may use this if you want to tune up kanban performance.
23 |
24 | Use `id` as key value for SortableItem and SortableList components. This avoids wierd behaviour when changing component's internal state.
25 |
26 | ### 0.0.12
27 | Now `Kanban` component gets `DragDropManager` from context or creates a new one. This is useful for scenarios where react-dnd is used inside decorators.
28 |
29 | ### 0.0.11
30 | Added guard in SortableList component when list reference is not set.
31 |
32 | ### 0.0.10
33 | Fixed a bug that was causing out of index errors when moving an row between lists.
34 |
35 | Now when moving a row to a list, the row will be placed at the beginning of that list (previously was at the end).
36 |
37 | ### 0.0.9
38 | Updated PropTypes for List decorator. Also pass `rows` prop.
39 |
40 | ### 0.0.8
41 | Pass `containerWidth` for preview components
42 |
43 | Use explicit props for decorated components
44 |
45 | ### 0.0.7
46 | Use generated lib for demo application
47 |
48 | ### 0.0.6
49 | Added jest test runner :green_apple:. Also added tests for `updateList` module.
50 |
51 | Update param names for `moveRow` and `moveList` callbacks on `SortableList` component.
52 |
53 | Defend wierd out of bounds edge case when dragging too fast. Need more investigation though.
54 |
55 | Added travis ci.
56 |
57 | ### 0.0.5
58 | Pass `listIndex` to `onDropList` callback.
59 |
60 | ### 0.0.4
61 | Pass `rowIndex` and `listIndex` to `onDropRow` callback.
62 |
63 | ### 0.0.3
64 | Pass updated list to `onDropRow` and `onDropList` callbacks.
65 |
66 | ### 0.0.2
67 | Fixed missing dom-helpers dependency.
68 |
69 | ### 0.0.1
70 | Initial release.
71 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | @import 'react-virtualized/styles.css';
2 |
3 | .KanbanDragLayer {
4 | position: fixed;
5 | pointer-events: none;
6 | z-index: 100;
7 | left: 0;
8 | top: 0;
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | .KanbanGrid {
14 | outline: none;
15 | }
16 |
17 | .KanbanList {
18 | outline: none;
19 | }
20 |
21 | /* ListPreview */
22 | .ListPreviewWrapper {
23 | transform: rotate(4deg);
24 | }
25 |
26 | /* List */
27 | .ListWrapper {
28 | padding: 8px 4px 8px 4px;
29 | margin: 0 4px;
30 | box-sizing: border-box;
31 | position: relative;
32 | }
33 |
34 | .ListContainer {
35 | border-radius: 3px;
36 | display: flex;
37 | flex-direction: column;
38 | color: #4d4d4d;
39 | background-color: #e6e6e6;
40 | height: 100%;
41 | align-items: stretch;
42 | }
43 |
44 | .ListPlaceholder {
45 | background-color: rgba(0, 0, 0, .2);
46 | }
47 |
48 | .ListPlaceholder .ListHeader,
49 | .ListPlaceholder .ListContent,
50 | .ListPlaceholder .ListFooter {
51 | opacity: 0;
52 | }
53 |
54 | .ListHeader {
55 | padding: 8px 10px;
56 | flex: 0 1 10px;
57 | /*flex-shrink: 0;
58 | flex-basis: auto;*/
59 | }
60 |
61 | .ListContent {
62 | flex-grow: 1;
63 | flex-shrink: 1;
64 | flex-basis: auto;
65 | margin: 0 4px;
66 | padding: 0 4px;
67 | }
68 |
69 | .ListFooter {
70 | flex: 0 1 auto;
71 | /*flex-grow: 1;
72 | flex-shrink: 0;
73 | flex-basis: auto;*/
74 | }
75 |
76 | .ListTitle {
77 | min-height: 20px;
78 | font-weight: bold;
79 | }
80 |
81 | .ListActions {
82 | padding: 8px 10px;
83 | display: flex;
84 | align-items: center;
85 | }
86 |
87 | .ListActionItem {
88 | background: none;
89 | border: medium none;
90 | cursor: pointer;
91 | font-size: 14px;
92 | color: #8c8c8c;
93 | }
94 |
95 | /* ItemPreview */
96 | .ItemPreviewWrapper {
97 | transform: rotate(4deg);
98 | }
99 |
100 | /* Item */
101 | .ItemWrapper {
102 | display: flex;
103 | flex-direction: column;
104 | height: 100%;
105 | width: 100%;
106 | padding: 5px;
107 | box-sizing: border-box;
108 | }
109 |
110 | .ItemContainer {
111 | padding: 6px 6px 2px 8px;
112 | width: 100%;
113 | height: 100%;
114 | border-color: darkgray;
115 | border-radius: 3px;
116 | background-color: white;
117 | flex: 1;
118 | box-shadow: 1px 1px 0px 0px #ccc;
119 | box-sizing: border-box;
120 | }
121 |
122 | .ItemPlaceholder {
123 | background-color: #C4C9CC;
124 | }
125 |
126 | .ItemPlaceholder .ItemContent {
127 | opacity: 0;
128 | }
129 |
130 | .ItemContent p {
131 | margin: 0;
132 | }
133 |
--------------------------------------------------------------------------------
/src/Kanban/__tests__/updateLists.test.js:
--------------------------------------------------------------------------------
1 | import { updateLists } from '../updateLists';
2 |
3 | const lists = [
4 | {
5 | id: 1,
6 | rows: [
7 | {id: 1},
8 | {id: 2}
9 | ]
10 | },
11 | {
12 | id: 2,
13 | rows: [
14 | {id: 3},
15 | {id: 4}
16 | ]
17 | }
18 | ];
19 |
20 | test('move lists', () => {
21 | const updatedList = updateLists(lists, {
22 | from: {listId: 1},
23 | to: {listId: 2}
24 | });
25 |
26 | expect(JSON.stringify(updatedList)).toMatchSnapshot();
27 | });
28 |
29 | test('move item inside same list', () => {
30 | const updatedList = updateLists(lists, {
31 | from: {itemId: 1},
32 | to: {itemId: 2}
33 | });
34 |
35 | expect(JSON.stringify(updatedList)).toMatchSnapshot();
36 | });
37 |
38 | test('move between lists', () => {
39 | const updatedList = updateLists(lists, {
40 | from: {itemId: 2},
41 | to: {itemId: 3}
42 | });
43 |
44 | expect(JSON.stringify(updatedList)).toMatchSnapshot();
45 | });
46 |
47 | test('move item to an empty list', () => {
48 | const otherLists = [
49 | {
50 | id: 1,
51 | rows: [
52 | {id: 1},
53 | {id: 2}
54 | ]
55 | },
56 | {
57 | id: 2,
58 | rows: []
59 | }
60 | ];
61 |
62 | const updatedList = updateLists(otherLists, {
63 | from: {itemId: 1},
64 | to: {listId: 2}
65 | });
66 |
67 | expect(JSON.stringify(updatedList)).toMatchSnapshot();
68 | });
69 |
70 | test('move item from a list with a single element', () => {
71 | const otherLists = [
72 | {
73 | id: 1,
74 | rows: [
75 | {id: 1},
76 | {id: 2}
77 | ]
78 | },
79 | {
80 | id: 2,
81 | rows: [
82 | {id: 3}
83 | ]
84 | }
85 | ];
86 |
87 | const updatedList = updateLists(otherLists, {
88 | from: {itemId: 3},
89 | to: {itemId: 2}
90 | });
91 |
92 | expect(JSON.stringify(updatedList)).toMatchSnapshot();
93 | });
94 |
95 | test('null move', () => {
96 | const updatedList = updateLists(lists, {
97 | from: {itemId: 1},
98 | to: {itemId: 1}
99 | });
100 |
101 | expect(updatedList).toMatchObject(lists);
102 | });
103 |
104 | test('lists immutability', () => {
105 | const updatedList = updateLists(lists, {
106 | from: {listId: 1},
107 | to: {listId: 2}
108 | });
109 |
110 | expect(updatedList).not.toBe(lists);
111 | });
112 |
113 | test('single list equality', () => {
114 | const updatedList = updateLists(lists, {
115 | from: {listId: 1},
116 | to: {listId: 2}
117 | });
118 |
119 | expect(updatedList[1]).toBe(lists[0]);
120 | });
121 |
122 | test('single item equality', () => {
123 | const updatedList = updateLists(lists, {
124 | from: {itemId: 1},
125 | to: {itemId: 2}
126 | });
127 |
128 | expect(updatedList[0][1]).toBe(lists[0][0]);
129 | });
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Virtual Kanban
2 | 
3 | 
4 | 
5 |
6 | A Kanban component in React.
7 |
8 | Demo available here:
9 | https://edulan.github.io/react-virtual-kanban/
10 |
11 | ## Features
12 | * Fully virtualized
13 | * Built-in drag and drop support
14 | * Dynamic heights
15 | * Custom Item and List components
16 |
17 | ## Installation
18 | Via npm:
19 | ```shell
20 | npm install react-virtual-kanban --save
21 | ```
22 |
23 | ## Usage
24 | ### Basic example
25 | ```javascript
26 | import React from 'react';
27 | import ReactDOM from 'react-dom';
28 | import { VirtualKanban } from 'react-virtual-kanban';
29 |
30 | // Import only once
31 | import 'react-virtual-kanban/lib/styles.css';
32 |
33 | // Declare lists with the following structure
34 | const lists = [
35 | {
36 | id: 'list#1',
37 | rows: [
38 | {id: 'item#1'},
39 | {id: 'item#2'},
40 | {id: 'item#3'},
41 | {id: 'item#4'}
42 | ]
43 | },
44 | {
45 | id: 'list#2',
46 | rows: [
47 | {id: 'item#5'},
48 | {id: 'item#6'},
49 | {id: 'item#7'},
50 | {id: 'item#8'}
51 | ]
52 | },
53 | {
54 | id: 'list#3',
55 | rows: [
56 | {id: 'item#9'},
57 | {id: 'item#10'},
58 | {id: 'item#11'},
59 | {id: 'item#12'}
60 | ]
61 | }
62 | ];
63 |
64 | ReactDOM.render(
65 | ,
71 | document.getElementById('root')
72 | );
73 | ```
74 |
75 | ## API
76 | | Property | Type | Default | Description |
77 | |:---------------------------|:------------------|:-----------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
78 | | lists | Array | `[]` | Array of lists to be rendered |
79 | | width | Number | | The width of the kanban board |
80 | | height | Number | | The height of the kanban board |
81 | | listWidth | Number | | Width of each list |
82 | | listComponent | Function | `List` | List decorator component |
83 | | itemComponent | Function | `Item` | Item decorator component |
84 | | itemPreviewComponent | Function | ItemPreview | Item preview decorator component |
85 | | listPreviewComponent | Function | | List preview decorator component |
86 | | onMoveRow | Function | | Move row callback |
87 | | onMoveList | Function | | Move list callback |
88 | | onDropRow | Function | | Drop row callback |
89 | | onDropList | Function | | Drop list callback |
90 | | overscanListCount | Number | 2 | Number of lists to render before/after the visible part |
91 | | overscanRowCount | Number | 2 | Number of row items to render before/after the visible part |
92 | | itemCacheKey | Function | `id` | Key generator function for caching Item components |
93 |
94 | ## TODO
95 | * Auto scrolling
96 | * Performance++
97 | * Doc and examples
98 | * Integration with state managers (Redux, Mobx...)
99 | * Animations
100 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.NODE_ENV = 'production';
3 |
4 | var chalk = require('chalk');
5 | var fs = require('fs');
6 | var path = require('path');
7 | var filesize = require('filesize');
8 | var gzipSize = require('gzip-size').sync;
9 | var rimrafSync = require('rimraf').sync;
10 | var webpack = require('webpack');
11 | var config = require('../config/webpack.config.prod');
12 | var paths = require('../config/paths');
13 | var recursive = require('recursive-readdir');
14 | var stripAnsi = require('strip-ansi');
15 |
16 | // Input: /User/dan/app/build/static/js/main.82be8.js
17 | // Output: /static/js/main.js
18 | function removeFileNameHash(fileName) {
19 | return fileName
20 | .replace(paths.appBuild, '')
21 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
22 | }
23 |
24 | // Input: 1024, 2048
25 | // Output: "(+1 KB)"
26 | function getDifferenceLabel(currentSize, previousSize) {
27 | var FIFTY_KILOBYTES = 1024 * 50;
28 | var difference = currentSize - previousSize;
29 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
30 | if (difference >= FIFTY_KILOBYTES) {
31 | return chalk.red('+' + fileSize);
32 | } else if (difference < FIFTY_KILOBYTES && difference > 0) {
33 | return chalk.yellow('+' + fileSize);
34 | } else if (difference < 0) {
35 | return chalk.green(fileSize);
36 | } else {
37 | return '';
38 | }
39 | }
40 |
41 | // First, read the current file sizes in build directory.
42 | // This lets us display how much they changed later.
43 | recursive(paths.appBuild, (err, fileNames) => {
44 | var previousSizeMap = (fileNames || [])
45 | .filter(fileName => /\.(js|css)$/.test(fileName))
46 | .reduce((memo, fileName) => {
47 | var contents = fs.readFileSync(fileName);
48 | var key = removeFileNameHash(fileName);
49 | memo[key] = gzipSize(contents);
50 | return memo;
51 | }, {});
52 |
53 | // Remove all content but keep the directory so that
54 | // if you're in it, you don't end up in Trash
55 | rimrafSync(paths.appBuild + '/*');
56 |
57 | // Start the webpack build
58 | build(previousSizeMap);
59 | });
60 |
61 | // Print a detailed summary of build files.
62 | function printFileSizes(stats, previousSizeMap) {
63 | var assets = stats.toJson().assets
64 | .filter(asset => /\.(js|css)$/.test(asset.name))
65 | .map(asset => {
66 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
67 | var size = gzipSize(fileContents);
68 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)];
69 | var difference = getDifferenceLabel(size, previousSize);
70 | return {
71 | folder: path.join('build', path.dirname(asset.name)),
72 | name: path.basename(asset.name),
73 | size: size,
74 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '')
75 | };
76 | });
77 | assets.sort((a, b) => b.size - a.size);
78 | var longestSizeLabelLength = Math.max.apply(null,
79 | assets.map(a => stripAnsi(a.sizeLabel).length)
80 | );
81 | assets.forEach(asset => {
82 | var sizeLabel = asset.sizeLabel;
83 | var sizeLength = stripAnsi(sizeLabel).length;
84 | if (sizeLength < longestSizeLabelLength) {
85 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength);
86 | sizeLabel += rightPadding;
87 | }
88 | console.log(
89 | ' ' + sizeLabel +
90 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
91 | );
92 | });
93 | }
94 |
95 | // Create the production build and print the deployment instructions.
96 | function build(previousSizeMap) {
97 | console.log('Creating an optimized production build...');
98 | webpack(config).run((err, stats) => {
99 | if (err) {
100 | console.error('Failed to create a production build. Reason:');
101 | console.error(err.message || err);
102 | process.exit(1);
103 | }
104 |
105 | console.log(chalk.green('Compiled successfully.'));
106 | console.log();
107 |
108 | console.log('File sizes after gzip:');
109 | console.log();
110 | printFileSizes(stats, previousSizeMap);
111 | });
112 | }
113 |
--------------------------------------------------------------------------------
/src/SortableList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List as VirtualScroll, CellMeasurer, AutoSizer } from 'react-virtualized';
3 | import { DragSource, DropTarget } from 'react-dnd';
4 | import { getEmptyImage } from 'react-dnd-html5-backend';
5 |
6 | import { ItemCache } from './itemCache';
7 | import SortableItem from '../SortableItem';
8 |
9 | import { LIST_TYPE, ROW_TYPE } from '../types';
10 | import * as dragSpec from './dragSpec';
11 | import * as dropSpec from './dropSpec';
12 | import * as propTypes from './propTypes';
13 |
14 | import PureComponent from '../PureComponent';
15 |
16 | const identity = (c) => c;
17 |
18 | class SortableList extends PureComponent {
19 | static propTypes = propTypes;
20 |
21 | constructor(props) {
22 | super(props);
23 |
24 | this.renderRow = this.renderRow.bind(this);
25 | this.renderItemForMeasure = this.renderItemForMeasure.bind(this);
26 | this.renderList = this.renderList.bind(this);
27 | }
28 |
29 | componentDidMount() {
30 | this.props.connectDragPreview(getEmptyImage(), {
31 | captureDraggingState: true
32 | });
33 | }
34 |
35 | componentDidUpdate(prevProps) {
36 | if (prevProps.list.rows !== this.props.list.rows && !!this._list) {
37 | this._list.recomputeRowHeights();
38 | }
39 | }
40 |
41 | renderRow({ index, key, style }) {
42 | const row = this.props.list.rows[index];
43 |
44 | return (
45 |
58 | );
59 | }
60 |
61 | renderItemForMeasure({ rowIndex }) {
62 | const { itemComponent: DecoratedItem } = this.props;
63 | const row = this.props.list.rows[rowIndex];
64 |
65 | return (
66 |
75 | );
76 | }
77 |
78 | renderList({ width, height }) {
79 | // TODO: Check whether scrollbar is visible or not :/
80 |
81 | return (
82 |
89 | {({ getRowHeight }) => (
90 | (this._list = c)}
92 | className='KanbanList'
93 | width={width}
94 | height={height}
95 | rowHeight={getRowHeight}
96 | rowCount={this.props.list.rows.length}
97 | rowRenderer={this.renderRow}
98 | overscanRowCount={this.props.overscanRowCount}
99 | />
100 | )}
101 |
102 | );
103 | }
104 |
105 | render() {
106 | const {
107 | list,
108 | listId,
109 | listComponent: DecoratedList,
110 | isDragging,
111 | connectDragSource,
112 | connectDropTarget,
113 | listStyle,
114 | } = this.props;
115 |
116 | return (
117 |
126 |
127 | {(dimensions) => this.renderList(dimensions)}
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | const connectDrop = DropTarget([LIST_TYPE, ROW_TYPE], dropSpec, connect => ({
135 | connectDropTarget: connect.dropTarget(),
136 | }))
137 |
138 | const connectDrag = DragSource(LIST_TYPE, dragSpec, (connect, monitor) => ({
139 | connectDragSource: connect.dragSource(),
140 | connectDragPreview: connect.dragPreview(),
141 | isDragging: monitor.isDragging(),
142 | }))
143 |
144 | export default connectDrop(connectDrag(SortableList));
145 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-virtual-kanban",
3 | "description": "A Kanban component in React",
4 | "version": "0.2.0",
5 | "author": "Eduardo Lanchares ",
6 | "homepage": "https://edulan.github.io/react-virtual-kanban/",
7 | "main": "lib/index.js",
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/edulan/react-virtual-kanban.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/edulan/react-virtual-kanban/issues"
15 | },
16 | "keywords": [
17 | "react",
18 | "react-component",
19 | "kanban",
20 | "virtual-scroll"
21 | ],
22 | "files": [
23 | "dist",
24 | "lib"
25 | ],
26 | "devDependencies": {
27 | "autoprefixer": "6.4.0",
28 | "babel-cli": "^6.18.0",
29 | "babel-core": "6.14.0",
30 | "babel-eslint": "6.1.2",
31 | "babel-loader": "6.2.5",
32 | "babel-plugin-add-module-exports": "^0.2.1",
33 | "babel-plugin-transform-class-display-name": "0.0.3",
34 | "babel-plugin-transform-class-properties": "6.11.5",
35 | "babel-plugin-transform-export-extensions": "^6.8.0",
36 | "babel-plugin-transform-object-rest-spread": "6.8.0",
37 | "babel-plugin-transform-react-constant-elements": "6.9.1",
38 | "babel-plugin-transform-react-remove-prop-types": "^0.2.11",
39 | "babel-plugin-transform-regenerator": "6.14.0",
40 | "babel-plugin-transform-remove-console": "^6.8.0",
41 | "babel-plugin-transform-runtime": "6.12.0",
42 | "babel-preset-es2015": "^6.18.0",
43 | "babel-preset-latest": "6.14.0",
44 | "babel-preset-react": "6.11.1",
45 | "babel-preset-stage-0": "^6.16.0",
46 | "case-sensitive-paths-webpack-plugin": "1.1.3",
47 | "chalk": "1.1.3",
48 | "connect-history-api-fallback": "1.3.0",
49 | "cross-spawn": "4.0.0",
50 | "css-loader": "0.23.1",
51 | "decorate-component-with-props": "^1.0.2",
52 | "detect-port": "1.0.0",
53 | "eslint": "3.1.1",
54 | "eslint-config-react-app": "^0.2.1",
55 | "eslint-loader": "1.4.1",
56 | "eslint-plugin-flowtype": "2.4.0",
57 | "eslint-plugin-import": "1.12.0",
58 | "eslint-plugin-jsx-a11y": "2.0.1",
59 | "eslint-plugin-react": "5.2.2",
60 | "extract-text-webpack-plugin": "1.0.1",
61 | "file-loader": "0.9.0",
62 | "filesize": "3.3.0",
63 | "fs-extra": "0.30.0",
64 | "gh-pages": "^0.12.0",
65 | "gzip-size": "3.0.0",
66 | "html-webpack-plugin": "2.22.0",
67 | "http-proxy-middleware": "0.17.0",
68 | "jest": "^19.0.2",
69 | "json-loader": "0.5.4",
70 | "object-assign": "4.1.0",
71 | "opn": "4.0.2",
72 | "postcss": "^5.2.6",
73 | "postcss-cli": "^2.6.0",
74 | "postcss-import": "^9.0.0",
75 | "postcss-loader": "0.9.1",
76 | "promise": "7.1.1",
77 | "react": "15.4.1",
78 | "react-addons-perf": "15.4.1",
79 | "react-addons-shallow-compare": "15.4.1",
80 | "react-dom": "15.4.1",
81 | "recursive-readdir": "2.0.0",
82 | "rimraf": "2.5.4",
83 | "strip-ansi": "3.0.1",
84 | "style-loader": "0.13.1",
85 | "url-loader": "0.5.7",
86 | "webpack": "1.13.1",
87 | "webpack-dev-server": "1.14.1",
88 | "whatwg-fetch": "1.0.0"
89 | },
90 | "dependencies": {
91 | "babel-runtime": "6.11.6",
92 | "classnames": "2.2.5",
93 | "dnd-core": "^2.2.3",
94 | "dom-helpers": "^2.4.0",
95 | "react-addons-update": "15.4.1",
96 | "react-dnd": "2.1.4",
97 | "react-dnd-html5-backend": "2.1.2",
98 | "react-dnd-scrollzone": "3.1.0",
99 | "react-virtualized": "8.8.1"
100 | },
101 | "peerDependencies": {
102 | "react": "^0.14.0 || ^15.0.0",
103 | "react-addons-shallow-compare": "^0.14.0 || ^15.0.0",
104 | "react-dom": "^0.14.0 || ^15.0.0"
105 | },
106 | "scripts": {
107 | "build:clean": "rm -rf ./lib ./dist",
108 | "build:lib": "NODE_ENV=production BABEL_ENV=commonjs babel src --out-dir lib --ignore src/demo/ --ignore src/**/__tests__",
109 | "build:umd": "NODE_ENV=production webpack --config config/webpack.config.umd.js --bail",
110 | "build:demo": "node ./scripts/build.js",
111 | "build:css": "postcss --use autoprefixer --use postcss-import src/styles.css > lib/styles.css",
112 | "build": "npm run build:clean && npm run build:umd && npm run build:lib && npm run build:css && npm run build:demo",
113 | "deploy": "gh-pages -d build",
114 | "prepublish": "npm run build",
115 | "postpublish": "npm run deploy",
116 | "start": "npm run build:lib && npm run build:css && node ./scripts/start.js",
117 | "test": "jest"
118 | },
119 | "eslintConfig": {
120 | "extends": "./config/eslint.js"
121 | },
122 | "jest": {
123 | "collectCoverageFrom": [
124 | "src/**/*.js"
125 | ],
126 | "testPathIgnorePatterns": [
127 | "(lib|build|docs|node_modules)[/\\\\]"
128 | ]
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Kanban/updateLists.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update';
2 |
3 | function rotateRight(range, offset) {
4 | const length = range.length;
5 |
6 | return range.map((_, index, list) => list[(index + offset) % length]);
7 | }
8 |
9 | function rotateLeft(range, offset) {
10 | return rotateRight(range, range.length - Math.abs(offset % range.length));
11 | }
12 |
13 | function buildUpdateOperation(list, { from, to }) {
14 | const lower = Math.min(from, to);
15 | const upper = Math.max(from, to);
16 | const range = list.slice(lower, upper + 1);
17 | const rotated = to - from > 0 ? rotateRight(range, 1) : rotateLeft(range, 1);
18 |
19 | return [lower, rotated.length, ...rotated];
20 | }
21 |
22 | export function findListIndex(lists, listId) {
23 | return lists.findIndex(({ id }) => id === listId);
24 | }
25 |
26 | export function findItemIndex(lists, itemId) {
27 | let index = -1;
28 |
29 | lists.forEach(({ rows }) => {
30 | if (index !== -1) return;
31 | index = rows.findIndex(({ id }) => id === itemId);
32 | });
33 |
34 | return index;
35 | }
36 |
37 | export function findItemListIndex(lists, itemId) {
38 | let index = -1;
39 |
40 | lists.forEach(({ rows }, i) => {
41 | if (index !== -1) return;
42 |
43 | if (rows.some(({ id }) => id === itemId)) {
44 | index = i;
45 | }
46 | });
47 |
48 | return index;
49 | }
50 |
51 | export function findItemListId(lists, itemId) {
52 | const list = lists.find(({ rows }) => {
53 | return rows.some(({ id }) => id === itemId);
54 | });
55 |
56 | return list && list.id;
57 | }
58 |
59 | function moveLists(lists, { fromId, toId }) {
60 | const fromIndex = findListIndex(lists, fromId);
61 | const toIndex = findListIndex(lists, toId);
62 |
63 | // Sanity checks
64 | if (fromIndex === -1 || toIndex === -1) {
65 | console.warn(`List not in bounds`);
66 | return lists;
67 | }
68 |
69 | const fromList = lists[fromIndex];
70 |
71 | if (!fromList) {
72 | console.warn(`List is not an object`);
73 | return lists;
74 | }
75 |
76 | return update(lists, {
77 | $splice: [
78 | [fromIndex, 1],
79 | [toIndex, 0, fromList],
80 | ]
81 | });
82 | }
83 |
84 | function moveItems(lists, { fromId, toId }) {
85 | const fromListIndex = findItemListIndex(lists, fromId);
86 | const toListIndex = findItemListIndex(lists, toId);
87 | const fromIndex = findItemIndex(lists, fromId);
88 | const toIndex = findItemIndex(lists, toId);
89 |
90 | // Sanity checks
91 | if (fromListIndex === -1) {
92 | console.warn(`List not in bounds`);
93 | return lists;
94 | }
95 |
96 | if (fromIndex === -1 || toIndex === -1) {
97 | console.warn(`Item not in bounds`);
98 | return lists;
99 | }
100 |
101 | const fromList = lists[fromListIndex];
102 |
103 | if (fromListIndex === toListIndex) {
104 | return update(lists, {
105 | [fromListIndex]: {
106 | rows: {
107 | $splice: [
108 | buildUpdateOperation(fromList.rows, {from: fromIndex, to: toIndex})
109 | ]
110 | }
111 | }
112 | });
113 | }
114 |
115 | const fromItem = fromList.rows[fromIndex];
116 |
117 | return update(lists, {
118 | // Remove item from source list
119 | [fromListIndex]: {
120 | rows: {
121 | $splice: [
122 | [fromIndex, 1],
123 | ]
124 | }
125 | },
126 | // Add item to target list
127 | [toListIndex]: {
128 | rows: {
129 | $splice: [
130 | [toIndex, 0, fromItem]
131 | ]
132 | }
133 | },
134 | });
135 | }
136 |
137 | function moveItemToList(lists, { fromId, toId }) {
138 | const fromIndex = findItemIndex(lists, fromId);
139 | const fromListIndex = findItemListIndex(lists, fromId);
140 | const toListIndex = findListIndex(lists, toId);
141 |
142 | if (fromIndex === -1) {
143 | console.warn(`Item not in bounds`);
144 | return lists;
145 | }
146 |
147 | const fromList = lists[fromListIndex];
148 | const toList = lists[toListIndex];
149 |
150 | if (!toList) {
151 | console.warn(`List is not an object`);
152 | return lists;
153 | }
154 |
155 | // Only move when list is empty
156 | if (toList.rows.length > 0) {
157 | return lists;
158 | }
159 |
160 | const fromItem = fromList.rows[fromIndex];
161 |
162 | return update(lists, {
163 | // Remove item from source list
164 | [fromListIndex]: {
165 | rows: {
166 | $splice: [
167 | [fromIndex, 1],
168 | ]
169 | }
170 | },
171 | // Add item to target list
172 | [toListIndex]: {
173 | rows: {
174 | $push: [
175 | fromItem
176 | ]
177 | }
178 | },
179 | });
180 | }
181 |
182 | export function updateLists(lists, { from, to }) {
183 | const { itemId: fromItemId, listId: fromListId } = from;
184 | const { itemId: toItemId, listId: toListId } = to;
185 |
186 | // Deprecation checks
187 | if (from.listIndex || from.rowIndex || to.listIndex || to.rowIndex) {
188 | console.warn(`Deprecated listIndex and rowIndex param. Use listId or itemId`);
189 | return lists;
190 | }
191 |
192 | // Move lists
193 | if (fromListId !== toListId && fromItemId === void 0 && toItemId === void 0) {
194 | return moveLists(lists, { fromId: fromListId, toId: toListId });
195 | }
196 |
197 | // Move item inside same list
198 | if (fromListId === toListId && fromItemId !== void 0 && toItemId !== void 0) {
199 | return moveItems(lists, { fromId: fromItemId, toId: toItemId });
200 | }
201 |
202 | // Move item to a different list
203 | if (fromListId === void 0 && toListId !== void 0 && fromItemId !== void 0 && toItemId === void 0) {
204 | return moveItemToList(lists, { fromId: fromItemId, toId: toListId });
205 | }
206 |
207 | return lists;
208 | }
209 |
--------------------------------------------------------------------------------
/config/eslint.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/airbnb/javascript but less opinionated.
2 |
3 | // We use eslint-loader so even warnings are very visibile.
4 | // This is why we only use "WARNING" level for potential errors,
5 | // and we don't use "ERROR" level at all.
6 |
7 | // In the future, we might create a separate list of rules for production.
8 | // It would probably be more strict.
9 |
10 | module.exports = {
11 | root: true,
12 |
13 | parser: 'babel-eslint',
14 |
15 | // import plugin is termporarily disabled, scroll below to see why
16 | plugins: [/*'import', */'flowtype', 'jsx-a11y', 'react'],
17 |
18 | env: {
19 | browser: true,
20 | commonjs: true,
21 | es6: true,
22 | node: true,
23 | jest: true
24 | },
25 |
26 | parserOptions: {
27 | ecmaVersion: 6,
28 | sourceType: 'module',
29 | ecmaFeatures: {
30 | jsx: true,
31 | generators: true,
32 | experimentalObjectRestSpread: true
33 | }
34 | },
35 |
36 | settings: {
37 | 'import/ignore': [
38 | 'node_modules',
39 | '\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$',
40 | ],
41 | 'import/extensions': ['.js'],
42 | 'import/resolver': {
43 | node: {
44 | extensions: ['.js', '.json']
45 | }
46 | }
47 | },
48 |
49 | rules: {
50 | // http://eslint.org/docs/rules/
51 | 'array-callback-return': 'warn',
52 | 'default-case': ['warn', { commentPattern: '^no default$' }],
53 | 'dot-location': ['warn', 'property'],
54 | eqeqeq: ['warn', 'allow-null'],
55 | 'guard-for-in': 'warn',
56 | 'new-parens': 'warn',
57 | 'no-array-constructor': 'warn',
58 | 'no-caller': 'warn',
59 | 'no-cond-assign': ['warn', 'always'],
60 | 'no-const-assign': 'warn',
61 | 'no-control-regex': 'warn',
62 | 'no-delete-var': 'warn',
63 | 'no-dupe-args': 'warn',
64 | 'no-dupe-class-members': 'warn',
65 | 'no-dupe-keys': 'warn',
66 | 'no-duplicate-case': 'warn',
67 | 'no-empty-character-class': 'warn',
68 | 'no-empty-pattern': 'warn',
69 | 'no-eval': 'warn',
70 | 'no-ex-assign': 'warn',
71 | 'no-extend-native': 'warn',
72 | 'no-extra-bind': 'warn',
73 | 'no-extra-label': 'warn',
74 | 'no-fallthrough': 'warn',
75 | 'no-func-assign': 'warn',
76 | 'no-implied-eval': 'warn',
77 | 'no-invalid-regexp': 'warn',
78 | 'no-iterator': 'warn',
79 | 'no-label-var': 'warn',
80 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
81 | 'no-lone-blocks': 'warn',
82 | 'no-loop-func': 'warn',
83 | 'no-mixed-operators': ['warn', {
84 | groups: [
85 | ['&', '|', '^', '~', '<<', '>>', '>>>'],
86 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
87 | ['&&', '||'],
88 | ['in', 'instanceof']
89 | ],
90 | allowSamePrecedence: false
91 | }],
92 | 'no-multi-str': 'warn',
93 | 'no-native-reassign': 'warn',
94 | 'no-negated-in-lhs': 'warn',
95 | 'no-new-func': 'warn',
96 | 'no-new-object': 'warn',
97 | 'no-new-symbol': 'warn',
98 | 'no-new-wrappers': 'warn',
99 | 'no-obj-calls': 'warn',
100 | 'no-octal': 'warn',
101 | 'no-octal-escape': 'warn',
102 | 'no-redeclare': 'warn',
103 | 'no-regex-spaces': 'warn',
104 | 'no-restricted-syntax': [
105 | 'warn',
106 | 'LabeledStatement',
107 | 'WithStatement',
108 | ],
109 | 'no-return-assign': 'warn',
110 | 'no-script-url': 'warn',
111 | 'no-self-assign': 'warn',
112 | 'no-self-compare': 'warn',
113 | 'no-sequences': 'warn',
114 | 'no-shadow-restricted-names': 'warn',
115 | 'no-sparse-arrays': 'warn',
116 | 'no-this-before-super': 'warn',
117 | 'no-throw-literal': 'warn',
118 | 'no-undef': 'warn',
119 | 'no-unexpected-multiline': 'warn',
120 | 'no-unreachable': 'warn',
121 | 'no-unused-expressions': 'warn',
122 | 'no-unused-labels': 'warn',
123 | "no-unused-vars": ['warn', {vars: 'local', args: 'none', 'varsIgnorePattern': '^_'}],
124 | 'no-use-before-define': ['warn', 'nofunc'],
125 | 'no-useless-computed-key': 'warn',
126 | 'no-useless-concat': 'warn',
127 | 'no-useless-constructor': 'warn',
128 | 'no-useless-escape': 'warn',
129 | 'no-useless-rename': ['warn', {
130 | ignoreDestructuring: false,
131 | ignoreImport: false,
132 | ignoreExport: false,
133 | }],
134 | 'no-with': 'warn',
135 | 'no-whitespace-before-property': 'warn',
136 | 'operator-assignment': ['warn', 'always'],
137 | radix: 'warn',
138 | 'require-yield': 'warn',
139 | 'rest-spread-spacing': ['warn', 'never'],
140 | strict: ['warn', 'never'],
141 | 'unicode-bom': ['warn', 'never'],
142 | 'use-isnan': 'warn',
143 | 'valid-typeof': 'warn',
144 |
145 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/
146 |
147 | // TODO: import rules are temporarily disabled because they don't play well
148 | // with how eslint-loader only checks the file you change. So if module A
149 | // imports module B, and B is missing a default export, the linter will
150 | // record this as an issue in module A. Now if you fix module B, the linter
151 | // will not be aware that it needs to re-lint A as well, so the error
152 | // will stay until the next restart, which is really confusing.
153 |
154 | // This is probably fixable with a patch to eslint-loader.
155 | // When file A is saved, we want to invalidate all files that import it
156 | // *and* that currently have lint errors. This should fix the problem.
157 |
158 | // 'import/default': 'warn',
159 | // 'import/export': 'warn',
160 | // 'import/named': 'warn',
161 | // 'import/namespace': 'warn',
162 | // 'import/no-amd': 'warn',
163 | // 'import/no-duplicates': 'warn',
164 | // 'import/no-extraneous-dependencies': 'warn',
165 | // 'import/no-named-as-default': 'warn',
166 | // 'import/no-named-as-default-member': 'warn',
167 | // 'import/no-unresolved': ['warn', { commonjs: true }],
168 |
169 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
170 | 'react/jsx-equals-spacing': ['warn', 'never'],
171 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
172 | 'react/jsx-no-undef': 'warn',
173 | 'react/jsx-pascal-case': ['warn', {
174 | allowAllCaps: true,
175 | ignore: [],
176 | }],
177 | 'react/jsx-uses-react': 'warn',
178 | 'react/jsx-uses-vars': 'warn',
179 | 'react/no-deprecated': 'warn',
180 | 'react/no-direct-mutation-state': 'warn',
181 | 'react/no-is-mounted': 'warn',
182 | 'react/react-in-jsx-scope': 'warn',
183 | 'react/require-render-return': 'warn',
184 |
185 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
186 | 'jsx-a11y/aria-role': 'warn',
187 | 'jsx-a11y/img-has-alt': 'warn',
188 | 'jsx-a11y/img-redundant-alt': 'warn',
189 | 'jsx-a11y/no-access-key': 'warn',
190 |
191 | // https://github.com/gajus/eslint-plugin-flowtype
192 | 'flowtype/define-flow-type': 'warn',
193 | 'flowtype/require-valid-file-annotation': 'warn',
194 | 'flowtype/use-flow-type': 'warn'
195 | }
196 | };
197 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var autoprefixer = require('autoprefixer');
3 | var atImport = require('postcss-import');
4 | var webpack = require('webpack');
5 | var HtmlWebpackPlugin = require('html-webpack-plugin');
6 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
7 | var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin');
8 | var paths = require('./paths');
9 | var env = require('./env');
10 |
11 | // This is the development configuration.
12 | // It is focused on developer experience and fast rebuilds.
13 | // The production configuration is different and lives in a separate file.
14 | module.exports = {
15 | // This makes the bundle appear split into separate modules in the devtools.
16 | // We don't use source maps here because they can be confusing:
17 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875
18 | // You may want 'cheap-module-source-map' instead if you prefer source maps.
19 | devtool: 'eval',
20 | // These are the "entry points" to our application.
21 | // This means they will be the "root" imports that are included in JS bundle.
22 | // The first two entry points enable "hot" CSS and auto-refreshes for JS.
23 | entry: [
24 | // Include WebpackDevServer client. It connects to WebpackDevServer via
25 | // sockets and waits for recompile notifications. When WebpackDevServer
26 | // recompiles, it sends a message to the client by socket. If only CSS
27 | // was changed, the app reload just the CSS. Otherwise, it will refresh.
28 | // The "?/" bit at the end tells the client to look for the socket at
29 | // the root path, i.e. /sockjs-node/. Otherwise visiting a client-side
30 | // route like /todos/42 would make it wrongly request /todos/42/sockjs-node.
31 | // The socket server is a part of WebpackDevServer which we are using.
32 | // The /sockjs-node/ path I'm referring to is hardcoded in WebpackDevServer.
33 | require.resolve('webpack-dev-server/client') + '?/',
34 | // Include Webpack hot module replacement runtime. Webpack is pretty
35 | // low-level so we need to put all the pieces together. The runtime listens
36 | // to the events received by the client above, and applies updates (such as
37 | // new CSS) to the running application.
38 | require.resolve('webpack/hot/dev-server'),
39 | // We ship a few polyfills by default.
40 | require.resolve('./polyfills'),
41 | // Finally, this is your app's code:
42 | path.join(paths.appSrc, 'demo/index')
43 | // We include the app code last so that if there is a runtime error during
44 | // initialization, it doesn't blow up the WebpackDevServer client, and
45 | // changing JS code would still trigger a refresh.
46 | ],
47 | output: {
48 | // Next line is not used in dev but WebpackDevServer crashes without it:
49 | path: paths.appBuild,
50 | // Add /* filename */ comments to generated require()s in the output.
51 | pathinfo: true,
52 | // This does not produce a real file. It's just the virtual path that is
53 | // served by WebpackDevServer in development. This is the JS bundle
54 | // containing code from all our entry points, and the Webpack runtime.
55 | filename: 'static/js/bundle.js',
56 | // In development, we always serve from the root. This makes config easier.
57 | publicPath: '/'
58 | },
59 | module: {
60 | // First, run the linter.
61 | // It's important to do this before Babel processes the JS.
62 | preLoaders: [
63 | {
64 | test: /\.js$/,
65 | loader: 'eslint',
66 | include: paths.appSrc,
67 | }
68 | ],
69 | loaders: [
70 | // Process JS with Babel.
71 | {
72 | test: /\.js$/,
73 | include: paths.appSrc,
74 | loader: 'babel',
75 | query: require('./babel.dev')
76 | },
77 | // "postcss" loader applies autoprefixer to our CSS.
78 | // "css" loader resolves paths in CSS and adds assets as dependencies.
79 | // "style" loader turns CSS into JS modules that inject