├── .gitignore
├── .babelrc
├── src
├── nodeInRoot.js
├── index.js
├── isNodeIn.js
├── getBoundsForNode.js
├── createSelectable.js
├── doObjectsCollide.js
└── selectable-group.js
├── example
├── example.js
├── Album.js
├── index.html
├── sample-data.js
├── App.js
└── npm-debug.log
├── webpack.config.example.js
├── react-selectable.d.ts
├── LICENSE
├── package.json
├── webpack.config.js
├── UPGRADING.md
├── example.js
├── README.md
└── dist
└── react-selectable.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | example/bundle.js
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-2"]
3 | }
--------------------------------------------------------------------------------
/src/nodeInRoot.js:
--------------------------------------------------------------------------------
1 | import isNodeIn from './isNodeIn';
2 |
3 | const isNodeInRoot = (node, root) => (
4 | isNodeIn(node, currentNode => currentNode === root)
5 | );
6 |
7 | export default isNodeInRoot;
8 |
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import data from './sample-data';
5 |
6 | ReactDOM.render(, document.getElementById('app'));
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import SelectableGroup from './selectable-group';
2 | import createSelectable from './createSelectable';
3 | import isNodeIn from './isNodeIn';
4 | import nodeInRoot from './nodeInRoot';
5 |
6 | export {
7 | SelectableGroup,
8 | createSelectable,
9 | isNodeIn,
10 | nodeInRoot,
11 | };
--------------------------------------------------------------------------------
/example/Album.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Album = ({
4 | selected,
5 | title,
6 | year
7 | }) => {
8 | const classes = selected ? 'item selected' : 'item';
9 | return (
10 |
11 |
{title}
12 | {year}
13 |
14 | )
15 | };
16 |
17 | export default Album;
18 |
--------------------------------------------------------------------------------
/src/isNodeIn.js:
--------------------------------------------------------------------------------
1 | const isNodeIn = (node, predicate) => {
2 | if (typeof predicate !== 'function') {
3 | throw new Error('isNodeIn second parameter must be a function');
4 | }
5 |
6 | let currentNode = node;
7 | while (currentNode) {
8 | if (predicate(currentNode)) {
9 | return true;
10 | }
11 | currentNode = currentNode.parentNode;
12 | }
13 |
14 | return false;
15 | };
16 |
17 | export default isNodeIn;
18 |
--------------------------------------------------------------------------------
/src/getBoundsForNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Given a node, get everything needed to calculate its boundaries
3 | * @param {HTMLElement} node
4 | * @return {Object}
5 | */
6 | export default node => {
7 | const rect = node.getBoundingClientRect();
8 |
9 | return {
10 | top: rect.top+document.body.scrollTop,
11 | left: rect.left+document.body.scrollLeft,
12 | offsetWidth: node.offsetWidth,
13 | offsetHeight: node.offsetHeight
14 | };
15 | };
--------------------------------------------------------------------------------
/webpack.config.example.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | entry: './example/example.js',
4 | output: {
5 | path: path.resolve(__dirname,'example'), // This is where images AND js will go
6 | publicPath: '', // This is used to generate URLs to e.g. images
7 | filename: 'bundle.js'
8 | },
9 |
10 | module: {
11 | rules: [
12 | {
13 | test: /\.js$/,
14 | loader: 'babel-loader'
15 | }
16 | ]
17 | },
18 | resolve: {
19 | modules: [path.resolve(__dirname),"node_modules","dist"]
20 | }
21 |
22 | };
23 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
40 |
41 |
42 |
43 |
44 |
46 |
47 |
--------------------------------------------------------------------------------
/src/createSelectable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {findDOMNode} from 'react-dom';
3 | import PropTypes from 'prop-types';
4 |
5 | const createSelectable = (WrappedComponent) => {
6 | class SelectableItem extends React.Component {
7 |
8 | componentDidMount () {
9 | this.context.selectable.register(this.props.selectableKey, findDOMNode(this));
10 | }
11 |
12 |
13 | componentWillUnmount () {
14 | this.context.selectable.unregister(this.props.selectableKey);
15 | }
16 |
17 |
18 | render () {
19 | return
20 |
21 | {this.props.children}
22 |
23 |
24 | }
25 | }
26 |
27 | SelectableItem.contextTypes = {
28 | selectable: PropTypes.object
29 | };
30 |
31 | SelectableItem.propTypes = {
32 | children: PropTypes.node,
33 | selectableKey: PropTypes.any.isRequired
34 | };
35 |
36 | return SelectableItem;
37 | };
38 |
39 | export default createSelectable;
40 |
--------------------------------------------------------------------------------
/react-selectable.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module 'react-selectable' {
3 | interface ReactSelectableGroupProps {
4 | onSelection?: (selectedItems: Array) => void;
5 | onNonItemClick?: () => void;
6 | onBeginSelection?: () => void;
7 | onEndSelection?: () => void;
8 | selectingClassName?: string;
9 | tolerance?: number;
10 | component?: string;
11 | fixedPosition?: boolean;
12 | preventDefault?: boolean;
13 | enabled?: boolean;
14 | [key: string]: any;
15 | }
16 |
17 | interface ReactSelectableComponentProps {
18 | key?: number|string;
19 | selected?: boolean;
20 | selectableKey?: number|string;
21 | [key: string]: any;
22 | }
23 |
24 | export class SelectableGroup extends React.Component {
25 |
26 | }
27 |
28 | class SelectableComponent extends React.Component {
29 |
30 | }
31 | export const createSelectable: (component: React.ReactNode) => typeof SelectableComponent;
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 unclecheese
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-selectable",
3 | "version": "2.1.1",
4 | "description": "",
5 | "main": "dist/react-selectable.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/unclecheese/react-selectable.git"
9 | },
10 | "scripts": {
11 | "build": "NODE_ENV=production node node_modules/webpack/bin/webpack",
12 | "dev": "NODE_ENV=development node node_modules/webpack/bin/webpack",
13 | "example": "node node_modules/webpack/bin/webpack --config webpack.config.example.js",
14 | "watch": "NODE_ENV=development node node_modules/webpack/bin/webpack --watch"
15 | },
16 | "homepage": "http://unclecheese.github.io/react-selectable",
17 | "keywords": [
18 | "selectable",
19 | "selection",
20 | "mouse",
21 | "drag",
22 | "react"
23 | ],
24 | "author": "Uncle Cheese",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/unclecheese/react-selectable/issues"
28 | },
29 | "devDependencies": {
30 | "babel-core": "^6.26.0",
31 | "babel-loader": "^7.1.2",
32 | "babel-preset-es2015": "^6.24.1",
33 | "babel-preset-react": "^6.24.1",
34 | "babel-preset-stage-2": "^6.24.1",
35 | "classnames": "^2.2.5",
36 | "lodash.throttle": "^4.1.1",
37 | "prop-types": "^15.6.0",
38 | "react": "^16.0.0",
39 | "react-dom": "^16.0.0",
40 | "webpack": "^3.8.1"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/example/sample-data.js:
--------------------------------------------------------------------------------
1 | const data = [
2 | { title: 'My Aim is True', year: '1977' },
3 | { title: "This Year's Model", year: '1978' },
4 | { title: 'Armed Forces', year: '1979' },
5 | { title: 'Get Happy', year: '1980' },
6 | { title: 'Trust', year: '1981' },
7 | { title: 'Almost Blue', year: '1981' },
8 | { title: 'Imperial Bedroom', year: '1982'},
9 | { title: 'Punch the Clock', year: '1983' },
10 | { title: 'Goodbye Cruel World', year: '1984'},
11 | { title: 'King of America', year: '1986' },
12 | { title: 'Blood and Chocolate', year: '1986' },
13 | { title: 'Spike', year: '1989' },
14 | { title: 'Mighty Like a Rose', year: '1991' },
15 | { title: 'The Juliette Letters', year: '1993' },
16 | { title: 'Brutal Youth', year: '1994' },
17 | { title: 'Kojak Variety', year: '1995' },
18 | { title: 'All This Useless Beauty', year: '1996' },
19 | { title: 'Painted from Memory', year: '1998' },
20 | { title: 'When I Was Cruel', year: '2002' },
21 | { title: 'North', year: '2003' },
22 | { title: 'The Delivery Man', year: '2004' },
23 | { title: 'The River in Reverse', year: '2006' },
24 | { title: 'Momofuku', year: '2008'},
25 | { title: 'Secret, Profane & Sugarcane', year: '2009' },
26 | { title: 'National Ransom', year: '2009' },
27 | { title: 'Wise Up Ghost', year: '2013' }
28 | ];
29 |
30 | export default data;
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: './src/index.js',
6 | output: {
7 | path: path.resolve(__dirname,'dist'), // This is where images AND js will go
8 | publicPath: '', // This is used to generate URLs to e.g. images
9 | filename: 'react-selectable.js',
10 | libraryTarget: 'umd'
11 | },
12 | externals: {
13 | 'react': {
14 | root: 'React',
15 | commonjs2: 'react',
16 | commonjs: 'react',
17 | amd: 'react',
18 | },
19 | 'react-dom': {
20 | root: 'ReactDOM',
21 | commonjs2: 'react-dom',
22 | commonjs: 'react-dom',
23 | amd: 'react-dom'
24 | }
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.js$/,
30 | loader: 'babel-loader'
31 | }
32 | ]
33 | },
34 | resolve: {
35 | modules: ["node_modules"],
36 | },
37 | plugins: process.env.NODE_ENV === 'production' ? [
38 | new webpack.optimize.ModuleConcatenationPlugin(),
39 | new webpack.optimize.UglifyJsPlugin({
40 | compress: {
41 | warnings: false,
42 | screw_ie8: true,
43 | conditionals: true,
44 | unused: true,
45 | comparisons: true,
46 | sequences: true,
47 | dead_code: true,
48 | evaluate: true,
49 | if_return: true,
50 | join_vars: true
51 | },
52 | output: {
53 | comments: false
54 | }
55 | }),
56 | ] : []
57 | };
58 |
--------------------------------------------------------------------------------
/src/doObjectsCollide.js:
--------------------------------------------------------------------------------
1 | import getBoundsForNode from './getBoundsForNode';
2 |
3 | /**
4 | * Given offsets, widths, and heights of two objects, determine if they collide (overlap).
5 | * @param {int} aTop The top position of the first object
6 | * @param {int} aLeft The left position of the first object
7 | * @param {int} bTop The top position of the second object
8 | * @param {int} bLeft The left position of the second object
9 | * @param {int} aWidth The width of the first object
10 | * @param {int} aHeight The height of the first object
11 | * @param {int} bWidth The width of the second object
12 | * @param {int} bHeight The height of the second object
13 | * @param {int} tolerance Amount of forgiveness an item will offer to the selectbox before registering a selection
14 | * @return {bool}
15 | */
16 | const coordsCollide = (aTop, aLeft, bTop, bLeft, aWidth, aHeight, bWidth, bHeight, tolerance) => {
17 | return !(
18 | // 'a' bottom doesn't touch 'b' top
19 | ( (aTop + aHeight - tolerance) < bTop ) ||
20 | // 'a' top doesn't touch 'b' bottom
21 | ( (aTop + tolerance) > (bTop + bHeight) ) ||
22 | // 'a' right doesn't touch 'b' left
23 | ( (aLeft + aWidth - tolerance) < bLeft ) ||
24 | // 'a' left doesn't touch 'b' right
25 | ( (aLeft + tolerance) > (bLeft + bWidth) )
26 | );
27 | };
28 |
29 | /**
30 | * Given two objects containing "top", "left", "offsetWidth" and "offsetHeight"
31 | * properties, determine if they collide.
32 | * @param {Object|HTMLElement} a
33 | * @param {Object|HTMLElement} b
34 | * @param {int} tolerance
35 | * @return {bool}
36 | */
37 | export default (a, b, tolerance = 0) => {
38 | const aObj = (a instanceof HTMLElement) ? getBoundsForNode(a) : a;
39 | const bObj = (b instanceof HTMLElement) ? getBoundsForNode(b) : b;
40 |
41 | return coordsCollide(
42 | aObj.top,
43 | aObj.left,
44 | bObj.top,
45 | bObj.left,
46 | aObj.offsetWidth,
47 | aObj.offsetHeight,
48 | bObj.offsetWidth,
49 | bObj.offsetHeight,
50 | tolerance
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | ## Significant API changes since 0.1
2 | This module now does a lot _less_ than it used to. The sole function of `react-selectable` is now to provide mouse events that draw a box around the UI, and fire an event telling you which of components are in the group. You are responsible for wiring up everything else, and that's a good thing.
3 |
4 | The primary change in this version is that `Selectable` no longer assumes that all of its children, and only its children, are selectable. This is a false premise and precludes you from creating lists that may include non-selectable items, or lists that are grouped in other child components. You now have to explicitly compose a component as selectable by running it through the `createSelectable` higher-order component.
5 |
6 | `const MySelectableItem = createSelectable(MyItem);`.
7 |
8 | Note that this is merely sugar for wiring up `this.context.selectable.register(key, domNode)` and `this.context.selectable.unregister(key)` on lifecycle methods.
9 |
10 | To disambiguate the two, the `` component should now be referred to as ``
11 |
12 | ### In addition the following features have been removed in 0.2:
13 |
14 | * **Cmd-clicking** to concatenate items (just wire up your own `keyup` listener to toggle a multiselection state in your store(s))
15 |
16 | * **The `distance` prop**: This assumed that you had the mouse events attached above the Selectable node (i.e. `document`), which gives this plugin too much scope. If you want the entire document to be selectable, just make the root component a ``. If you want distance padding, just place the `` at the level at which you want the selection box to be available. It will only select those items that are composed with `createSelectable`.
17 |
18 | * **The `globalMouse` prop**: For many of the same reasons as `distance`.
19 |
20 | * **Managing your `onClick` events**: You can do that on your own now. By default, a selectable item will not become selected on click. You should wire up a click handler that updates your store, similar to how you would wire up your cmd-clicking.
21 |
22 | * **You must now provide `selectableKey`**: Your selectable items have a required prop of `selectableKey`, which is the key that will be passed to the `onSelection` handler of your `SelectableGroup`.
23 |
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SelectableGroup, createSelectable } from 'react-selectable';
3 | import Album from './Album';
4 |
5 | const isNodeInRoot = (node, root) => {
6 | while (node) {
7 | if (node === root) {
8 | return true;
9 | }
10 | node = node.parentNode;
11 | }
12 |
13 | return false;
14 | };
15 |
16 | const SelectableAlbum = createSelectable(Album);
17 |
18 | class App extends React.Component {
19 |
20 | constructor (props) {
21 | super(props);
22 |
23 | this.state = {
24 | selectedItems: [],
25 | tolerance: 0,
26 | selectOnMouseMove: false,
27 | }
28 |
29 | this.handleSelection = this.handleSelection.bind(this);
30 | this.clearItems = this.clearItems.bind(this);
31 | this.handleToleranceChange = this.handleToleranceChange.bind(this);
32 | this.toggleSelectOnMouseMove = this.toggleSelectOnMouseMove.bind(this);
33 | }
34 |
35 |
36 | componentDidMount () {
37 | document.addEventListener('click', this.clearItems);
38 | }
39 |
40 |
41 | componentWillUnmount () {
42 | document.removeEventListener('click', this.clearItems);
43 | }
44 |
45 |
46 | handleSelection (keys) {
47 | this.setState({
48 | selectedItems: keys
49 | });
50 | }
51 |
52 |
53 | clearItems (e) {
54 | if(!isNodeInRoot(e.target, this.refs.selectable)) {
55 | this.setState({
56 | selectedItems: []
57 | });
58 | }
59 | }
60 |
61 |
62 | handleToleranceChange (e) {
63 | this.setState({
64 | tolerance: parseInt(e.target.value)
65 | });
66 | }
67 |
68 | toggleSelectOnMouseMove () {
69 | this.setState({
70 | selectOnMouseMove: !this.state.selectOnMouseMove
71 | });
72 | }
73 |
74 | render () {
75 | return (
76 |
77 |
React Selectable Demo
78 |
79 |
80 |
Tolerance:
{this.state.tolerance}
81 |
The number of pixels that must be in the bounding box in order for an item to be selected.
82 |
83 |
84 |
88 |
89 | {this.state.selectedItems.length > 0 &&
90 |
You have selected the following items:
91 | }
92 | {this.state.selectedItems.length === 0 &&
93 |
Please select some items from the right by clicking and dragging a box around them.
94 | }
95 |
96 | {this.state.selectedItems.map(function (key,i) {
97 | return - {this.props.items[key].title}
98 | }.bind(this))}
99 |
100 |
101 |
102 |
108 |
109 | {this.props.items.map((item, i) => {
110 | const selected = this.state.selectedItems.indexOf(i) > -1;
111 | return (
112 |
118 | );
119 | })}
120 |
121 |
122 |
123 | );
124 | }
125 | }
126 |
127 | export default App;
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | import data from './sample-data';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import SelectableGroup from './selectable-group';
5 | import createSelectable from './createSelectable';
6 |
7 |
8 | const isNodeInRoot = (node, root) => {
9 | while (node) {
10 | if (node === root) {
11 | return true;
12 | }
13 | node = node.parentNode;
14 | }
15 |
16 | return false;
17 | };
18 |
19 |
20 | class App extends React.Component {
21 |
22 | constructor (props) {
23 | super(props);
24 |
25 | this.state = {
26 | selectedItems: [],
27 | tolerance: 0,
28 | distance: 0,
29 | }
30 |
31 | this.handleSelection = this.handleSelection.bind(this);
32 | this.clearItems = this.clearItems.bind(this);
33 | this.handleToleranceChange = this.handleToleranceChange.bind(this);
34 | }
35 |
36 |
37 | componentDidMount () {
38 | document.addEventListener('click', this.clearItems);
39 | }
40 |
41 |
42 | componentWillUnmount () {
43 | document.removeEventListener('click', this.clearItems);
44 | }
45 |
46 |
47 | handleSelection (keys) {
48 | this.setState({
49 | selectedItems: keys
50 | });
51 | }
52 |
53 |
54 | clearItems (e) {
55 | if(!isNodeInRoot(e.target, this.refs.selectable)) {
56 | this.setState({
57 | selectedItems: []
58 | });
59 | }
60 | }
61 |
62 |
63 | handleToleranceChange (e) {
64 | this.setState({
65 | tolerance: e.target.value
66 | });
67 | }
68 |
69 |
70 | render () {
71 | return (
72 |
73 |
React Selectable Demo
74 |
75 |
76 |
Tolerance:
{this.state.tolerance}
77 |
The number of pixels that must be in the bounding box in order for an item to be selected.
78 |
79 |
80 | {this.state.selectedItems.length > 0 &&
81 |
You have selected the following items:
82 | }
83 | {this.state.selectedItems.length === 0 &&
84 |
Please select some items from the right by clicking and dragging a box around them.
85 | }
86 |
87 | {this.state.selectedItems.map(function (key,i) {
88 | return - {this.props.items[key].title}
89 | }.bind(this))}
90 |
91 |
92 |
93 |
100 |
101 | {this.props.items.map((item, i) => {
102 | const selected = this.state.selectedItems.indexOf(i) > -1;
103 | return (
104 |
110 | );
111 | })}
112 |
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | const Item = ({
120 | selected,
121 | title,
122 | year
123 | }) => {
124 | const classes = selected ? 'item selected' : 'item';
125 | return (
126 |
127 |
{title}
128 | {year}
129 |
130 | )
131 | };
132 |
133 | const SelectableItem = createSelectable(Item);
134 |
135 | render(, document.getElementById('app'));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Selectable items for React
2 |
3 | Allows individual or group selection of items using the mouse.
4 |
5 | ## Demo
6 | [Try it out](http://unclecheese.github.io/react-selectable/example)
7 |
8 | ## Upgrading from 0.1 to 0.2
9 | There have been significant changes in the 0.2 release. Please [read about them here](UPGRADING.md).
10 | ## Getting started
11 | ```
12 | npm install react-selectable
13 | ```
14 |
15 | ```js
16 | import React from 'react';
17 | import { render } from 'react-dom';
18 | import { SelectableGroup, createSelectable } from 'react-selectable';
19 | import SomeComponent from './some-component';
20 |
21 | const SelectableComponent = createSelectable(SomeComponent);
22 |
23 | class App extends React.Component {
24 |
25 | constructor (props) {
26 | super(props);
27 | this.state = {
28 | selectedKeys: []
29 | };
30 | }
31 |
32 | handleSelection (selectedKeys) {
33 | this.setState({ selectedKeys });
34 | }
35 |
36 | render () {
37 | return (
38 |
39 | {this.props.items.map((item, i) => {
40 | let selected = this.state.selectedKeys.indexOf(item.id) > -1;
41 | return (
42 |
43 | {item.title}
44 |
45 | );
46 | })}
47 |
48 | );
49 | }
50 |
51 | }
52 | ```
53 | ## Configuration
54 |
55 | The `` component accepts a few optional props:
56 | * **`onBeginSelection(event)`** (Function) Callback fired when the selection was started.
57 | * **`onSelection(items, event)`** (Function) Callback fired while the mouse is moving. Throttled to 50ms for performance in IE/Edge.
58 | * **`onEndSelection(items, event)`** (Function) Callback fired after user completes selection.
59 | * **`onNonItemClick(event)`** (Function) Callback fired when a click happens within the selectable group component, but not in a selectable item. Useful for clearing selection.
60 | * **`tolerance`** (Number) The amount of buffer to add around your `` container, in pixels.
61 | * **`component`** (String) The component to render. Defaults to `div`.
62 | * **`fixedPosition`** (Boolean) Whether the `` container is a fixed/absolute position element or the grandchild of one. Note: if you get an error that `Value must be omitted for boolean attributes` when you try ``, simply use Javascript's boolean object function: ``.
63 | * **`preventDefault`** (Boolean) Allows to enable/disable preventing the default action of the onmousedown event (with e.preventDefault). True by default. Disable if your app needs to capture this event for other functionalities.
64 | * **`enabled`** (Boolean) If false, all of the selectable features are disabled, and event handlers removed.
65 | * **`className`** (String) A CSS class to add to the containing element.
66 | * **`selectingClassName`** (String) A CSS class to add to the containing element when we select.
67 |
68 | ### Decorators
69 |
70 | Though they are optional, you can use decorators with this `react-selectable`.
71 |
72 | A side by side comparison is the best way to illustrate the difference:
73 |
74 | #### Without Decorators
75 | ```javascript
76 | class SomeComponent extends Component {
77 |
78 | }
79 | export default createSelectable(SomeComponent)
80 | ```
81 | vs.
82 |
83 | #### With Decorators
84 | ```javascript
85 | @createSelectable
86 | export default class SomeComponent extends Component {
87 |
88 | }
89 | ```
90 |
91 | In order to enable this functionality, you will most likely need to install a plugin (depending on your build setup). For Babel, you will need to make sure you have installed and enabled [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy) by doing the following:
92 |
93 | 1. run `npm i --save-dev babel-plugin-transform-decorators-legacy`
94 | 2. Add the following line to your `.babelrc`:
95 |
96 | ```json
97 | {
98 | "plugins": ["transform-decorators-legacy"]
99 | }
100 | ```
101 |
--------------------------------------------------------------------------------
/example/npm-debug.log:
--------------------------------------------------------------------------------
1 | 0 info it worked if it ends with ok
2 | 1 verbose cli [ '/usr/local/bin/node',
3 | 1 verbose cli '/usr/local/bin/npm',
4 | 1 verbose cli 'install',
5 | 1 verbose cli 'unclecheese/react-selectable' ]
6 | 2 info using npm@3.3.6
7 | 3 info using node@v5.0.0
8 | 4 silly loadCurrentTree Starting
9 | 5 silly install loadCurrentTree
10 | 6 silly install readLocalPackageData
11 | 7 silly fetchPackageMetaData unclecheese/react-selectable
12 | 8 silly fetchOtherPackageData unclecheese/react-selectable
13 | 9 silly cache add args [ 'unclecheese/react-selectable', null ]
14 | 10 verbose cache add spec unclecheese/react-selectable
15 | 11 silly cache add parsed spec Result {
16 | 11 silly cache add raw: 'unclecheese/react-selectable',
17 | 11 silly cache add scope: null,
18 | 11 silly cache add name: null,
19 | 11 silly cache add rawSpec: 'unclecheese/react-selectable',
20 | 11 silly cache add spec: 'github:unclecheese/react-selectable',
21 | 11 silly cache add type: 'hosted',
22 | 11 silly cache add hosted:
23 | 11 silly cache add { type: 'github',
24 | 11 silly cache add ssh: 'git@github.com:unclecheese/react-selectable.git',
25 | 11 silly cache add sshUrl: 'git+ssh://git@github.com/unclecheese/react-selectable.git',
26 | 11 silly cache add httpsUrl: 'git+https://github.com/unclecheese/react-selectable.git',
27 | 11 silly cache add gitUrl: 'git://github.com/unclecheese/react-selectable.git',
28 | 11 silly cache add shortcut: 'github:unclecheese/react-selectable',
29 | 11 silly cache add directUrl: 'https://raw.githubusercontent.com/unclecheese/react-selectable/master/package.json' } }
30 | 12 verbose addRemoteGit caching unclecheese/react-selectable
31 | 13 verbose addRemoteGit unclecheese/react-selectable is a repository hosted by github
32 | 14 silly tryGitProto attempting to clone git://github.com/unclecheese/react-selectable.git
33 | 15 silly tryClone cloning unclecheese/react-selectable via git://github.com/unclecheese/react-selectable.git
34 | 16 verbose tryClone git-github-com-unclecheese-react-selectable-git-6ba2a011d3c0c4706ee6ea8f3adb5c40 not in flight; caching
35 | 17 verbose makeDirectory /Users/aaroncarlino/.npm/_git-remotes creation not in flight; initializing
36 | 18 silly makeDirectory /Users/aaroncarlino/.npm/_git-remotes uid: 501 gid: 20
37 | 19 info git [ 'clone',
38 | 19 info git '--template=/Users/aaroncarlino/.npm/_git-remotes/_templates',
39 | 19 info git '--mirror',
40 | 19 info git 'git://github.com/unclecheese/react-selectable.git',
41 | 19 info git '/Users/aaroncarlino/.npm/_git-remotes/git-github-com-unclecheese-react-selectable-git-6ba2a011d3c0c4706ee6ea8f3adb5c40' ]
42 | 20 verbose mirrorRemote unclecheese/react-selectable git clone git://github.com/unclecheese/react-selectable.git
43 | 21 verbose setPermissions unclecheese/react-selectable set permissions on /Users/aaroncarlino/.npm/_git-remotes/git-github-com-unclecheese-react-selectable-git-6ba2a011d3c0c4706ee6ea8f3adb5c40
44 | 22 verbose resolveHead unclecheese/react-selectable original treeish: master
45 | 23 info git [ 'rev-list', '-n1', 'master' ]
46 | 24 silly resolveHead unclecheese/react-selectable resolved treeish: 828a233e5468205528bb6342c03b510cbe638351
47 | 25 verbose resolveHead unclecheese/react-selectable resolved Git URL: git://github.com/unclecheese/react-selectable.git#828a233e5468205528bb6342c03b510cbe638351
48 | 26 silly resolveHead Git working directory: /var/folders/08/9c4szhh15px1hdjpfyd8061r0000gn/T/npm-8289-2b1aa411/git-cache-2e0d300d610edd35c9ccb42284f49719/828a233e5468205528bb6342c03b510cbe638351
49 | 27 info git [ 'clone',
50 | 27 info git '/Users/aaroncarlino/.npm/_git-remotes/git-github-com-unclecheese-react-selectable-git-6ba2a011d3c0c4706ee6ea8f3adb5c40',
51 | 27 info git '/var/folders/08/9c4szhh15px1hdjpfyd8061r0000gn/T/npm-8289-2b1aa411/git-cache-2e0d300d610edd35c9ccb42284f49719/828a233e5468205528bb6342c03b510cbe638351' ]
52 | 28 verbose cloneResolved unclecheese/react-selectable clone Cloning into '/var/folders/08/9c4szhh15px1hdjpfyd8061r0000gn/T/npm-8289-2b1aa411/git-cache-2e0d300d610edd35c9ccb42284f49719/828a233e5468205528bb6342c03b510cbe638351'...
53 | 28 verbose cloneResolved done.
54 | 29 info git [ 'checkout', '828a233e5468205528bb6342c03b510cbe638351' ]
55 | 30 verbose checkoutTreeish unclecheese/react-selectable checkout Note: checking out '828a233e5468205528bb6342c03b510cbe638351'.
56 | 30 verbose checkoutTreeish
57 | 30 verbose checkoutTreeish You are in 'detached HEAD' state. You can look around, make experimental
58 | 30 verbose checkoutTreeish changes and commit them, and you can discard any commits you make in this
59 | 30 verbose checkoutTreeish state without impacting any branches by performing another checkout.
60 | 30 verbose checkoutTreeish
61 | 30 verbose checkoutTreeish If you want to create a new branch to retain commits you create, you may
62 | 30 verbose checkoutTreeish do so (now or later) by using -b with the checkout command again. Example:
63 | 30 verbose checkoutTreeish
64 | 30 verbose checkoutTreeish git checkout -b
65 | 30 verbose checkoutTreeish
66 | 30 verbose checkoutTreeish HEAD is now at 828a233... Several major API changes
67 | 31 verbose addLocalDirectory /Users/aaroncarlino/.npm/react-selectable/0.2.0/package.tgz not in flight; packing
68 | 32 verbose tar pack [ '/Users/aaroncarlino/.npm/react-selectable/0.2.0/package.tgz',
69 | 32 verbose tar pack '/var/folders/08/9c4szhh15px1hdjpfyd8061r0000gn/T/npm-8289-2b1aa411/git-cache-2e0d300d610edd35c9ccb42284f49719/828a233e5468205528bb6342c03b510cbe638351' ]
70 | 33 verbose tarball /Users/aaroncarlino/.npm/react-selectable/0.2.0/package.tgz
71 | 34 verbose folder /var/folders/08/9c4szhh15px1hdjpfyd8061r0000gn/T/npm-8289-2b1aa411/git-cache-2e0d300d610edd35c9ccb42284f49719/828a233e5468205528bb6342c03b510cbe638351
72 | 35 verbose addLocalTarball adding from inside cache /Users/aaroncarlino/.npm/react-selectable/0.2.0/package.tgz
73 | 36 verbose addRemoteGit data._from: unclecheese/react-selectable
74 | 37 verbose addRemoteGit data._resolved: git://github.com/unclecheese/react-selectable.git#828a233e5468205528bb6342c03b510cbe638351
75 | 38 silly cache afterAdd react-selectable@0.2.0
76 | 39 verbose afterAdd /Users/aaroncarlino/.npm/react-selectable/0.2.0/package/package.json not in flight; writing
77 | 40 verbose afterAdd /Users/aaroncarlino/.npm/react-selectable/0.2.0/package/package.json written
78 | 41 silly install normalizeTree
79 | 42 silly loadCurrentTree Finishing
80 | 43 silly loadIdealTree Starting
81 | 44 silly install loadIdealTree
82 | 45 silly cloneCurrentTree Starting
83 | 46 silly install cloneCurrentTreeToIdealTree
84 | 47 silly cloneCurrentTree Finishing
85 | 48 silly loadShrinkwrap Starting
86 | 49 silly install loadShrinkwrap
87 | 50 silly loadShrinkwrap Finishing
88 | 51 silly loadAllDepsIntoIdealTree Starting
89 | 52 silly install loadAllDepsIntoIdealTree
90 | 53 silly rollbackFailedOptional Starting
91 | 54 silly rollbackFailedOptional Finishing
92 | 55 silly runTopLevelLifecycles Starting
93 | 56 silly runTopLevelLifecycles Finishing
94 | 57 silly install printInstalled
95 | 58 verbose stack Error: Refusing to install react-selectable as a dependency of itself
96 | 58 verbose stack at checkSelf (/usr/local/lib/node_modules/npm/lib/install/validate-args.js:40:14)
97 | 58 verbose stack at Array. (/usr/local/lib/node_modules/npm/node_modules/slide/lib/bind-actor.js:15:8)
98 | 58 verbose stack at LOOP (/usr/local/lib/node_modules/npm/node_modules/slide/lib/chain.js:15:14)
99 | 58 verbose stack at chain (/usr/local/lib/node_modules/npm/node_modules/slide/lib/chain.js:20:5)
100 | 58 verbose stack at /usr/local/lib/node_modules/npm/lib/install/validate-args.js:15:5
101 | 58 verbose stack at /usr/local/lib/node_modules/npm/node_modules/slide/lib/async-map.js:52:35
102 | 58 verbose stack at Array.forEach (native)
103 | 58 verbose stack at /usr/local/lib/node_modules/npm/node_modules/slide/lib/async-map.js:52:11
104 | 58 verbose stack at Array.forEach (native)
105 | 58 verbose stack at asyncMap (/usr/local/lib/node_modules/npm/node_modules/slide/lib/async-map.js:51:8)
106 | 59 verbose cwd /Users/aaroncarlino/Sites/react-selectable/example
107 | 60 error Darwin 15.3.0
108 | 61 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "install" "unclecheese/react-selectable"
109 | 62 error node v5.0.0
110 | 63 error npm v3.3.6
111 | 64 error code ENOSELF
112 | 65 error Refusing to install react-selectable as a dependency of itself
113 | 66 error If you need help, you may report this error at:
114 | 66 error
115 | 67 verbose exit [ 1, true ]
116 |
--------------------------------------------------------------------------------
/src/selectable-group.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {findDOMNode} from 'react-dom';
3 | import PropTypes from 'prop-types';
4 | import cx from 'classnames';
5 | import isNodeInRoot from './nodeInRoot';
6 | import isNodeIn from './isNodeIn';
7 | import getBoundsForNode from './getBoundsForNode';
8 | import doObjectsCollide from './doObjectsCollide';
9 | import throttle from 'lodash.throttle';
10 |
11 | class SelectableGroup extends Component {
12 |
13 | constructor (props) {
14 | super(props);
15 |
16 | this.state = {
17 | isBoxSelecting: false,
18 | boxWidth: 0,
19 | boxHeight: 0
20 | };
21 |
22 | this._mouseDownData = null;
23 | this._rect = null;
24 | this._registry = [];
25 |
26 | this._openSelector = this._openSelector.bind(this);
27 | this._mouseDown = this._mouseDown.bind(this);
28 | this._mouseUp = this._mouseUp.bind(this);
29 | this._selectElements = this._selectElements.bind(this);
30 | this._registerSelectable = this._registerSelectable.bind(this);
31 | this._unregisterSelectable = this._unregisterSelectable.bind(this);
32 |
33 | this._throttledSelect = throttle(this._selectElements, 50);
34 | }
35 |
36 |
37 | getChildContext () {
38 | return {
39 | selectable: {
40 | register: this._registerSelectable,
41 | unregister: this._unregisterSelectable
42 | }
43 | };
44 | }
45 |
46 |
47 | componentDidMount () {
48 | this._applyMousedown(this.props.enabled);
49 | this._rect = this._getInitialCoordinates();
50 | }
51 |
52 |
53 | /**
54 | * Remove global event listeners
55 | */
56 | componentWillUnmount () {
57 | this._applyMousedown(false);
58 | }
59 |
60 | componentWillReceiveProps (nextProps) {
61 | if (nextProps.enabled !== this.props.enabled) {
62 | this._applyMousedown(nextProps.enabled);
63 | }
64 | }
65 |
66 | _registerSelectable (key, domNode) {
67 | this._registry.push({key, domNode});
68 | }
69 |
70 |
71 | _unregisterSelectable (key) {
72 | this._registry = this._registry.filter(data => data.key !== key);
73 | }
74 |
75 | _applyMousedown (apply) {
76 | const funcName = apply ? 'addEventListener' : 'removeEventListener';
77 | findDOMNode(this)[funcName]('mousedown', this._mouseDown);
78 | }
79 |
80 | /**
81 | * Called while moving the mouse with the button down. Changes the boundaries
82 | * of the selection box
83 | */
84 | _openSelector (e) {
85 | const w = Math.abs(this._mouseDownData.initialW - e.pageX + this._rect.x);
86 | const h = Math.abs(this._mouseDownData.initialH - e.pageY + this._rect.y);
87 |
88 | this.setState({
89 | isBoxSelecting: true,
90 | boxWidth: w,
91 | boxHeight: h,
92 | boxLeft: Math.min(e.pageX - this._rect.x, this._mouseDownData.initialW),
93 | boxTop: Math.min(e.pageY - this._rect.y, this._mouseDownData.initialH)
94 | });
95 |
96 | this._throttledSelect(e);
97 | }
98 |
99 | _getInitialCoordinates() {
100 | if (this.props.fixedPosition) {
101 | return { x: 0, y: 0 }
102 | }
103 |
104 | const style = window.getComputedStyle(document.body);
105 | const t = style.getPropertyValue('margin-top');
106 | const l = style.getPropertyValue('margin-left');
107 | const mLeft = parseInt(l.slice(0, l.length - 2), 10);
108 | const mTop = parseInt(t.slice(0, t.length - 2), 10);
109 |
110 | const bodyRect = document.body.getBoundingClientRect();
111 | const elemRect = findDOMNode(this).getBoundingClientRect();
112 | return {
113 | x: Math.round(elemRect.left - bodyRect.left + mLeft),
114 | y: Math.round(elemRect.top - bodyRect.top + mTop)
115 | };
116 | }
117 |
118 |
119 | /**
120 | * Called when a user presses the mouse button. Determines if a select box should
121 | * be added, and if so, attach event listeners
122 | */
123 | _mouseDown (e) {
124 | const {onBeginSelection, preventDefault} = this.props;
125 |
126 | // Disable if target is control by react-dnd
127 | if (isNodeIn(e.target, node => !!node.draggable)) return;
128 |
129 | // Allow onBeginSelection to cancel selection by return an explicit false
130 | if (typeof onBeginSelection === 'function' && onBeginSelection(e) === false) {
131 | return;
132 | }
133 |
134 | const node = findDOMNode(this);
135 | let collides, offsetData;
136 | window.addEventListener('mouseup', this._mouseUp);
137 |
138 | // Right clicks
139 | if (e.which === 3 || e.button === 2) return;
140 |
141 | if (!isNodeInRoot(e.target, node)) {
142 | offsetData = getBoundsForNode(node);
143 | collides = doObjectsCollide(
144 | {
145 | top: offsetData.top,
146 | left: offsetData.left,
147 | bottom: offsetData.offsetHeight,
148 | right: offsetData.offsetWidth
149 | },
150 | {
151 | top: e.pageY - this._rect.y,
152 | left: e.pageX - this._rect.x,
153 | offsetWidth: 0,
154 | offsetHeight: 0
155 | }
156 | );
157 | if (!collides) return;
158 | }
159 | this._rect = this._getInitialCoordinates();
160 |
161 | this._mouseDownData = {
162 | boxLeft: e.pageX - this._rect.x,
163 | boxTop: e.pageY - this._rect.y,
164 | initialW: e.pageX - this._rect.x,
165 | initialH: e.pageY - this._rect.y
166 | };
167 |
168 | if (preventDefault) e.preventDefault();
169 |
170 | window.addEventListener('mousemove', this._openSelector);
171 | }
172 |
173 |
174 | /**
175 | * Called when the user has completed selection
176 | */
177 | _mouseUp (e) {
178 | const {onNonItemClick} = this.props;
179 | const {isBoxSelecting} = this.state;
180 |
181 | e.stopPropagation();
182 |
183 | window.removeEventListener('mousemove', this._openSelector);
184 | window.removeEventListener('mouseup', this._mouseUp);
185 |
186 | if (!this._mouseDownData) return;
187 |
188 | // Mouse up when not box selecting is a heuristic for a "click"
189 | if (onNonItemClick && !isBoxSelecting) {
190 | if (!this._registry.some(({domNode}) => isNodeInRoot(e.target, domNode))) {
191 | onNonItemClick(e);
192 | }
193 | }
194 |
195 | this._selectElements(e, true);
196 |
197 | this._mouseDownData = null;
198 | this.setState({
199 | isBoxSelecting: false,
200 | boxWidth: 0,
201 | boxHeight: 0
202 | });
203 | }
204 |
205 |
206 | /**
207 | * Selects multiple children given x/y coords of the mouse
208 | */
209 | _selectElements (e, isEnd = false) {
210 | const {tolerance, onSelection, onEndSelection} = this.props;
211 |
212 | const currentItems = [];
213 | const _selectbox = findDOMNode(this.refs.selectbox);
214 |
215 | if (!_selectbox) return;
216 |
217 | this._registry.forEach(itemData => {
218 | if (
219 | itemData.domNode
220 | && doObjectsCollide(_selectbox, itemData.domNode, tolerance)
221 | && !currentItems.includes(itemData.key)
222 | ) {
223 | currentItems.push(itemData.key);
224 | }
225 | });
226 |
227 | if (isEnd) {
228 | if (typeof onEndSelection === 'function') onEndSelection(currentItems, e);
229 | } else {
230 | if (typeof onSelection === 'function') onSelection(currentItems, e);
231 | }
232 | }
233 |
234 |
235 | /**
236 | * Renders the component
237 | * @return {ReactComponent}
238 | */
239 | render () {
240 | const {children, enabled, fixedPosition, className, selectingClassName} = this.props;
241 | const {isBoxSelecting, boxLeft, boxTop, boxWidth, boxHeight} = this.state;
242 | const Component = this.props.component;
243 |
244 | if (!enabled) {
245 | return (
246 |
247 | {children}
248 |
249 | );
250 | }
251 |
252 | const boxStyle = {
253 | left: boxLeft,
254 | top: boxTop,
255 | width: boxWidth,
256 | height: boxHeight,
257 | zIndex: 9000,
258 | position: fixedPosition ? 'fixed' : 'absolute',
259 | cursor: 'default'
260 | };
261 |
262 | const spanStyle = {
263 | backgroundColor: 'transparent',
264 | border: '1px dashed #999',
265 | width: '100%',
266 | height: '100%',
267 | float: 'left'
268 | };
269 |
270 | const wrapperStyle = {
271 | position: 'relative',
272 | overflow: 'visible'
273 | };
274 |
275 | return (
276 |
283 | {
284 | isBoxSelecting ?
285 |
289 |
292 |
293 | : null
294 | }
295 | {children}
296 |
297 | );
298 | }
299 | }
300 |
301 | SelectableGroup.propTypes = {
302 | /**
303 | * @typedef {Object} MouseEvent
304 | * @typedef {Object} HTMLElement
305 | */
306 |
307 | /**
308 | * @type {HTMLElement} node
309 | */
310 | children: PropTypes.node,
311 |
312 | /**
313 | * Event that will fire when selection was started
314 | *
315 | * @type {Function}
316 | * @param {MouseEvent} event - MouseEvent
317 | */
318 | onBeginSelection: PropTypes.func,
319 |
320 | /**
321 | * Event that will fire when selection was finished. Passes an array of keys
322 | *
323 | * @type {Function}
324 | * @param {Array} items - The array of selected items
325 | * @param {MouseEvent} event - MouseEvent
326 | */
327 | onEndSelection: PropTypes.func,
328 |
329 | /**
330 | * Event that will fire when items are selected. Passes an array of keys
331 | *
332 | * @type {Function}
333 | * @param {Array} items - The array of selected items
334 | * @param {MouseEvent} event - MouseEvent
335 | */
336 | onSelection: PropTypes.func,
337 |
338 | /**
339 | * The component that will represent the Selectable DOM node
340 | *
341 | * @type {HTMLElement} node
342 | */
343 | component: PropTypes.node,
344 |
345 | /**
346 | * Amount of forgiveness an item will offer to the selectbox before registering
347 | * a selection, i.e. if only 1px of the item is in the selection, it shouldn't be
348 | * included.
349 | *
350 | * @type {Number}
351 | */
352 | tolerance: PropTypes.number,
353 |
354 | /**
355 | * In some cases, it the bounding box may need fixed positioning, if your layout
356 | * is relying on fixed positioned elements, for instance.
357 | *
358 | * @type {Boolean}
359 | */
360 | fixedPosition: PropTypes.bool,
361 |
362 | /**
363 | * Allows to enable/disable preventing the default action of the onmousedown event (with e.preventDefault).
364 | * True by default. Disable if your app needs to capture this event for other functionalities.
365 | *
366 | * @type {Boolean}
367 | */
368 | preventDefault: PropTypes.bool,
369 |
370 | /**
371 | * Triggered when the user clicks in the component, but not on an item, e.g. whitespace
372 | *
373 | * @type {Function}
374 | */
375 | onNonItemClick: PropTypes.func,
376 |
377 | /**
378 | * If false, all of the selectble features are turned off.
379 | * @type {[type]}
380 | */
381 | enabled: PropTypes.bool,
382 |
383 | /**
384 | * A CSS class to add to the containing element
385 | * @type {string}
386 | */
387 | className: PropTypes.string,
388 |
389 | /**
390 | * A CSS class to add to the containing element when we select
391 | * @type {string}
392 | */
393 | selectingClassName: PropTypes.string
394 |
395 | };
396 |
397 | SelectableGroup.defaultProps = {
398 | component: 'div',
399 | tolerance: 0,
400 | fixedPosition: false,
401 | preventDefault: true,
402 | enabled: true
403 | };
404 |
405 | SelectableGroup.childContextTypes = {
406 | selectable: PropTypes.object
407 | };
408 |
409 | export default SelectableGroup;
410 |
--------------------------------------------------------------------------------
/dist/react-selectable.js:
--------------------------------------------------------------------------------
1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t(require("react"),require("react-dom"));else if("function"==typeof define&&define.amd)define(["react","react-dom"],t);else{var n="object"==typeof exports?t(require("react"),require("react-dom")):t(e.React,e.ReactDOM);for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(this,function(e,t){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=12)}([function(e,t,n){"use strict";function r(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function i(e){if(s===setTimeout)return setTimeout(e,0);if((s===r||!s)&&setTimeout)return s=setTimeout,setTimeout(e,0);try{return s(e,0)}catch(t){try{return s.call(null,e,0)}catch(t){return s.call(this,e,0)}}}function u(e){if(p===clearTimeout)return clearTimeout(e);if((p===o||!p)&&clearTimeout)return p=clearTimeout,clearTimeout(e);try{return p(e)}catch(t){try{return p.call(null,e)}catch(t){return p.call(this,e)}}}function a(){b&&y&&(b=!1,y.length?h=y.concat(h):v=-1,h.length&&c())}function c(){if(!b){var e=i(a);b=!0;for(var t=h.length;t;){for(y=h,h=[];++v1)for(var n=1;n1?t-1:0),r=1;r2?n-2:0),o=2;o1&&void 0!==arguments[1]&&arguments[1],n=this.props,r=n.tolerance,o=n.onSelection,i=n.onEndSelection,u=[],a=(0,l.findDOMNode)(this.refs.selectbox);a&&(this._registry.forEach(function(e){e.domNode&&(0,x.default)(a,e.domNode,r)&&!u.includes(e.key)&&u.push(e.key)}),t?"function"==typeof i&&i(u,e):"function"==typeof o&&o(u,e))}},{key:"render",value:function(){var e=this.props,t=e.children,n=e.enabled,r=e.fixedPosition,o=e.className,i=e.selectingClassName,u=this.state,a=u.isBoxSelecting,c=u.boxLeft,l=u.boxTop,s=u.boxWidth,p=u.boxHeight,d=this.props.component;if(!n)return f.default.createElement(d,{className:o},t);var h={left:c,top:l,width:s,height:p,zIndex:9e3,position:r?"fixed":"absolute",cursor:"default"},b={backgroundColor:"transparent",border:"1px dashed #999",width:"100%",height:"100%",float:"left"},v={position:"relative",overflow:"visible"};return f.default.createElement(d,{className:(0,y.default)(o,a?i:null),style:v},a?f.default.createElement("div",{style:h,ref:"selectbox"},f.default.createElement("span",{style:b})):null,t)}}]),t}(c.Component);j.propTypes={children:p.default.node,onBeginSelection:p.default.func,onEndSelection:p.default.func,onSelection:p.default.func,component:p.default.node,tolerance:p.default.number,fixedPosition:p.default.bool,preventDefault:p.default.bool,onNonItemClick:p.default.func,enabled:p.default.bool,className:p.default.string,selectingClassName:p.default.string},j.defaultProps={component:"div",tolerance:0,fixedPosition:!1,preventDefault:!0,enabled:!0},j.childContextTypes={selectable:p.default.object},t.default=j},function(e,t,n){"use strict";(function(t){var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n(1),i=n(2),u=n(8),a=n(15),c=n(3),f=n(16);e.exports=function(e,n){function l(e){var t=e&&(N&&e[N]||e[P]);if("function"==typeof t)return t}function s(e,t){return e===t?0!==e||1/e==1/t:e!==e&&t!==t}function p(e){this.message=e,this.stack=""}function d(e){function r(r,f,l,s,d,y,h){if(s=s||k,y=y||l,h!==c)if(n)i(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types");else if("production"!==t.env.NODE_ENV&&"undefined"!=typeof console){var b=s+":"+l;!o[b]&&a<3&&(u(!1,"You are manually calling a React.PropTypes validation function for the `%s` prop on `%s`. This is deprecated and will throw in the standalone `prop-types` package. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details.",y,s),o[b]=!0,a++)}return null==f[l]?r?new p(null===f[l]?"The "+d+" `"+y+"` is marked as required in `"+s+"`, but its value is `null`.":"The "+d+" `"+y+"` is marked as required in `"+s+"`, but its value is `undefined`."):null:e(f,l,s,d,y)}if("production"!==t.env.NODE_ENV)var o={},a=0;var f=r.bind(null,!1);return f.isRequired=r.bind(null,!0),f}function y(e){function t(t,n,r,o,i,u){var a=t[n];if(S(a)!==e)return new p("Invalid "+o+" `"+i+"` of type `"+j(a)+"` supplied to `"+r+"`, expected `"+e+"`.");return null}return d(t)}function h(e){function t(t,n,r,o,i){if("function"!=typeof e)return new p("Property `"+i+"` of component `"+r+"` has invalid PropType notation inside arrayOf.");var u=t[n];if(!Array.isArray(u)){return new p("Invalid "+o+" `"+i+"` of type `"+S(u)+"` supplied to `"+r+"`, expected an array.")}for(var a=0;a>",D={array:y("array"),bool:y("boolean"),func:y("function"),number:y("number"),object:y("object"),string:y("string"),symbol:y("symbol"),any:function(){return d(o.thatReturnsNull)}(),arrayOf:h,element:function(){function t(t,n,r,o,i){var u=t[n];if(!e(u)){return new p("Invalid "+o+" `"+i+"` of type `"+S(u)+"` supplied to `"+r+"`, expected a single ReactElement.")}return null}return d(t)}(),instanceOf:b,node:function(){function e(e,t,n,r,o){return x(e[t])?null:new p("Invalid "+r+" `"+o+"` supplied to `"+n+"`, expected a ReactNode.")}return d(e)}(),objectOf:m,oneOf:v,oneOfType:g,shape:_,exact:w};return p.prototype=Error.prototype,D.checkPropTypes=f,D.PropTypes=D,D}}).call(t,n(0))},function(e,t,n){"use strict";function r(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}var o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,u=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,a,c=r(e),f=1;fn+a||t+o-cr+u)};t.default=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,r=e instanceof HTMLElement?(0,o.default)(e):e,u=t instanceof HTMLElement?(0,o.default)(t):t;return i(r.top,r.left,u.top,u.left,r.offsetWidth,r.offsetHeight,u.offsetWidth,u.offsetHeight,n)}},function(e,t,n){"use strict";(function(t){function n(e,t,n){function r(t){var n=h,r=b;return h=b=void 0,w=t,m=e.apply(r,n)}function i(e){return w=e,g=setTimeout(l,t),j?r(e):m}function u(e){var n=e-_,r=e-w,o=t-n;return E?O(o,v-r):o}function c(e){var n=e-_,r=e-w;return void 0===_||n>=t||n<0||E&&r>=v}function l(){var e=S();if(c(e))return s(e);g=setTimeout(l,u(e))}function s(e){return g=void 0,T&&h?r(e):(h=b=void 0,m)}function p(){void 0!==g&&clearTimeout(g),w=0,h=_=b=g=void 0}function d(){return void 0===g?m:s(S())}function y(){var e=S(),n=c(e);if(h=arguments,b=this,_=e,n){if(void 0===g)return i(_);if(E)return g=setTimeout(l,t),r(_)}return void 0===g&&(g=setTimeout(l,t)),m}var h,b,v,m,g,_,w=0,j=!1,E=!1,T=!0;if("function"!=typeof e)throw new TypeError(f);return t=a(t)||0,o(n)&&(j=!!n.leading,E="maxWait"in n,v=E?x(a(n.maxWait)||0,t):v,T="trailing"in n?!!n.trailing:T),y.cancel=p,y.flush=d,y}function r(e,t,r){var i=!0,u=!0;if("function"!=typeof e)throw new TypeError(f);return o(r)&&(i="leading"in r?!!r.leading:i,u="trailing"in r?!!r.trailing:u),n(e,t,{leading:i,maxWait:t,trailing:u})}function o(e){var t=void 0===e?"undefined":c(e);return!!e&&("object"==t||"function"==t)}function i(e){return!!e&&"object"==(void 0===e?"undefined":c(e))}function u(e){return"symbol"==(void 0===e?"undefined":c(e))||i(e)&&w.call(e)==s}function a(e){if("number"==typeof e)return e;if(u(e))return l;if(o(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=o(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(p,"");var n=y.test(e);return n||h.test(e)?b(e.slice(2),n?2:8):d.test(e)?l:+e}var c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f="Expected a function",l=NaN,s="[object Symbol]",p=/^\s+|\s+$/g,d=/^[-+]0x[0-9a-f]+$/i,y=/^0b[01]+$/i,h=/^0o[0-7]+$/i,b=parseInt,v="object"==(void 0===t?"undefined":c(t))&&t&&t.Object===Object&&t,m="object"==("undefined"==typeof self?"undefined":c(self))&&self&&self.Object===Object&&self,g=v||m||Function("return this")(),_=Object.prototype,w=_.toString,x=Math.max,O=Math.min,S=function(){return g.Date.now()};e.exports=r}).call(t,n(21))},function(e,t,n){"use strict";var r,o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};r=function(){return this}();try{r=r||Function("return this")()||(0,eval)("this")}catch(e){"object"===("undefined"==typeof window?"undefined":o(window))&&(r=window)}e.exports=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function u(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;n