diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..66ec89e3 --- /dev/null +++ b/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + "react" + ], + "env": { + "mocha": { + "plugins": [ + "istanbul" + ] + } + } +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..fc876f04 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends":[ + "jakesidsmith/base", + "jakesidsmith/browser" + ] +} diff --git a/.mocharc b/.mocharc new file mode 100644 index 00000000..4a66a5eb --- /dev/null +++ b/.mocharc @@ -0,0 +1,4 @@ +--require babel-core/register +--require tests/helpers/test-setup.js +--bail +--recursive diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..3c024323 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +6.10.3 diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..3b31fc89 --- /dev/null +++ b/.nycrc @@ -0,0 +1,26 @@ +{ + "lines": 100, + "statements": 100, + "functions": 100, + "branches": 100, + "require": [], + "include": [ + "src/**/*.js", + "tests/helpers/mount.js", + "tests/helpers/children-stub.js" + ], + "exclude": [], + "extension": [ + ".js" + ], + "reporter": [ + "lcov", + "text-summary" + ], + "cache": true, + "all": true, + "check-coverage": false, + "sourceMap": false, + "instrument": false, + "report-dir": "coverage" +} diff --git a/README.md b/README.md index c6bc0c06..42de9be6 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,136 @@ -# React Reorder (v2) +# React Reorder __Drag & drop, touch enabled, reorder / sortable list, React component__ -If you are using v3 alpha, please refer to [this documentation](https://github.com/JakeSidSmith/react-reorder/blob/rework/README.md). +[![CircleCI](https://circleci.com/gh/JakeSidSmith/react-reorder.svg?style=svg)](https://circleci.com/gh/JakeSidSmith/react-reorder) + +If you are using v2, please refer to [this documentation](https://github.com/JakeSidSmith/react-reorder/blob/609de5a6be9ae7ea4b032b0b260b08bc524b362e/README.md). ## About -React Reorder is a React component that allows the user to drag-and-drop items in a list (horizontal / vertical) or a grid. +React Reorder is a React component that allows the user to drag-and-drop items in a list (horizontal / vertical), or a grid. You can also allow dragging items from one list to another. -It fully supports touch devices and auto-scrolls when a component is being dragged (check out the demo, link below). +It fully supports touch devices and auto-scrolls when a component is being dragged (check out the [demo](https://jakesidsmith.github.io/react-reorder/)). It also allows the user to set a hold time (duration before drag begins) allowing additional events like clicking / tapping to be applied. -Although this project is very new, it has been thoroughly tested in all modern browsers (see supported browsers). - -__[Demo](http://jakesidsmith.github.io/react-reorder/)__ - ## Installation -* Using npm - ``` - npm install react-reorder - ``` - Add `--save` or `-S` to update your package.json +Using npm -* Using bower - ``` - bower install react-reorder - ``` - Add `--save` or `-S` to update your bower.json +Add `--save` or `-S` to update your package.json -## Example - -1. If using requirejs: add `react-reorder` to your `require.config` - - ```javascript - paths: - // : - 'react-reorder': 'react-reorder/reorder' - } - ``` +``` +npm install react-reorder +``` -2. If using a module loader (requirejs / browserfiy / commonjs): require `react-reorder` where it will be used in your project - - ```javascript - var Reorder = require('react-reorder'); - ``` - - If using requirejs you'll probably want to wrap your module e.g. - - ```javascript - define(function (require) { - // Require react-reorder here - }); - ``` - -3. Configuration - - **Note: If your array is an array of primitives (e.g. strings) then `itemKey` is not required as the string itself will be used as the key, however item keys must be unique in any case** - - 1. Using JSX - - ```javascript - - ``` - - 2. Using standard Javascript - - ```javascript - React.createElement(Reorder, { - // The key of each object in your list to use as the element key - itemKey: 'name', - // Lock horizontal to have a vertical list - lock: 'horizontal', - // The milliseconds to hold an item for before dragging begins - holdTime: '500', - // The list to display - list: [ - {name: 'Item 1'}, - {name: 'Item 2'}, - {name: 'Item 3'} - ], - // A template to display for each list item - template: ListItem, - // Function that is called once a reorder has been performed - callback: this.callback, - // Class to be applied to the outer list element - listClass: 'my-list', - // Class to be applied to each list item's wrapper element - itemClass: 'list-item', - // A function to be called if a list item is clicked (before hold time is up) - itemClicked: this.itemClicked, - // The item to be selected (adds 'selected' class) - selected: this.state.selected, - // The key to compare from the selected item object with each item object - selectedKey: 'uuid', - // Allows reordering to be disabled - disableReorder: false - }) - ``` - -5. Callback functions - - 1. The `callback` function that is called once a reorder has been performed - - ```javascript - function callback(event, itemThatHasBeenMoved, itemsPreviousIndex, itemsNewIndex, reorderedArray) { - // ... - } - ``` - - 2. The `itemClicked` function that is called when an item is clicked before any dragging begins - - ```javascript - function itemClicked(event, itemThatHasBeenClicked, itemsIndex) { - // ... - } - ``` - - **Note: `event` will be the synthetic React event for either `mouseup` or `touchend`, and both contain `clientX` & `clientY` values (for ease of use)** - -## Compatibility / Requirements +Using bower -* Version `2.x` tested and working with React `0.14` +Add `--save` or `-S` to update your bower.json -* Versions `1.x` tested and working with React `0.12` - `0.13` +``` +bower install react-reorder +``` -* requirejs / commonjs / browserify (__Optional, but recommended*__) - -\* Has only been tested with requirejs & browserify - -## Supported Browsers - -### Desktop - -* Internet Explorer 9+ (may support IE8**) - -* Google Chrome (tested in version 39.0.2171.95(64-bit)) - -* Mozilla Firefox (tested in version 33.0) +## Example -* Opera (tested in version 26.0.1656.60) +If using a module loader (requirejs / browserfiy / commonjs): require `react-reorder` where it will be used in your project + +```javascript +var Reorder = require('react-reorder'); +var reorder = Reorder.reorder; +var reorderImmutable = Reorder.reorderImmutable; +var reorderFromTo = Reorder.reorderFromTo; +var reorderFromToImmutable = Reorder.reorderFromToImmutable; + +// Or ES6 + +import Reorder, { + reorder, + reorderImmutable, + reorderFromTo, + reorderFromToImmutable +} from 'react-reorder'; +``` + +### Configuration + +```javascript + // Custom placeholder element (optional), defaults to clone of dragged element + } +> + { + this.state.list.map((item) => ( +
  • + {item.name} +
  • + )).toArray() + /* + Note this example is an ImmutableJS List so we must convert it to an array. + I've left this up to you to covert to an array, as react-reorder updates a lot, + and if I called this internally it could get rather slow, + whereas you have greater control over your component updates. + */ + } +
    +``` -* Safari (tested in version 7.1.2 (9537.85.11.5)) +### Callback functions -\** Have not had a chance to test in IE8, but IE8 is supported by React +The `onReorder` function that is called once a reorder has been performed. +You can use our helper functions for reordering your arrays. -### Mobile +```javascript +import { reorder, reorderImmutable, reorderFromTo, reorderFromToImmutable } from 'react-reorder'; -* Chrome (tested in version 40.0.2214.89) +onReorder (event, previousIndex, nextIndex, fromId, toId) { + this.setState({ + myList: reorder(this.state.myList, previousIndex, nextIndex); + }); +} + +onReorderGroup (event, previousIndex, nextIndex, fromId, toId) { + if (fromId === toId) { + const list = reorderImmutable(this.state[fromId], previousIndex, nextIndex); + + this.setState({ + [fromId]: list + }); + } else { + const lists = reorderFromToImmutable({ + from: this.state[fromId], + to: this.state[toId] + }, previousIndex, nextIndex); + + this.setState({ + [fromId]: lists.from, + [toId]: lists.to + }); + } +} +``` -* Safari (tested on iOS 8) +## Compatibility / Requirements -## Untested Browsers +* Version `3.x` tested and working with React `15`, but should be backward compatible at least a couple of versions. -* Internet Explorer 8*** (the lowest version that React supports) +## Weird Scrolling Behavior? -\*** If anyone could confirm that this works in IE8, that'd be awesome +It is recommended that you apply `overflow-anchor: none;` to any parent elements with overflow auto (including on either of just the x/y axis) to prevent unwanted auto-scrolling, or surprisingly fast auto-scrolling. diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..d6d2ba9f --- /dev/null +++ b/circle.yml @@ -0,0 +1,15 @@ +machine: + node: + version: 6.9.1 + +general: + branches: + ignore: + - gh-pages + +deployment: + master: + branch: master + commands: + - npm run build + - scripts/deploy.sh diff --git a/examples/.babelrc b/examples/.babelrc new file mode 100644 index 00000000..bd20dc17 --- /dev/null +++ b/examples/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "es2015", + "react" + ] +} diff --git a/examples/.eslintrc.json b/examples/.eslintrc.json new file mode 100644 index 00000000..0a53a4e7 --- /dev/null +++ b/examples/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends":[ + "jakesidsmith/commonjs", + "jakesidsmith/es6", + "jakesidsmith/react", + "jakesidsmith/browser" + ] +} diff --git a/examples/index.html b/examples/index.html index d052dada..5084ab2c 100644 --- a/examples/index.html +++ b/examples/index.html @@ -4,7 +4,27 @@ React Reorder - + @@ -12,7 +32,9 @@

    Please enable javascript

    diff --git a/examples/src/js/grid.js b/examples/src/js/grid.js new file mode 100644 index 00000000..ca33ea3c --- /dev/null +++ b/examples/src/js/grid.js @@ -0,0 +1,62 @@ +import Immutable from 'immutable'; +import React, { Component } from 'react'; +import Reorder, { reorderImmutable } from '../../../src/index'; + +import { classNames } from './styles'; + +export class Grid extends Component { + constructor () { + super(); + + this.state = { + list: Immutable.List(Immutable.Range(0, 10).map(function (value) { + return { + name: ['Thing', value].join(' '), + color: ['rgb(', (value + 1) * 25, ',', 250 - ((value + 1) * 25), ',0)'].join('') + }; + })) + }; + } + + onReorder (event, previousIndex, nextIndex) { + const list = reorderImmutable(this.state.list, previousIndex, nextIndex); + + this.setState({ + list: list + }); + } + + render () { + return ( +
    +

    + No lock (grid) +

    +

    + This example has a hold time of 0 milliseconds +

    + + + { + this.state.list.map(({name, color}) => ( +
  • + {name} +
  • + )).toArray() + } +
    +
    + ); + } +} diff --git a/examples/src/js/index.js b/examples/src/js/index.js index e41f782b..385f4d0a 100644 --- a/examples/src/js/index.js +++ b/examples/src/js/index.js @@ -1,128 +1,34 @@ -'use strict'; - -var React = require('react'); -var ReactDOM = require('react-dom'); -var createReactClass = require('create-react-class'); -var Reorder = require('../../../index'); - -var ListItem = createReactClass({ - render: function () { - return React.createElement('div', { - className: 'inner', - style: { - color: this.props.item.color - } - }, this.props.sharedProps ? this.props.sharedProps.prefix : undefined, this.props.item.name); - } -}); - -var Main = createReactClass({ - callback: function (event, item, index, newIndex, list) { - this.setState({arr: list}); - }, - itemClicked: function (event, item) { - this.setState({ - clickedItem: item === this.state.clickedItem ? undefined : item - }); - }, - itemClicked2: function (event, item) { - this.setState({clickedItem2: item}); - }, - disableToggled: function () { - this.setState({disableReorder: !this.state.disableReorder}); - }, - prefixChanged: function (event) { - var target = event.currentTarget; - this.setState({prefix: target.value}); - }, - - // ---- - - getInitialState: function () { - var list = []; - - for (var i = 0; i < 10; i += 1) { - list.push({name: ['Thing', i].join(' '), color: ['rgb(',(i + 1) * 25, ',', 250 - ((i + 1) * 25),',0)'].join('')}); - } - - return { - arr: list, - prefix: 'Prefix' - }; - }, - render: function () { - return React.createElement('div', {className: 'app'}, - - React.createElement('p', null, React.createElement('strong', null, 'Lock horizontal')), - React.createElement('small', null, 'This example has a hold time of 500 milliseconds before dragging begins, allowing for other events like clicking / tapping to be attached'), - - React.createElement('p', null, 'Selected item: ', this.state.clickedItem ? this.state.clickedItem.name : undefined), - - React.createElement('p', null, - 'Prefix (shared props): ', - React.createElement('input', { - type: 'text', - onChange: this.prefixChanged, - value: this.state.prefix - }) - ), - - React.createElement(Reorder, { - itemKey: 'name', - lock: 'horizontal', - holdTime: '500', - list: this.state.arr, - template: ListItem, - callback: this.callback, - listClass: 'my-list', - itemClass: 'list-item', - itemClicked: this.itemClicked, - selected: this.state.clickedItem, - selectedKey: 'name', - sharedProps: { - prefix: [this.state.prefix, ': '].join('') - }}), - - React.createElement('p', null, React.createElement('strong', null, 'Lock vertical')), - React.createElement('small', null, 'This example has a hold time of 250 milliseconds'), - - React.createElement('p', null, - 'Reorder disabled: ', - React.createElement('input', { - type: 'checkbox', - onChange: this.disableToggled, - value: this.state.disableReorder || false - }), - 'Last item clicked: ', - this.state.clickedItem2 ? this.state.clickedItem2.name : undefined - ), - - React.createElement(Reorder, { - itemKey: 'name', - lock: 'vertical', - holdTime: '250', - list: this.state.arr, - template: ListItem, - callback: this.callback, - listClass: 'my-list-2', - itemClass: 'list-item', - itemClicked: this.itemClicked2, - disableReorder: this.state.disableReorder}), - - React.createElement('p', null, React.createElement('strong', null, 'No lock (grid)')), - React.createElement('small', null, 'This example has a hold time of 0 milliseconds'), - - React.createElement(Reorder, { - itemKey: 'name', - holdTime: '0', - list: this.state.arr, - template: ListItem, - callback: this.callback, - listClass: 'my-list-3', - itemClass: 'list-item'}) - +import { classNames } from './styles'; + +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; + +import { LockHorizontal } from './lock-horizontal'; +import { LockVertical } from './lock-vertical'; +import { Grid } from './grid'; +import { MultiList } from './multi-list'; +import { Kanban } from './kanban'; + +class Main extends Component { + render () { + return ( +
    +

    + React Reorder +

    +

    + Examples +

    + + + + + + + +
    ); } -}); +} ReactDOM.render(React.createElement(Main), document.getElementById('app')); diff --git a/examples/src/js/kanban.js b/examples/src/js/kanban.js new file mode 100644 index 00000000..6c1ac031 --- /dev/null +++ b/examples/src/js/kanban.js @@ -0,0 +1,156 @@ +import Immutable from 'immutable'; +import React, { Component } from 'react'; +import Reorder, { reorderImmutable, reorderFromToImmutable } from '../../../src/index'; + +import { classNames } from './styles'; + +let listInt = 1; +let itemInt = 1; + +const Wrapper = (props) => ( +
      + {props.children} +
    +); + +export class Kanban extends Component { + constructor () { + super(); + + this.state = { + lists: Immutable.List.of( + Immutable.Map({ + id: 'list-' + listInt, + items: Immutable.List.of( + Immutable.Map({ + name: 'item-' + itemInt + }) + ) + }) + ) + }; + } + + addList () { + this.setState({ + lists: this.state.lists.push(Immutable.Map({ + id: 'list-' + (listInt += 1), + items: Immutable.List() + })) + }); + } + + deleteList (index) { + this.setState({ + lists: this.state.lists.delete(index) + }); + } + + addItem (index) { + let list = this.state.lists.getIn([index, 'items']); + list = list.push(Immutable.Map({name: 'item-' + (itemInt += 1)})); + + this.setState({ + lists: this.state.lists.setIn([index, 'items'], list) + }); + } + + deleteItem (index, itemIndex) { + this.setState({ + lists: this.state.lists.deleteIn([index, 'items', itemIndex]) + }); + } + + onReorderGroup (event, previousIndex, nextIndex, fromId, toId) { + if (fromId === toId) { + const index = this.state.lists.findIndex((list) => list.get('id') === fromId); + let list = this.state.lists.getIn([index, 'items']); + list = reorderImmutable(list, previousIndex, nextIndex); + + this.setState({ + lists: this.state.lists.setIn([index, 'items'], list) + }); + } else { + const fromIndex = this.state.lists.findIndex((list) => list.get('id') === fromId); + const toIndex = this.state.lists.findIndex((list) => list.get('id') === toId); + + let fromList = this.state.lists.getIn([fromIndex, 'items']); + let toList = this.state.lists.getIn([toIndex, 'items']); + + const lists = reorderFromToImmutable({ + from: fromList, + to: toList + }, previousIndex, nextIndex); + + this.setState({ + lists: this.state.lists.setIn([fromIndex, 'items'], lists.from).setIn([toIndex, 'items'], lists.to) + }); + } + } + + render () { + return ( +
    +

    + Kanban Board +

    +

    + In this example users can add and remove lists and items, and drag items between lists +

    + +
    + { + this.state.lists.map((list, index) => ( +
    +
    + {list.get('id')} + + X + +
    + + { + list.get('items').map((item, itemIndex) => ( +
  • +
    + {item.get('name')} + + X + +
    +
  • + )).toArray() + } +
    +
    + Add item + +
    +
    + )) + } + +
    + Add list + +
    +
    +
    + ); + } +} diff --git a/examples/src/js/lock-horizontal.js b/examples/src/js/lock-horizontal.js new file mode 100644 index 00000000..6d3bc3a0 --- /dev/null +++ b/examples/src/js/lock-horizontal.js @@ -0,0 +1,104 @@ +import Immutable from 'immutable'; +import React, { Component } from 'react'; +import Reorder, { reorderImmutable } from '../../../src/index'; + +import { classNames } from './styles'; + +export class LockHorizontal extends Component { + constructor () { + super(); + + this.state = { + list: Immutable.List(Immutable.Range(0, 10).map(function (value) { + return { + name: ['Thing', value].join(' '), + color: ['rgb(', (value + 1) * 25, ',', 250 - ((value + 1) * 25), ',0)'].join('') + }; + })), + prefix: 'Prefix', + clickedItem: null + }; + } + + onPrefixChange (event) { + const target = event.currentTarget; + + this.setState({ + prefix: target.value + }); + } + + onReorder (event, previousIndex, nextIndex) { + const list = reorderImmutable(this.state.list, previousIndex, nextIndex); + + this.setState({ + list: list + }); + } + + onClickItem (name) { + this.setState({ + clickedItem: name + }); + } + + render () { + return ( +
    +

    + Lock horizontal +

    +

    + This example has a hold time of 500 milliseconds before dragging begins, + allowing for other events like clicking / tapping to be attached +

    +

    + Selected item: {this.state.clickedItem ? this.state.clickedItem.name : undefined} +

    +

    + {'Prefix: '} + {' '} + Last item clicked: {this.state.clickedItem} +

    + + } + > + { + this.state.list.map(({name, color}) => ( +
  • +
    + + {this.state.prefix} {name} + + +
    +
  • + )).toArray() + } +
    +
    + ); + } +} diff --git a/examples/src/js/lock-vertical.js b/examples/src/js/lock-vertical.js new file mode 100644 index 00000000..9c101065 --- /dev/null +++ b/examples/src/js/lock-vertical.js @@ -0,0 +1,97 @@ +import Immutable from 'immutable'; +import React, { Component } from 'react'; +import Reorder, { reorderImmutable } from '../../../src/index'; + +import { classNames } from './styles'; + +export class LockVertical extends Component { + constructor () { + super(); + + this.state = { + list: Immutable.List(Immutable.Range(0, 10).map(function (value) { + return { + name: ['Thing', value].join(' '), + color: ['rgb(', (value + 1) * 25, ',', 250 - ((value + 1) * 25), ',0)'].join('') + }; + })), + prefix: 'Prefix', + clickedItem: null + }; + } + + onPrefixChange (event) { + const target = event.currentTarget; + + this.setState({ + prefix: target.value + }); + } + + onDisableToggle () { + this.setState({ + disableReorder: !this.state.disableReorder + }); + } + + onReorder (event, previousIndex, nextIndex) { + const list = reorderImmutable(this.state.list, previousIndex, nextIndex); + + this.setState({ + list: list + }); + } + + onClickItem (name) { + this.setState({ + clickedItem: name + }); + } + + render () { + return ( +
    +

    + Lock vertical +

    +

    + This example has a hold time of 250 milliseconds +

    +

    + {'Reorder disabled: '} + {' '} + Last item clicked: {this.state.clickedItem} +

    + + + { + this.state.list.map(({name, color}) => ( +
  • + {name} +
  • + )).toArray() + } +
    +
    + ); + } +} diff --git a/examples/src/js/multi-list.js b/examples/src/js/multi-list.js new file mode 100644 index 00000000..2c6bc40f --- /dev/null +++ b/examples/src/js/multi-list.js @@ -0,0 +1,123 @@ +import Immutable from 'immutable'; +import React, { Component } from 'react'; +import Reorder, { reorderImmutable, reorderFromToImmutable } from '../../../src/index'; + +import { classNames } from './styles'; + +export class MultiList extends Component { + constructor () { + super(); + + this.state = { + listA: Immutable.List(Immutable.Range(0, 5).map(function (value) { + return { + name: ['List A - Item', value].join(' '), + color: ['rgb(', (value + 1) * 25, ',', 250 - ((value + 1) * 25), ',0)'].join('') + }; + })), + listB: Immutable.List(Immutable.Range(0, 5).map(function (value) { + return { + name: ['List B - Item', value].join(' '), + color: ['rgb(', (value + 1) * 25, ',', 250 - ((value + 1) * 25), ',0)'].join('') + }; + })) + }; + } + + onReorderGroup (event, previousIndex, nextIndex, fromId, toId) { + if (fromId === toId) { + const list = reorderImmutable(this.state[fromId], previousIndex, nextIndex); + + this.setState({ + [fromId]: list + }); + } else { + const lists = reorderFromToImmutable({ + from: this.state[fromId], + to: this.state[toId] + }, previousIndex, nextIndex); + + this.setState({ + [fromId]: lists.from, + [toId]: lists.to + }); + } + } + + render () { + return ( +
    +

    + Drag between lists +

    +

    + This example has a group of lists that you can drag items between +

    + + + { + this.state.listA.map(({name, color}) => ( +
  • +
    + + {name} + + +
    +
  • + )).toArray() + } +
    + + + { + this.state.listB.map(({name, color}) => ( +
  • +
    + + {name} + + +
    +
  • + )).toArray() + } +
    +
    + ); + } +} + + diff --git a/examples/src/js/styles.js b/examples/src/js/styles.js new file mode 100644 index 00000000..165438bf --- /dev/null +++ b/examples/src/js/styles.js @@ -0,0 +1,199 @@ +import ReactStyleSheets from 'react-style-sheets'; + +ReactStyleSheets.setOptions({ + vendorPrefixes: { + userSelect: ['webkit', 'khtml', 'moz', 'ms', 'o'], + transform: ['webkit', 'moz', 'ms', 'o'], + transition: ['webkit', 'moz', 'ms', 'o'], + transformOrigin: ['webkit', 'moz', 'ms', 'o'] + } +}); + +const htmlBody = { + padding: 0, + margin: 0, + fontFamily: ['arial', 'helvetica', 'sans-serif'], + fontSize: 14, + color: '#333', + WebkitTouchCallout: 'none', + userSelect: 'none', + WebkitTapHighlightColor: 'transparent' +}; + +ReactStyleSheets.createGlobalTagStyles({ + '*': { + boxSizing: 'border-box' + }, + html: htmlBody, + body: htmlBody, + p: { + margin: [10, 'auto'] + } +}); + +export const classNames = ReactStyleSheets.createUniqueClassStyles({ + app: { + position: 'relative', + width: '100%', + maxWidth: 768, + overflow: 'hidden', + margin: 'auto', + padding: 8 + }, + clearfix: { + before: { + content: '\'\'', + display: 'table', + clear: 'both' + }, + after: { + content: '\'\'', + display: 'table', + clear: 'both' + } + }, + myList: { + float: 'left', + width: '100%', + height: 'auto', + border: [1, 'solid', 'grey'], + padding: 8, + paddingBottom: 0, + listStyle: 'none' + }, + myList1: { + height: 200, + overflow: 'auto', + overflowAnchor: 'none', + paddingBottom: 0 + }, + myList2: { + overflowX: 'auto', + overflowAnchor: 'none', + overflowY: 'hidden', + height: 62, + whiteSpace: 'nowrap' + }, + mylist3: {}, + multiList: { + width: '50%', + minHeight: 100, + maxHeight: 400, + overflowX: 'hidden', + overflowY: 'auto', + overflowAnchor: 'none' + }, + listItem: { + float: 'left', + width: '100%', + height: 46, + padding: 12, + border: [2, 'solid', 'lightblue'], + marginBottom: 8, + transformOrigin: '50% 50%' + }, + listItem2: { + float: 'none', + width: 80, + marginBottom: 0, + whiteSpace: 'nowrap', + overflow: 'hidden', + display: 'inline-block' + }, + listItem3: { + float: 'left', + width: '50%' + }, + multiListItem: {}, + placeholder: { + backgroundColor: '#CCC', + border: [1, 'solid', '#CCC'] + }, + customPlaceholder: { + opacity: 0.2 + }, + dragged: { + backgroundColor: '#EEE', + transform: 'scale(0.98, 0.98)', + opacity: 0.8 + }, + selected: { + border: [2, 'solid', 'red'] + }, + contentHolder: { + display: 'table', + width: '100%' + }, + itemName: { + display: 'table-cell' + }, + input: { + display: 'table-cell', + width: '100%' + }, + kanban: { + overflowY: 'auto', + height: 300, + whiteSpace: 'nowrap', + overflowAnchor: 'none' + }, + kanbanListOuter: { + width: 200, + border: [1, 'solid', '#ddd'], + backgroundColor: '#fafafa', + borderRadius: 4, + display: 'inline-block', + verticalAlign: 'top', + whiteSpace: 'normal', + marginRight: 8 + }, + kanbanListInner: { + border: 'none', + width: '100%', + minHeight: 100, + maxHeight: 200, + overflowX: 'hidden', + overflowY: 'auto', + overflowAnchor: 'none', + margin: 0 + }, + kanbanItem: { + borderRadius: 4, + border: [1, 'solid', '#ccc'], + backgroundColor: '#eee' + }, + kanbanHeader: { + float: 'left', + width: '100%', + padding: 8, + borderBottom: [1, 'solid', '#ddd'], + backgroundColor: '#eee', + fontWeight: 'bold' + }, + kanbanFooter: { + float: 'left', + width: '100%', + padding: 8, + borderTop: [1, 'solid', '#ddd'], + backgroundColor: '#eee', + cursor: 'pointer', + textAlign: 'center' + }, + delete: { + float: 'right', + color: '#888', + cursor: 'pointer' + }, + kanbanAddList: { + width: 200, + padding: 8, + border: [1, 'solid', '#ddd'], + backgroundColor: '#fafafa', + borderRadius: 4, + display: 'inline-block', + verticalAlign: 'top', + whiteSpace: 'normal', + cursor: 'pointer', + textAlign: 'center' + } +}); diff --git a/examples/src/less/index.less b/examples/src/less/index.less deleted file mode 100644 index dd886d39..00000000 --- a/examples/src/less/index.less +++ /dev/null @@ -1,131 +0,0 @@ -@BaseFontColor: #333; - -@BaseFontSize: 14px; - -* { - box-sizing: border-box; -} - -html, body { - padding: 0; - margin: 0; - font-family: arial, helvetica, sans-serif; - font-size: @BaseFontSize; - color: @BaseFontColor; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-tap-highlight-color: transparent; -} - -p, small { - float: left; - width: 100%; - padding: 12px; - margin: 0; -} - -small { - padding-top: 0; - font-size: @BaseFontSize - 2px; -} - -.app { - position: relative; - width: 100%; - max-width: 768px; - overflow: hidden; - margin: auto; - padding: 8px; -} - -.loading { - position: relative; - width: 100%; - margin: 100px auto auto auto; - text-align: center; - padding: 8px; - - p { - float: left; - width: 100%; - font-size: 18px; - margin: 0; - padding: 0; - - small { - font-size: 14px; - } - } -} - -.my-list, -.my-list-2, -.my-list-3 { - float: left; - width: 100%; - height: auto; - border: 1px solid grey; - padding: 8px; -} - -.my-list { - height: 200px; - overflow: auto; - padding-bottom: 0; -} - -.list-item { - float: left; - width: 100%; - height: auto; - padding: 12px; - border: 2px solid lightblue; - margin-bottom: 8px; - transform-origin: 50% 50%; - - &.dragged { - background-color: #EEE; - transform: scale(0.98, 0.98); - opacity: 0.7; - } - - &.selected { - border: 2px solid red; - } - - &.placeholder { - background-color: #CCC; - border: 2px solid #CCC; - - .inner { - visibility: hidden; - } - } -} - -.my-list-2 { - overflow-x: auto; - overflow-y: hidden; - height: 62px; - white-space: nowrap; - - .list-item { - float: none; - width: 80px; - margin-bottom: 0; - white-space: nowrap; - overflow: hidden; - display: inline-block; - } -} - -.my-list-3 { - .list-item { - float: left; - width: percentage(1/2); - } -} diff --git a/index.js b/index.js deleted file mode 100644 index 4cfd3aa0..00000000 --- a/index.js +++ /dev/null @@ -1,493 +0,0 @@ -(function () { - 'use strict'; - - var getReorderComponent = function (React, ReactDOM, createReactClass) { - - return createReactClass({ - displayName: 'Reorder', - nonCollisionElement: new RegExp('(^|\\s)(placeholder|dragged)($|\\s)', ''), - constants: { - HOLD_THRESHOLD: 8, - SCROLL_RATE: 1000 / 60, - SCROLL_DISTANCE: 1, - SCROLL_AREA: 50, - SCROLL_MULTIPLIER: 5 - }, - preventDefault: function (event) { - event.preventDefault(); - }, - persistEvent: function (event) { - if (typeof event.persist === 'function') { - event.persist(); - } - }, - handleTouchEvents: function (event) { - if (event.touches && event.touches.length) { - this.persistEvent(event); - - event.clientX = event.touches[0].clientX; - event.clientY = event.touches[0].clientY; - } - }, - startDrag: function (dragOffset, draggedStyle) { - if (!this.props.disableReorder) { - this.setState({ - dragOffset: dragOffset, - draggedStyle: draggedStyle, - originalPosition: draggedStyle, - held: true, - moved: false - }); - } - }, - itemDown: function (item, index, event) { - this.handleTouchEvents(event); - - var self = this; - var target = event.currentTarget; - var rect = target.getBoundingClientRect(); - - this.setState({ - held: false, - moved: false - }); - - var dragOffset = { - top: event.clientY - rect.top, - left: event.clientX - rect.left - }; - - this.setState({ - dragged: { - target: target, - item: item, - index: index - } - }); - - var draggedStyle = { - position: 'fixed', - top: rect.top, - left: rect.left, - width: rect.width, - height: rect.height - }; - - // Timeout if holdTime is defined - var holdTime = Math.abs(parseInt(this.props.holdTime)); - - if (holdTime) { - this.holdTimeout = setTimeout(function () { - self.startDrag(dragOffset, draggedStyle); - }, holdTime); - } else { - self.startDrag(dragOffset, draggedStyle); - } - }, - listDown: function (event) { - this.handleTouchEvents(event); - - var self = this; - - var downPos = { - clientY: event.clientY, - clientX: event.clientX, - scrollTop: ReactDOM.findDOMNode(self).scrollTop, - scrollLeft: ReactDOM.findDOMNode(self).scrollLeft - }; - - this.setState({ - downPos: downPos, - pointer: { - clientY: downPos.clientY, - clientX: downPos.clientX - }, - velocity: { - y: 0, - x: 0 - }, - movedALittle: false - }); - - // Mouse events - window.addEventListener('mouseup', this.onMouseUp); // Mouse up - window.addEventListener('mousemove', this.onMouseMove); // Mouse move - - // Touch events - window.addEventListener('touchend', this.onMouseUp); // Touch up - window.addEventListener('touchmove', this.onMouseMove); // Touch move - - window.addEventListener('contextmenu', this.preventDefault); - }, - onMouseUp: function (event) { - if (event.type.indexOf('touch') >= 0 && !this.state.movedALittle) { - event.preventDefault(); - } - - // Item clicked - if (typeof this.props.itemClicked === 'function' && !this.state.held && !this.state.moved && this.state.dragged) { - this.props.itemClicked(event, this.state.dragged.item, this.state.dragged.index); - } - - // Reorder callback - if (this.state.held && this.state.dragged && typeof this.props.callback === 'function') { - var listElements = this.nodesToArray(ReactDOM.findDOMNode(this).childNodes); - var newIndex = listElements.indexOf(this.state.dragged.target); - - this.props.callback(event, this.state.dragged.item, this.state.dragged.index, newIndex, this.state.list); - } - - this.setState({ - dragged: undefined, - draggedStyle: undefined, - dragOffset: undefined, - originalPosition: undefined, - downPos: undefined, - held: false, - moved: false - }); - - clearTimeout(this.holdTimeout); - clearInterval(this.scrollIntervalY); - this.scrollIntervalY = undefined; - clearInterval(this.scrollIntervalX); - this.scrollIntervalX = undefined; - - // Mouse events - window.removeEventListener('mouseup', this.onMouseUp); // Mouse up - window.removeEventListener('mousemove', this.onMouseMove); // Mouse move - // Touch events - window.removeEventListener('touchend', this.onMouseUp); // Touch up - window.removeEventListener('touchmove', this.onMouseMove); // Touch move - - window.removeEventListener('contextmenu', this.preventDefault); - }, - getScrollArea: function (value) { - return Math.max(Math.min(value / 4, this.constants.SCROLL_AREA), this.constants.SCROLL_AREA / 5); - }, - dragScrollY: function () { - var element = ReactDOM.findDOMNode(this); - var rect = element.getBoundingClientRect(); - var scrollArea = this.getScrollArea(rect.height); - - var distanceInArea; - if (this.state.pointer.clientY < rect.top + scrollArea) { - distanceInArea = Math.min((rect.top + scrollArea) - this.state.pointer.clientY, scrollArea * 2); - element.scrollTop -= distanceInArea / this.constants.SCROLL_MULTIPLIER; - } else if (this.state.pointer.clientY > rect.bottom - scrollArea) { - distanceInArea = Math.min(this.state.pointer.clientY - (rect.bottom - scrollArea), scrollArea * 2); - element.scrollTop += distanceInArea / this.constants.SCROLL_MULTIPLIER; - } - }, - dragScrollX: function () { - var element = ReactDOM.findDOMNode(this); - var rect = element.getBoundingClientRect(); - var scrollArea = this.getScrollArea(rect.width); - - var distanceInArea; - if (this.state.pointer.clientX < rect.left + scrollArea) { - distanceInArea = Math.min((rect.left + scrollArea) - this.state.pointer.clientX, scrollArea * 2); - element.scrollLeft -= distanceInArea / this.constants.SCROLL_MULTIPLIER; - } else if (this.state.pointer.clientX > rect.right - scrollArea) { - distanceInArea = Math.min(this.state.pointer.clientX - (rect.right - scrollArea), scrollArea * 2); - element.scrollLeft += distanceInArea / this.constants.SCROLL_MULTIPLIER; - } - }, - handleDragScrollY: function (event) { - var rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); - - if (!this.scrollIntervalY && this.props.lock !== 'vertical') { - if (event.clientY < rect.top + this.constants.SCROLL_AREA) { - this.scrollIntervalY = setInterval(this.dragScrollY, this.constants.SCROLL_RATE); - } else if (event.clientY > rect.bottom - this.constants.SCROLL_AREA) { - this.scrollIntervalY = setInterval(this.dragScrollY, this.constants.SCROLL_RATE); - } - } else { - if (event.clientY <= rect.bottom - this.constants.SCROLL_AREA && event.clientY >= rect.top + this.constants.SCROLL_AREA) { - clearInterval(this.scrollIntervalY); - this.scrollIntervalY = undefined; - } - } - }, - handleDragScrollX: function (event) { - var rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); - - if (!this.scrollIntervalX && this.props.lock !== 'horizontal') { - if (event.clientX < rect.left + this.constants.SCROLL_AREA) { - this.scrollIntervalX = setInterval(this.dragScrollX, this.constants.SCROLL_RATE); - } else if (event.clientX > rect.right - this.constants.SCROLL_AREA) { - this.scrollIntervalX = setInterval(this.dragScrollX, this.constants.SCROLL_RATE); - } - } else { - if (event.clientX <= rect.right - this.constants.SCROLL_AREA && event.clientX >= rect.left + this.constants.SCROLL_AREA) { - clearInterval(this.scrollIntervalX); - this.scrollIntervalX = undefined; - } - } - }, - onMouseMove: function (event) { - this.handleTouchEvents(event); - - var pointer = { - clientY: event.clientY, - clientX: event.clientX - }; - - this.setState({ - pointer: pointer, - velocity: { - y: this.state.pointer.clientY - event.clientY, - x: this.state.pointer.clientX - event.clientX - }, - movedALittle: true - }); - - if (this.state.held && this.state.dragged) { - event.preventDefault(); - this.setDraggedPosition(event); - - var listElements = this.nodesToArray(ReactDOM.findDOMNode(this).childNodes); - var collision = this.findCollision(listElements, event); - - if (collision) { - var previousIndex = listElements.indexOf(this.state.dragged.target); - var newIndex = listElements.indexOf(collision); - - this.state.list.splice(newIndex, 0, this.state.list.splice(previousIndex, 1)[0]); - this.setState({list: this.state.list}); - } - - this.handleDragScrollY(event); - this.handleDragScrollX(event); - } else { - if (this.state.downPos) { - // Cancel hold if mouse has moved - if (this.xHasMoved(event) || this.yHasMoved(event)) { - clearTimeout(this.holdTimeout); - this.setState({moved: true}); - } - } - } - }, - xHasMoved: function (event) { - return Math.abs(this.state.downPos.clientX - event.clientX) > this.constants.HOLD_THRESHOLD; - }, - yHasMoved: function (event) { - return Math.abs(this.state.downPos.clientY - event.clientY) > this.constants.HOLD_THRESHOLD; - }, - elementHeightMinusBorders: function (element) { - var rect = element.getBoundingClientRect(); - var computedStyle; - - if (getComputedStyle) { - computedStyle = getComputedStyle(element); - } else { - computedStyle = element.currentStyle; - } - - return rect.height - - parseInt(computedStyle.getPropertyValue('border-top-width') || computedStyle.borderTopWidth) - - parseInt(computedStyle.getPropertyValue('border-bottom-width') || computedStyle.borderBottomWidth); - }, - elementWidthMinusBorders: function (element) { - var rect = element.getBoundingClientRect(); - var computedStyle; - - if (getComputedStyle) { - computedStyle = getComputedStyle(element); - } else { - computedStyle = element.currentStyle; - } - - return rect.width - - parseInt(computedStyle.getPropertyValue('border-left-width') || computedStyle.borderLeftWidth) - - parseInt(computedStyle.getPropertyValue('border-right-width') || computedStyle.borderRightWidth); - }, - setDraggedPosition: function (event) { - var draggedStyle = { - position: this.state.draggedStyle.position, - top: this.state.draggedStyle.top, - left: this.state.draggedStyle.left, - width: this.state.draggedStyle.width, - height: this.state.draggedStyle.height - }; - - if (this.props.lock === 'horizontal') { - draggedStyle.top = event.clientY - this.state.dragOffset.top; - draggedStyle.left = this.state.originalPosition.left; - } else if (this.props.lock === 'vertical') { - draggedStyle.top = this.state.originalPosition.top; - draggedStyle.left = event.clientX - this.state.dragOffset.left; - } else { - draggedStyle.top = event.clientY - this.state.dragOffset.top; - draggedStyle.left = event.clientX - this.state.dragOffset.left; - } - - this.setState({draggedStyle: draggedStyle}); - }, - - // Collision methods - - nodesToArray: function (nodes) { - return Array.prototype.slice.call(nodes, 0); - }, - xCollision: function (rect, event) { - return event.clientX >= rect.left && event.clientX <= rect.right; - }, - yCollision: function (rect, event) { - return event.clientY >= rect.top && event.clientY <= rect.bottom; - }, - findCollision: function (listElements, event) { - for (var i = 0; i < listElements.length; i += 1) { - if (!this.nonCollisionElement.exec(listElements[i].className)) { - var rect = listElements[i].getBoundingClientRect(); - - if (this.props.lock === 'horizontal') { - if (this.yCollision(rect, event)) { - return listElements[i]; - } - } else if (this.props.lock === 'vertical') { - if (this.xCollision(rect, event)) { - return listElements[i]; - } - } else { - if (this.yCollision(rect, event)) { - if (this.xCollision(rect, event)) { - return listElements[i]; - } - } - } - - } - } - - return undefined; - }, - - // ---- View methods - - getDraggedStyle: function (item) { - if (this.state.held && this.state.dragged && this.state.dragged.item === item) { - return this.state.draggedStyle; - } - return undefined; - }, - getDraggedClass: function (item) { - if (this.state.held && this.state.dragged && this.state.dragged.item === item) { - return 'dragged'; - } - return undefined; - }, - getPlaceholderClass: function (item) { - if (this.state.held && this.state.dragged && this.state.dragged.item === item) { - return 'placeholder'; - } - return undefined; - }, - getSelectedClass: function (item) { - if (typeof this.props.selected !== 'undefined') { - if (typeof this.props.selectedKey !== 'undefined') { - return this.props.selected[this.props.selectedKey] === item[this.props.selectedKey] ? 'selected' : undefined; - } - return this.props.selected === item ? 'selected' : undefined; - } - return undefined; - }, - - // ---- Default methods - - componentWillUnmount: function () { - clearTimeout(this.holdTimeout); - - clearInterval(this.scrollIntervalY); - this.scrollIntervalY = undefined; - clearInterval(this.scrollIntervalX); - this.scrollIntervalX = undefined; - }, - componentWillReceiveProps: function (props) { - // Updates list when props changed - this.setState({ - list: props.list - }); - }, - getInitialState: function () { - return { - list: this.props.list || [] - }; - }, - render: function () { - var self = this; - - var getPropsTemplate = function (item) { - if (self.props.template) { - return React.createElement(self.props.template, { - item: item, - sharedProps: self.props.sharedProps - }); - } - return item; - }; - - var list = this.state.list.map(function (item, index) { - var itemKey = item[self.props.itemKey] || item; - var itemClass = [self.props.itemClass, self.getPlaceholderClass(item), self.getSelectedClass(item)].join(' '); - return React.createElement('div', { - key: itemKey, - className: itemClass, - onMouseDown: self.itemDown.bind(self, item, index), - onTouchStart: self.itemDown.bind(self, item, index), - }, getPropsTemplate(item)); - }); - - var targetClone = function () { - if (self.state.held && self.state.dragged) { - var itemKey = self.state.dragged.item[self.props.itemKey] || self.state.dragged.item; - var itemClass = [self.props.itemClass, self.getDraggedClass(self.state.dragged.item), self.getSelectedClass(self.state.dragged.item)].join(' '); - return React.createElement('div', { - key: itemKey, - className: itemClass, - style: self.getDraggedStyle(self.state.dragged.item) - }, getPropsTemplate(self.state.dragged.item)); - } - return undefined; - }; - - return React.createElement('div', { - className: this.props.listClass, - onMouseDown: self.listDown, - onTouchStart: self.listDown - }, list, targetClone()); - } - }); - - }; - - // Export for commonjs / browserify - if (typeof exports === 'object' && typeof module !== 'undefined') { - var React = require('react'); - var ReactDOM = require('react-dom'); - var createReactClass = require('create-react-class'); - module.exports = getReorderComponent(React, ReactDOM, createReactClass); - // Export for amd / require - } else if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef - define(['react', 'react-dom', 'create-react-class'], function (ReactAMD, ReactDOMAMD, createReactClassAMD) { // eslint-disable-line no-undef - return getReorderComponent(ReactAMD, ReactDOMAMD, createReactClassAMD); - }); - // Export globally - } else { - var root; - - if (typeof window !== 'undefined') { - root = window; - } else if (typeof global !== 'undefined') { - root = global; - } else if (typeof self !== 'undefined') { - root = self; - } else { - root = this; - } - - root.Reorder = getReorderComponent(root.React, root.ReactDOM, root.createReactClass); - } - -})(); diff --git a/package.json b/package.json index 2d450fcb..3780ee0b 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,20 @@ { "name": "react-reorder", - "version": "2.2.1", + "version": "3.0.0-alpha.7", "description": "Drag & drop, touch enabled, reorderable / sortable list, React component", "author": "Jake 'Sid' Smith", "license": "MIT", - "main": "index.js", + "main": "src/index.js", "scripts": { - "start": "http-server examples/ -c-1 -o", - "build-less": "lessc examples/src/less/index.less examples/build/css/index.css && echo 'Less compiled'", - "build-js": "browserify examples/src/js/index.js -o examples/build/js/index.js", - "watch-js": "watchify examples/src/js/index.js -o examples/build/js/index.js -v", - "build-dirs": "mkdir -p examples/build/css/ && mkdir -p examples/build/js/", - "build": "npm run build-dirs && npm run build-less && npm run build-js", - "watch": "npm run watch-js" + "start": "./scripts/start", + "build-js": "browserify -t babelify examples/src/js/index.js -o examples/build/js/index.js", + "watch-js": "watchify -t babelify examples/src/js/index.js -o examples/build/js/index.js -v", + "build-dirs": "mkdir -p examples/build/js/", + "build": "npm run build-dirs && npm run build-js", + "watch": "npm run watch-js", + "lint": "eslint src/ tests/ examples/js/", + "mocha": "BABEL_ENV=mocha nyc mocha --opts .mocharc 'tests/**/*.test.js'", + "test": "npm run lint && npm run mocha" }, "bugs": "https://github.com/JakeSidSmith/react-reorder/issues", "repository": { @@ -31,18 +33,33 @@ "touch" ], "dependencies": { - "create-react-class": "^15.5.2" + "create-react-class": "*", + "prop-types": "*" }, "devDependencies": { - "browserify": "=12.0.1", - "http-server": "=0.8.5", - "less": "=2.7.2", - "react": ">=0.14.7", - "react-dom": ">=0.14.7", - "watchify": "=3.6.1" + "babel-plugin-istanbul": "4.0.0", + "babel-preset-es2015": "6.18.0", + "babel-preset-react": "6.11.1", + "babelify": "7.3.0", + "browserify": "12.0.1", + "chai": "3.5.0", + "concurrently": "3.5.0", + "eslint-config-jakesidsmith": "github:jakesidsmith/eslint-config-jakesidsmith#v2.1.0", + "http-server": "0.8.5", + "immutable": "3.8.1", + "jquery": "3.1.1", + "jsdom": "9.8.3", + "mocha": "3.2.0", + "nyc": "10.0.0", + "prop-types": "15.6.0", + "react": "16.2.0", + "react-dom": "16.2.0", + "react-style-sheets": "0.1.0", + "sinon": "1.17.6", + "sinon-chai": "2.8.0", + "watchify": "3.6.1" }, "peerDependencies": { - "react": "*", - "react-dom": "*" + "react": "*" } } diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..72fcf059 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +BRANCH=gh-pages # Branch to deploy to, probably gh-pages +DIST_DIR=examples # Location of build static files to deploy +DEPLOY_DIR=site_deploy # Temp directory to clone to during deployment +USERNAME=JakeSidSmith # Repo account username +REPO=react-reorder # repo name + + +if [[ -z "$GH_TOKEN" ]]; then + echo "> No Github token found!" + exit 1 +fi + +if [ ! -d "$DIST_DIR" ]; then + echo "> Can't find dist directory, aborting..." + exit +fi + +set -e + +echo "> Starting deployment to Github Pages" + +# Setup Details for deploy commit + +# Clone existing deployment branch into temp directory +mkdir $DEPLOY_DIR +git clone --depth=1 --quiet --branch=$BRANCH --single-branch https://${GH_TOKEN}@github.com/${USERNAME}/${REPO}.git $DEPLOY_DIR + +# Copy new static build into temp directory +rsync -rv $DIST_DIR/* circle.yml --exclude=src/* $DEPLOY_DIR + +# Commit changes +cd $DEPLOY_DIR +git config user.email "jake@dabapps.com" +git config user.name "Automated Example Deployer" +git add -f . +git commit -m "Deployment - build $CIRCLE_BUILD_NUM" + +# Deploy to branch +git push -fq origin $BRANCH + +echo "> Deployment completed." + +# Cleanup +rm -rf $DEPLOY_DIR diff --git a/scripts/start b/scripts/start new file mode 100755 index 00000000..1a7e0237 --- /dev/null +++ b/scripts/start @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p examples/build/js/ +mkdir -p examples/build/css/ + +concurrently --kill-others \ + --prefix='name' \ + --names='watch-js ,http-server' \ + --prefix-colors='red,green' \ + 'watchify -d -t babelify examples/src/js/index.js -o examples/build/js/index.js -v' \ + 'http-server -c-0 examples/ -o' diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..b9cc050b --- /dev/null +++ b/src/index.js @@ -0,0 +1,849 @@ +'use strict'; + +(function () { + + var CONSTANTS = { + HOLD_THRESHOLD: 8, + SCROLL_INTERVAL: 1000 / 60, + SCROLL_AREA_MAX: 50, + SCROLL_SPEED: 20 + }; + + var downPos = null; + var mouseOffset = null; + var mouseDown = null; + + function createOffsetStyles (event, props) { + var top = (!props.lock || props.lock === 'horizontal') ? mouseOffset.clientY - mouseDown.clientY : 0; + var left = (!props.lock || props.lock === 'vertical') ? mouseOffset.clientX - mouseDown.clientX : 0; + + return 'translate(' + left + 'px,' + top + 'px)'; + } + + function getScrollOffsetX (rect, node) { + var positionInScrollArea; + var scrollLeft = node.scrollLeft; + var scrollWidth = node.scrollWidth; + + var scrollAreaX = Math.min(rect.width / 3, CONSTANTS.SCROLL_AREA_MAX); + + if (scrollLeft > 0 && mouseOffset.clientX <= rect.left + scrollAreaX) { + positionInScrollArea = Math.min(Math.abs(rect.left + scrollAreaX - mouseOffset.clientX), scrollAreaX); + return -positionInScrollArea / scrollAreaX * CONSTANTS.SCROLL_SPEED; + } + + if (scrollLeft < scrollWidth - rect.width && mouseOffset.clientX >= rect.right - scrollAreaX) { + positionInScrollArea = Math.min(Math.abs(rect.right - scrollAreaX - mouseOffset.clientX), scrollAreaX); + return positionInScrollArea / scrollAreaX * CONSTANTS.SCROLL_SPEED; + } + + return 0; + } + + function getScrollOffsetY (rect, node) { + var positionInScrollArea; + var scrollTop = node.scrollTop; + var scrollHeight = node.scrollHeight; + + var scrollAreaY = Math.min(rect.height / 3, CONSTANTS.SCROLL_AREA_MAX); + + if (scrollTop > 0 && mouseOffset.clientY <= rect.top + scrollAreaY) { + positionInScrollArea = Math.min(Math.abs(rect.top + scrollAreaY - mouseOffset.clientY), scrollAreaY); + return -positionInScrollArea / scrollAreaY * CONSTANTS.SCROLL_SPEED; + } + + if (scrollTop < scrollHeight - rect.height && mouseOffset.clientY >= rect.bottom - scrollAreaY) { + positionInScrollArea = Math.min(Math.abs(rect.bottom - scrollAreaY - mouseOffset.clientY), scrollAreaY); + return positionInScrollArea / scrollAreaY * CONSTANTS.SCROLL_SPEED; + } + + return 0; + } + + function scrollParentsX (node) { + var parent = node.parentNode; + + while (parent && parent !== document) { + var rect = parent.getBoundingClientRect(); + + var scrollOffsetX = getScrollOffsetX(rect, parent); + + if (!scrollOffsetX) { + scrollParentsX(parent); + } else if (scrollOffsetX) { + parent.scrollLeft = parent.scrollLeft + scrollOffsetX; + return; + } + + parent = parent.parentNode; + } + } + + function scrollParentsY (node) { + var parent = node.parentNode; + + while (parent && parent !== document) { + var rect = parent.getBoundingClientRect(); + + var scrollOffsetY = getScrollOffsetY(rect, parent); + + if (!scrollOffsetY) { + scrollParentsX(parent); + } else if (scrollOffsetY) { + parent.scrollTop = parent.scrollTop + scrollOffsetY; + return; + } + + parent = parent.parentNode; + } + } + + function Store () { + var activeGroup = null; + var draggedId = null; + var placedId = null; + var draggedElement = null; + var scrollInterval = null; + var target = null; + + var draggedStyle = null; + var draggedIndex = -1; + var placedIndex = -1; + + var reorderComponents = {}; + var reorderGroups = {}; + + function autoScroll () { + if (target && target.props.autoScroll && target.rootNode) { + var rect = target.rootNode.getBoundingClientRect(); + + if (target.props.lock !== 'horizontal') { + var scrollOffsetX = getScrollOffsetX(rect, target.rootNode); + + if (target.props.autoScrollParents && !scrollOffsetX) { + scrollParentsX(target.rootNode); + } else if (scrollOffsetX) { + target.rootNode.scrollLeft = target.rootNode.scrollLeft + scrollOffsetX; + } + } + + if (target.props.lock !== 'vertical') { + var scrollOffsetY = getScrollOffsetY(rect, target.rootNode); + + if (target.props.autoScrollParents && !scrollOffsetY) { + scrollParentsY(target.rootNode); + } else if (scrollOffsetY) { + target.rootNode.scrollTop = target.rootNode.scrollTop + scrollOffsetY; + } + } + } + } + + function getState () { + return { + draggedId: draggedId, + placedId: placedId, + activeGroup: activeGroup, + draggedStyle: draggedStyle, + draggedIndex: draggedIndex, + placedIndex: placedIndex, + draggedElement: draggedElement + }; + } + + function trigger (clear) { + var state = getState(); + + if (clear) { + for (var i = 0; i < clear.length; i += 1) { + state[clear[i]] = null; + } + } + + reorderComponents[draggedId].setDragState(state); + } + + function triggerGroup (clear) { + var state = getState(); + + if (clear) { + for (var i = 0; i < clear.length; i += 1) { + state[clear[i]] = null; + } + } + + for (var reorderId in reorderGroups[activeGroup]) { + reorderGroups[activeGroup][reorderId].setDragState(state); + } + } + + function validateComponentIdAndGroup (reorderId, reorderGroup) { + if (typeof reorderId !== 'string') { + throw new Error('Expected reorderId to be a string. Instead got ' + (typeof reorderId)); + } + + if (typeof reorderGroup !== 'undefined' && typeof reorderGroup !== 'string') { + throw new Error('Expected reorderGroup to be a string. Instead got ' + (typeof reorderGroup)); + } + } + + function registerReorderComponent (component) { + var reorderId = component.props.reorderId; + var reorderGroup = component.props.reorderGroup; + + validateComponentIdAndGroup(reorderId, reorderGroup); + + if (typeof reorderGroup !== 'undefined') { + if ((reorderGroup in reorderGroups) && (reorderId in reorderGroups[reorderGroup])) { + throw new Error('Duplicate reorderId: ' + reorderId + ' in reorderGroup: ' + reorderGroup); + } + + reorderGroups[reorderGroup] = reorderGroups[reorderGroup] || {}; + reorderGroups[reorderGroup][reorderId] = component; + } else { + if (reorderId in reorderComponents) { + throw new Error('Duplicate reorderId: ' + reorderId); + } + + reorderComponents[reorderId] = component; + } + } + + function unregisterReorderComponent (component) { + var reorderId = component.props.reorderId; + var reorderGroup = component.props.reorderGroup; + + validateComponentIdAndGroup(reorderId, reorderGroup); + + if (typeof reorderGroup !== 'undefined') { + if (!(reorderGroup in reorderGroups)) { + throw new Error('Unknown reorderGroup: ' + reorderGroup); + } + + if ((reorderGroup in reorderGroups) && !(reorderId in reorderGroups[reorderGroup])) { + throw new Error('Unknown reorderId: ' + reorderId + ' in reorderGroup: ' + reorderGroup); + } + + delete reorderGroups[reorderGroup][reorderId]; + } else { + if (!(reorderId in reorderComponents)) { + throw new Error('Unknown reorderId: ' + reorderId); + } + + delete reorderComponents[reorderId]; + } + } + + function startDrag (reorderId, reorderGroup, index, element, component) { + target = component; + + clearInterval(scrollInterval); + scrollInterval = setInterval(autoScroll, CONSTANTS.SCROLL_INTERVAL); + + validateComponentIdAndGroup(reorderId, reorderGroup); + + draggedIndex = index; + placedIndex = index; + draggedStyle = null; + draggedElement = element; + + draggedId = reorderId; + placedId = reorderId; + activeGroup = null; + + if (typeof reorderGroup !== 'undefined') { + activeGroup = reorderGroup; + + triggerGroup(); + } else if (draggedId !== null && reorderId === draggedId) { + trigger(); + } + } + + function stopDrag (reorderId, reorderGroup) { + target = null; + + clearInterval(scrollInterval); + + validateComponentIdAndGroup(reorderId, reorderGroup); + + if (activeGroup !== null) { + if (reorderGroup === activeGroup) { + draggedIndex = -1; + placedIndex = -1; + draggedStyle = null; + draggedElement = null; + + // These need to be cleared after trigger to allow state updates to these components + triggerGroup(['activeGroup']); + + draggedId = null; + placedId = null; + activeGroup = null; + } + } else if (draggedId !== null && reorderId === draggedId) { + draggedIndex = -1; + placedIndex = -1; + draggedStyle = null; + draggedElement = null; + + // These need to be cleared after trigger to allow state updates to these components + trigger(['activeGroup']); + + draggedId = null; + placedId = null; + activeGroup = null; + } + } + + function setPlacedIndex (reorderId, reorderGroup, index, component) { + target = component; + + validateComponentIdAndGroup(reorderId, reorderGroup); + + if (typeof reorderGroup !== 'undefined') { + if (reorderGroup === activeGroup) { + placedId = reorderId; + placedIndex = index; + + triggerGroup(); + } + } else if (draggedId !== null && reorderId === draggedId) { + placedIndex = index; + + trigger(); + } + } + + function setDraggedStyle (reorderId, reorderGroup, style) { + validateComponentIdAndGroup(reorderId, reorderGroup); + + if (typeof reorderGroup !== 'undefined') { + if (reorderGroup === activeGroup) { + draggedStyle = style; + + triggerGroup(); + } + } else if (draggedId !== null && reorderId === draggedId) { + draggedStyle = style; + + trigger(); + } + } + + this.getState = getState; + this.registerReorderComponent = registerReorderComponent; + this.unregisterReorderComponent = unregisterReorderComponent; + this.startDrag = startDrag; + this.stopDrag = stopDrag; + this.setPlacedIndex = setPlacedIndex; + this.setDraggedStyle = setDraggedStyle; + } + + var store = new Store(); + + function reorder (list, previousIndex, nextIndex) { + var copy = [].concat(list); + var item = copy.splice(previousIndex, 1)[0]; + + copy.splice(nextIndex, 0, item); + + return copy; + } + + function reorderImmutable (list, previousIndex, nextIndex) { + var item = list.get(previousIndex); + return list.delete(previousIndex).splice(nextIndex, 0, item); + } + + function reorderFromTo (lists, previousIndex, nextIndex) { + var previousList = [].concat(lists.from); + var nextList = [].concat(lists.to); + + var item = previousList.splice(previousIndex, 1)[0]; + nextList.splice(nextIndex, 0, item); + + return { + from: previousList, + to: nextList + }; + } + + function reorderFromToImmutable (lists, previousIndex, nextIndex) { + var item = lists.from.get(previousIndex); + + return { + from: lists.from.delete(previousIndex), + to: lists.to.splice(nextIndex, 0, item) + }; + } + + function withReorderMethods (Reorder) { + Reorder.reorder = reorder; + Reorder.reorderImmutable = reorderImmutable; + Reorder.reorderFromTo = reorderFromTo; + Reorder.reorderFromToImmutable = reorderFromToImmutable; + return Reorder; + } + + function assign () { + var args = Array.prototype.slice.call(arguments); + + if (!args.length) { + return undefined; + } + + if (args.length === 1) { + return args[0]; + } + + var obj = args.shift(); + + while (args.length) { + var arg = args.shift(); + + for (var key in arg) { + obj[key] = arg[key]; + } + } + + return obj; + } + + function getReorderComponent (React, ReactDOM, createReactClass, PropTypes) { + + var Reorder = createReactClass({ + displayName: 'Reorder', + + getInitialState: function () { + return store.getState(); + }, + + isDragging: function () { + return this.state.draggedIndex >= 0; + }, + + isPlacing: function () { + return this.state.placedIndex >= 0; + }, + + isDraggingFrom: function () { + return this.props.reorderId === this.state.draggedId; + }, + + isPlacingTo: function () { + return this.props.reorderId === this.state.placedId; + }, + + isInvolvedInDragging: function () { + return this.props.reorderId === this.state.draggedId || this.props.reorderGroup === this.state.activeGroup; + }, + + preventContextMenu: function (event) { + if (downPos && this.props.disableContextMenus) { + event.preventDefault(); + } + }, + + preventNativeScrolling: function (event) { + event.preventDefault(); + }, + + persistEvent: function (event) { + if (typeof event.persist === 'function') { + event.persist(); + } + }, + + copyTouchKeys: function (event) { + if (event.touches && event.touches[0]) { + this.persistEvent(event); + + event.clientX = event.touches[0].clientX; + event.clientY = event.touches[0].clientY; + } + }, + + xCollision: function (event, rect) { + return event.clientX >= rect.left && event.clientX <= rect.right; + }, + + yCollision: function (event, rect) { + return event.clientY >= rect.top && event.clientY <= rect.bottom; + }, + + findCollisionIndex: function (event, listElements) { + for (var i = 0; i < listElements.length; i += 1) { + if (!listElements[i].getAttribute('data-placeholder') && !listElements[i].getAttribute('data-dragged')) { + + var rect = listElements[i].getBoundingClientRect(); + + switch (this.props.lock) { + case 'horizontal': + if (this.yCollision(event, rect)) { + return i; + } + break; + case 'vertical': + if (this.xCollision(event, rect)) { + return i; + } + break; + default: + if (this.yCollision(event, rect) && this.xCollision(event, rect)) { + return i; + } + break; + } + + } + + } + + return -1; + }, + + collidesWithElement: function (event, element) { + var rect = element.getBoundingClientRect(); + return this.yCollision(event, rect) && this.xCollision(event, rect); + }, + + getHoldTime: function (event) { + if (event.touches && typeof this.props.touchHoldTime !== 'undefined') { + return parseInt(this.props.touchHoldTime, 10) || 0; + } else if (typeof this.props.mouseHoldTime !== 'undefined') { + return parseInt(this.props.mouseHoldTime, 10) || 0; + } + + return parseInt(this.props.holdTime, 10) || 0; + }, + + startDrag: function (event, target, index) { + if (!this.moved) { + var rect = target.getBoundingClientRect(); + + var draggedStyle = { + position: 'fixed', + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height + }; + + store.startDrag(this.props.reorderId, this.props.reorderGroup, index, this.props.children[index], this); + store.setDraggedStyle(this.props.reorderId, this.props.reorderGroup, draggedStyle); + + mouseOffset = { + clientX: event.clientX, + clientY: event.clientY + }; + + mouseDown = { + clientX: event.clientX, + clientY: event.clientY + }; + } + }, + + // Begin dragging index, set initial drag style, set placeholder position, calculate mouse offset + onItemDown: function (callback, index, event) { + if (typeof callback === 'function') { + callback(event); + } + + if (event.button === 2 || this.props.disabled) { + return; + } + + this.copyTouchKeys(event); + + this.moved = false; + downPos = { + clientX: event.clientX, + clientY: event.clientY + }; + + var holdTime = this.getHoldTime(event); + var target = event.currentTarget; + + if (holdTime) { + this.persistEvent(event); + this.holdTimeout = setTimeout(this.startDrag.bind(this, event, target, index), holdTime); + } else { + this.startDrag(event, target, index); + } + }, + + // Stop dragging - reset style & draggedIndex, handle reorder + onWindowUp: function (event) { + clearTimeout(this.holdTimeout); + + if (this.isDragging() && this.isDraggingFrom()) { + var fromIndex = this.state.draggedIndex; + var toIndex = this.state.placedIndex; + + store.stopDrag(this.props.reorderId, this.props.reorderGroup); + + if ( + fromIndex >= 0 && + (fromIndex !== toIndex || this.state.draggedId !== this.state.placedId) && + typeof this.props.onReorder === 'function' + ) { + this.props.onReorder( + event, + fromIndex, + toIndex - (this.state.draggedId === this.state.placedId && fromIndex < toIndex ? 1 : 0), + this.state.draggedId, + this.state.placedId + ); + } + } + + downPos = null; + mouseOffset = null; + mouseDown = null; + }, + + // Update dragged position & placeholder index, invalidate drag if moved + onWindowMove: function (event) { + this.copyTouchKeys(event); + + if ( + downPos && ( + Math.abs(event.clientX - downPos.clientX) >= CONSTANTS.HOLD_THRESHOLD || + Math.abs(event.clientY - downPos.clientY) >= CONSTANTS.HOLD_THRESHOLD + ) + ) { + this.moved = true; + } + + if (this.isDragging() && this.isInvolvedInDragging()) { + this.preventNativeScrolling(event); + + var element = this.rootNode; + + if (this.collidesWithElement(event, element)) { + + var children = element.childNodes; + var collisionIndex = this.findCollisionIndex(event, children); + + if ( + collisionIndex <= this.props.children.length && + collisionIndex >= 0 + ) { + store.setPlacedIndex(this.props.reorderId, this.props.reorderGroup, collisionIndex, this); + } else if ( + typeof this.props.reorderGroup !== 'undefined' && // Is part of a group + ( + (!this.props.children || !this.props.children.length) || // If all items removed + (this.isDraggingFrom() && this.props.children.length === 1) // If dragging back to a now empty list + ) + ) { + store.setPlacedIndex(this.props.reorderId, this.props.reorderGroup, 0, this); + } + + } + + this.state.draggedStyle.transform = createOffsetStyles(event, this.props); + store.setDraggedStyle(this.props.reorderId, this.props.reorderGroup, this.state.draggedStyle); + + mouseOffset = { + clientX: event.clientX, + clientY: event.clientY + }; + } + }, + + setDragState: function (state) { + var isPartOfGroup = this.props.reorderGroup; + var isGroupDragged = state.activeGroup; + var storedActiveGroup = this.state.activeGroup; + + var wasGroupDragged = !isGroupDragged && storedActiveGroup; + + var isActiveGroup = isPartOfGroup && isGroupDragged && + state.activeGroup === this.props.reorderGroup; + + var isDragged = this.props.reorderId === state.draggedId; + var isPlaced = this.props.reorderId === state.placedId; + var wasPlaced = this.props.reorderId === this.state.placedId; + + // This check is like a shouldComponentUpdate but specific to our store state + // Allowing prop changes to update the component + if ( + (!isGroupDragged && !isPartOfGroup && (isDragged || isPlaced)) || + (isPartOfGroup && (!storedActiveGroup || wasGroupDragged)) || + wasGroupDragged || + (isActiveGroup && (isDragged || isPlaced || wasPlaced)) + ) { + this.setState(state); + } + }, + + // Add listeners and store root node + componentDidMount: function () { + store.registerReorderComponent(this); + window.addEventListener('mouseup', this.onWindowUp, {passive: false}); + window.addEventListener('touchend', this.onWindowUp, {passive: false}); + window.addEventListener('mousemove', this.onWindowMove, {passive: false}); + window.addEventListener('touchmove', this.onWindowMove, {passive: false}); + window.addEventListener('contextmenu', this.preventContextMenu, {passive: false}); + this.storeRootNode(); + }, + + // Remove listeners + componentWillUnmount: function () { + store.unregisterReorderComponent(this); + clearTimeout(this.holdTimeout); + + window.removeEventListener('mouseup', this.onWindowUp); + window.removeEventListener('touchend', this.onWindowUp); + window.removeEventListener('mousemove', this.onWindowMove); + window.removeEventListener('touchmove', this.onWindowMove); + window.removeEventListener('contextmenu', this.preventContextMenu); + }, + + storeRootNode: function () { + var element = ReactDOM.findDOMNode(this); + this.rootNode = element; + + if (typeof this.props.getRef === 'function') { + this.props.getRef(element); + } + }, + + render: function () { + var children = this.props.children && this.props.children.map(function (child, index) { + var isDragged = this.isDragging() && this.isDraggingFrom() && index === this.state.draggedIndex; + + var draggedStyle = isDragged ? assign({}, child.props.style, this.state.draggedStyle) : child.props.style; + + var draggedClass = [ + child.props.className || '', + (isDragged ? this.props.draggedClassName : '') + ].join(' '); + + return React.cloneElement( + isDragged ? this.state.draggedElement : child, + { + style: draggedStyle, + className: draggedClass, + onMouseDown: this.onItemDown.bind(this, child.props.onMouseDown, index), + onTouchStart: this.onItemDown.bind(this, child.props.onTouchStart, index), + 'data-dragged': isDragged ? true : null + } + ); + }.bind(this)); + + var placeholderElement = this.props.placeholder || this.state.draggedElement; + + if (this.isPlacing() && this.isPlacingTo() && placeholderElement) { + var placeholder = React.cloneElement( + placeholderElement, + { + key: 'react-reorder-placeholder', + className: [placeholderElement.props.className || '', this.props.placeholderClassName].join(' '), + 'data-placeholder': true + } + ); + + children.splice(this.state.placedIndex, 0, placeholder); + } + + return React.createElement( + this.props.component, + { + className: this.props.className, + id: this.props.id, + style: this.props.style, + onClick: this.props.onClick + }, + children + ); + } + + }); + + Reorder.propTypes = { + component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + getRef: PropTypes.func, + reorderId: PropTypes.string, + reorderGroup: PropTypes.string, + placeholderClassName: PropTypes.string, + draggedClassName: PropTypes.string, + lock: PropTypes.string, + holdTime: PropTypes.number, + touchHoldTime: PropTypes.number, + mouseHoldTime: PropTypes.number, + onReorder: PropTypes.func, + placeholder: PropTypes.element, + autoScroll: PropTypes.bool, + autoScrollParents: PropTypes.bool, + disabled: PropTypes.bool, + disableContextMenus: PropTypes.bool + }; + + Reorder.defaultProps = { + component: 'div', + // getRef: function, + // reorderId: id, + // reorderGroup: group, + placeholderClassName: 'placeholder', + draggedClassName: 'dragged', + // lock: direction, + holdTime: 0, + // touchHoldTime: 0, + // mouseHoldTime: 0, + // onReorder: function, + // placeholder: react element + autoScroll: true, + autoScrollParents: true, + disabled: false, + disableContextMenus: true + }; + + return Reorder; + + } + + /* istanbul ignore next */ + + // Export for commonjs / browserify + if (typeof exports === 'object' && typeof module !== 'undefined') { + var React = require('react'); // eslint-disable-line no-undef + var ReactDOM = require('react-dom'); // eslint-disable-line no-undef + var createReactClass = require('create-react-class'); // eslint-disable-line no-undef + var PropTypes = require('prop-types'); // eslint-disable-line no-undef + module.exports = withReorderMethods( // eslint-disable-line no-undef + getReorderComponent(React, ReactDOM, createReactClass, PropTypes) + ); + // Export for amd / require + } else if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef + define( // eslint-disable-line no-undef + ['react', 'react-dom', 'create-react-class', 'prop-types'], + function (ReactAMD, ReactDOMAMD, createReactClassAMD, PropTypesAMD) { + return withReorderMethods( + getReorderComponent(ReactAMD, ReactDOMAMD, createReactClassAMD, PropTypesAMD) + ); + } + ); + // Export globally + } else { + var root; + + if (typeof window !== 'undefined') { + root = window; + } else if (typeof global !== 'undefined') { + root = global; // eslint-disable-line no-undef + } else if (typeof self !== 'undefined') { + root = self; // eslint-disable-line no-undef + } else { + root = this; + } + + root.Reorder = withReorderMethods( + getReorderComponent(root.React, root.ReactDOM, root.createReactClass, root.PropTypes) + ); + } + +})(); diff --git a/tests/.babelrc b/tests/.babelrc new file mode 100644 index 00000000..bd20dc17 --- /dev/null +++ b/tests/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "es2015", + "react" + ] +} diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json new file mode 100644 index 00000000..5a58739b --- /dev/null +++ b/tests/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends":[ + "jakesidsmith/commonjs", + "jakesidsmith/es6", + "jakesidsmith/react", + "jakesidsmith/browser", + "jakesidsmith/mocha" + ] +} diff --git a/tests/basic.test.js b/tests/basic.test.js new file mode 100644 index 00000000..fd68374d --- /dev/null +++ b/tests/basic.test.js @@ -0,0 +1,267 @@ +import { expect } from 'chai'; +import { spy } from 'sinon'; +import mount from './helpers/mount'; + +import React, { Component } from 'react'; +import { List } from 'immutable'; +import Reorder, { reorder, reorderImmutable, reorderFromTo, reorderFromToImmutable } from '../src/index'; + +describe('basic', function () { + + const items = [ + { + name: 'Foo', + id: 'foo' + }, + { + name: 'Bar', + id: 'bar' + }, + { + name: 'Fizz', + id: 'fizz' + }, + { + name: 'Buzz', + id: 'buzz' + } + ]; + + describe('exports', function () { + + it('should provide a react component and some helpful functions', function () { + expect(typeof Reorder).to.equal('function'); + expect(typeof reorder).to.equal('function'); + expect(typeof reorderImmutable).to.equal('function'); + + const requiredReorder = require('../src/index'); + + expect(typeof requiredReorder).to.equal('function'); + expect(typeof requiredReorder.reorder).to.equal('function'); + expect(typeof requiredReorder.reorderImmutable).to.equal('function'); + }); + + }); + + describe('basic rendering', function () { + + it('should render itself & its children', function () { + const wrapper = mount( + + { + items.map((item) => ( + + {item.name} + + )) + } + + ); + + const children = wrapper.children(); + + expect(wrapper.tagName()).to.equal('div'); + expect(children.length).to.equal(4); + + children.forEach(function (child) { + expect(child.tagName()).to.equal('span'); + }); + + wrapper.unmount(); + }); + + it('should have a name & default props', function () { + const wrapper = mount( + + { + items.map((item) => ( + + {item.name} + + )) + } + + ); + + expect(wrapper.name()).to.equal('Reorder'); + + const props = wrapper.props(); + + expect(props.component).to.equal('div'); + expect(props.placeholderClassName).to.equal('placeholder'); + expect(props.draggedClassName).to.equal('dragged'); + expect(props.holdTime).to.equal(0); + + wrapper.unmount(); + }); + + it('should store a reference to its root node', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + expect(instance.rootNode).to.be.ok; + + wrapper.unmount(); + }); + + }); + + describe('props', function () { + + it('should allow defining the root component (string)', function () { + const wrapper = mount(); + + expect(wrapper.tagName()).to.equal('ul'); + + wrapper.unmount(); + }); + + it('should allow defining the root component (function)', function () { + function MyComponent () { + return

    ; + } + + const wrapper = mount(); + + expect(wrapper.name()).to.equal('Reorder'); + expect(wrapper.tagName()).to.equal('h1'); + + wrapper.unmount(); + }); + + it('should allow defining the root component (component)', function () { + class MyComponent extends Component { + render () { + return

    ; + } + } + + const wrapper = mount(); + + expect(wrapper.name()).to.equal('Reorder'); + expect(wrapper.tagName()).to.equal('h2'); + + wrapper.unmount(); + }); + + it('should call a ref function (if provided) with the root element', function () { + const refSpy = spy(); + + const wrapper = mount(); + + expect(refSpy).to.have.been.calledOnce; + + wrapper.unmount(); + }); + + }); + + describe('mounting & unmounting', function () { + + it('should add and remove event listeners on mount and unmount', function () { + const addEventListenerSpy = spy(window, 'addEventListener'); + const removeEventListenerSpy = spy(window, 'removeEventListener'); + + const events = [ + 'mouseup', + 'touchend', + 'mousemove', + 'touchmove', + 'contextmenu' + ]; + + const wrapper = mount(); + + events.forEach(function (event) { + expect(addEventListenerSpy).to.have.been.calledWith(event); + expect(removeEventListenerSpy).not.to.have.been.calledWith(event); + }); + + addEventListenerSpy.reset(); + removeEventListenerSpy.reset(); + + wrapper.unmount(); + + events.forEach(function (event) { + expect(addEventListenerSpy).not.to.have.been.calledWith(event); + expect(removeEventListenerSpy).to.have.been.calledWith(event); + }); + + addEventListenerSpy.restore(); + removeEventListenerSpy.restore(); + + wrapper.unmount(); + }); + + it('should clear timeouts & intervals on unmount', function () { + const clearTimeoutSpy = spy(global, 'clearTimeout'); + const clearIntervalSpy = spy(global, 'clearInterval'); + + const wrapper = mount(); + const instance = wrapper.instance(); + + instance.holdTimeout = { + foo: 'bar' + }; + instance.scrollInterval = { + bar: 'foo' + }; + + wrapper.unmount(); + + expect(clearTimeoutSpy).to.have.been.calledOnce; + expect(clearIntervalSpy).not.to.have.been.calledOnce; + + expect(clearTimeoutSpy).to.have.been.calledWith(instance.holdTimeout); + + clearTimeoutSpy.restore(); + clearIntervalSpy.restore(); + + wrapper.unmount(); + }); + + }); + + describe('helper functions', function () { + + it('should reorder an array', function () { + const arr = [1, 2, 3, 4, 5]; + + const reordered = reorder(arr, 1, 3); + + expect(reordered).not.to.equal(arr); + expect(reordered).to.eql([1, 3, 4, 2, 5]); + }); + + it('should reorder an immutable list', function () { + const list = List([1, 2, 3, 4, 5]); + + const reordered = reorderImmutable(list, 4, 0); + + expect(reordered).not.to.equal(list); + expect(reordered.toJS()).to.eql([5, 1, 2, 3, 4]); + }); + + it('should reorder an item from one array to another', function () { + const from = [1, 2, 3, 4, 5]; + const to = [1, 2, 3, 4, 5]; + + const reordered = reorderFromTo({from, to}, 0, 2); + + expect(reordered.from).to.eql([2, 3, 4, 5]); + expect(reordered.to).to.eql([1, 2, 1, 3, 4, 5]); + }); + + it('should reorder an item from one array to another', function () { + const from = List([1, 2, 3, 4, 5]); + const to = List([1, 2, 3, 4, 5]); + + const reordered = reorderFromToImmutable({from, to}, 4, 1); + + expect(reordered.from.toJS()).to.eql([1, 2, 3, 4]); + expect(reordered.to.toJS()).to.eql([1, 5, 2, 3, 4, 5]); + }); + + }); + +}); diff --git a/tests/children-stub.test.js b/tests/children-stub.test.js new file mode 100644 index 00000000..6e343b9e --- /dev/null +++ b/tests/children-stub.test.js @@ -0,0 +1,58 @@ +import { expect } from 'chai'; +import { verticalChildren, horizontalChildren } from './helpers/children-stub'; + +describe('children stub', function () { + + it('should create some stub children (vertical)', function () { + expect(verticalChildren.length).to.equal(5); + expect(verticalChildren[0].getAttribute('data-placeholder')).to.be.false; + expect(verticalChildren[1].getAttribute('data-placeholder')).to.be.true; + expect(verticalChildren[1].getAttribute('data-dragged')).to.be.false; + expect(verticalChildren[2].getAttribute('data-dragged')).to.be.true; + + expect(verticalChildren[0].getBoundingClientRect()).to.eql({ + top: 20, + left: 10, + bottom: 40, + right: 110, + width: 100, + height: 20 + }); + + expect(verticalChildren[3].getBoundingClientRect()).to.eql({ + top: 20 + 20 * 3, + left: 10, + bottom: 40 + 20 * 3, + right: 110, + width: 100, + height: 20 + }); + }); + + it('should create some stub children (horizontal)', function () { + expect(horizontalChildren.length).to.equal(5); + expect(horizontalChildren[0].getAttribute('data-placeholder')).to.be.false; + expect(horizontalChildren[1].getAttribute('data-placeholder')).to.be.true; + expect(horizontalChildren[1].getAttribute('data-dragged')).to.be.false; + expect(horizontalChildren[2].getAttribute('data-dragged')).to.be.true; + + expect(horizontalChildren[0].getBoundingClientRect()).to.eql({ + top: 20, + left: 10, + bottom: 40, + right: 110, + width: 100, + height: 20 + }); + + expect(horizontalChildren[3].getBoundingClientRect()).to.eql({ + top: 20, + left: 10 + 100 * 3, + bottom: 40, + right: 110 + 100 * 3, + width: 100, + height: 20 + }); + }); + +}); diff --git a/tests/helpers/children-stub.js b/tests/helpers/children-stub.js new file mode 100644 index 00000000..d523b9e7 --- /dev/null +++ b/tests/helpers/children-stub.js @@ -0,0 +1,63 @@ +// [item, placeholder, dragged, item, item] + +export const verticalChildren = []; +export const horizontalChildren = []; + +const itemSize = { + width: 100, + height: 20, + left: 10, + top: 20 +}; + +for (let i = 0; i < 5; i += 1) { + const index = i; + + verticalChildren.push({ + getAttribute: function (attr) { + if (attr === 'data-placeholder' && index === 1) { + return true; + } + + if (attr === 'data-dragged' && index === 2) { + return true; + } + + return false; + }, + getBoundingClientRect: function () { + return { + top: itemSize.top + (itemSize.height * index), + bottom: itemSize.top + (itemSize.height * index) + itemSize.height, + left: itemSize.left, + right: itemSize.left + itemSize.width, + width: itemSize.width, + height: itemSize.height + }; + } + }); + + horizontalChildren.push({ + getAttribute: function (attr) { + if (attr === 'data-placeholder' && index === 1) { + return true; + } + + if (attr === 'data-dragged' && index === 2) { + return true; + } + + return false; + }, + getBoundingClientRect: function () { + return { + top: itemSize.top, + bottom: itemSize.top + itemSize.height, + left: itemSize.left + (itemSize.width * index), + right: itemSize.left + (itemSize.width * index) + itemSize.width, + width: itemSize.width, + height: itemSize.height + }; + } + }); +} diff --git a/tests/helpers/mount.js b/tests/helpers/mount.js new file mode 100644 index 00000000..eb139413 --- /dev/null +++ b/tests/helpers/mount.js @@ -0,0 +1,68 @@ +import $ from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-dom/test-utils'; + +// Override trigger method with one from TestUtils +$.fn.trigger = function (type, data) { + TestUtils.Simulate[type](this[0], data); +}; + +$.fn.tagName = function () { + return this[0].tagName.toLowerCase(); +}; + +$.fn.forEach = function (fn) { + return this.each(function (index) { + if (typeof fn === 'function') { + fn($(this), index); + } + }); +}; + +function defineProperty (obj, prop, value) { + Object.defineProperty(obj, prop, {value, enumerable: false}); +} + +function internalMount (component, element) { + element = typeof element !== 'undefined' ? element : document.createElement('div'); + + let instance = ReactDOM.render(component, element); + let wrapper = $(ReactDOM.findDOMNode(instance)); + + defineProperty(wrapper, 'instance', function () { + return instance; + }); + + defineProperty(wrapper, 'name', function () { + return instance.constructor.displayName || instance.displayName; + }); + + defineProperty(wrapper, 'props', function () { + return instance.props; + }); + + defineProperty(wrapper, 'state', function () { + return instance.state; + }); + + defineProperty(wrapper, 'setProps', function (props) { + const clone = React.cloneElement(component, props); + + internalMount(clone, element); + }); + + defineProperty(wrapper, 'setState', function (state) { + instance.setState(state); + }); + + defineProperty(wrapper, 'unmount', function () { + ReactDOM.unmountComponentAtNode(element); + }); + + return wrapper; +} + +export default function mount (component) { + return internalMount(component); +} diff --git a/tests/helpers/test-setup.js b/tests/helpers/test-setup.js new file mode 100644 index 00000000..c6ad85af --- /dev/null +++ b/tests/helpers/test-setup.js @@ -0,0 +1,21 @@ +import chai from 'chai'; +import sinonChai from 'sinon-chai'; +import jsdom from 'jsdom'; + +// Jsdom document & window +const doc = jsdom.jsdom(''); +const win = doc.defaultView; + +// Add to global +global.document = doc; // eslint-disable-line no-undef +global.window = win; // eslint-disable-line no-undef + +// Add window keys to global window +Object.keys(window).forEach((key) => { + if (!(key in global)) { // eslint-disable-line no-undef + global[key] = window[key]; // eslint-disable-line no-undef + } +}); + +chai.expect(); +chai.use(sinonChai); diff --git a/tests/methods.test.js b/tests/methods.test.js new file mode 100644 index 00000000..3ea9b3ca --- /dev/null +++ b/tests/methods.test.js @@ -0,0 +1,496 @@ +import { expect } from 'chai'; +import { spy, stub } from 'sinon'; +import mount from './helpers/mount'; + +import React from 'react'; +import Reorder from '../src/index'; +import { verticalChildren, horizontalChildren } from './helpers/children-stub'; + +describe('methods', function () { + + it('should return true if the draggedIndex is greater than or equal to zero', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + expect(instance.isDragging()).to.be.false; + + wrapper.setState({draggedIndex: 0}); + + expect(instance.isDragging()).to.be.true; + + wrapper.setState({draggedIndex: 10}); + + expect(instance.isDragging()).to.be.true; + + wrapper.unmount(); + }); + + it('should preventDefault on events', function () { + const event = { + preventDefault: spy() + }; + + const wrapper = mount(); + const instance = wrapper.instance(); + + expect(event.preventDefault).not.to.have.been.called; + instance.preventNativeScrolling(event); + expect(event.preventDefault).to.have.been.calledOnce; + + event.preventDefault.reset(); + + expect(event.preventDefault).not.to.have.been.called; + instance.preventContextMenu(event); + expect(event.preventDefault).not.to.have.been.called; + + wrapper.unmount(); + }); + + it('should persist an event if available', function () { + const event = {}; + + const wrapper = mount(); + const instance = wrapper.instance(); + + instance.persistEvent(event); + + event.persist = spy(); + + expect(event.persist).not.to.have.been.called; + + instance.persistEvent(event); + + expect(event.persist).to.have.been.calledOnce; + + wrapper.unmount(); + }); + + it('should copy clientX and clientY from touches & persist event', function () { + const event = { + persist: spy() + }; + + const wrapper = mount(); + const instance = wrapper.instance(); + + expect(event.persist).not.to.have.been.called; + + instance.copyTouchKeys(event); + + expect(event.persist).not.to.have.been.called; + expect(event.clientX).not.to.be.ok; + expect(event.clientY).not.to.be.ok; + + event.touches = [ + { + clientX: 123, + clientY: 456 + } + ]; + + instance.copyTouchKeys(event); + + expect(event.persist).to.have.been.calledOnce; + expect(event.clientX).to.equal(123); + expect(event.clientY).to.equal(456); + + wrapper.unmount(); + }); + + it('should check a collision on the x-axis', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + const rect = { + left: 50, + right: 100 + }; + + const event = { + clientX: 20 + }; + + expect(instance.xCollision(event, rect)).to.be.false; + + event.clientX = 120; + + expect(instance.xCollision(event, rect)).to.be.false; + + event.clientX = 80; + + expect(instance.xCollision(event, rect)).to.be.true; + + event.clientX = 100; + + expect(instance.xCollision(event, rect)).to.be.true; + + wrapper.unmount(); + }); + + it('should check a collision on the y-axis', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + const rect = { + top: 20, + bottom: 80 + }; + + const event = { + clientY: 10 + }; + + expect(instance.yCollision(event, rect)).to.be.false; + + event.clientY = 100; + + expect(instance.yCollision(event, rect)).to.be.false; + + event.clientY = 50; + + expect(instance.yCollision(event, rect)).to.be.true; + + event.clientY = 80; + + expect(instance.yCollision(event, rect)).to.be.true; + + wrapper.unmount(); + }); + + it('should find the first collision of the pointer & children (no lock)', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + const event = { + clientX: 0, + clientY: 0 + }; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientX = 30; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 30; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(0); + + event.clientY = 50; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 70; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 90; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(3); + + event.clientX = 200; + event.clientY = 110; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientX = 50; + event.clientY = 110; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(4); + + event.clientX = 50; + event.clientY = 130; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + wrapper.unmount(); + }); + + it('should find the first collision of the pointer & children (horizontal)', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + const event = { + clientX: 0, + clientY: 0 + }; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 30; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(0); + + event.clientX = 30; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(0); + + event.clientY = 50; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 70; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + event.clientY = 90; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(3); + + event.clientX = 200; + event.clientY = 110; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(4); + + event.clientX = 50; + event.clientY = 110; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(4); + + event.clientX = 50; + event.clientY = 130; + + expect(instance.findCollisionIndex(event, verticalChildren)).to.equal(-1); + + wrapper.unmount(); + }); + + it('should find the first collision of the pointer & children (vertical)', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + const event = { + clientX: 0, + clientY: 0 + }; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(-1); + + event.clientX = 30; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(0); + + event.clientY = 30; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(0); + + event.clientX = 150; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(-1); + + event.clientX = 250; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(-1); + + event.clientX = 350; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(3); + + event.clientX = 450; + event.clientY = 110; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(4); + + event.clientX = 450; + event.clientY = 0; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(4); + + event.clientX = 550; + event.clientY = 130; + + expect(instance.findCollisionIndex(event, horizontalChildren)).to.equal(-1); + + wrapper.unmount(); + }); + + it('should return the relevant holdTime', function () { + const wrapper = mount(); + const instance = wrapper.instance(); + + expect(instance.getHoldTime({})).to.equal(0); + expect(instance.getHoldTime({touches: []})).to.equal(0); + + wrapper.setProps({holdTime: 50}); + + expect(instance.getHoldTime({})).to.equal(50); + expect(instance.getHoldTime({touches: []})).to.equal(50); + + wrapper.setProps({holdTime: 50, touchHoldTime: 500}); + + expect(instance.getHoldTime({})).to.equal(50); + expect(instance.getHoldTime({touches: []})).to.equal(500); + + wrapper.setProps({holdTime: 50, mouseHoldTime: 250, touchHoldTime: 500}); + + expect(instance.getHoldTime({})).to.equal(250); + expect(instance.getHoldTime({touches: []})).to.equal(500); + + wrapper.setProps({holdTime: NaN}); + + expect(instance.getHoldTime({})).to.equal(0); + expect(instance.getHoldTime({touches: []})).to.equal(0); + + wrapper.setProps({holdTime: NaN, mouseHoldTime: NaN, touchHoldTime: NaN}); + + expect(instance.getHoldTime({})).to.equal(0); + expect(instance.getHoldTime({touches: []})).to.equal(0); + + wrapper.unmount(); + }); + + xit('should return the scroll offset x for auto-scrolling (max scroll area)', function () { + const maxScrollArea = 50; + const scrollSpeed = 20; + const wrapper = mount({[
  • Item
  • ]}
    ); + const instance = wrapper.instance(); + + const rect = { + top: 200, + left: maxScrollArea, + height: 50, // Larger than maxScrollArea * 3 (as scroll area is divided by 3) + width: maxScrollArea * 4, // Larger than maxScrollArea * 3 (as scroll area is divided by 3) + bottom: 250, + right: maxScrollArea * 5 + }; + + const node = { + scrollTop: 1000, // More than zero, less than scrollHeight + scrollLeft: 1, // More than zero, less than scrollWidth + scrollHeight: 2000, // Larger than width & height + scrollWidth: maxScrollArea * 5 // Larger than width & height + }; + + const expectedScrollOffsets = [-scrollSpeed, -scrollSpeed, 0, 0, 0, scrollSpeed, scrollSpeed]; + + const listItem = wrapper.find('li'); + + expectedScrollOffsets.forEach(function (expectedScrollOffset, index) { + const event = {clientX: maxScrollArea * index}; + + listItem.trigger('mouseDown', event); + + expect(instance.getScrollOffsetX(rect, node)).to.equal(expectedScrollOffset); + }); + + listItem.trigger('mouseDown', {clientX: maxScrollArea * 1.5}); + expect(instance.getScrollOffsetX(rect, node)).to.equal(-scrollSpeed / 2); + listItem.trigger('mouseDown', {clientX: maxScrollArea * 4.5}); + expect(instance.getScrollOffsetX(rect, node)).to.equal(scrollSpeed / 2); + + instance.onWindowUp(); // Stop drag + wrapper.unmount(); + }); + + xit('should return the scroll offset y for auto-scrolling (max scroll area)', function () { + const maxScrollArea = 50; + const scrollSpeed = 20; + const wrapper = mount({[
  • Item
  • ]}
    ); + const instance = wrapper.instance(); + + const rect = { + top: maxScrollArea, + left: 200, + height: maxScrollArea * 4, // Larger than maxScrollArea * 3 (as scroll area is divided by 3) + width: 50, // Larger than maxScrollArea * 3 (as scroll area is divided by 3) + bottom: maxScrollArea * 5, + right: 250 + }; + + const node = { + scrollTop: 1, // More than zero, less than scrollHeight + scrollLeft: 1000, // More than zero, less than scrollWidth + scrollHeight: maxScrollArea * 5, // Larger than width & height + scrollWidth: 2000 // Larger than width & height + }; + + const expectedScrollOffsets = [-scrollSpeed, -scrollSpeed, 0, 0, 0, scrollSpeed, scrollSpeed]; + + const listItem = wrapper.find('li'); + + expectedScrollOffsets.forEach(function (expectedScrollOffset, index) { + const event = {clientY: maxScrollArea * index}; + + listItem.trigger('mouseDown', event); + + expect(instance.getScrollOffsetY(rect, node)).to.equal(expectedScrollOffset); + }); + + listItem.trigger('mouseDown', {clientY: maxScrollArea * 1.5}); + expect(instance.getScrollOffsetY(rect, node)).to.equal(-scrollSpeed / 2); + listItem.trigger('mouseDown', {clientY: maxScrollArea * 4.5}); + expect(instance.getScrollOffsetY(rect, node)).to.equal(scrollSpeed / 2); + + instance.onWindowUp(); // Stop drag + wrapper.unmount(); + }); + + xit('should scroll the root node if auto-scroll enabled & pointer is in the right location', function () { + const wrapper = mount({[
  • Item
  • ]}
    ); + const instance = wrapper.instance(); + const listItem = wrapper.find('li'); + + expect(instance.rootNode).to.be.ok; + + stub(instance.rootNode, 'getBoundingClientRect', function () { + return { + top: 0, + left: 0, + height: 100, + width: 100, + bottom: 100, + right: 100 + }; + }); + + instance.rootNode.scrollTop = 50; + instance.rootNode.scrollLeft = 50; + instance.rootNode.scrollHeight = 200; + instance.rootNode.scrollWidth = 200; + + listItem.trigger('mouseDown', { + clientY: 50, + clientX: 50 + }); + + instance.autoScroll(); + expect(instance.rootNode.scrollTop).to.equal(50); + expect(instance.rootNode.scrollLeft).to.equal(50); + + listItem.trigger('mouseDown', { + clientY: 100, + clientX: 50 + }); + + instance.autoScroll(); + expect(instance.rootNode.scrollTop).to.equal(70); + expect(instance.rootNode.scrollLeft).to.equal(50); + + wrapper.setProps({lock: 'vertical'}); + + listItem.trigger('mouseDown', { + clientY: 100, + clientX: 100 + }); + + instance.autoScroll(); + expect(instance.rootNode.scrollTop).to.equal(70); + expect(instance.rootNode.scrollLeft).to.equal(70); + + wrapper.setProps({lock: 'horizontal'}); + instance.autoScroll(); + expect(instance.rootNode.scrollTop).to.equal(90); + expect(instance.rootNode.scrollLeft).to.equal(70); + + wrapper.setProps({autoScroll: false}); + instance.autoScroll(); + expect(instance.rootNode.scrollTop).to.equal(90); + expect(instance.rootNode.scrollLeft).to.equal(70); + + instance.rootNode.getBoundingClientRect.restore(); + + instance.onWindowUp(); // Stop drag + wrapper.unmount(); + }); + +}); diff --git a/tests/mount.test.js b/tests/mount.test.js new file mode 100644 index 00000000..57c99b65 --- /dev/null +++ b/tests/mount.test.js @@ -0,0 +1,166 @@ +import { expect } from 'chai'; +import { spy } from 'sinon'; +import createReactClass from 'create-react-class'; +import mount from './helpers/mount'; +import React, { Component } from 'react'; + +describe('mount', function () { + + let wrapper; + + class MyComponent extends Component { + constructor () { + super(); + + this.displayName = 'MyComponent'; + this.state = { + foo: 'bar' + }; + } + + shouldComponentUpdate () { + return true; + } + + componentDidUpdate () {} + + componentWillMount () {} + + componentDidMount () {} + + componentWillUnmount () {} + + onClick (event) { + expect(event.foo).to.equal('bar'); + } + + render () { + return ( +
    + Foo + Bar +
    + ); + } + } + + const AnotherComponent = createReactClass({ + render: function () { + return
    ; + } + }); + + const renderSpy = spy(MyComponent.prototype, 'render'); + const shouldComponentUpdateSpy = spy(MyComponent.prototype, 'shouldComponentUpdate'); + const componentDidUpdateSpy = spy(MyComponent.prototype, 'componentDidUpdate'); + const componentWillMountSpy = spy(MyComponent.prototype, 'componentWillMount'); + const componentDidMountSpy = spy(MyComponent.prototype, 'componentDidMount'); + const componentWillUnmountSpy = spy(MyComponent.prototype, 'componentWillUnmount'); + const onClickSpy = spy(MyComponent.prototype, 'onClick'); + + it('should render a component & call its lifecycle methods', function () { + expect(renderSpy).not.to.have.been.called; + expect(componentWillMountSpy).not.to.have.been.called; + expect(componentDidMountSpy).not.to.have.been.called; + + wrapper = mount(); + + expect(renderSpy).to.have.been.calledOnce; + expect(componentWillMountSpy).to.have.been.calledOnce; + expect(componentDidMountSpy).to.have.been.calledOnce; + + renderSpy.reset(); + componentWillMountSpy.reset(); + componentDidMountSpy.reset(); + }); + + it('should return the components name & tag name', function () { + expect(wrapper.name()).to.equal('MyComponent'); + expect(wrapper.tagName()).to.equal('div'); + + const anotherComponent = mount(); + + expect(anotherComponent.name()).to.equal('AnotherComponent'); + }); + + it('should return the components children & loop over a jquery collection', function () { + let childCount = 0; + const childText = ['Foo', 'Bar']; + const children = wrapper.children(); + + children.forEach(); + + children.forEach(function (child, index) { + childCount += 1; + expect(child.text()).to.equal(childText[index]); + }); + + expect(childCount).to.equal(2); + }); + + it('should return and update the component\'s props', function () { + const originalProps = wrapper.props(); + + expect(originalProps).to.eql({}); + expect(shouldComponentUpdateSpy).not.to.have.been.called; + expect(componentDidUpdateSpy).not.to.have.been.called; + + expect(componentWillMountSpy).not.to.have.been.called; + expect(componentDidMountSpy).not.to.have.been.called; + + wrapper.setProps({foo: 'bar'}); + + expect(shouldComponentUpdateSpy).to.have.been.calledOnce; + expect(componentDidUpdateSpy).to.have.been.calledOnce; + + const updatedProps = wrapper.props(); + + expect(updatedProps).to.eql({foo: 'bar'}); + + expect(originalProps).not.to.eql(updatedProps); + + shouldComponentUpdateSpy.reset(); + componentDidUpdateSpy.reset(); + }); + + it('should return and update the component\'s state', function () { + const originalState = wrapper.state(); + + expect(originalState).to.eql({foo: 'bar'}); + expect(shouldComponentUpdateSpy).not.to.have.been.called; + expect(componentDidUpdateSpy).not.to.have.been.called; + + expect(componentWillMountSpy).not.to.have.been.called; + expect(componentDidMountSpy).not.to.have.been.called; + + wrapper.setState({foo: 'foo', bar: 'foo'}); + + expect(shouldComponentUpdateSpy).to.have.been.calledOnce; + expect(componentDidUpdateSpy).to.have.been.calledOnce; + + const updatedState = wrapper.state(); + + expect(updatedState).to.eql({foo: 'foo', bar: 'foo'}); + + expect(originalState).not.to.eql(updatedState); + + shouldComponentUpdateSpy.reset(); + componentDidUpdateSpy.reset(); + }); + + it('should trigger event listeners', function () { + const event = {foo: 'bar'}; + wrapper.trigger('click', event); + + expect(onClickSpy).to.have.been.calledOnce; + }); + + it('should unmount a component', function () { + expect(componentWillUnmountSpy).not.to.have.been.called; + + wrapper.unmount(); + + expect(componentWillUnmountSpy).to.have.been.calledOnce; + }); + +});