├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── demo.js
├── demo
├── AbsoluteGrid.js
├── SampleDisplay.jsx
├── demo.css
├── index.html
└── sampleData.js
├── index.js
├── lib
├── AbsoluteGrid.jsx
├── BaseDisplayObject.jsx
├── DragManager.js
└── LayoutManager.js
├── package.json
└── webpack
├── build.config.js
├── demo.config.js
└── dev.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-0"],
3 | "plugins": ["lodash"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [package.json]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | // I want to use babel-eslint for parsing!
3 | "parser": "babel-eslint",
4 | "ecmaFeatures": {
5 | "jsx": true
6 | },
7 | "env": {
8 | // I write for browser
9 | "browser": true,
10 | // in CommonJS
11 | "node": true
12 | },
13 | "plugins": [
14 | "react"
15 | ],
16 | // To give you an idea how to override rule options:
17 | "rules": {
18 | "quotes": [2, "single"]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .idea
4 | .cache/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo/
2 | node_modules/
3 | .idea
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jonathan Rowny
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 | React Absolute Grid
2 | ===================
3 |
4 | Unmaintained:
5 | -----
6 | This library is unmaintained and has not been tested to work with React 16+. We're looking for maintainers. If you're interested, file an issue.
7 |
8 | Description:
9 | ------
10 |
11 | An absolute layout grid with animations, filtering, zooming, and drag and drop support. Use your own component as the grid item. See the [Demo](http://jrowny.github.io/react-absolute-grid/demo/).
12 |
13 | Usage:
14 | ------
15 |
16 | Install with `npm install react-absolute-grid`
17 |
18 | ```javascript
19 | import React from 'react';
20 | import createAbsoluteGrid from './lib/AbsoluteGrid.jsx';
21 |
22 | // This is the component that will display your data
23 | import YourDisplayComponent from 'your-display-component';
24 |
25 | var sampleItems = [
26 | {key: 1, name: 'Test', sort: 0, filtered: 0},
27 | {key: 2, name: 'Test 1', sort: 1, filtered: 0},
28 | ];
29 |
30 | // Pass your display component to create a new grid
31 | const AbsoluteGrid = createAbsoluteGrid(YourDisplayComponent, {someProp: 'my component needs this'});
32 | React.render( , document.getElementById('Container'));
33 | ```
34 |
35 | CreateAbsoluteGrid
36 | ------
37 | ```javascript
38 | createAbsoluteGrid(DisplayComponent, displayProps = {}, forceImpure = false)
39 | ```
40 |
41 | * `DisplayComponent`: is a react component to display in your grid
42 | * `displayProps`: *optional* : are properties you want passed down to the DisplayComponent such as event handlers.
43 | * `forceImpure`: *optional* : **not recommended** Will make this function as an impure component, meaning it always renders.
44 |
45 | Options (Properties)
46 | ------
47 | | Property | Default | Description |
48 | |---|:---:|---|
49 | | **items** | [] | The array of items in the grid |
50 | | **keyProp** | 'key' | The property to be used as a key |
51 | | **filterProp** | 'filtered' | The property to be used for filtering, if the filtered value is true, the item won't be displayed. It's important to not remove items from the array because that will cause React to remove the DOM, for performance we would rather hide it then remove it. |
52 | | **sortProp** | 'sort' | The property to sort on |
53 | | **itemWidth** | 128 | The width of an item |
54 | | **itemHeight** | 128 | The height of an item |
55 | | **verticalMargin** | -1 | How much space between rows, -1 means the same as columns margin which is dynamically calculated based on width |
56 | | **responsive** | false | If the container has a dynamic width, set this to true to update when the browser resizes |
57 | | **dragEnabled** | false | Enables drag and drop listeners, onMove will be called with the keys involved in a drag and drop |
58 | | **animation** | 'transform 300ms ease' | The CSS animation to use on elements. Pass a blank string or `false` for no animation. |
59 | | **zoom** | 1 | Zooms the contents of the grid, 1 = 100% |
60 | | **onMove** | `fn(from, to)` | This function is called when an item is dragged over another item. It is your responsibility to update the sort of all items when this happens. |
61 | | **onDragStart** | `fn(e)` | This function is called when an item starts dragging, this is NOT required. |
62 | | **onDragMove** | `fn(e)` | This function is called when as an item is being moved, this is NOT required. |
63 | | **onDragEnd** | `fn(e)` | This function is called when an item has finished its drag, this is NOT required. |
64 |
65 | Your Component
66 | ------
67 | Your component will receive `item`, `index` and `itemsLength` as props, as well as anything you pass into the createAbsoluteGrid function. Here's the simplest possible example:
68 |
69 | ```javascript
70 | import React from 'react';
71 |
72 | export default function SampleDisplay(props) {
73 | // Supposing your item shape is something like {name: 'foo'}
74 | const { item, index, itemsLength } = props;
75 | return
Item {index} of {itemsLength}: {item.name}
;
76 | }
77 | ```
78 |
79 | What Makes AbsoluteGrid Unique?
80 | ----
81 | The idea behind AbsoluteGrid is high performance. This is achieved by using Translate3d to position each item in the layout. Items are never removed from the DOM, instead they are hidden. For best performance you should avoid re-arranging or removing items which you pass into AbsoluteGrid, instead you can use the `filtered` and `sort` properties to hide or sort an item. Those properties are customizable using the `keyProp` and `filterProp` properties. In addition, all data passed must be immutable so that we don't waste any renders.
82 |
83 | Your Display Component props
84 | ----
85 | Each Component is passed the following props, as well as anything passed into the second parameter of `createAbsoluteGrid`
86 |
87 | | Property | Description |
88 | |---|:---|
89 | | **item** | The data associated with the GridItem |
90 | | **index** | The index of the item data in the `items` array |
91 | | **itemsLength** | The total length of the `items` array |
92 | | **...displayProps** | props passed into `createAbsoluteGrid` |
93 |
94 | ToDo:
95 | -----
96 | * Tests
97 | * Improve Drag & Drop browser support and reliability
98 |
99 | Browser Compatibility
100 | -----
101 | This component should work in all browsers that [support CSS3 3D Transforms](http://caniuse.com/#feat=transforms3d). If you need IE9 support you can modify it to use transform rather than transform3d. Pull requests welcome!
102 |
103 | Drag and Drop only works on IE11+ due to lack of pointer events, although there is a workaround coming soon.
104 |
105 | Migrating from 2.x
106 | -----
107 |
108 | Instead of passing `displayObject` to the AbsoluteGrid component, pass the component directly into the composer function, `createAbsoluteGrid` which returns an AbsoluteGrid component. That's it!
109 |
--------------------------------------------------------------------------------
/demo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | // import Perf from 'react-addons-perf';
6 | import createAbsoluteGrid from './index.js';
7 | import SampleDisplay from './demo/SampleDisplay.jsx';
8 | import * as data from './demo/sampleData.js';
9 | import * as _ from 'lodash';
10 |
11 | demo();
12 |
13 | /**
14 | * This demo is meant to show you all of the things that are possible with ReactAbsoluteGrid
15 | * If implemented in a Flux project, the grid would be in a render method with the
16 | * event handlers calling Actions which would update a Store. For the sake of brevity,
17 | * the "store" is implemented locally and the changes re-rendered manually
18 | *
19 | * TODO: implement inside a react component rather than doing this all manually
20 | **/
21 |
22 | function demo() {
23 |
24 | let sampleItems = data.screens;
25 | let render;
26 | let zoom = 0.7;
27 |
28 | //We set a property on each item to let the grid know not to show it
29 | var onFilter = function(event){
30 | var search = new RegExp(event.target.value, 'i');
31 | sampleItems = sampleItems.map(function(item){
32 | const isMatched = !item.name.match(search);
33 | if(!item.filtered || isMatched !== item.filtered) {
34 | return {
35 | ...item,
36 | filtered: isMatched
37 | }
38 | }
39 | return item;
40 | });
41 | render();
42 | };
43 |
44 | //Change the item's sort order
45 | var onMove = function(source, target){
46 | source = _.find(sampleItems, {key: parseInt(source, 10)});
47 | target = _.find(sampleItems, {key: parseInt(target, 10)});
48 |
49 | const targetSort = target.sort;
50 |
51 | //CAREFUL, For maximum performance we must maintain the array's order, but change sort
52 | sampleItems = sampleItems.map(function(item){
53 | //Decrement sorts between positions when target is greater
54 | if(item.key === source.key) {
55 | return {
56 | ...item,
57 | sort: targetSort
58 | }
59 | } else if(target.sort > source.sort && (item.sort <= target.sort && item.sort > source.sort)){
60 | return {
61 | ...item,
62 | sort: item.sort - 1
63 | };
64 | //Increment sorts between positions when source is greater
65 | } else if (item.sort >= target.sort && item.sort < source.sort){
66 | return {
67 | ...item,
68 | sort: item.sort + 1
69 | };
70 | }
71 | return item;
72 | });
73 | //Perf.start();
74 | render();
75 | //Perf.stop();
76 | //Perf.printWasted();
77 | };
78 |
79 | var onMoveDebounced = _.debounce(onMove, 40);
80 |
81 | var unMountTest = function(){
82 | if(ReactDOM.unmountComponentAtNode(document.getElementById('Demo'))){
83 | ReactDOM.render(Remount , document.getElementById('UnmountButton'));
84 | }else{
85 | render();
86 | ReactDOM.render(Test Unmount , document.getElementById('UnmountButton'));
87 | }
88 | };
89 |
90 | const AbsoluteGrid = createAbsoluteGrid(SampleDisplay);
91 | render = function(){
92 | ReactDOM.render(, document.getElementById('Demo'));
100 | };
101 |
102 | var renderDebounced = _.debounce(render, 150);
103 |
104 | //Update the zoom value
105 | var onZoom = function(event){
106 | zoom = parseFloat(event.target.value);
107 | renderDebounced();
108 | };
109 |
110 | ReactDOM.render( , document.getElementById('Zoom'));
111 | ReactDOM.render( , document.getElementById('Filter'));
112 | ReactDOM.render(Test Unmount , document.getElementById('UnmountButton'));
113 | render();
114 | }
115 |
--------------------------------------------------------------------------------
/demo/SampleDisplay.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 |
5 | export default class SampleDisplay extends React.Component {
6 |
7 | render() {
8 | const itemStyle = {
9 | display: 'block',
10 | width: '100%',
11 | height: '100%',
12 | backgroundImage: `url('${this.props.item.url}')`
13 | };
14 |
15 | return {this.props.item.name}
;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-family: 'Open Sans', sans-serif;
3 | text-align: center;
4 | }
5 |
6 | .wrap{
7 | max-width:1100px;
8 | margin-left: auto;
9 | margin-right: auto;
10 | padding-bottom:70px;
11 | }
12 |
13 | h1{
14 | color:#00489F;
15 | text-transform: uppercase;
16 | font-weight: 800;
17 | letter-spacing: -6px;
18 | font-size:60px;
19 | }
20 |
21 | .demo{
22 | width:100%;
23 | padding:20px;
24 | margin:10px 0 30px 0;
25 | box-sizing: border-box;
26 | }
27 |
28 | .zoom{
29 | display:inline-block;
30 | margin-left:10px;
31 | }
32 | .zoom i{
33 | color:#777;
34 | }
35 | .zoom input{
36 | margin:5px 5px 0 5px;
37 | }
38 |
39 | .gridItem{
40 | background-size: 100%;
41 | box-shadow: 0 0 1.25em 0 rgba(0,0,0,.2);
42 | background-color:#fff;
43 | cursor: move;
44 | }
45 |
46 | .gridItem .name{
47 | display:block;
48 | bottom:-22px;
49 | width:100%;
50 | font-size:12px;
51 | position:absolute;
52 | color:#555;
53 | text-transform: capitalize;
54 | }
55 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Absolute Grid
13 |
Sortable, filterable, zoomable, ReactJS grid component using an absolute transform3d layout for React. Read more here .
14 |
15 |
Sample display items courtesy of InVisionApp
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/demo/sampleData.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | screens: [
3 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-1-login.jpg', 'name': 'login', 'sort': 1, 'key': 1},
4 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-2-sign-up.jpg', 'name': 'signup', 'sort': 2, 'key': 2},
5 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 3, 'key': 3},
6 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-4-home.jpg', 'name': 'home', 'sort': 4, 'key': 4},
7 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-5-calendar.jpg', 'name': 'calendar', 'sort': 5, 'key': 5},
8 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-6-overview.jpg', 'name': 'overview', 'sort': 6, 'key': 6},
9 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-7-groups.jpg', 'name': 'groups', 'sort': 7, 'key': 7},
10 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-8-list.jpg', 'name': 'list', 'sort': 8, 'key': 8},
11 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-9-create.jpg', 'name': 'create', 'sort': 9, 'key': 9},
12 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-10-profile.jpg', 'name': 'profile', 'sort': 10, 'key': 10},
13 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-11-timeline.jpg', 'name': 'timeline', 'sort': 11, 'key': 11},
14 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-12-settings.jpg', 'name': 'settings', 'sort': 12, 'key': 12},
15 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-1-13-navigation.jpg', 'name': 'navigation', 'sort': 13, 'key': 13},
16 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-1-login.jpg', 'name': 'login', 'sort': 14, 'key': 14},
17 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-2-sign-up.jpg', 'name': 'signup', 'sort': 15, 'key': 15},
18 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 16, 'key': 16},
19 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-4-home.jpg', 'name': 'home', 'sort': 17, 'key': 17},
20 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-5-calendar.jpg', 'name': 'calendar', 'sort': 18, 'key': 18},
21 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-6-overview.jpg', 'name': 'overview', 'sort': 19, 'key': 19},
22 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-7-groups.jpg', 'name': 'groups', 'sort': 20, 'key': 20},
23 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-8-list.jpg', 'name': 'list', 'sort': 21, 'key': 21},
24 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-9-create.jpg', 'name': 'create', 'sort': 22, 'key': 22},
25 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-10-profile.jpg', 'name': 'profile', 'sort': 23, 'key': 23},
26 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-11-timeline.jpg', 'name': 'timeline', 'sort': 24, 'key': 24},
27 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-12-settings.jpg', 'name': 'settings', 'sort': 25, 'key': 25},
28 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-2-13-navigation.jpg', 'name': 'navigation', 'sort': 26, 'key': 26},
29 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-1-login.jpg', 'name': 'login', 'sort': 27, 'key': 27},
30 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-2-sign-up.jpg', 'name': 'signup', 'sort': 28, 'key': 28},
31 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 29, 'key': 29},
32 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-4-home.jpg', 'name': 'home', 'sort': 30, 'key': 30},
33 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-5-calendar.jpg', 'name': 'calendar', 'sort': 31, 'key': 31},
34 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-6-overview.jpg', 'name': 'overview', 'sort': 32, 'key': 32},
35 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-7-groups.jpg', 'name': 'groups', 'sort': 33, 'key': 33},
36 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-8-list.jpg', 'name': 'list', 'sort': 34, 'key': 34},
37 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-9-create.jpg', 'name': 'create', 'sort': 35, 'key': 35},
38 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-10-profile.jpg', 'name': 'profile', 'sort': 36, 'key': 36},
39 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-11-timeline.jpg', 'name': 'timeline', 'sort': 37, 'key': 37},
40 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-12-settings.jpg', 'name': 'settings', 'sort': 38, 'key': 38},
41 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-3-13-navigation.jpg', 'name': 'navigation', 'sort': 39, 'key': 39},
42 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-1-login.jpg', 'name': 'login', 'sort': 40, 'key': 40},
43 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-2-sign-up.jpg', 'name': 'signup', 'sort': 41, 'key': 41},
44 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 42, 'key': 42},
45 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-4-home.jpg', 'name': 'home', 'sort': 43, 'key': 43},
46 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-5-calendar.jpg', 'name': 'calendar', 'sort': 44, 'key': 44},
47 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-6-overview.jpg', 'name': 'overview', 'sort': 45, 'key': 45},
48 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-7-groups.jpg', 'name': 'groups', 'sort': 46, 'key': 46},
49 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-8-list.jpg', 'name': 'list', 'sort': 47, 'key': 47},
50 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-9-create.jpg', 'name': 'create', 'sort': 48, 'key': 48},
51 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-10-profile.jpg', 'name': 'profile', 'sort': 49, 'key': 49},
52 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-11-timeline.jpg', 'name': 'timeline', 'sort': 50, 'key': 50},
53 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-12-settings.jpg', 'name': 'settings', 'sort': 51, 'key': 51},
54 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-4-13-navigation.jpg', 'name': 'navigation', 'sort': 52, 'key': 52},
55 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-1-login.jpg', 'name': 'login', 'sort': 53, 'key': 53},
56 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-2-sign-up.jpg', 'name': 'signup', 'sort': 54, 'key': 54},
57 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 55, 'key': 55},
58 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-4-home.jpg', 'name': 'home', 'sort': 56, 'key': 56},
59 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-5-calendar.jpg', 'name': 'calendar', 'sort': 57, 'key': 57},
60 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-6-overview.jpg', 'name': 'overview', 'sort': 58, 'key': 58},
61 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-7-groups.jpg', 'name': 'groups', 'sort': 59, 'key': 59},
62 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-8-list.jpg', 'name': 'list', 'sort': 60, 'key': 60},
63 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-9-create.jpg', 'name': 'create', 'sort': 61, 'key': 61},
64 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-10-profile.jpg', 'name': 'profile', 'sort': 62, 'key': 62},
65 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-11-timeline.jpg', 'name': 'timeline', 'sort': 63, 'key': 63},
66 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-12-settings.jpg', 'name': 'settings', 'sort': 64, 'key': 64},
67 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-5-13-navigation.jpg', 'name': 'navigation', 'sort': 65, 'key': 65},
68 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-1-login.jpg', 'name': 'login', 'sort': 66, 'key': 66},
69 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-2-sign-up.jpg', 'name': 'signup', 'sort': 67, 'key': 67},
70 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 68, 'key': 68},
71 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-4-home.jpg', 'name': 'home', 'sort': 69, 'key': 69},
72 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-5-calendar.jpg', 'name': 'calendar', 'sort': 70, 'key': 70},
73 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-6-overview.jpg', 'name': 'overview', 'sort': 71, 'key': 71},
74 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-7-groups.jpg', 'name': 'groups', 'sort': 72, 'key': 72},
75 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-8-list.jpg', 'name': 'list', 'sort': 73, 'key': 73},
76 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-9-create.jpg', 'name': 'create', 'sort': 74, 'key': 74},
77 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-10-profile.jpg', 'name': 'profile', 'sort': 75, 'key': 75},
78 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-11-timeline.jpg', 'name': 'timeline', 'sort': 76, 'key': 76},
79 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-12-settings.jpg', 'name': 'settings', 'sort': 77, 'key': 77},
80 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-6-13-navigation.jpg', 'name': 'navigation', 'sort': 78, 'key': 78},
81 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-1-login.jpg', 'name': 'login', 'sort': 79, 'key': 79},
82 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-2-sign-up.jpg', 'name': 'signup', 'sort': 80, 'key': 80},
83 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 81, 'key': 81},
84 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-4-home.jpg', 'name': 'home', 'sort': 82, 'key': 82},
85 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-5-calendar.jpg', 'name': 'calendar', 'sort': 83, 'key': 83},
86 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-6-overview.jpg', 'name': 'overview', 'sort': 84, 'key': 84},
87 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-7-groups.jpg', 'name': 'groups', 'sort': 85, 'key': 85},
88 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-8-lists.jpg', 'name': 'lists', 'sort': 86, 'key': 86},
89 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-9-create.jpg', 'name': 'create', 'sort': 87, 'key': 87},
90 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-10-profile.jpg', 'name': 'profile', 'sort': 88, 'key': 88},
91 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-11-timeline.jpg', 'name': 'timeline', 'sort': 89, 'key': 89},
92 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-12-settings.jpg', 'name': 'settings', 'sort': 90, 'key': 90},
93 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-7-13-navigation.jpg', 'name': 'navigation', 'sort': 91, 'key': 91},
94 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-1-login.jpg', 'name': 'login', 'sort': 92, 'key': 92},
95 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-2-sign-up.jpg', 'name': 'signup', 'sort': 93, 'key': 93},
96 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 94, 'key': 94},
97 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-4-home.jpg', 'name': 'home', 'sort': 95, 'key': 95},
98 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-5-calendar.jpg', 'name': 'calendar', 'sort': 96, 'key': 96},
99 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-6-overview.jpg', 'name': 'overview', 'sort': 97, 'key': 97},
100 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-7-groups.jpg', 'name': 'groups', 'sort': 98, 'key': 98},
101 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-8-list.jpg', 'name': 'list', 'sort': 99, 'key': 99},
102 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-9-create.jpg', 'name': 'create', 'sort': 100, 'key': 100},
103 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-10-profile.jpg', 'name': 'profile', 'sort': 101, 'key': 101},
104 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-11-timeline.jpg', 'name': 'timeline', 'sort': 102, 'key': 102},
105 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-12-settings.jpg', 'name': 'settings', 'sort': 103, 'key': 103},
106 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-8-13-navigation.jpg', 'name': 'navigation', 'sort': 104, 'key': 104},
107 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-1-login.jpg', 'name': 'login', 'sort': 105, 'key': 105},
108 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-2-sign-up.jpg', 'name': 'signup', 'sort': 106, 'key': 106},
109 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 107, 'key': 107},
110 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-4-home.jpg', 'name': 'home', 'sort': 108, 'key': 108},
111 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-5-calendar.jpg', 'name': 'calendar', 'sort': 109, 'key': 109},
112 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-6-overview.jpg', 'name': 'overview', 'sort': 110, 'key': 110},
113 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-7-groups.jpg', 'name': 'groups', 'sort': 111, 'key': 111},
114 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-8-list.jpg', 'name': 'list', 'sort': 112, 'key': 112},
115 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-9-create.jpg', 'name': 'create', 'sort': 113, 'key': 113},
116 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-10-profile.jpg', 'name': 'profile', 'sort': 114, 'key': 114},
117 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-11-timeline.jpg', 'name': 'timeline', 'sort': 115, 'key': 115},
118 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-12-settings.jpg', 'name': 'settings', 'sort': 116, 'key': 116},
119 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-9-13-navigation.jpg', 'name': 'navigation', 'sort': 117, 'key': 117},
120 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-1-login.jpg', 'name': 'login', 'sort': 118, 'key': 118},
121 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-2-sign-up.jpg', 'name': 'signup', 'sort': 119, 'key': 119},
122 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-3-walkthrough.jpg', 'name': 'walkthrough', 'sort': 120, 'key': 120},
123 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-4-home.jpg', 'name': 'home', 'sort': 121, 'key': 121},
124 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-5-calendar.jpg', 'name': 'calendar', 'sort': 122, 'key': 122},
125 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-6-overview.jpg', 'name': 'overview', 'sort': 123, 'key': 123},
126 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-7-groups.jpg', 'name': 'groups', 'sort': 124, 'key': 124},
127 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-8-lists.jpg', 'name': 'lists', 'sort': 125, 'key': 125},
128 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-9-create.jpg', 'name': 'create', 'sort': 126, 'key': 126},
129 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-10-profile.jpg', 'name': 'profile', 'sort': 127, 'key': 127},
130 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-11-timeline.jpg', 'name': 'timeline', 'sort': 128, 'key': 128},
131 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-12-settings.jpg', 'name': 'settings', 'sort': 129, 'key': 129},
132 | {'url': 'http://invisionapp.com/subsystems/do_ui_kit/assets/img/screens/original-1x/screen-10-13-navigation.jpg', 'name': 'navigation', 'sort': 130, 'key': 130}
133 | ]
134 | };
135 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/AbsoluteGrid.jsx');
2 |
--------------------------------------------------------------------------------
/lib/AbsoluteGrid.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component, PureComponent } from 'react';
4 | import { debounce, sortBy } from 'lodash';
5 |
6 | import createDisplayObject from './BaseDisplayObject.jsx';
7 | import DragManager from './DragManager.js';
8 | import LayoutManager from './LayoutManager.js';
9 | import PropTypes from 'prop-types';
10 |
11 | export default function createAbsoluteGrid(DisplayObject, displayProps = {}, forceImpure = false) {
12 |
13 | const Comp = forceImpure ? Component : PureComponent;
14 | const WrappedDisplayObject = createDisplayObject(DisplayObject, displayProps, forceImpure);
15 |
16 | return class extends Comp {
17 | static defaultProps = {
18 | items: [],
19 | keyProp: 'key',
20 | filterProp: 'filtered',
21 | sortProp: 'sort',
22 | itemWidth: 128,
23 | itemHeight: 128,
24 | verticalMargin: -1,
25 | responsive: false,
26 | dragEnabled: false,
27 | animation: 'transform 300ms ease',
28 | zoom: 1,
29 | onMove: ()=>{},
30 | onDragStart: ()=>{},
31 | onDragMove: ()=>{},
32 | onDragEnd: ()=>{}
33 | }
34 |
35 | static propTypes = {
36 | items: PropTypes.arrayOf(PropTypes.object).isRequired,
37 | itemWidth: PropTypes.number,
38 | itemHeight: PropTypes.number,
39 | verticalMargin: PropTypes.number,
40 | zoom: PropTypes.number,
41 | responsive: PropTypes.bool,
42 | dragEnabled: PropTypes.bool,
43 | keyProp: PropTypes.string,
44 | sortProp: PropTypes.string,
45 | filterProp: PropTypes.string,
46 | animation: PropTypes.string,
47 | onMove: PropTypes.func,
48 | onDragStart: PropTypes.func,
49 | onDragMove: PropTypes.func,
50 | onDragEnd: PropTypes.func
51 | }
52 |
53 | constructor(props, context){
54 | super(props, context);
55 | this.onResize = debounce(this.onResize, 150);
56 | this.dragManager = new DragManager(
57 | this.props.onMove,
58 | this.props.onDragStart,
59 | this.props.onDragEnd,
60 | this.props.onDragMove,
61 | this.props.keyProp);
62 | this.state = {
63 | layoutWidth: 0
64 | };
65 | }
66 |
67 | render() {
68 | if(!this.state.layoutWidth || !this.props.items.length){
69 | return this.container = node}>
;
70 | }
71 |
72 | let filteredIndex = 0;
73 | let sortedIndex = {};
74 |
75 | /*
76 | If we actually sorted the array, React would re-render the DOM nodes
77 | Creating a sort index just tells us where each item should be
78 | This also clears out filtered items from the sort order and
79 | eliminates gaps and duplicate sorts
80 | */
81 | sortBy(this.props.items, this.props.sortProp).forEach(item => {
82 | if(!item[this.props.filterProp]){
83 | const key = item[this.props.keyProp];
84 | sortedIndex[key] = filteredIndex;
85 | filteredIndex++;
86 | }
87 | });
88 |
89 | const itemsLength = this.props.items.length;
90 | const gridItems = this.props.items.map(item => {
91 | const key = item[this.props.keyProp];
92 | const index = sortedIndex[key];
93 | return (
94 |
110 | );
111 | });
112 |
113 | const options = {
114 | itemWidth: this.props.itemWidth,
115 | itemHeight: this.props.itemHeight,
116 | verticalMargin: this.props.verticalMargin,
117 | zoom: this.props.zoom
118 | };
119 | const layout = new LayoutManager(options, this.state.layoutWidth);
120 | const gridStyle = {
121 | position: 'relative',
122 | display: 'block',
123 | height: layout.getTotalHeight(filteredIndex)
124 | };
125 |
126 | return (
127 | this.container = node}
131 | >
132 | {gridItems}
133 |
134 | );
135 | }
136 |
137 | componentDidMount() {
138 | //If responsive, listen for resize
139 | if(this.props.responsive){
140 | window.addEventListener('resize', this.onResize);
141 | }
142 | this.onResize();
143 | }
144 |
145 | componentWillUnmount() {
146 | window.removeEventListener('resize', this.onResize);
147 | }
148 |
149 | onResize = () => {
150 | if (window.requestAnimationFrame) {
151 | window.requestAnimationFrame(this.getDOMWidth);
152 | } else {
153 | setTimeout(this.getDOMWidth, 66);
154 | }
155 | }
156 |
157 | getDOMWidth = () => {
158 | const width = this.container && this.container.clientWidth;
159 |
160 | if(this.state.layoutWidth !== width){
161 | this.setState({layoutWidth: width});
162 | }
163 |
164 | }
165 |
166 |
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/lib/BaseDisplayObject.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component, PureComponent } from 'react';
4 |
5 | import LayoutManager from './LayoutManager.js';
6 | import PropTypes from 'prop-types';
7 |
8 | export default function createDisplayObject(DisplayObject, displayProps, forceImpure) {
9 |
10 | const Comp = forceImpure ? Component : PureComponent;
11 |
12 | return class extends Comp {
13 | static propTypes = {
14 | item: PropTypes.object,
15 | style: PropTypes.object,
16 | index: PropTypes.number,
17 | dragEnabled: PropTypes.bool,
18 | dragManager: PropTypes.object,
19 | itemsLength: PropTypes.number
20 | }
21 |
22 | state = {}
23 |
24 | updateDrag(x, y) {
25 | //Pause Animation lets our item return to a snapped position without being animated
26 | let pauseAnimation = false;
27 | if(!this.props.dragManager.dragItem){
28 | pauseAnimation = true;
29 | setTimeout(() => {
30 | this.setState({pauseAnimation: false});
31 | }, 20);
32 | }
33 | this.setState({
34 | dragX: x,
35 | dragY: y,
36 | pauseAnimation: pauseAnimation
37 | });
38 | }
39 |
40 | onDrag = (e) => {
41 | if(this.props.dragManager){
42 | this.props.dragManager.startDrag(e, this.domNode, this.props.item, this.updateDrag.bind(this));
43 | }
44 | }
45 |
46 | getStyle() {
47 | const options = {
48 | itemWidth: this.props.itemWidth,
49 | itemHeight: this.props.itemHeight,
50 | verticalMargin: this.props.verticalMargin,
51 | zoom: this.props.zoom
52 | };
53 | const layout = new LayoutManager(options, this.props.layoutWidth);
54 | const style = layout.getStyle(this.props.index,
55 | this.props.animation,
56 | this.props.item[this.props.filterProp]);
57 | //If this is the object being dragged, return a different style
58 | if (this.props.dragManager.dragItem &&
59 | this.props.dragManager.dragItem[this.props.keyProp] === this.props.item[this.props.keyProp]) {
60 | const dragStyle = this.props.dragManager.getStyle(this.state.dragX, this.state.dragY);
61 | return {...style, ...dragStyle};
62 | } else if (this.state && this.state.pauseAnimation) {
63 | const pauseAnimationStyle = {...style};
64 | pauseAnimationStyle.WebkitTransition = 'none';
65 | pauseAnimationStyle.MozTransition = 'none';
66 | pauseAnimationStyle.msTransition = 'none';
67 | pauseAnimationStyle.transition = 'none';
68 | return pauseAnimationStyle;
69 | }
70 | return style;
71 | }
72 |
73 | componentDidMount() {
74 | if (this.props.dragEnabled) {
75 | this.domNode.addEventListener('mousedown', this.onDrag);
76 | this.domNode.addEventListener('touchstart', this.onDrag);
77 | this.domNode.setAttribute('data-key', this.props.item[this.props.keyProp]);
78 | }
79 | }
80 |
81 | componentWillUnmount() {
82 | if (this.props.dragEnabled) {
83 | this.props.dragManager.endDrag();
84 | this.domNode.removeEventListener('mousedown', this.onDrag);
85 | this.domNode.removeEventListener('touchstart', this.onDrag);
86 | }
87 | }
88 |
89 | render() {
90 | return (
91 | this.domNode = node}
92 | style={this.getStyle()}>
93 |
98 |
99 | );
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/lib/DragManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default class DragManager {
4 |
5 | dragItem;
6 | initialMouseX;
7 | initialMouseY;
8 | initialEventX;
9 | initialEventY;
10 | dragX;
11 | dragY;
12 | keyProp;
13 |
14 | constructor(moveFn, dragStartFn, dragEndFn, dragMoveFn, keyProp){
15 | this.dragMove = this.dragMove.bind(this);
16 | this.endDrag = this.endDrag.bind(this);
17 | this.moveFn = moveFn;
18 | this.dragStartFn = dragStartFn;
19 | this.dragEndFn = dragEndFn;
20 | this.dragMoveFn = dragMoveFn;
21 | this.keyProp = keyProp;
22 | }
23 |
24 | dragMove(e) {
25 | const tolerance = 3;
26 | const isTouch = e.touches && e.touches.length;
27 | const pageX = isTouch ? e.touches[0].pageX : e.pageX;
28 | const pageY = isTouch ? e.touches[0].pageY : e.pageY;
29 |
30 | const xMovement = Math.abs(this.initialEventX - pageX);
31 | const yMovement = Math.abs(this.initialEventY - pageY);
32 |
33 | if(xMovement > tolerance || yMovement > tolerance){
34 | const clientX = isTouch ? e.touches[0].clientX : e.clientX;
35 | const clientY = isTouch ? e.touches[0].clientY : e.clientY;
36 |
37 | this.dragX = clientX - this.initialMouseX;
38 | this.dragY = clientY - this.initialMouseY;
39 |
40 | this.update(this.dragX, this.dragY);
41 |
42 |
43 | let targetKey;
44 | let targetElement = document.elementFromPoint(clientX, clientY);
45 | while(targetElement.parentNode){
46 | if(targetElement.getAttribute('data-key')){
47 | targetKey = targetElement.getAttribute('data-key');
48 | break;
49 | }
50 | targetElement = targetElement.parentNode;
51 | }
52 |
53 | if(targetKey && targetKey !== this.dragItem[this.keyProp]){
54 | this.moveFn(this.dragItem[this.keyProp], targetKey);
55 | }
56 |
57 | e.stopPropagation();
58 | e.preventDefault();
59 | }
60 |
61 | this.dragMoveFn(e);
62 | }
63 |
64 | endDrag() {
65 |
66 | document.removeEventListener('mousemove', this.dragMove);
67 | document.removeEventListener('mouseup', this.endDrag);
68 |
69 | this.dragItem = null;
70 | if(this.update && typeof this.update === 'function'){
71 | this.update(null, null);
72 | }
73 | this.update = null;
74 |
75 | this.dragEndFn();
76 | }
77 |
78 | startDrag(e, domNode, item, fnUpdate){
79 | const isTouch = e.targetTouches && e.targetTouches.length === 1;
80 | if(e.button === 0 || isTouch){
81 | const rect = domNode.getBoundingClientRect();
82 |
83 | this.update = fnUpdate;
84 | this.dragItem = item;
85 | const pageX = isTouch ? e.targetTouches[0].pageX : e.pageX;
86 | const pageY = isTouch ? e.targetTouches[0].pageY : e.pageY;
87 |
88 | this.initialMouseX = Math.round(pageX - (rect.left + window.pageXOffset));
89 | this.initialMouseY = Math.round(pageY - (rect.top + window.pageYOffset));
90 | this.initialEventX = pageX;
91 | this.initialEventY = pageY;
92 |
93 | document.addEventListener('mousemove', this.dragMove);
94 | document.addEventListener('touchmove', this.dragMove);
95 | document.addEventListener('mouseup', this.endDrag);
96 | document.addEventListener('touchend', this.endDrag);
97 | document.addEventListener('touchcancel', this.endDrag);
98 |
99 | //This is needed to stop text selection in most browsers
100 | e.preventDefault();
101 | }
102 |
103 | this.dragStartFn(e);
104 | }
105 |
106 | getStyle(x, y){
107 | const dragStyle = {};
108 | const transform = `translate3d(${x}px, ${y}px, 0)`;
109 | //Makes positioning simpler if we're fixed
110 | dragStyle.position = 'fixed';
111 | dragStyle.zIndex = 1000;
112 | //Required for Fixed positioning
113 | dragStyle.left = 0;
114 | dragStyle.top = 0;
115 | dragStyle.WebkitTransform = transform;
116 | dragStyle.MozTransform = transform;
117 | dragStyle.msTransform = transform;
118 | dragStyle.transform = transform;
119 |
120 | //Turn off animations for this item
121 | dragStyle.WebkitTransition = 'none';
122 | dragStyle.MozTransition = 'none';
123 | dragStyle.msTransition = 'none';
124 | dragStyle.transition = 'none';
125 |
126 | //Allows mouseover to work
127 | dragStyle.pointerEvents = 'none';
128 |
129 | return dragStyle;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/lib/LayoutManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default class LayoutManager {
4 |
5 | columns;
6 | horizontalMargin;
7 | verticalMargin;
8 | layoutWidth;
9 | itemHeight;
10 | itemWidth;
11 | rowHeight;
12 |
13 | constructor(options, width){
14 | this.update(options, width);
15 | }
16 |
17 | update(options, width){
18 |
19 | //Calculates layout
20 | this.layoutWidth = width;
21 | this.zoom = options.zoom;
22 | this.itemWidth = Math.round(options.itemWidth * this.zoom);
23 | this.itemHeight = Math.round(options.itemHeight * this.zoom);
24 | this.columns = Math.floor(this.layoutWidth / this.itemWidth);
25 | this.horizontalMargin = (this.columns === 1) ? 0 : Math.round(this.layoutWidth - (this.columns * this.itemWidth)) / (this.columns - 1);
26 | this.verticalMargin = (options.verticalMargin === -1) ? this.horizontalMargin : options.verticalMargin;
27 | this.rowHeight = this.itemHeight + this.verticalMargin;
28 | }
29 |
30 | getTotalHeight(filteredTotal){
31 | return (Math.ceil(filteredTotal / this.columns) * this.rowHeight) - this.verticalMargin;
32 | }
33 |
34 | getRow(index){
35 | return Math.floor(index / this.columns);
36 | }
37 |
38 | getColumn(index){
39 | return index - (this.getRow(index) * this.columns);
40 | }
41 |
42 | getPosition(index){
43 | const col = this.getColumn(index);
44 | const row = this.getRow(index);
45 | const margin = this.horizontalMargin;
46 | const width = this.itemWidth;
47 |
48 | return {
49 | x: Math.round((col * width) + (col * margin)),
50 | y: Math.round((this.itemHeight + this.verticalMargin) * row)
51 | };
52 | }
53 |
54 | getTransform(index){
55 | const position = this.getPosition(index);
56 | return 'translate3d(' + position.x + 'px, ' + position.y + 'px, 0)';
57 | }
58 |
59 | getStyle(index, animation, isFiltered){
60 |
61 | const transform = this.getTransform(index);
62 | const style = {
63 | width: this.itemWidth + 'px',
64 | height: this.itemHeight + 'px',
65 | WebkitTransform: transform,
66 | MozTransform: transform,
67 | msTransform: transform,
68 | transform: transform,
69 | position: 'absolute',
70 | boxSizing: 'border-box',
71 | display: isFiltered ? 'none' : 'block'
72 | };
73 |
74 | if(animation){
75 | style.WebkitTransition = '-webkit-' + animation;
76 | style.MozTransition = '-moz-' + animation;
77 | style.msTransition = 'ms-' + animation;
78 | style.transition = animation;
79 | }
80 |
81 | return style;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-absolute-grid",
3 | "version": "3.0.1",
4 | "description": "An absolutely positioned responsive touch-enabled fully configurable React grid with drag/drop, filtering, and sorting",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "NODE_ENV=production webpack --config webpack/build.config.js --optimize-minimize",
8 | "build-demo": "webpack --config 'webpack/demo.config.js' --optimize-minimize --content-base demo",
9 | "dev": "webpack-dev-server --config 'webpack/dev.config.js' --devtool eval --progress --colors --hot --content-base demo",
10 | "lint": "eslint lib",
11 | "prepare": "npm run lint && npm run build"
12 | },
13 | "author": "Jonathan Rowny",
14 | "contributors": [
15 | {
16 | "name": "Andrey Okonetchnikov",
17 | "email": "andrej.okonetschnikow@gmail.com",
18 | "url": "http://okonet.ru"
19 | }
20 | ],
21 | "license": "MIT",
22 | "dependencies": {},
23 | "devDependencies": {
24 | "babel-core": "^6.23.1",
25 | "babel-eslint": "^7.1.1",
26 | "babel-loader": "^6.3.2",
27 | "babel-plugin-lodash": "^3.2.11",
28 | "babel-preset-es2015": "^6.22.0",
29 | "babel-preset-react": "^6.23.0",
30 | "babel-preset-stage-0": "^6.22.0",
31 | "eslint": "^3.17.0",
32 | "eslint-plugin-react": "^6.10.0",
33 | "lodash": "^4.17.1",
34 | "node-libs-browser": "^2.0.0",
35 | "prop-types": "^15.5.10",
36 | "react": "^15.4.2",
37 | "react-addons-perf": "^15.4.2",
38 | "react-dom": "^15.4.2",
39 | "webpack": "^1.12.4",
40 | "webpack-dev-server": "^1.12.1"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/webpack/build.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = {
4 | entry: './index.js',
5 | output: {
6 | path: './dist',
7 | filename: 'index.js',
8 | libraryTarget: 'umd',
9 | library: 'AbsoluteGrid'
10 | },
11 | module: {
12 | loaders: [{
13 | test: /\.jsx?$/, // A regexp to test the require path. accepts either js or jsx
14 | loader: 'babel' // The module to load. "babel" is short for "babel-loader"
15 | }]
16 | },
17 | plugins: [
18 | new webpack.DefinePlugin({
19 | 'process.env': {
20 | NODE_ENV: JSON.stringify('production')
21 | }
22 | }),
23 | new webpack.optimize.UglifyJsPlugin()
24 | ],
25 | externals: {
26 | react: 'react'
27 | }
28 | };
29 |
30 |
31 |
--------------------------------------------------------------------------------
/webpack/demo.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | var config = {
5 | entry: [path.resolve(__dirname, '../demo.js')],
6 | output: {
7 | path: path.resolve(__dirname, '../demo'),
8 | filename: 'AbsoluteGrid.js'
9 | },
10 | plugins: [
11 | new webpack.DefinePlugin({
12 | 'process.env': {
13 | NODE_ENV: JSON.stringify('production')
14 | }
15 | }),
16 | new webpack.optimize.UglifyJsPlugin()
17 | ],
18 | module: {
19 | loaders: [{
20 | test: /\.jsx?$/, // A regexp to test the require path. accepts either js or jsx
21 | loader: 'babel' // The module to load. "babel" is short for "babel-loader"
22 | }]
23 | }
24 | };
25 |
26 | module.exports = config;
27 |
--------------------------------------------------------------------------------
/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var config = {
4 | entry: ['webpack/hot/dev-server', path.resolve(__dirname, '../demo.js')],
5 | output: {
6 | path: path.resolve(__dirname, '../demo'),
7 | filename: 'AbsoluteGrid.js'
8 | },
9 | module: {
10 | loaders: [{
11 | test: /\.jsx?$/, // A regexp to test the require path. accepts either js or jsx
12 | loader: 'babel' // The module to load. "babel" is short for "babel-loader"
13 | }]
14 | }
15 | };
16 |
17 | module.exports = config;
18 |
--------------------------------------------------------------------------------