Repository: aurbano/react-ds
Branch: master
Commit: 80c92e3d1497
Files: 28
Total size: 74.4 KB
Directory structure:
gitextract_m4l_eywj/
├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .flowconfig
├── .gitignore
├── .travis.yml
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│ ├── index.js
│ └── index.js.flow
├── example/
│ └── app/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ └── manifest.json
│ └── src/
│ ├── Example.js
│ ├── Examples.js
│ ├── example.css
│ ├── example.less
│ ├── index.js
│ ├── react-ds.css
│ └── registerServiceWorker.js
├── package.json
├── src/
│ └── index.js
└── test/
├── index.test.js
└── util/
└── test-bundler.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
[
"env",
{
"modules": "commonjs",
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}
],
"flow",
"react",
"stage-0"
],
"plugins": [
"transform-es2015-destructuring",
["transform-object-rest-spread",
{
"useBuiltIns": true
}
]
],
"env": {
"development": {
"plugins": [
"flow-react-proptypes",
"transform-react-jsx-source"
]
},
"production": {
"plugins": [
"transform-react-remove-prop-types",
"transform-react-constant-elements"
]
},
"test": {
"plugins": [
"transform-es2015-modules-commonjs",
"dynamic-import-node"
]
}
}
}
================================================
FILE: .eslintignore
================================================
.gitignore
dist/**/*
example/**/*
test/**/*
gulpfile.js
rollup.config.js
================================================
FILE: .eslintrc.json
================================================
{
"parser": "babel-eslint",
"extends": [
"airbnb",
"plugin:flowtype/recommended"
],
"globals": {
"API_URL": false,
"ENVIRONMENT": false,
"VERSION": false
},
"env": {
"browser": true,
"node": true,
"jest": true,
"es6": true
},
"plugins": [
"react",
"jsx-a11y",
"flowtype"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"eol-last": 0,
"jsx-quotes": [
"error",
"prefer-single"
],
"no-plusplus": 0,
"no-nested-ternary": 1,
"arrow-parens": 0,
"object-curly-spacing": [
2,
"always",
{
"arraysInObjects": false,
"objectsInObjects": false
}
],
"arrow-body-style": [
2,
"as-needed"
],
"comma-dangle": [
2,
"always-multiline"
],
"import/imports-first": 0,
"import/newline-after-import": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default": 0,
"import/no-unresolved": 2,
"import/prefer-default-export": 0,
"indent": [
2,
2,
{
"SwitchCase": 1
}
],
"jsx-a11y/aria-props": 2,
"jsx-a11y/heading-has-content": 0,
"jsx-a11y/href-no-hash": "off",
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "hrefLeft", "hrefRight" ],
"aspects": [ "noHref", "invalidHref", "preferButton" ]
}],
"jsx-a11y/label-has-for": [ "error", {
"required": {
"every": [ "nesting", "id" ]
},
"allowChildren": true
}],
"jsx-a11y/mouse-events-have-key-events": 2,
"jsx-a11y/role-has-required-aria-props": 2,
"jsx-a11y/role-supports-aria-props": 2,
"jsx-a11y/no-static-element-interactions": 1,
"jsx-a11y/img-has-alt": 0,
"max-len": 0,
"newline-per-chained-call": 0,
"no-confusing-arrow": 0,
"no-console": 1,
"no-use-before-define": 0,
"prefer-template": 2,
"class-methods-use-this": 0,
"react/sort-comp": [
2,
{
"order": [
"type-annotations",
"static-methods",
"lifecycle",
"everything-else",
"render"
]
}
],
"react/jsx-wrap-multilines": 2,
"react/forbid-prop-types": 0,
"react/jsx-first-prop-new-line": [
2,
"multiline"
],
"react/jsx-filename-extension": 0,
"react/jsx-no-bind": 1,
"react/jsx-curly-spacing": [
2,
"always"
],
"react/jsx-no-target-blank": 0,
"react/no-multi-comp": 1,
"react/require-extension": 0,
"react/self-closing-comp": 0,
"require-yield": 0,
"import/no-webpack-loader-syntax": 0,
"react/require-default-props": 0,
"react/no-array-index-key": 0,
"flowtype/object-type-delimiter": [
2,
"comma"
]
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}
================================================
FILE: .flowconfig
================================================
[ignore]
<PROJECT_ROOT>/node_modules/babel-plugin-transform-react-remove-prop-types/.*
<PROJECT_ROOT>/node_modules/babel-plugin-flow-runtime/.*
<PROJECT_ROOT>/node_modules/eslint-plugin-jsx-a11y/.*
<PROJECT_ROOT>/node_modules/flow-runtime/.*
<PROJECT_ROOT>/node_modules/.staging/.*
<PROJECT_ROOT>/node_modules/kefir/.*
<PROJECT_ROOT>/example/.*
<PROJECT_ROOT>/dist/.*
[include]
[libs]
[lints]
[options]
# Support ES7 classes
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
esproposal.export_star_as=enable
# Warn on @decorators
esproposal.decorators=warn
# Experimental settings
experimental.const_params=true
[strict]
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
test-report
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.idea
*.iml
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
node_js:
- stable
script:
- npm test
after_success:
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
- cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage
env:
global:
secure: o+QC0UCfqDo+Py3i6Wa0eOMGfPgqrXXMCrINxzvS8WNe5Lh9bIGbFfE4BctPu6YJfFFv8h1krHlA+oHFaFLzsWUqe70pEKu9+hFWWpR4WMdlRW5oHMjwmO3M5cKzcPYsvJ9YzjoEEU9wsLx/pt3i+8VRl0E+0F2B4Zo3PfcGny3WYhEO3r/B8shU81EE1sajhhesPj5dGWHRTfQhcQlyvtBlWqy+Atp+RFGDyB7N0KJhIjw15vLvPqCI0UsaWrpCIC8c3uV3um92ChKTaxbxBJS7cdG9wIMoardBY1UxxMahn9ohgO5/6obr9h6FTpfotblGpFQ6qKVySd2wic8eODPbplP+pq0TaMhw37GuE+ZEL3Yg7pwfeav2nKf8xwvGA5yxZLq8WKnTZZdOUMRT9DX3MFTKNkUusINv0ehWoVZSRsFqFrsXF0C9cx9/HjEpG33j3I2bH9fo0CPcgfwM9frGHcTfqirq86bwESIYfGXX7+e6qkV9yozEtpDDueaQK0hHWrEAMsNQgYX63r8lGF1oYCdeQ14I2xQjs0Yufi8TTcpDPy3977A68HkASj0mNLH24u4fK6zQYkDLCc7Q4BXIO1lVKeDjw7/obvjQ/+QM0BT1F757OWak5YjhAy9SAttZnsCYcBa9VVIlEQYrrHKzwR3RQDGhmE2g85dzWOc=
================================================
FILE: .vscode/settings.json
================================================
{
"flow.useNPMPackagedFlow": true,
"javascript.validate.enable": false,
"files.associations": {
"*.overrides": "less",
"*.variables": "less",
"theme.config": "less",
},
"flow.showUncovered": true,
"flow.logLevel": "warn"
}
================================================
FILE: CHANGELOG.md
================================================
# Change Log
> I haven't been maintaining this changelog.
>
> Check out the [releases](https://github.com/aurbano/react-ds/releases) section instead.
------
# Old Changelog
## [v1.7.0](https://github.com/aurbano/react-ds/tree/v1.7.0) (2017-07-27)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.6.0...v1.7.0)
**Closed issues:**
- Add option to ignore certain targets when initiating selects [\#7](https://github.com/aurbano/react-ds/issues/7)
## [v1.6.0](https://github.com/aurbano/react-ds/tree/v1.6.0) (2017-07-25)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.5.0...v1.6.0)
## [v1.5.0](https://github.com/aurbano/react-ds/tree/v1.5.0) (2017-07-13)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.4.1...v1.5.0)
## [v1.4.1](https://github.com/aurbano/react-ds/tree/v1.4.1) (2017-07-13)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.4.0...v1.4.1)
**Closed issues:**
- Move react to peerDependencies [\#5](https://github.com/aurbano/react-ds/issues/5)
## [v1.4.0](https://github.com/aurbano/react-ds/tree/v1.4.0) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.3.0...v1.4.0)
**Fixed bugs:**
- Items not properly selected when scrolled [\#4](https://github.com/aurbano/react-ds/issues/4)
## [v1.3.0](https://github.com/aurbano/react-ds/tree/v1.3.0) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.2.0...v1.3.0)
## [v1.2.0](https://github.com/aurbano/react-ds/tree/v1.2.0) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.1.0...v1.2.0)
**Implemented enhancements:**
- Add examples [\#1](https://github.com/aurbano/react-ds/issues/1)
## [v1.1.0](https://github.com/aurbano/react-ds/tree/v1.1.0) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.0.3...v1.1.0)
## [v1.0.3](https://github.com/aurbano/react-ds/tree/v1.0.3) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.0.2...v1.0.3)
**Implemented enhancements:**
- Setup CI [\#3](https://github.com/aurbano/react-ds/issues/3)
## [v1.0.2](https://github.com/aurbano/react-ds/tree/v1.0.2) (2017-07-12)
[Full Changelog](https://github.com/aurbano/react-ds/compare/v1.0.1...v1.0.2)
**Implemented enhancements:**
- Generate dist folder with babel without flow [\#2](https://github.com/aurbano/react-ds/issues/2)
## [v1.0.1](https://github.com/aurbano/react-ds/tree/v1.0.1) (2017-07-12)
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Alejandro U. Alvarez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# React DS
> Tiny (7KB) React Drag-to-Select component (with no dependencies! with support for touch devices!
[](https://travis-ci.org/aurbano/react-ds)
[](https://www.npmjs.com/package/react-ds)
[](https://coveralls.io/github/aurbano/react-ds?branch=master)
[](https://www.npmjs.com/package/react-ds)
[](https://www.npmjs.com/package/react-ds)
[](https://www.codacy.com/app/aurbano/react-ds)
[](https://gfycat.com/gifs/detail/fatyellowkid)
I wrote this library because I couldn't find any existing one to do selections without having to wrap the items in their component.
In some cases you really need an unobtrusive way to make items selectable, this will do just that.
## Installation
```console
$ npm i react-ds
```
Or if you prefer yarn
```console
$ yarn add react-ds
```
## Usage
```jsx
import Selection from 'react-ds';
// target (ref) is the parent component (so that selects only happen when clicking and dragging on it)
// elements (refs[]) is an array of refs to the components that are selectable
<Selection
target={ ref }
elements={ refs[] }
onSelectionChange={ this.handleSelection }
/>
```
### Props
#### `target`
Element where the selection should be applied to. This is to scope the mouse/touch event handlers and make sure that it doesn't affect your whole web app.
It must be a React `ref`, it should also exist, so you may want to check if it's already initialized before rendering the `Selection` component.
#### `elements`
Array of refs to the elements that are selectable. The `Selection` component will use this to get their location and sizes to determine whether they are within the selection area.
The should exist before rendering the `Selection` component.
#### `onSelectionChange`
Function that will be executed when the selection changes. An array of element indexes will be passed (with the same indexes as the `elements` prop).
This is where you want to update your state, to highlight them as selected for example.
#### `onHighlightChange`
While dragging, `onHighlightChange` is called only when the highlighted elements have changed.
When the mouse is released, it will be called with an empty array.

#### `offset` *(Optional)*
This is used to calculate the coordinates of the mouse when drawing the Selection box, since the mouse events gives coordinates relative to the document, but the Selection box may have a different parent.
Essentially you need to pass the offset of the parent element where the Selection is being rendered. If it's rendered in the same component as the items to be selected then the default value will work fine.
If passing your own offset keep in mind that `getBoundingClientRect()` depends on the scroll, so you may want to do something like this:
```js
const boundingBox = target.getBoundingClientRect();
const offset = {
top: boundingBox.top + window.scrollY,
left: boundingBox.left + window.scrollX,
};
```
#### `style` *(Optional)*
If you want to override the styles for the selection area, you can either pass any styles here, or use css and declare any styles on the `.react-ds-border` class.
The styles are merged, so you can override just one property if you need (typically the `zIndex`).
The default styles are:
```js
const style = {
position: 'absolute',
background: 'rgba(159, 217, 255, 0.3)',
border: 'solid 1px rgba(123, 123, 123, 0.61)',
zIndex: 9,
cursor: 'crosshair',
}
```
#### `ignoreTargets` *(Optional)*
Specify an array of CSS3 selectors for DOM targets that should be ignored when initiating a selection. i.e. `['div', 'div > p', '#someId']`
>This is specially useful because `react-ds` uses native browser events that bypass React's event queue, so you won't be able to `stopPropagation` as usual.
```jsx
<Selection
target={ this.state.ref}
elements={ this.state.elRefs }
onSelectionChange={ this.handleSelection }
ignoreTargets={ ['.handle'] }
/>
```
## Example
This example was taken from [`example/app/src/Example.js`](https://github.com/aurbano/react-ds/blob/master/example/app/src/Example.js) which you can see running at https://aurbano.eu/react-ds/
```jsx
import React from 'react';
import PropTypes from 'prop-types';
import Selection from 'react-ds';
export default class Example extends React.PureComponent {
constructor() {
super();
this.state = {
ref: null,
elRefs: [],
selectedElements: [], // track the elements that are selected
};
}
handleSelection = (indexes) => {
this.setState({
selectedElements: indexes,
});
};
getStyle = (index) => {
if (this.state.selectedElements.indexOf(index) > -1) {
// Selected state
return {
background: '#2185d0',
borderColor: '#2185d0',
color: 'white',
};
}
return {};
};
addElementRef = (ref) => {
const elRefs = this.state.elRefs;
elRefs.push(ref);
this.setState({
elRefs,
});
};
renderSelection() {
if (!this.state.ref || !this.state.elRefs) {
return null;
}
return (
<Selection
target={ this.state.ref}
elements={ this.state.elRefs }
onSelectionChange={ this.handleSelection }
style={ this.props.style }
/>
);
}
render() {
const selectableElements = [
'one',
'another',
'hey there',
'item',
'two',
'three',
'something longer?',
'last'
];
return (
<div ref={ (ref) => { this.setState({ ref }); } } className='item-container'>
{ selectableElements.map((el, index) => (
<div
key={ el }
ref={ this.addElementRef }
style={ this.getStyle(index) }
className='item'
>
{ el }
</div>
)) }
{ this.renderSelection() }
</div>
);
}
}
Example.PropTypes = {
style: PropTypes.object,
};
```
## Contributing
Only edit the files in the `src` folder. I'll update `dist` manually before publishing new versions to npm.
To run the tests simply run `npm test`. Add tests as you see fit to the `test` folder, they must be called `{string}.test.js`.
## Meta
Copyright © [Alejandro U. Alvarez](https:/aurbano.eu) 2017. MIT Licensed.
================================================
FILE: dist/index.js
================================================
"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _jsx=function(){var REACT_ELEMENT_TYPE=typeof Symbol==="function"&&Symbol.for&&Symbol.for("react.element")||60103;return function createRawReactElement(type,props,key,children){var defaultProps=type&&type.defaultProps;var childrenLength=arguments.length-3;if(!props&&childrenLength!==0){props={}}if(props&&defaultProps){for(var propName in defaultProps){if(props[propName]===void 0){props[propName]=defaultProps[propName]}}}else if(!props){props=defaultProps||{}}if(childrenLength===1){props.children=children}else if(childrenLength>1){var childArray=Array(childrenLength);for(var i=0;i<childrenLength;i++){childArray[i]=arguments[i+3]}props.children=childArray}return{$$typeof:REACT_ELEMENT_TYPE,type:type,key:key===undefined?null:""+key,ref:null,props:props,_owner:null}}}();var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _react=require("react");var _react2=_interopRequireDefault(_react);var _propTypes=require("prop-types");var _propTypes2=_interopRequireDefault(_propTypes);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2=Array(arr.length);i<arr.length;i++){arr2[i]=arr[i]}return arr2}else{return Array.from(arr)}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}function getOffset(props){var offset={top:0,left:0};if(props.offset){offset=Object.assign({},props.offset)}else if(props.target){var boundingBox=props.target.getBoundingClientRect();offset.top=boundingBox.top+window.scrollY;offset.left=boundingBox.left+window.scrollX}return offset}var Selection=function(_React$PureComponent){_inherits(Selection,_React$PureComponent);function Selection(props){_classCallCheck(this,Selection);var _this=_possibleConstructorReturn(this,(Selection.__proto__||Object.getPrototypeOf(Selection)).call(this,props));_this.bind=function(){_this.props.target.addEventListener("mousedown",_this.onMouseDown);_this.props.target.addEventListener("touchstart",_this.onTouchStart)};_this.reset=function(){if(_this.props.target){_this.props.target.removeEventListener("mousedown",_this.onMouseDown)}};_this.init=function(e,x,y){if(_this.props.ignoreTargets){var Target=e.target;if(!Target.matches){var defaultMatches=function defaultMatches(s){return[].indexOf.call(window.document.querySelectorAll(s),_this)!==-1};Target.matches=Target.matchesSelector||Target.mozMatchesSelector||Target.msMatchesSelector||Target.oMatchesSelector||Target.webkitMatchesSelector||defaultMatches}if(Target.matches&&Target.matches(_this.props.ignoreTargets.join(","))){return false}}var nextState={};nextState.mouseDown=true;nextState.startPoint={x:(x-_this.state.offset.left)/_this.state.zoom,y:(y-_this.state.offset.top)/_this.state.zoom};_this.setState(nextState);return true};_this.onMouseDown=function(e){if(_this.props.disabled||e.button===2||e.nativeEvent&&e.nativeEvent.which===2){return}if(_this.init(e,e.pageX,e.pageY)){window.document.addEventListener("mousemove",_this.onMouseMove);window.document.addEventListener("mouseup",_this.onMouseUp)}};_this.onTouchStart=function(e){if(_this.props.disabled||!e.touches||!e.touches[0]||e.touches.length>1){return}if(_this.init(e,e.touches[0].pageX,e.touches[0].pageY)){window.document.addEventListener("touchmove",_this.onTouchMove);window.document.addEventListener("touchend",_this.onMouseUp)}};_this.onMouseUp=function(){window.document.removeEventListener("touchmove",_this.onTouchMove);window.document.removeEventListener("mousemove",_this.onMouseMove);window.document.removeEventListener("mouseup",_this.onMouseUp);window.document.removeEventListener("touchend",_this.onMouseUp);_this.setState({mouseDown:false,startPoint:null,endPoint:null,selectionBox:null});if(_this.props.onSelectionChange){_this.props.onSelectionChange(_this.selectedChildren)}if(_this.props.onHighlightChange){_this.highlightedChildren=[];_this.props.onHighlightChange(_this.highlightedChildren)}_this.selectedChildren=[]};_this.onMouseMove=function(e){e.preventDefault();if(_this.state.mouseDown){var _endPoint={x:(e.pageX-_this.state.offset.left)/_this.state.zoom,y:(e.pageY-_this.state.offset.top)/_this.state.zoom};_this.setState({endPoint:_endPoint,selectionBox:_this.calculateSelectionBox(_this.state.startPoint,_endPoint)})}};_this.onTouchMove=function(e){e.preventDefault();if(_this.state.mouseDown){var _endPoint2={x:(e.touches[0].pageX-_this.state.offset.left)/_this.state.zoom,y:(e.touches[0].pageY-_this.state.offset.top)/_this.state.zoom};_this.setState({endPoint:_endPoint2,selectionBox:_this.calculateSelectionBox(_this.state.startPoint,_endPoint2)})}};_this.lineIntersects=function(lineA,lineB){return lineA[1]>=lineB[0]&&lineB[1]>=lineA[0]};_this.boxIntersects=function(boxA,boxB){var boxAProjection={x:[boxA.left,boxA.left+boxA.width],y:[boxA.top,boxA.top+boxA.height]};var boxBProjection={x:[boxB.left,boxB.left+boxB.width],y:[boxB.top,boxB.top+boxB.height]};return _this.lineIntersects(boxAProjection.x,boxBProjection.x)&&_this.lineIntersects(boxAProjection.y,boxBProjection.y)};_this.updateCollidingChildren=function(selectionBox){_this.selectedChildren=[];if(_this.props.elements){_this.props.elements.forEach(function(ref,$index){if(ref){var refBox=ref.getBoundingClientRect();var tmpBox={top:(refBox.top-_this.state.offset.top+window.scrollY)/_this.state.zoom,left:(refBox.left-_this.state.offset.left+window.scrollX)/_this.state.zoom,width:ref.clientWidth,height:ref.clientHeight};if(_this.boxIntersects(selectionBox,tmpBox)){_this.selectedChildren.push($index)}}})}if(_this.props.onHighlightChange&&JSON.stringify(_this.highlightedChildren)!==JSON.stringify(_this.selectedChildren)){var _onHighlightChange=_this.props.onHighlightChange;_this.highlightedChildren=[].concat(_toConsumableArray(_this.selectedChildren));if(window.requestAnimationFrame){window.requestAnimationFrame(function(){_onHighlightChange(_this.highlightedChildren)})}else{_onHighlightChange(_this.highlightedChildren)}}};_this.calculateSelectionBox=function(startPoint,endPoint){if(!_this.state.mouseDown||!startPoint||!endPoint){return null}var left=Math.min(startPoint.x,endPoint.x)-1;var top=Math.min(startPoint.y,endPoint.y)-1;var width=Math.abs(startPoint.x-endPoint.x)+1;var height=Math.abs(startPoint.y-endPoint.y)+1;return{left:left,top:top,width:width,height:height}};_this.state={mouseDown:false,startPoint:null,endPoint:null,selectionBox:null,offset:getOffset(props),zoom:props.zoom||1};_this.selectedChildren=[];_this.highlightedChildren=[];return _this}_createClass(Selection,[{key:"componentDidMount",value:function componentDidMount(){this.reset();this.bind()}},{key:"componentWillReceiveProps",value:function componentWillReceiveProps(nextProps){this.setState({offset:getOffset(nextProps)})}},{key:"componentDidUpdate",value:function componentDidUpdate(){this.reset();this.bind();if(this.state.mouseDown&&this.state.selectionBox){this.updateCollidingChildren(this.state.selectionBox)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.reset();window.document.removeEventListener("mousemove",this.onMouseMove);window.document.removeEventListener("mouseup",this.onMouseUp)}},{key:"render",value:function render(){var style=Object.assign({position:"absolute",background:"rgba(159, 217, 255, 0.3)",border:"solid 1px rgba(123, 123, 123, 0.61)",zIndex:9,cursor:"crosshair"},this.props.style);if(this.state.selectionBox){style=Object.assign({},style,this.state.selectionBox)}if(!this.state.mouseDown||!this.state.endPoint||!this.state.startPoint){return null}return _jsx("div",{className:"react-ds-border",style:style})}}]);return Selection}(_react2.default.PureComponent);exports.default=Selection;
================================================
FILE: dist/index.js.flow
================================================
// @flow
import React from 'react';
import PropTypes from 'prop-types';
export type Point = {
x: number,
y: number,
}
export type Box = {
left: number,
top: number,
width: number,
height: number,
}
type Props = {
disabled?: boolean,
target: HTMLElement,
onSelectionChange?: (elements: Array<any>) => void,
onHighlightChange?: (elements: Array<any>) => void,
elements: Array<HTMLElement>,
// eslint-disable-next-line react/no-unused-prop-types
offset?: {
// eslint-disable-next-line react/no-unused-prop-types
top: number,
// eslint-disable-next-line react/no-unused-prop-types
left: number,
},
style?: any,
zoom?: number,
ignoreTargets?: Array<string>,
};
type State = {
mouseDown: boolean,
startPoint: ?Point,
endPoint: ?Point,
selectionBox: ?Box,
offset: {
top: number,
left: number,
},
zoom: number,
};
function getOffset(props: Props) {
let offset = {
top: 0,
left: 0,
};
if (props.offset) {
offset = {
...props.offset,
};
} else if (props.target) {
const boundingBox = props.target.getBoundingClientRect();
offset.top = boundingBox.top + window.scrollY;
offset.left = boundingBox.left + window.scrollX;
}
return offset;
}
export default class Selection extends React.PureComponent<Props, State> { // eslint-disable-line react/prefer-stateless-function
props: Props;
state: State;
selectedChildren: Array<number>;
highlightedChildren: Array<number>;
constructor(props: Props) {
super(props);
this.state = {
mouseDown: false,
startPoint: null,
endPoint: null,
selectionBox: null,
offset: getOffset(props),
zoom: props.zoom || 1,
};
this.selectedChildren = [];
this.highlightedChildren = [];
}
componentDidMount() {
this.reset();
this.bind();
}
componentWillReceiveProps(nextProps: Props) {
this.setState({
offset: getOffset(nextProps),
});
}
componentDidUpdate() {
this.reset();
this.bind();
if (this.state.mouseDown && this.state.selectionBox) {
this.updateCollidingChildren(this.state.selectionBox);
}
}
componentWillUnmount() {
this.reset();
window.document.removeEventListener('mousemove', this.onMouseMove);
window.document.removeEventListener('mouseup', this.onMouseUp);
}
bind = () => {
this.props.target.addEventListener('mousedown', this.onMouseDown);
this.props.target.addEventListener('touchstart', this.onTouchStart);
};
reset = () => {
if (this.props.target) {
this.props.target.removeEventListener('mousedown', this.onMouseDown);
}
};
init = (e: Event, x: number, y: number): boolean => {
if (this.props.ignoreTargets) {
const Target = (e.target: any);
if (!Target.matches) {
// polyfill matches
const defaultMatches = (s: string) => (
[].indexOf.call(window.document.querySelectorAll(s), this) !== -1
);
Target.matches =
Target.matchesSelector ||
Target.mozMatchesSelector ||
Target.msMatchesSelector ||
Target.oMatchesSelector ||
Target.webkitMatchesSelector ||
defaultMatches;
}
if (Target.matches && Target.matches(this.props.ignoreTargets.join(','))) {
return false;
}
}
const nextState = {};
nextState.mouseDown = true;
nextState.startPoint = {
x: (x - this.state.offset.left) / this.state.zoom,
y: (y - this.state.offset.top) / this.state.zoom,
};
this.setState(nextState);
return true;
};
/**
* On root element mouse down
* The event should be a MouseEvent | TouchEvent, but flow won't get it...
* @private
*/
onMouseDown = (e: MouseEvent | any) => {
if (this.props.disabled || e.button === 2 || (e.nativeEvent && e.nativeEvent.which === 2)) {
return;
}
if (this.init(e, e.pageX, e.pageY)) {
window.document.addEventListener('mousemove', this.onMouseMove);
window.document.addEventListener('mouseup', this.onMouseUp);
}
};
onTouchStart = (e: TouchEvent) => {
if (this.props.disabled || !e.touches || !e.touches[0] || e.touches.length > 1) {
return;
}
if (this.init(e, e.touches[0].pageX, e.touches[0].pageY)) {
window.document.addEventListener('touchmove', this.onTouchMove);
window.document.addEventListener('touchend', this.onMouseUp);
}
};
/**
* On document element mouse up
* @private
*/
onMouseUp = () => {
window.document.removeEventListener('touchmove', this.onTouchMove);
window.document.removeEventListener('mousemove', this.onMouseMove);
window.document.removeEventListener('mouseup', this.onMouseUp);
window.document.removeEventListener('touchend', this.onMouseUp);
this.setState({
mouseDown: false,
startPoint: null,
endPoint: null,
selectionBox: null,
});
if (this.props.onSelectionChange) {
this.props.onSelectionChange(this.selectedChildren);
}
if (this.props.onHighlightChange) {
this.highlightedChildren = [];
this.props.onHighlightChange(this.highlightedChildren);
}
this.selectedChildren = [];
};
/**
* On document element mouse move
* @private
*/
onMouseMove = (e: MouseEvent) => {
e.preventDefault();
if (this.state.mouseDown) {
const endPoint: Point = {
x: (e.pageX - this.state.offset.left) / this.state.zoom,
y: (e.pageY - this.state.offset.top) / this.state.zoom,
};
this.setState({
endPoint,
selectionBox: this.calculateSelectionBox(
this.state.startPoint,
endPoint
),
});
}
};
onTouchMove = (e: TouchEvent) => {
e.preventDefault();
if (this.state.mouseDown) {
const endPoint: Point = {
x: (e.touches[0].pageX - this.state.offset.left) / this.state.zoom,
y: (e.touches[0].pageY - this.state.offset.top) / this.state.zoom,
};
this.setState({
endPoint,
selectionBox: this.calculateSelectionBox(
this.state.startPoint,
endPoint
),
});
}
};
/**
* Calculate if two segments overlap in 1D
* @param lineA [min, max]
* @param lineB [min, max]
*/
lineIntersects = (lineA: [number, number], lineB: [number, number]): boolean => (
lineA[1] >= lineB[0] && lineB[1] >= lineA[0]
);
/**
* Detect 2D box intersection - the two boxes will intersect
* if their projections to both axis overlap
* @private
*/
boxIntersects = (boxA: Box, boxB: Box): boolean => {
// calculate coordinates of all points
const boxAProjection = {
x: [boxA.left, boxA.left + boxA.width],
y: [boxA.top, boxA.top + boxA.height],
};
const boxBProjection = {
x: [boxB.left, boxB.left + boxB.width],
y: [boxB.top, boxB.top + boxB.height],
};
return this.lineIntersects(boxAProjection.x, boxBProjection.x) &&
this.lineIntersects(boxAProjection.y, boxBProjection.y);
};
/**
* Updates the selected items based on the
* collisions with selectionBox,
* also updates the highlighted items if they have changed
* @private
*/
updateCollidingChildren = (selectionBox: Box) => {
this.selectedChildren = [];
if (this.props.elements) {
this.props.elements.forEach((ref, $index) => {
if (ref) {
const refBox = ref.getBoundingClientRect();
const tmpBox = {
top: ((refBox.top - this.state.offset.top) + window.scrollY) / this.state.zoom,
left: ((refBox.left - this.state.offset.left) + window.scrollX) / this.state.zoom,
width: ref.clientWidth,
height: ref.clientHeight,
};
if (this.boxIntersects(selectionBox, tmpBox)) {
this.selectedChildren.push($index);
}
}
});
}
if (this.props.onHighlightChange && JSON.stringify(this.highlightedChildren) !== JSON.stringify(this.selectedChildren)) {
const { onHighlightChange } = this.props;
this.highlightedChildren = [...this.selectedChildren];
if (window.requestAnimationFrame) {
window.requestAnimationFrame(() => {
onHighlightChange(this.highlightedChildren);
});
} else {
onHighlightChange(this.highlightedChildren);
}
}
};
/**
* Calculate selection box dimensions
* @private
*/
calculateSelectionBox = (startPoint: ?Point, endPoint: ?Point) => {
if (!this.state.mouseDown || !startPoint || !endPoint) {
return null;
}
// The extra 1 pixel is to ensure that the mouse is on top
// of the selection box and avoids triggering clicks on the target.
const left = Math.min(startPoint.x, endPoint.x) - 1;
const top = Math.min(startPoint.y, endPoint.y) - 1;
const width = Math.abs(startPoint.x - endPoint.x) + 1;
const height = Math.abs(startPoint.y - endPoint.y) + 1;
return {
left,
top,
width,
height,
};
};
/**
* Render
*/
render() {
let style: any = {
position: 'absolute',
background: 'rgba(159, 217, 255, 0.3)',
border: 'solid 1px rgba(123, 123, 123, 0.61)',
zIndex: 9,
cursor: 'crosshair',
...this.props.style,
};
if (this.state.selectionBox) {
style = {
...style,
...this.state.selectionBox,
};
}
if (!this.state.mouseDown || !this.state.endPoint || !this.state.startPoint) {
return null;
}
return (
<div className='react-ds-border' style={ style } />
);
}
}
Selection.propTypes = {
target: PropTypes.object,
disabled: PropTypes.bool,
onSelectionChange: PropTypes.func.isRequired,
onHighlightChange: PropTypes.func,
elements: PropTypes.array.isRequired,
// eslint-disable-next-line react/no-unused-prop-types
offset: PropTypes.object,
zoom: PropTypes.number,
style: PropTypes.object,
ignoreTargets: PropTypes.array,
};
================================================
FILE: example/app/.gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: example/app/README.md
================================================
Example application using `create-react-app`.
## NPM Commands
* `npm start` - to run locally
* `npm build` - to generate a build
Unless you're developing the component here you probably shouldn't be editing this.
================================================
FILE: example/app/package.json
================================================
{
"name": "app",
"version": "0.1.0",
"private": true,
"homepage": "https://aurbano.eu/react-ds/",
"dependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-ds": "file:../../",
"react-scripts": "1.0.14"
},
"scripts": {
"build:less": "./node_modules/.bin/lessc src/example.less src/example.css",
"start": "npm run build:less && react-scripts start",
"build": "npm run build:less && react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"postinstall": "linklocal"
},
"devDependencies": {
"less": "^2.7.2"
}
}
================================================
FILE: example/app/public/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
<!--<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">-->
<meta name="keywords" content="react,reactjs,react component,component,select,control,ui,javascript,drag,selection,items">
<meta name="description" content="Simple React Drag-to-Select component with mouse and touch support, and 0 dependencies">
<meta property="og:locale" content="en-us">
<meta property="og:title" content="React-DS">
<meta property="og:description" content="Simple React Drag-to-Select component with mouse and touch support, and 0 dependencies">
<meta property="og:url" content="https://aurbano.eu/react-ds">
<meta property="og:site_name" content="React-DS">
<meta property="og:type" content="article">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React DS Example</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<header class="page-header">
<div class="container">
<h1>React DS</h1>
<p>Tiny (7KB) <a href="http://facebook.github.io/react/" target="_blank">ReactJS</a> Drag-to-Select component (with no dependencies! with support for touch devices!</p>
</div>
</header>
<div class="page-subheader">
<div class="container">
<a href="http://github.com/aurbano/react-ds" class="page-subheader__link" target="_blank">Code and Docs on GitHub</a>
<span class="page-subheader__button">
<a id="github-stars-button" class="github-button" data-style="" href="https://github.com/aurbano/react-ds" data-count-href="/aurbano/react-ds/stargazers" data-count-api="/repos/aurbano/react-ds#stargazers_count" data-count-aria-label="# stargazers on GitHub" aria-label="Star aurbano/react-ds on GitHub">Star</a>
</span>
</div>
</div>
<div class="page-body">
<div class="container">
<form>
<!-- the React application is loaded in the #root element -->
<div id="root"></div>
</form>
</div>
</div>
<footer class="page-footer">
<div class="container">
<span>Copyright © <a href="https://aurbano.eu" target="_blank">Alejandro U. Alvarez</a> 2016. MIT Licensed.</span>
</div>
</footer>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script async defer id="github-bjs" src="https://buttons.github.io/buttons.js"></script>
</body>
</html>
================================================
FILE: example/app/public/manifest.json
================================================
{
"short_name": "React DS",
"name": "React DS Example",
"icons": [
{
"src": "favicon.ico",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: example/app/src/Example.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Selection from 'react-ds';
export default class Example extends React.PureComponent {
constructor() {
super();
this.state = {
ref: null,
elRefs: [],
selectedElements: [], // track the elements that are selected
};
}
handleSelection = (indexes) => { // eslint-disable-line no-undef
this.setState({
selectedElements: indexes,
});
};
getStyle = (index) => { // eslint-disable-line no-undef
if (this.state.selectedElements.indexOf(index) > -1) {
// Selected state
return {
background: '#2185d0',
borderColor: '#2185d0',
color: 'white',
};
}
return {};
};
addElementRef = (ref) => { // eslint-disable-line no-undef
const elRefs = this.state.elRefs;
elRefs.push(ref);
this.setState({
elRefs,
});
};
renderSelection() {
if (!this.state.ref || !this.state.elRefs) {
return null;
}
return (
<Selection
target={ this.state.ref}
elements={ this.state.elRefs }
onSelectionChange={ this.handleSelection }
style={ this.props.style }
ignoreTargets={ this.props.ignoreTargets }
/>
);
}
render() {
const selectableElements = [
'one',
'another',
'hey there',
'item',
'two',
'three',
'something longer?',
'last'
];
return (
<div ref={ (ref) => { this.setState({ ref }); } } className='item-container'>
{ selectableElements.map((el, index) => (
<div
key={ el }
ref={ this.addElementRef }
style={ this.getStyle(index) }
className={ `item item-${el}` }
>
{ el }
</div>
)) }
{ this.renderSelection() }
</div>
);
}
}
Example.PropTypes = {
style: PropTypes.object,
ignoreTargets: PropTypes.array,
};
================================================
FILE: example/app/src/Examples.js
================================================
import React from 'react';
import Example from './Example';
export default class Examples extends React.PureComponent {
render() {
return (
<div>
<h2>
<a href="https://github.com/aurbano/react-ds/blob/master/example/app/src/Example.js" className="right">
<code><Source></code>
</a>
Example
</h2>
<p>The box below is the <code>target</code> for the Selection component. You'll see that you can't initiate selections outside of it.</p>
<Example />
<h2 style={ { marginTop: '2em' } }>Custom styles</h2>
<p>Here's an example using custom styles for the selection box. These are simply passed on the <code>style</code> prop of the <code>Selection</code> component.</p>
<Example style={ { background: 'rgba(0,0,0,0.3)' } } />
<h2 style={ { marginTop: '2em' } }>Ignore certain targets</h2>
<p>You can also ignore certain targets from initiating selects. This is specially useful because <code>react-ds</code> uses native events which bypass React's synthetic event queue.</p>
<p>In this example, events initiated over the item <b>"three"</b> are ignored.</p>
<Example ignoreTargets={ ['.item-three'] }/>
</div>
);
}
}
================================================
FILE: example/app/src/example.css
================================================
body {
color: #333;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
}
a {
color: #007eff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
padding: 0.2em 0.5em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 600px;
padding: 0 30px;
}
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
color: black;
font-weight: 500;
line-height: 1;
margin-bottom: .66em;
margin-top: 0;
}
h1 .right,
h2 .right,
h3 .right,
h4 .right,
h5 .right,
h6 .right,
.h1 .right,
.h2 .right,
.h3 .right,
.h4 .right,
.h5 .right,
.h6 .right {
font-size: 0.6em;
}
h1,
.h1 {
font-size: 3em;
}
h2,
.h2 {
font-size: 2em;
font-weight: 300;
}
h3,
.h3 {
font-size: 1.25em;
}
h4,
.h4 {
font-size: 1em;
}
h5,
.h5 {
font-size: .85em;
}
h6,
.h6 {
font-size: .75em;
}
.page-body,
.page-footer,
.page-header {
padding: 30px 0;
}
.page-header {
background: linear-gradient(135deg, #F97794 0%, #623AA2 100%);
color: #d8cee8;
}
.page-header h1,
.page-header h2,
.page-header h3 {
color: white;
}
.page-header p {
font-size: 1.2em;
margin: 0;
}
.page-header a {
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
color: white;
text-decoration: none;
}
.page-header a:hover,
.page-header a:focus {
border-bottom-color: white;
outline: none;
text-decoration: none;
}
.right {
float: right;
position: relative;
bottom: -0.2em;
}
.page-subheader {
background-color: #e6f2ff;
line-height: 20px;
padding: 30px 0;
}
.page-subheader__button {
float: right;
}
.page-subheader__link {
border-bottom: 1px solid rgba(0, 126, 255, 0.3);
outline: none;
text-decoration: none;
}
.page-subheader__link:hover,
.page-subheader__link:focus {
border-bottom-color: #007eff;
outline: none;
text-decoration: none;
}
.page-footer {
background-color: #fafafa;
color: #999;
padding: 30px 0;
text-align: center;
}
.page-footer a {
color: black;
}
@media (min-width: 480px) {
.page-body,
.page-header {
padding: 60px 0;
}
.page-header {
font-size: 1.4em;
}
.page-subheader {
font-size: 1.125em;
line-height: 28px;
}
}
.switcher {
color: #999;
cursor: default;
font-size: 12px;
margin: 10px 0;
text-transform: uppercase;
}
.switcher .link {
color: #007eff;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
}
.switcher .link:hover {
text-decoration: underline;
}
.switcher .active {
color: #666;
font-weight: bold;
margin-left: 10px;
}
.section {
margin-bottom: 40px;
}
.hint {
font-size: .85em;
margin: 15px 0;
color: #666;
}
.virtual-scroll {
z-index: 1;
}
================================================
FILE: example/app/src/example.less
================================================
//
// Common Example Styles
// ------------------------------
// Constants
// ------------------------------
// example site
@heading-color: black;
@text-color: #333;
@link-color: #007eff;
@gutter: 30px;
@headerStart: #F97794;
@headerEnd: #623AA2;
@headerBackground: linear-gradient( 135deg, @headerStart 0%, @headerEnd 100%);
// Base
// ------------------------------
body {
color: @text-color;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
}
a {
color: @link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
code {
padding: 0.2em 0.5em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 600px;
padding: 0 @gutter;
}
// Headings
// ------------------------------
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
color: @heading-color;
font-weight: 500;
line-height: 1;
margin-bottom: .66em;
margin-top: 0;
.right {
font-size: 0.6em;
}
}
h1, .h1 {
font-size: 3em;
}
h2, .h2 {
font-size: 2em;
font-weight: 300;
}
h3, .h3 {
font-size: 1.25em;
// text-transform: uppercase;
}
h4, .h4 {
font-size: 1em;
}
h5, .h5 {
font-size: .85em;
}
h6, .h6 {
font-size: .75em;
}
// Layout
// ------------------------------
// common
.page-body,
.page-footer,
.page-header {
padding: @gutter 0;
}
// header
.page-header {
background: @headerBackground;
color: mix(white, @headerEnd, 75%);
h1, h2, h3 {
color: white;
}
p {
font-size: 1.2em;
margin: 0;
}
a {
border-bottom: 1px solid fade(white, 30%);
color: white;
text-decoration: none;
&:hover,
&:focus {
border-bottom-color: white;
outline: none;
text-decoration: none;
}
}
}
.right {
float: right;
position: relative;
bottom: -0.2em;
}
// subheader
.page-subheader {
background-color: mix(@link-color, white, 10%);
line-height: 20px;
padding: @gutter 0;
}
.page-subheader__button {
float: right;
}
.page-subheader__link {
border-bottom: 1px solid fade(@link-color, 30%);
outline: none;
text-decoration: none;
&:hover,
&:focus {
border-bottom-color: @link-color;
outline: none;
text-decoration: none;
}
}
// footer
.page-footer {
background-color: #fafafa;
// border-top: 1px solid #eee;
color: #999;
padding: @gutter 0;
text-align: center;
a {
color: black;
}
}
// layout changes based on screen dimensions
@media (min-width: 480px) {
.page-body,
.page-header {
padding: (@gutter * 2) 0;
}
.page-header {
font-size: 1.4em;
}
.page-subheader {
font-size: 1.125em;
line-height: 28px;
}
}
// Switcher
// ------------------------------
.switcher {
color: #999;
cursor: default;
font-size: 12px;
margin: 10px 0;
text-transform: uppercase;
.link {
color: @link-color;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
&:hover {
text-decoration: underline;
}
}
.active {
color: #666;
font-weight: bold;
margin-left: 10px;
}
}
// Miscellaneous
// ------------------------------
.section {
margin-bottom: 40px;
}
.hint {
font-size: .85em;
margin: 15px 0;
color: #666;
}
.virtual-scroll {
z-index: 1;
}
================================================
FILE: example/app/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import './example.css';
import './react-ds.css';
import Examples from './Examples';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<Examples />, document.getElementById('root'));
registerServiceWorker();
================================================
FILE: example/app/src/react-ds.css
================================================
.item-container {
border: solid 1px #ccc;
position: relative;
box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);
margin: 1rem 0;
padding: 1em 1em;
border-radius: .28571429rem;
}
.item {
display: inline-block;
min-width: 40px;
min-height: 40px;
border: solid 1px #ccc;
text-align: center;
padding: 1em;
margin: 1em;
border-radius: .28571429rem;
}
================================================
FILE: example/app/src/registerServiceWorker.js
================================================
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (!isLocalhost) {
// Is not local host. Just register service worker
registerValidSW(swUrl);
} else {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
================================================
FILE: package.json
================================================
{
"name": "react-ds",
"version": "1.13.0",
"description": "Simple React Drag-to-Select component",
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"install:example": "cd example/app && npm install",
"build": "npm run build:clean && cross-env NODE_ENV=production npm run build:dist && npm run build:flow && npm run build:minify",
"build:dev": "npm run build:clean && cross-env NODE_ENV=production npm run build:dist && npm run build:flow",
"build:clean": "rimraf dist",
"build:dist": "babel -d dist src --ignore '**/__tests__/**'",
"build:flow": "flow-copy-source -v -i '**/__tests__/**' src dist",
"build:minify": "./node_modules/.bin/uglifyjs dist/index.js -o dist/index.js",
"lint": "npm run lint:js",
"lint:eslint": "eslint",
"lint:js": "npm run lint:eslint -- . ",
"lint:fix": "npm run lint:eslint -- --fix . ",
"lint:staged": "lint-staged && npm run flow",
"pretest": "npm run test:clean && npm run lint && npm run flow",
"test:clean": "rimraf ./coverage",
"test": "cross-env NODE_ENV=test jest --coverage",
"test:watch": "cross-env NODE_ENV=test jest --watchAll",
"flow": "flow status",
"changelog": "github_changelog_generator -u aurbano -p react-ds",
"start": "cd example/app && npm start",
"deploy": "cd example/app && npm run build && gh-pages -d build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aurbano/react-ds.git"
},
"keywords": [
"react",
"select",
"drag",
"component",
"library",
"items",
"selection"
],
"author": "Alejandro U. Alvarez",
"license": "MIT",
"bugs": {
"url": "https://github.com/aurbano/react-ds/issues"
},
"homepage": "https://github.com/aurbano/react-ds#readme",
"peerDependencies": {
"react": "^16.3"
},
"devDependencies": {
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-eslint": "8.2.2",
"babel-jest": "^23.0.0-alpha.0",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-flow-react-proptypes": "^12.1.0",
"babel-plugin-flow-runtime": "^0.17.0",
"babel-plugin-react-intl": "^2.4.0",
"babel-plugin-react-transform": "3.0.0",
"babel-plugin-transform-es2015-destructuring": "^6.23.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-react-inline-elements": "^6.22.0",
"babel-plugin-transform-react-jsx-source": "^6.22.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-preset-env": "^1.7.0",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"codacy-coverage": "^2.1.0",
"coveralls": "^3.0.8",
"cross-env": "^5.2.1",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"eslint": "^4.19.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.16.0",
"flow-bin": "^0.112.0",
"flow-copy-source": "^1.3.0",
"flow-runtime": "^0.17.0",
"gh-pages": "^1.1.0",
"gulp": "^3.9.1",
"jasmine": "^3.5.0",
"jasmine-reporters": "^2.3.2",
"jest": "^22.4.2",
"jest-cli": "^23.0.0-alpha.0",
"lint-staged": "^9.4.3",
"lodash": "^4.17.15",
"pre-commit": "^1.2.2",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-test-renderer": "^16.12.0",
"rimraf": "^2.6.2",
"sinon": "^4.4.6",
"uglify-es": "^3.3.9"
},
"lint-staged": {
"*.js": "npm run lint:eslint"
},
"pre-commit": "lint:staged",
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"moduleDirectories": [
"node_modules",
"src"
],
"unmockedModulePathPatterns": [
"./node_modules/react",
"./node_modules/react-addons-test-utils",
"./node_modules/jasmine-reporters"
],
"setupTestFrameworkScriptFile": "<rootDir>/test/util/test-bundler.js",
"testRegex": ".*\\.test\\.js$"
}
}
================================================
FILE: src/index.js
================================================
// @flow
import React from 'react';
import PropTypes from 'prop-types';
export type Point = {
x: number,
y: number,
}
export type Box = {
left: number,
top: number,
width: number,
height: number,
}
type Props = {
disabled?: boolean,
confineSelectionBox?: boolean,
target: HTMLElement,
onSelectionChange?: (elements: Array<any>) => void,
onHighlightChange?: (elements: Array<any>) => void,
elements: Array<HTMLElement>,
// eslint-disable-next-line react/no-unused-prop-types
offset?: {
// eslint-disable-next-line react/no-unused-prop-types
top: number,
// eslint-disable-next-line react/no-unused-prop-types
left: number,
},
style?: any,
zoom?: number,
ignoreTargets?: Array<string>,
};
type State = {
mouseDown: boolean,
startPoint: ?Point,
endPoint: ?Point,
selectionBox: ?Box,
offset: {
top: number,
left: number,
},
zoom: number,
};
function getOffset(props: Props) {
let offset = {
top: 0,
left: 0,
};
if (props.offset) {
offset = {
...props.offset,
};
} else if (props.target) {
const boundingBox = props.target.getBoundingClientRect();
offset.top = boundingBox.top + window.scrollY;
offset.left = boundingBox.left + window.scrollX;
}
return offset;
}
export default class Selection extends React.PureComponent<Props, State> { // eslint-disable-line react/prefer-stateless-function
props: Props;
state: State;
selectedChildren: Array<number>;
highlightedChildren: Array<number>;
constructor(props: Props) {
super(props);
this.state = {
mouseDown: false,
startPoint: null,
endPoint: null,
selectionBox: null,
offset: getOffset(props),
zoom: props.zoom || 1,
};
this.selectedChildren = [];
this.highlightedChildren = [];
}
static getDerivedStateFromProps(nextProps: Props) {
return {
offset: getOffset(nextProps),
};
}
componentDidMount() {
this.reset();
this.bind();
}
componentDidUpdate() {
this.reset();
this.bind();
if (this.state.mouseDown && this.state.selectionBox) {
this.updateCollidingChildren(this.state.selectionBox);
}
}
componentWillUnmount() {
this.reset();
window.document.removeEventListener('mousemove', this.onMouseMove);
window.document.removeEventListener('mouseup', this.onMouseUp);
}
bind = () => {
this.props.target.addEventListener('mousedown', this.onMouseDown);
this.props.target.addEventListener('touchstart', this.onTouchStart);
};
reset = () => {
if (this.props.target) {
this.props.target.removeEventListener('mousedown', this.onMouseDown);
}
};
init = (e: Event, x: number, y: number): boolean => {
if (this.props.ignoreTargets) {
const Target = (e.target: any);
if (!Target.matches) {
// polyfill matches
const defaultMatches = (s: string) => (
[].indexOf.call(window.document.querySelectorAll(s), this) !== -1
);
Target.matches =
Target.matchesSelector ||
Target.mozMatchesSelector ||
Target.msMatchesSelector ||
Target.oMatchesSelector ||
Target.webkitMatchesSelector ||
defaultMatches;
}
if (Target.matches && Target.matches(this.props.ignoreTargets.join(','))) {
return false;
}
}
const nextState = {};
nextState.mouseDown = true;
nextState.startPoint = {
x: (x - this.state.offset.left) / this.state.zoom,
y: (y - this.state.offset.top) / this.state.zoom,
};
this.setState(nextState);
return true;
};
/**
* On root element mouse down
* The event should be a MouseEvent | TouchEvent, but flow won't get it...
* @private
*/
onMouseDown = (e: MouseEvent | any) => {
if (this.props.disabled || e.button === 2 || (e.nativeEvent && e.nativeEvent.which === 2)) {
return;
}
if (this.init(e, e.pageX, e.pageY)) {
window.document.addEventListener('mousemove', this.onMouseMove);
window.document.addEventListener('mouseup', this.onMouseUp);
}
};
onTouchStart = (e: TouchEvent) => {
if (this.props.disabled || !e.touches || !e.touches[0] || e.touches.length > 1) {
return;
}
if (this.init(e, e.touches[0].pageX, e.touches[0].pageY)) {
window.document.addEventListener('touchmove', this.onTouchMove);
window.document.addEventListener('touchend', this.onMouseUp);
}
};
/**
* On document element mouse up
* @private
*/
onMouseUp = () => {
window.document.removeEventListener('touchmove', this.onTouchMove);
window.document.removeEventListener('mousemove', this.onMouseMove);
window.document.removeEventListener('mouseup', this.onMouseUp);
window.document.removeEventListener('touchend', this.onMouseUp);
this.setState({
mouseDown: false,
startPoint: null,
endPoint: null,
selectionBox: null,
});
if (this.props.onSelectionChange) {
this.props.onSelectionChange(this.selectedChildren);
}
if (this.props.onHighlightChange) {
this.highlightedChildren = [];
this.props.onHighlightChange(this.highlightedChildren);
}
this.selectedChildren = [];
};
/**
* On document element mouse move
* @private
*/
onMouseMove = (e: MouseEvent) => {
e.preventDefault();
if (this.state.mouseDown) {
const endPoint: Point = {
x: (e.pageX - this.state.offset.left) / this.state.zoom,
y: (e.pageY - this.state.offset.top) / this.state.zoom,
};
this.setState({
endPoint,
selectionBox: this.calculateSelectionBox(
this.state.startPoint,
endPoint
),
});
}
};
onTouchMove = (e: TouchEvent) => {
e.preventDefault();
if (this.state.mouseDown) {
const endPoint: Point = {
x: (e.touches[0].pageX - this.state.offset.left) / this.state.zoom,
y: (e.touches[0].pageY - this.state.offset.top) / this.state.zoom,
};
this.setState({
endPoint,
selectionBox: this.calculateSelectionBox(
this.state.startPoint,
endPoint
),
});
}
};
/**
* Calculate if two segments overlap in 1D
* @param lineA [min, max]
* @param lineB [min, max]
*/
lineIntersects = (lineA: [number, number], lineB: [number, number]): boolean => (
lineA[1] >= lineB[0] && lineB[1] >= lineA[0]
);
/**
* Detect 2D box intersection - the two boxes will intersect
* if their projections to both axis overlap
* @private
*/
boxIntersects = (boxA: Box, boxB: Box): boolean => {
// calculate coordinates of all points
const boxAProjection = {
x: [boxA.left, boxA.left + boxA.width],
y: [boxA.top, boxA.top + boxA.height],
};
const boxBProjection = {
x: [boxB.left, boxB.left + boxB.width],
y: [boxB.top, boxB.top + boxB.height],
};
return this.lineIntersects(boxAProjection.x, boxBProjection.x) &&
this.lineIntersects(boxAProjection.y, boxBProjection.y);
};
/**
* Updates the selected items based on the
* collisions with selectionBox,
* also updates the highlighted items if they have changed
* @private
*/
updateCollidingChildren = (selectionBox: Box) => {
this.selectedChildren = [];
if (this.props.elements) {
this.props.elements.forEach((ref, $index) => {
if (ref) {
const refBox = ref.getBoundingClientRect();
const tmpBox = {
top: ((refBox.top - this.state.offset.top) + window.scrollY) / this.state.zoom,
left: ((refBox.left - this.state.offset.left) + window.scrollX) / this.state.zoom,
width: ref.clientWidth,
height: ref.clientHeight,
};
if (this.boxIntersects(selectionBox, tmpBox)) {
this.selectedChildren.push($index);
}
}
});
}
if (this.props.onHighlightChange && JSON.stringify(this.highlightedChildren) !== JSON.stringify(this.selectedChildren)) {
const { onHighlightChange } = this.props;
this.highlightedChildren = [...this.selectedChildren];
if (window.requestAnimationFrame) {
window.requestAnimationFrame(() => {
onHighlightChange(this.highlightedChildren);
});
} else {
onHighlightChange(this.highlightedChildren);
}
}
};
/**
* Calculate selection box dimensions
* @private
*/
calculateSelectionBox = (startPoint: ?Point, endPoint: ?Point) => {
if (!this.state.mouseDown || !startPoint || !endPoint) {
return null;
}
let left, top, width, height = 0;
if (this.props.confineSelectionBox) {
var refBox = this.props.target.getBoundingClientRect();
// The extra 1 pixel is to ensure that the mouse is on top
// of the selection box and avoids triggering clicks on the target.
left = Math.max(0, Math.min(startPoint.x, endPoint.x)) - 1;
top = Math.max(0, Math.min(startPoint.y, endPoint.y)) - 1;
width = (startPoint.x < endPoint.x ? Math.min(refBox.width - startPoint.x, Math.abs(startPoint.x - endPoint.x)) : Math.min(startPoint.x, Math.abs(startPoint.x - endPoint.x))) + 1;
height = (startPoint.y < endPoint.y ? Math.min(refBox.height - startPoint.y, Math.abs(startPoint.y - endPoint.y)) : Math.min(startPoint.y, Math.abs(startPoint.y - endPoint.y))) + 1;
} else {
// The extra 1 pixel is to ensure that the mouse is on top
// of the selection box and avoids triggering clicks on the target.
left = Math.min(startPoint.x, endPoint.x) - 1;
top = Math.min(startPoint.y, endPoint.y) - 1;
width = Math.abs(startPoint.x - endPoint.x) + 1;
height = Math.abs(startPoint.y - endPoint.y) + 1;
}
return {
left,
top,
width,
height,
};
};
/**
* Render
*/
render() {
let style: any = {
position: 'absolute',
background: 'rgba(159, 217, 255, 0.3)',
border: 'solid 1px rgba(123, 123, 123, 0.61)',
zIndex: 9,
cursor: 'crosshair',
...this.props.style,
};
if (this.state.selectionBox) {
style = {
...style,
...this.state.selectionBox,
};
}
if (!this.state.mouseDown || !this.state.endPoint || !this.state.startPoint) {
return null;
}
return (
<div className='react-ds-border' style={ style } />
);
}
}
Selection.propTypes = {
target: PropTypes.object,
confineSelectionBox: PropTypes.bool,
disabled: PropTypes.bool,
onSelectionChange: PropTypes.func.isRequired,
onHighlightChange: PropTypes.func,
elements: PropTypes.array.isRequired,
// eslint-disable-next-line react/no-unused-prop-types
offset: PropTypes.object,
zoom: PropTypes.number,
style: PropTypes.object,
ignoreTargets: PropTypes.array,
};
================================================
FILE: test/index.test.js
================================================
import React from 'react';
import { shallow, mount } from 'enzyme';
import clone from 'lodash/clone';
import Selection from '../src/index';
const mockTarget = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const defaultProps = {
target: mockTarget,
disabled: false,
onSelectionChange: () => {},
elements: [],
offset: {
top: 0,
left: 0,
},
zoom: 1,
};
const initialState = {
mouseDown: false,
startPoint: null,
endPoint: null,
selectionBox: null,
offset: defaultProps.offset,
zoom: defaultProps.zoom,
};
describe('<Selection />', () => {
it('Should not render when not selecting', () => {
const props = defaultProps;
const renderedComponent = shallow(
<Selection { ...props } />
);
expect(renderedComponent.find('div.selection')).toHaveLength(0);
});
it('onMouseDown: Should start tracking', () => {
const mouseEvent = {
pageX: 100,
pageY: 400,
button: 1,
nativeEvent: {
which: 1,
},
};
mockTarget.addEventListener.mockClear();
mockTarget.removeEventListener.mockClear();
const renderedComponent = shallow(
<Selection { ...defaultProps } />
);
expect(renderedComponent.state()).toEqual(initialState);
renderedComponent.instance().onMouseDown(mouseEvent);
const mouseDownState = clone(initialState);
mouseDownState.startPoint = {
x: mouseEvent.pageX,
y: mouseEvent.pageY,
};
mouseDownState.mouseDown = true;
expect(renderedComponent.state()).toEqual(mouseDownState);
});
it('onMouseUp: Should stop tracking', () => {
const mouseEvent = {
pageX: 100,
pageY: 400,
button: 1,
nativeEvent: {
which: 1,
},
};
mockTarget.addEventListener.mockClear();
mockTarget.removeEventListener.mockClear();
const renderedComponent = shallow(
<Selection { ...defaultProps } />
);
renderedComponent.instance().onMouseDown(mouseEvent);
renderedComponent.instance().onMouseUp();
expect(renderedComponent.state()).toEqual(initialState);
});
it('onMouseMove: Should update selection box', () => {
const mouseEvent = {
pageX: 100,
pageY: 400,
button: 1,
nativeEvent: {
which: 1,
},
};
mockTarget.addEventListener.mockClear();
mockTarget.removeEventListener.mockClear();
const moveEvent = clone(mouseEvent);
moveEvent.pageX = -400;
moveEvent.pageY = -100;
moveEvent.preventDefault = jest.fn();
// Using mount because we need the selectionBox ref to exist
const renderedComponent = mount(
<Selection { ...defaultProps } />
);
renderedComponent.instance().onMouseDown(mouseEvent);
renderedComponent.instance().onMouseMove(moveEvent);
const moveState = clone(initialState);
moveState.endPoint = {
x: moveEvent.pageX,
y: moveEvent.pageY,
};
moveState.mouseDown = true;
moveState.selectionBox = {
height: Math.abs(moveEvent.pageX) + mouseEvent.pageX + 1,
width: Math.abs(moveEvent.pageY) + mouseEvent.pageY + 1,
left: moveEvent.pageX - 1,
top: moveEvent.pageY - 1,
};
moveState.startPoint = {
x: mouseEvent.pageX,
y: mouseEvent.pageY,
};
expect(renderedComponent.state()).toEqual(moveState);
});
it('boxIntersects: Calculates box intersections', () => {
const renderedComponent = shallow(
<Selection { ...defaultProps } />
);
const boxOne = {
left: 0,
top: 0,
width: 10,
height: 10,
};
const boxTwo = {
left: -5,
top: 0,
width: 10,
height: 10,
};
const boxThree = {
left: 40,
top: 10,
width: 10,
height: 10,
};
expect(renderedComponent.instance().boxIntersects(boxOne, boxTwo)).toBeTruthy();
expect(renderedComponent.instance().boxIntersects(boxOne, boxThree)).toBeFalsy();
});
it('updateCollidingChildren: Updates colliding children', () => {
const props = clone(defaultProps);
const refs = [];
const elementBoxes = [
{
top: 0,
left: 0,
width: 10,
height: 10,
},
{
top: 100,
left: 300,
width: 10,
height: 10,
},
];
// render the elements to get boxes for them
mount(
<div>
{ elementBoxes.map((box, $index) => (
<div ref={ (element) => { refs.push(element); } } key={ $index } style={ { position: 'absolute', ...box } } />
)) }
</div>
);
props.elements = refs;
const renderedComponent = shallow(
<Selection { ...props } />
);
const box = {
left: -50,
top: -50,
width: 100,
height: 100,
};
renderedComponent.instance().updateCollidingChildren(box);
expect(renderedComponent.instance().selectedChildren).toEqual([0, 1]);
});
});
================================================
FILE: test/util/test-bundler.js
================================================
/* globals jasmine */
// needed for regenerator-runtime
// (ES7 generator support is required by redux-saga)
import 'babel-polyfill';
// Enzyme setup for React 16
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
const reporters = require('jasmine-reporters');
jasmine.VERBOSE = true;
jasmine.getEnv().addReporter(
new reporters.JUnitXmlReporter({
savePath: 'test-report',
})
);
configure({ adapter: new Adapter() });
gitextract_m4l_eywj/
├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .flowconfig
├── .gitignore
├── .travis.yml
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│ ├── index.js
│ └── index.js.flow
├── example/
│ └── app/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ └── manifest.json
│ └── src/
│ ├── Example.js
│ ├── Examples.js
│ ├── example.css
│ ├── example.less
│ ├── index.js
│ ├── react-ds.css
│ └── registerServiceWorker.js
├── package.json
├── src/
│ └── index.js
└── test/
├── index.test.js
└── util/
└── test-bundler.js
SYMBOL INDEX (20 symbols across 5 files)
FILE: dist/index.js
function defineProperties (line 1) | function defineProperties(target,props){for(var i=0;i<props.length;i++){...
function _interopRequireDefault (line 1) | function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{def...
function _toConsumableArray (line 1) | function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2...
function _classCallCheck (line 1) | function _classCallCheck(instance,Constructor){if(!(instance instanceof ...
function _possibleConstructorReturn (line 1) | function _possibleConstructorReturn(self,call){if(!self){throw new Refer...
function _inherits (line 1) | function _inherits(subClass,superClass){if(typeof superClass!=="function...
function getOffset (line 1) | function getOffset(props){var offset={top:0,left:0};if(props.offset){off...
function Selection (line 1) | function Selection(props){_classCallCheck(this,Selection);var _this=_pos...
FILE: example/app/src/Example.js
class Example (line 5) | class Example extends React.PureComponent {
method constructor (line 7) | constructor() {
method renderSelection (line 43) | renderSelection() {
method render (line 58) | render() {
FILE: example/app/src/Examples.js
class Examples (line 4) | class Examples extends React.PureComponent {
method render (line 6) | render() {
FILE: example/app/src/registerServiceWorker.js
function register (line 21) | function register() {
function registerValidSW (line 46) | function registerValidSW(swUrl) {
function checkValidServiceWorker (line 75) | function checkValidServiceWorker(swUrl) {
function unregister (line 102) | function unregister() {
FILE: src/index.js
function getOffset (line 49) | function getOffset(props: Props) {
class Selection (line 66) | class Selection extends React.PureComponent<Props, State> { // eslint-di...
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (81K chars).
[
{
"path": ".babelrc",
"chars": 773,
"preview": "{\n \"presets\": [\n [\n \"env\",\n {\n \"modules\": \"commonjs\",\n \"targets\": {\n \"browsers\": [\""
},
{
"path": ".eslintignore",
"chars": 74,
"preview": ".gitignore\n\ndist/**/*\nexample/**/*\ntest/**/*\n\ngulpfile.js\nrollup.config.js"
},
{
"path": ".eslintrc.json",
"chars": 3021,
"preview": "{\n \"parser\": \"babel-eslint\",\n \"extends\": [\n \"airbnb\",\n \"plugin:flowtype/recommended\"\n ],\n \"globals\": {\n \"AP"
},
{
"path": ".flowconfig",
"chars": 655,
"preview": "[ignore]\n<PROJECT_ROOT>/node_modules/babel-plugin-transform-react-remove-prop-types/.*\n<PROJECT_ROOT>/node_modules/babel"
},
{
"path": ".gitignore",
"chars": 908,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
},
{
"path": ".travis.yml",
"chars": 930,
"preview": "sudo: false\nlanguage: node_js\nnode_js:\n- stable\nscript:\n- npm test\nafter_success:\n- cat ./coverage/lcov.info | ./node_mo"
},
{
"path": ".vscode/settings.json",
"chars": 271,
"preview": "{\n \"flow.useNPMPackagedFlow\": true,\n \"javascript.validate.enable\": false,\n \"files.associations\": {\n \"*.o"
},
{
"path": "CHANGELOG.md",
"chars": 2602,
"preview": "# Change Log\n\n> I haven't been maintaining this changelog.\n>\n> Check out the [releases](https://github.com/aurbano/react"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "MIT License\n\nCopyright (c) 2017 Alejandro U. Alvarez\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "README.md",
"chars": 6848,
"preview": "# React DS\n> Tiny (7KB) React Drag-to-Select component (with no dependencies! with support for touch devices!\n\n[![Travis"
},
{
"path": "dist/index.js",
"chars": 8930,
"preview": "\"use strict\";Object.defineProperty(exports,\"__esModule\",{value:true});var _jsx=function(){var REACT_ELEMENT_TYPE=typeof "
},
{
"path": "dist/index.js.flow",
"chars": 10046,
"preview": "// @flow\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nexport type Point = {\n x: number,\n y: number"
},
{
"path": "example/app/.gitignore",
"chars": 285,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/cov"
},
{
"path": "example/app/README.md",
"chars": 215,
"preview": "Example application using `create-react-app`.\n\n## NPM Commands\n\n* `npm start` - to run locally\n* `npm build` - to genera"
},
{
"path": "example/app/package.json",
"chars": 623,
"preview": "{\n \"name\": \"app\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"homepage\": \"https://aurbano.eu/react-ds/\",\n \"dependencie"
},
{
"path": "example/app/public/index.html",
"chars": 3700,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"initial-scale=1."
},
{
"path": "example/app/public/manifest.json",
"chars": 290,
"preview": "{\n \"short_name\": \"React DS\",\n \"name\": \"React DS Example\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n \"sizes\""
},
{
"path": "example/app/src/Example.js",
"chars": 1965,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Selection from 'react-ds';\n\nexport default class E"
},
{
"path": "example/app/src/Examples.js",
"chars": 1281,
"preview": "import React from 'react';\nimport Example from './Example';\n\nexport default class Examples extends React.PureComponent {"
},
{
"path": "example/app/src/example.css",
"chars": 2866,
"preview": "body {\n color: #333;\n font-family: Helvetica Neue, Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.4"
},
{
"path": "example/app/src/example.less",
"chars": 3560,
"preview": "//\n// Common Example Styles\n// ------------------------------\n\n\n// Constants\n// ------------------------------\n\n// examp"
},
{
"path": "example/app/src/index.js",
"chars": 296,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './example.css';\nimport './react-ds.css';\nimport Exa"
},
{
"path": "example/app/src/react-ds.css",
"chars": 395,
"preview": ".item-container {\n border: solid 1px #ccc;\n position: relative;\n box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);\n "
},
{
"path": "example/app/src/registerServiceWorker.js",
"chars": 4022,
"preview": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on su"
},
{
"path": "package.json",
"chars": 4224,
"preview": "{\n \"name\": \"react-ds\",\n \"version\": \"1.13.0\",\n \"description\": \"Simple React Drag-to-Select component\",\n \"main\": \"dist"
},
{
"path": "src/index.js",
"chars": 10907,
"preview": "// @flow\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nexport type Point = {\n x: number,\n y: number"
},
{
"path": "test/index.test.js",
"chars": 4932,
"preview": "import React from 'react';\nimport { shallow, mount } from 'enzyme';\nimport clone from 'lodash/clone';\n\nimport Selection "
},
{
"path": "test/util/test-bundler.js",
"chars": 461,
"preview": "/* globals jasmine */\n// needed for regenerator-runtime\n// (ES7 generator support is required by redux-saga)\nimport 'bab"
}
]
About this extraction
This page contains the full source code of the aurbano/react-ds GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (74.4 KB), approximately 21.4k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.