├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── UPGRADING.md
├── dist
└── react-selectable.js
├── example.js
├── example
├── Album.js
├── App.js
├── example.js
├── index.html
├── npm-debug.log
└── sample-data.js
├── package-lock.json
├── package.json
├── react-selectable.d.ts
├── src
├── createSelectable.js
├── doObjectsCollide.js
├── getBoundsForNode.js
├── index.js
├── isNodeIn.js
├── nodeInRoot.js
└── selectable-group.js
├── webpack.config.example.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-2"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | example/bundle.js
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 {
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 |
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 |