(this.aboveSpacer = div)} style={aboveSpacerStyle} className={'above-spacer'} />
449 |
(this.itemsContainer = div)}>
450 | {listToRender}
451 |
452 |
(this.belowSpacer = div)} style={belowSpacerStyle} className={'below-spacer'} />
453 |
454 |
455 | );
456 | }
457 | }
458 | DynamicVirtualizedScrollbar.propTypes = {
459 | minElemHeight: PropTypes.number.isRequired
460 | };
461 | export default DynamicVirtualizedScrollbar;
462 |
--------------------------------------------------------------------------------
/src/components/virtualized-scrollbar.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Scrollbars} from 'react-custom-scrollbars';
3 | //import {SpringSystem} from 'rebound';
4 | import Rebound from 'rebound';
5 |
6 | class VirtualizedScrollBar extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | elemHeight: this.props.staticElemHeight ? this.props.staticElemHeight : 50,
11 | scrollOffset: 0,
12 | elemOverScan: this.props.overScan ? this.props.overScan : 3,
13 | topSpacerHeight: 0,
14 | unrenderedBelow: 0,
15 | unrenderedAbove: 0
16 | };
17 | this.stickyElems = null;
18 | }
19 |
20 | componentDidMount() {
21 | this.springSystem = new Rebound.SpringSystem();
22 | this.spring = this.springSystem.createSpring();
23 | this.spring.setOvershootClampingEnabled(true);
24 | this.spring.addListener({onSpringUpdate: this.handleSpringUpdate.bind(this)});
25 | }
26 |
27 | componentWillUnmount() {
28 | this.springSystem.deregisterSpring(this.spring);
29 | this.springSystem.removeAllListeners();
30 | this.springSystem = undefined;
31 | this.spring.destroy();
32 | this.spring = undefined;
33 | }
34 |
35 | handleSpringUpdate(spring) {
36 | const val = spring.getCurrentValue();
37 | this.scrollBars.scrollTop(val);
38 | }
39 |
40 | // Find the first element to render, and render (containersize + overScan / index * height) elems after the first.
41 | getListToRender(list) {
42 | let listToRender = [];
43 | this.stickyElems = [];
44 | const elemHeight = this.state.elemHeight;
45 | const containerHeight = this.props.containerHeight;
46 | const maxVisibleElems = Math.floor(containerHeight / elemHeight);
47 | if (!containerHeight || this.state.scrollOffset == null) {
48 | return list;
49 | }
50 |
51 | let smallestIndexVisible = null;
52 | if (this.state.scrollOffset === 0 && (this.props.stickyElems && this.props.stickyElems.length === 0)) {
53 | smallestIndexVisible = 0;
54 | } else {
55 | for (let index = 0; index < list.length; index++) {
56 | const child = list[index];
57 | // Maintain elements that have the alwaysRender flag set. This is used to keep a dragged element rendered, even if its scroll parent would normally unmount it.
58 | if (this.props.stickyElems.find(id => id === child.props.draggableId)) {
59 | this.stickyElems.push(child);
60 | } else {
61 | const ySmallerThanList = (index + 1) * elemHeight < this.state.scrollOffset;
62 |
63 | if (ySmallerThanList) {
64 | // Keep overwriting to obtain the last element that is not smaller
65 | smallestIndexVisible = index;
66 | }
67 | }
68 | }
69 | }
70 | const start = Math.max(0, (smallestIndexVisible != null ? smallestIndexVisible : 0) - this.state.elemOverScan);
71 | // start plus number of visible elements plus overscan
72 | const end = smallestIndexVisible + maxVisibleElems + this.state.elemOverScan;
73 | // +1 because Array.slice isn't inclusive
74 | listToRender = list.slice(start, end + 1);
75 | // Remove any element from the list, if it was included in the stickied list
76 | if (this.stickyElems && this.stickyElems.length > 0) {
77 | listToRender = listToRender.filter(elem => !this.stickyElems.find(e => e.props.draggableId === elem.props.draggableId));
78 | }
79 | return listToRender;
80 | }
81 |
82 | // Save scroll position in state for virtualization
83 | handleScroll(e) {
84 | const scrollOffset = this.scrollBars ? this.scrollBars.getScrollTop() : 0;
85 | const scrollDiff = Math.abs(scrollOffset - this.state.scrollOffset);
86 | const leniency = Math.max(5, this.state.elemHeight * 0.1); // As to not update exactly on breakpoint, but instead 5px or 10% within an element being scrolled past
87 | if (!this.state.scrollOffset || scrollDiff >= this.state.elemHeight - leniency) {
88 | this.setState({scrollOffset: scrollOffset});
89 | }
90 | if (this.props.onScroll) {
91 | this.props.onScroll(e);
92 | }
93 | }
94 |
95 | // Animated scroll to top
96 | animateScrollTop(top) {
97 | const scrollTop = this.scrollBars.getScrollTop();
98 | this.spring.setCurrentValue(scrollTop).setAtRest();
99 | this.spring.setEndValue(top);
100 | }
101 |
102 | // Get height of virtualized scroll container
103 | getScrollHeight() {
104 | return this.scrollBars.getScrollHeight();
105 | }
106 | // Set scroll offset of virtualized scroll container
107 | scrollTop(val) {
108 | this.scrollBars.scrollTop(val);
109 | }
110 | // Get scroll offset of virtualized scroll container
111 | getScrollTop() {
112 | return this.scrollBars.getScrollTop();
113 | }
114 |
115 | render() {
116 | const {customScrollbars, children} = this.props;
117 | const UseScrollbars = customScrollbars || Scrollbars;
118 | const rowCount = children.length;
119 | const elemHeight = this.state.elemHeight;
120 |
121 | const height = rowCount * this.state.elemHeight;
122 | let childrenWithProps = React.Children.map(children, (child, index) => React.cloneElement(child, {originalindex: index}));
123 | this.numChildren = childrenWithProps.length;
124 |
125 | const hasScrolled = this.state.scrollOffset > 0;
126 |
127 | const listToRender = this.getListToRender(childrenWithProps);
128 |
129 | const unrenderedBelow = hasScrolled ? (listToRender && listToRender.length > 0 ? listToRender[0].props.originalindex : 0) - (this.stickyElems ? this.stickyElems.length : 0) : 0;
130 | const unrenderedAbove = listToRender && listToRender.length > 0 ? childrenWithProps.length - (listToRender[listToRender.length - 1].props.originalindex + 1) : 0;
131 | const belowSpacerStyle = this.props.disableVirtualization ? {width: '100%', height: 0} : {width: '100%', height: unrenderedBelow ? unrenderedBelow * elemHeight : 0};
132 |
133 | const aboveSpacerStyle = this.props.disableVirtualization ? {width: '100%', height: 0} : {width: '100%', height: unrenderedAbove ? unrenderedAbove * elemHeight : 0};
134 |
135 | if (this.stickyElems && this.stickyElems.length > 0) {
136 | listToRender.push(this.stickyElems[0]);
137 | }
138 |
139 | const innerStyle = {
140 | width: '100%',
141 | display: 'flex',
142 | flexDirection: 'column',
143 | flexGrow: '1'
144 | };
145 | if (!this.props.disableVirtualization) {
146 | innerStyle.minHeight = height;
147 | innerStyle.height = height;
148 | innerStyle.maxHeight = height;
149 | }
150 |
151 | return (
152 |
(this.scrollBars = div)}
155 | autoHeight={true}
156 | autoHeightMax={this.props.containerHeight}
157 | autoHeightMin={this.props.containerHeight}
158 | {...this.props.scrollProps}
159 | >
160 | (this._test = div)}>
161 |
162 | {listToRender}
163 |
164 |
165 |
166 | );
167 | }
168 | }
169 | VirtualizedScrollBar.propTypes = {};
170 | export default VirtualizedScrollBar;
171 |
--------------------------------------------------------------------------------
/src/examples/example-board.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import Droppable from '../components/droppable';
3 | import Draggable from '../components/draggable';
4 | import DragDropContext from '../components/drag_drop_context';
5 |
6 | class ExampleBoard extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | listData: [],
11 | numItems: 100,
12 | numColumns: 6
13 | };
14 | this.dragAndDropGroupName = 'exampleboard';
15 | this.droppables = [];
16 | }
17 |
18 | componentDidMount() {
19 | this.getListData();
20 | }
21 |
22 | getListData() {
23 | const numLists = this.state.numColumns;
24 | const newItemLists = [];
25 | for (let i = 0; i < numLists; i++) {
26 | newItemLists.push(this.generateTestList(i, this.state.numItems));
27 | }
28 | this.setState({listData: newItemLists});
29 | }
30 |
31 | generateTestList(num, numItems) {
32 | let entry = {name: 'droppable' + num + 'Items', items: [], index: num};
33 | for (let i = 0; i < numItems; i++) {
34 | entry.items.push({id: num + '-' + i, name: 'Item ' + num + '-' + i});
35 | }
36 | return entry;
37 | }
38 |
39 | getElemsToRender(list) {
40 | let dataToRender = [];
41 |
42 | list.forEach((entry, index) => {
43 | const list = [];
44 | entry.items.forEach(item => {
45 | list.push(
46 |
47 | alert('A click is not a drag')} className={'draggable-test'} style={{border: 'solid 1px black', height: '48px', backgroundColor: 'white', flexGrow: 1}}>
48 |
49 | {item.name}
50 |
51 |
52 |
53 | );
54 | });
55 | dataToRender.push({droppableId: 'droppable' + index, items: list});
56 | });
57 | return dataToRender;
58 | }
59 |
60 | componentDidUpdate(prevProps, prevState) {
61 | if (prevState.numItems !== this.state.numItems || prevState.numColumns !== this.state.numColumns) {
62 | this.getListData();
63 | }
64 | }
65 |
66 | handleInputChange(e) {
67 | if (Number(e.target.value) > 5000) {
68 | alert('Please, calm down.');
69 | return;
70 | }
71 | if (e.target.value !== this.state.numItems && e.target.value) {
72 | this.setState({numItems: Number(e.target.value)});
73 | }
74 | }
75 |
76 | handleColumnInputChange(e) {
77 | if (Number(e.target.value) > 100) {
78 | alert('Please, calm down.');
79 | return;
80 | }
81 | if (e.target.value !== this.state.numColumns && e.target.value) {
82 | this.setState({numColumns: Number(e.target.value)});
83 | }
84 | }
85 |
86 | scroll(ref) {
87 | if (ref) {
88 | ref.animateScrollTop(ref.getScrollTop() + 200);
89 | }
90 | }
91 |
92 | sideScroll(val) {
93 | this.dragDropContext.sideScroll(this.dragDropContext.getSideScroll() + val);
94 | }
95 |
96 | onDragEnd(source, destinationId, placeholderId) {
97 | const listToRemoveFrom = this.state.listData.find(list => list.name.includes(source.droppableId));
98 | const listToAddTo = this.state.listData.find(list => list.name.includes(destinationId));
99 | const elemToAdd = listToRemoveFrom.items.find(entry => entry.id === source.draggableId);
100 | let indexToRemove = listToRemoveFrom.items.findIndex(item => item.id === source.draggableId);
101 | let indexToInsert = placeholderId === 'END_OF_LIST' ? listToAddTo.items.length : placeholderId.includes('header') ? 0 : listToAddTo.items.findIndex(item => item.id === placeholderId);
102 | // Re-arrange within the same list
103 | if (listToRemoveFrom.name === listToAddTo.name) {
104 | if (indexToRemove === indexToInsert) {
105 | return;
106 | }
107 | // If we're moving an element below the insertion point, indexes will change.
108 | const direction = indexToRemove < indexToInsert ? 1 : 0;
109 | listToRemoveFrom.items.splice(indexToRemove, 1);
110 | listToAddTo.items.splice(indexToInsert - direction, 0, elemToAdd);
111 | } else {
112 | listToRemoveFrom.items.splice(indexToRemove, 1);
113 | listToAddTo.items.splice(indexToInsert, 0, elemToAdd);
114 | }
115 |
116 | const newData = this.state.listData;
117 | newData[listToRemoveFrom.index] = listToRemoveFrom;
118 | newData[listToAddTo.index] = listToAddTo;
119 | this.setState({testData: newData});
120 | }
121 |
122 | toggleSplit() {
123 | this.setState(prevState => {
124 | return {split: !prevState.split};
125 | });
126 | }
127 |
128 | render() {
129 | const elemsToRender = this.getElemsToRender(this.state.listData);
130 | const getListHeader = index => (
131 |
132 |
List {index}
133 |
this.scroll(this.droppables[index])}>
134 | Scroll
135 |
136 |
137 | );
138 |
139 | return (
140 |
141 |
(this.dragDropContext = div)}
143 | // 10px margin around page
144 | scrollContainerHeight={window.innerHeight - 10}
145 | dragAndDropGroup={this.dragAndDropGroupName}
146 | onDragEnd={this.onDragEnd.bind(this)}
147 | outerScrollBar={true}
148 | >
149 |
150 |
151 |
Example Board
152 |
153 |
157 |
158 |
159 | Single Row
160 |
161 |
162 | Multi Row
163 |
164 |
165 |
166 |
167 |
Items per column
168 |
(e.key === 'Enter' ? this.handleInputChange(e) : void 0)}
172 | onBlur={this.handleInputChange.bind(this)}
173 | />
174 |
175 |
176 |
Number of columns
177 |
178 |
(e.key === 'Enter' ? this.handleColumnInputChange(e) : void 0)}
182 | onBlur={this.handleColumnInputChange.bind(this)}
183 | />
184 |
185 |
186 | {elemsToRender.map((elem, index) =>
187 | !this.state.split || index < elemsToRender.length / 2 ? (
188 |
189 | this.droppables.push(div)}
195 | containerHeight={620}
196 | elemHeight={50}
197 | dragAndDropGroup={this.dragAndDropGroupName}
198 | droppableId={elem.droppableId}
199 | key={elem.droppableId}
200 | >
201 | {elem.items}
202 |
203 |
204 | ) : null
205 | )}
206 |
207 | {this.state.split ? (
208 |
209 | {elemsToRender.map((elem, index) =>
210 | index >= elemsToRender.length / 2 ? (
211 |
212 | this.droppables.push(div)}
217 | containerHeight={500}
218 | dragAndDropGroup={this.dragAndDropGroupName}
219 | droppableId={elem.droppableId}
220 | key={elem.droppableId}
221 | >
222 | {elem.items}
223 |
224 |
225 | ) : null
226 | )}
227 |
228 | ) : null}
229 |
230 |
231 | );
232 | }
233 | }
234 | ExampleBoard.propTypes = {};
235 | export default ExampleBoard;
236 |
--------------------------------------------------------------------------------
/src/examples/example-dynamic.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import Droppable from '../components/droppable';
3 | import Draggable from '../components/draggable';
4 | import DragDropContext from '../components/drag_drop_context';
5 |
6 | class DynamicHeightExample extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | listData: [],
11 | numItems: 50,
12 | numColumns: 6,
13 | showIndicators: false,
14 | useSections: false,
15 | lazyLoad: false
16 | };
17 | this.dragAndDropGroupName = 'exampleboard';
18 | this.droppables = [];
19 | this.TEST_ENV = window.location.href.includes('localhost');
20 | }
21 |
22 | componentDidMount() {
23 | this.getListData();
24 | }
25 |
26 | addMoreElements(e) {
27 | if (this.state.lazyLoad && e.scrollHeight - e.scrollTop < e.clientHeight + 1000) {
28 | this.setState({numItems: 100});
29 | }
30 | }
31 |
32 | toggleIndicators() {
33 | this.setState(prevState => {
34 | return {showIndicators: !prevState.showIndicators};
35 | });
36 | }
37 |
38 | toggleUseSections() {
39 | this.setState(prevState => {
40 | return {useSections: !prevState.useSections};
41 | });
42 | }
43 |
44 | getListData() {
45 | const numLists = this.state.numColumns;
46 | const newItemLists = [];
47 | for (let i = 0; i < numLists; i++) {
48 | newItemLists.push(this.generateTestList(i, this.state.numItems));
49 | }
50 | this.setState({listData: newItemLists});
51 | }
52 |
53 | generateTestList(num, numItems) {
54 | let entry = {name: 'droppable' + num + 'Items', items: [], index: num};
55 | const randomSize = () => 50 + Math.floor(Math.random() * Math.floor(250));
56 | const pseudoRandomSize = i =>
57 | 50 + (((i + 1) * (num + 1)) % 5 === 0 ? 200 : ((i + 1) * (num + 1)) % 4 === 0 ? 150 : ((i + 1) * (num + 1)) % 3 === 0 ? 100 : ((i + 1) * (num + 1)) % 2 === 0 ? 50 : 0);
58 | let sectionId = 0;
59 | for (let i = 0; i < numItems; i++) {
60 | if (i % 3 === 0) {
61 | sectionId = i;
62 | }
63 | entry.items.push({id: num + '-' + i, name: 'Item ' + num + '-' + i, height: this.state.lazyLoad ? pseudoRandomSize(i) : randomSize(), sectionId: 'Person ' + sectionId / 3});
64 | }
65 | return entry;
66 | }
67 |
68 | getElemsToRender(list) {
69 | let dataToRender = [];
70 | const seenSections = [];
71 | list.forEach((entry, index) => {
72 | const list = [];
73 | entry.items.forEach((item, idx) => {
74 | if (this.state.useSections && !seenSections.includes(entry.index + '-' + item.sectionId)) {
75 | list.push(
76 |
86 |
87 |
88 |
89 | {item.sectionId}
90 |
91 |
92 |
93 | );
94 | seenSections.push(entry.index + '-' + item.sectionId);
95 | }
96 | list.push(
97 |
98 | alert('A click is not a drag')}
100 | className={'draggable-test' + (this.state.recentlyMovedItem === item.id ? ' dropGlow' : '')}
101 | style={{border: 'solid 1px black', height: item.height, backgroundColor: 'white', flexGrow: 1, marginBottom: '2.5px', marginTop: '2.5px'}}
102 | >
103 |
104 | {item.name}
105 |
106 |
107 |
108 | );
109 | });
110 | dataToRender.push({droppableId: 'droppable' + index, items: list});
111 | });
112 | return dataToRender;
113 | }
114 |
115 | componentDidUpdate(prevProps, prevState) {
116 | if (prevState.numItems !== this.state.numItems || prevState.numColumns !== this.state.numColumns || prevState.lazyLoad !== this.state.lazyLoad) {
117 | this.getListData();
118 | }
119 | }
120 |
121 | handleInputChange(e) {
122 | if (Number(e.target.value) > 5000) {
123 | alert('Please, calm down.');
124 | return;
125 | }
126 | this.setState({numItems: Number(e.target.value)});
127 | }
128 |
129 | handleColumnInputChange(e) {
130 | if (Number(e.target.value) > 100) {
131 | alert('Please, calm down.');
132 | return;
133 | }
134 | this.setState({numColumns: Number(e.target.value)});
135 | }
136 |
137 | handleLazyLoadChange(e) {
138 | this.setState({lazyLoad: !this.state.lazyLoad});
139 | }
140 |
141 | scroll(ref) {
142 | if (ref) {
143 | ref.animateScrollTop(ref.getScrollTop() + 200);
144 | }
145 | }
146 |
147 | sideScroll(val) {
148 | this.dragDropContext.sideScroll(this.dragDropContext.getSideScroll() + val);
149 | }
150 |
151 | onDragCancel() {
152 | this.setState({recentlyMovedItem: null});
153 | }
154 |
155 | onDragEnd(source, destinationId, placeholderId, sectionId) {
156 | const listToRemoveFrom = this.state.listData.find(list => list.name.includes(source.droppableId));
157 | const listToAddTo = this.state.listData.find(list => list.name.includes(destinationId));
158 | const elemToAdd = listToRemoveFrom.items.find(entry => entry.id === source.draggableId);
159 | let indexToRemove = listToRemoveFrom.items.findIndex(item => item.id === source.draggableId);
160 | let indexToInsert =
161 | placeholderId != null
162 | ? placeholderId === 'END_OF_LIST'
163 | ? listToAddTo.items.length
164 | : placeholderId.includes('header')
165 | ? 0
166 | : listToAddTo.items.findIndex(item => item.id === placeholderId)
167 | : sectionId != null
168 | ? listToAddTo.items.findIndex(item => item.sectionId === sectionId) // Add at the first occurence of the section when dropping on top of a section
169 | : -1;
170 | const targetElem = listToAddTo.items[indexToInsert - 1];
171 | const isSameSection = targetElem && targetElem.sectionId && source.sectionId && targetElem.sectionId === source.sectionId;
172 | if (!isSameSection) {
173 | //indexToInsert += 1; // move into next section //TODO NOPE
174 | }
175 | // Re-arrange within the same list
176 | if (listToRemoveFrom.name === listToAddTo.name) {
177 | if (indexToRemove === indexToInsert) {
178 | return;
179 | }
180 | // If we're moving an element below the insertion point, indexes will change.
181 | const direction = indexToRemove < indexToInsert ? 1 : 0;
182 | listToRemoveFrom.items.splice(indexToRemove, 1);
183 | listToAddTo.items.splice(indexToInsert - direction, 0, elemToAdd);
184 | } else {
185 | listToRemoveFrom.items.splice(indexToRemove, 1);
186 | listToAddTo.items.splice(indexToInsert, 0, elemToAdd);
187 | }
188 |
189 | const newData = this.state.listData;
190 | newData[listToRemoveFrom.index] = listToRemoveFrom;
191 | newData[listToAddTo.index] = listToAddTo;
192 | this.setState({testData: newData, recentlyMovedItem: source.draggableId});
193 | }
194 |
195 | toggleSplit() {
196 | this.setState(prevState => {
197 | return {split: !prevState.split};
198 | });
199 | }
200 |
201 | render() {
202 | const elemsToRender = this.getElemsToRender(this.state.listData);
203 | const getListHeader = index => (
204 |
205 |
List {index}
206 |
this.scroll(this.droppables[index])}>
207 | Scroll
208 |
209 |
210 | );
211 |
212 | const scrollProps = {
213 | autoHide: true,
214 | hideTracksWhenNotNeeded: true
215 | };
216 |
217 | return (
218 |
219 |
(this.dragDropContext = div)}
221 | // 10px margin around page
222 | scrollContainerHeight={window.innerHeight - 10}
223 | dragAndDropGroup={this.dragAndDropGroupName}
224 | onDragEnd={this.onDragEnd.bind(this)}
225 | onDragCancel={this.onDragCancel.bind(this)}
226 | outerScrollBar={true}
227 | >
228 |
229 |
230 |
Dynamic Height Example
231 |
232 |
236 |
237 |
238 | Single Row
239 |
240 |
241 | Multi Row
242 |
243 |
244 | Show Virtualization Indicators
245 |
246 | {this.TEST_ENV ? (
247 |
248 | Use Sections
249 |
250 | ) : null}
251 |
252 |
253 |
254 |
Items per column
255 |
(e.key === 'Enter' ? this.handleInputChange(e) : void 0)}
259 | onBlur={this.handleInputChange.bind(this)}
260 | />
261 |
262 |
263 |
Number of columns
264 |
(e.key === 'Enter' ? this.handleColumnInputChange(e) : void 0)}
268 | onBlur={this.handleColumnInputChange.bind(this)}
269 | />
270 |
271 |
272 |
Lazy loading example
273 |
274 |
275 |
276 | {elemsToRender.map((elem, index) =>
277 | !this.state.split || index < elemsToRender.length / 2 ? (
278 |
279 | this.droppables.push(div)}
288 | containerHeight={800}
289 | dragAndDropGroup={this.dragAndDropGroupName}
290 | droppableId={elem.droppableId}
291 | key={elem.droppableId}
292 | onScroll={this.addMoreElements.bind(this)}
293 | >
294 | {elem.items}
295 |
296 |
297 | ) : null
298 | )}
299 |
300 | {this.state.split ? (
301 |
302 | {elemsToRender.map((elem, index) =>
303 | index >= elemsToRender.length / 2 ? (
304 |
305 | this.droppables.push(div)}
313 | containerHeight={620}
314 | dragAndDropGroup={this.dragAndDropGroupName}
315 | droppableId={elem.droppableId}
316 | key={elem.droppableId}
317 | >
318 | {elem.items}
319 |
320 |
321 | ) : null
322 | )}
323 |
324 | ) : null}
325 |
326 |
327 | );
328 | }
329 | }
330 | DynamicHeightExample.propTypes = {};
331 | export default DynamicHeightExample;
332 |
--------------------------------------------------------------------------------
/src/examples/example-multiple-droppables.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import Droppable from '../components/droppable';
3 | import Draggable from '../components/draggable';
4 | import DragDropContext from '../components/drag_drop_context';
5 | import DragScrollBar from '../components/drag_scroll_bar';
6 |
7 | class ExampleMultipleDroppables extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | listData: [],
12 | numItems: 100,
13 | numColumns: 6
14 | };
15 | this.dragAndDropGroupName = 'exampleboard';
16 | this.droppables = [];
17 | }
18 |
19 | componentDidMount() {
20 | this.getListData();
21 | }
22 |
23 | getListData() {
24 | const numLists = this.state.numColumns;
25 | const newItemLists = [];
26 | for (let i = 0; i < numLists; i++) {
27 | newItemLists.push(this.generateTestList(i, this.state.numItems));
28 | }
29 | this.setState({listData: newItemLists});
30 | }
31 |
32 | generateTestList(num, numItems) {
33 | let entry = {name: 'droppable' + num + 'Items', items: [], index: num};
34 | for (let i = 0; i < numItems; i++) {
35 | entry.items.push({id: num + '-' + i, name: 'Item ' + num + '-' + i});
36 | }
37 | return entry;
38 | }
39 |
40 | getElemsToRender(list) {
41 | let dataToRender = [];
42 |
43 | list.forEach((entry, index) => {
44 | const list = [];
45 | entry.items.forEach(item => {
46 | list.push(
47 |
48 | alert('A click is not a drag')} className={'draggable-test'} style={{border: 'solid 1px black', height: '48px', backgroundColor: 'white', flexGrow: 1}}>
49 |
50 | {item.name}
51 |
52 |
53 |
54 | );
55 | });
56 | dataToRender.push({droppableId: 'droppable' + index, items: list});
57 | });
58 | return dataToRender;
59 | }
60 |
61 | componentDidUpdate(prevProps, prevState) {
62 | if (prevState.numItems !== this.state.numItems || prevState.numColumns !== this.state.numColumns) {
63 | this.getListData();
64 | }
65 | }
66 |
67 | handleInputChange(e) {
68 | if (Number(e.target.value) > 5000) {
69 | alert('Please, calm down.');
70 | return;
71 | }
72 | if (e.target.value !== this.state.numItems && e.target.value) {
73 | this.setState({numItems: Number(e.target.value)});
74 | }
75 | }
76 |
77 | handleColumnInputChange(e) {
78 | if (Number(e.target.value) > 100) {
79 | alert('Please, calm down.');
80 | return;
81 | }
82 | if (e.target.value !== this.state.numColumns && e.target.value) {
83 | this.setState({numColumns: Number(e.target.value)});
84 | }
85 | }
86 |
87 | scroll(ref) {
88 | if (ref) {
89 | ref.animateScrollTop(ref.getScrollTop() + 200);
90 | }
91 | }
92 |
93 | sideScroll(val) {
94 | this.dragDropContext.sideScroll(this.dragDropContext.getSideScroll() + val);
95 | }
96 |
97 | onDragEnd(source, destinationId, placeholderId) {
98 | const listToRemoveFrom = this.state.listData.find(list => list.name.includes(source.droppableId));
99 | const listToAddTo = this.state.listData.find(list => list.name.includes(destinationId));
100 | const elemToAdd = listToRemoveFrom.items.find(entry => entry.id === source.draggableId);
101 | let indexToRemove = listToRemoveFrom.items.findIndex(item => item.id === source.draggableId);
102 | let indexToInsert = placeholderId === 'END_OF_LIST' ? listToAddTo.items.length : placeholderId.includes('header') ? 0 : listToAddTo.items.findIndex(item => item.id === placeholderId);
103 | // Re-arrange within the same list
104 | if (listToRemoveFrom.name === listToAddTo.name) {
105 | if (indexToRemove === indexToInsert) {
106 | return;
107 | }
108 | // If we're moving an element below the insertion point, indexes will change.
109 | const direction = indexToRemove < indexToInsert ? 1 : 0;
110 | listToRemoveFrom.items.splice(indexToRemove, 1);
111 | listToAddTo.items.splice(indexToInsert - direction, 0, elemToAdd);
112 | } else {
113 | listToRemoveFrom.items.splice(indexToRemove, 1);
114 | listToAddTo.items.splice(indexToInsert, 0, elemToAdd);
115 | }
116 |
117 | const newData = this.state.listData;
118 | newData[listToRemoveFrom.index] = listToRemoveFrom;
119 | newData[listToAddTo.index] = listToAddTo;
120 | this.setState({testData: newData});
121 | }
122 |
123 | toggleSplit() {
124 | this.setState(prevState => {
125 | return {split: !prevState.split};
126 | });
127 | }
128 |
129 | render() {
130 | const elemsToRender = this.getElemsToRender(this.state.listData);
131 | const getListHeader = index => (
132 |
133 |
List {index}
134 |
this.scroll(this.droppables[index])}>
135 | Scroll
136 |
137 |
138 | );
139 |
140 | return (
141 |
142 |
(this.dragDropContext = div)}
144 | // 10px margin around page
145 | scrollContainerHeight={window.innerHeight - 10}
146 | dragAndDropGroup={this.dragAndDropGroupName}
147 | onDragEnd={this.onDragEnd.bind(this)}
148 | outerScrollBar={true}
149 | >
150 |
151 |
152 |
Example Board
153 |
154 |
158 |
159 |
160 |
Items per column
161 |
(e.key === 'Enter' ? this.handleInputChange(e) : void 0)}
165 | onBlur={this.handleInputChange.bind(this)}
166 | />
167 |
168 |
169 |
Number of columns
170 |
(e.key === 'Enter' ? this.handleColumnInputChange(e) : void 0)}
174 | onBlur={this.handleColumnInputChange.bind(this)}
175 | />
176 |
177 | Droppable Group 1
178 |
179 |
180 | {elemsToRender.map((elem, index) =>
181 | !this.state.split || index < elemsToRender.length / 2 ? (
182 |
183 | this.droppables.push(div)}
189 | containerHeight={350}
190 | elemHeight={50}
191 | dragAndDropGroup={this.dragAndDropGroupName}
192 | droppableId={elem.droppableId}
193 | key={elem.droppableId}
194 | >
195 | {elem.items}
196 |
197 |
198 | ) : null
199 | )}
200 |
201 |
202 |
203 | Droppable Group 2
204 |
205 |
206 | {elemsToRender.map((elem, index) =>
207 | !this.state.split || index < elemsToRender.length / 2 ? (
208 |
209 | this.droppables.push(div)}
215 | containerHeight={350}
216 | elemHeight={50}
217 | dragAndDropGroup={this.dragAndDropGroupName}
218 | droppableId={elem.droppableId}
219 | key={elem.droppableId}
220 | >
221 | {elem.items}
222 |
223 |
224 | ) : null
225 | )}
226 |
227 |
228 |
229 |
230 | );
231 | }
232 | }
233 | ExampleMultipleDroppables.propTypes = {};
234 | export default ExampleMultipleDroppables;
235 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Draggable from './components/draggable';
2 | import Droppable from './components/droppable';
3 | import DragDropContext from './components/drag_drop_context';
4 | import DragScrollBar from './components/drag_scroll_bar';
5 | import VirtualizedScrollBar from './components/virtualized-scrollbar';
6 | import ExampleBoard from './examples/example-board';
7 | import DynamicHeightExample from './examples/example-dynamic';
8 | import ExampleMultipleDroppables from './examples/example-multiple-droppables';
9 |
10 | export {Draggable, Droppable, DragDropContext, DragScrollBar, VirtualizedScrollBar, ExampleBoard, DynamicHeightExample, ExampleMultipleDroppables};
11 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* add css styles here (optional) */
2 |
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | import ExampleComponent from './'
2 |
3 | describe('ExampleComponent', () => {
4 | it('is truthy', () => {
5 | expect(ExampleComponent).toBeTruthy()
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/src/util/event_manager.js:
--------------------------------------------------------------------------------
1 | const eventMap = new Map();
2 |
3 | const subscribe = (eventId, callback) => {
4 | if (!eventMap.has(eventId)) {
5 | eventMap.set(eventId, []);
6 | }
7 | eventMap.get(eventId).push(callback);
8 | };
9 |
10 | const unsubscribe = (eventId, callback) => {
11 | if (eventMap.has(eventId)) {
12 | const handlerArray = eventMap.get(eventId);
13 | const callbackIndex = handlerArray.indexOf(callback);
14 | if (callbackIndex >= 0) {
15 | handlerArray.splice(callbackIndex, 1);
16 | } else {
17 | console.warn('Unsubscription unsuccessful - callback not found.');
18 | }
19 | } else {
20 | console.warn('Unsubscription unsuccessful - eventId not found.');
21 | }
22 | };
23 |
24 | const dispatch = (eventId, ...args) => {
25 | if (!eventMap.has(eventId)) return;
26 | eventMap.get(eventId).forEach(callback => callback.call(this, ...args));
27 | };
28 |
29 | const EVENT_ID = {
30 | SCHEDULING_MODAL_MUTATION_SUCCESS: 0,
31 | WORKFLOW_DRAG_DESTINATION_PLACERHOLDER: 1,
32 | WORKFLOW_SAVE_DRAG: 2,
33 | WORKFLOW_MULTISELECT: 3,
34 | CANVAS_TIMELINE_FORCE_REDRAW: 4,
35 | SOCKET_NOTIFY: 5,
36 | DND_REGISTER_DRAG_MOVE: 6,
37 | DND_RESET_PLACEHOLDER: 7
38 | };
39 |
40 | module.exports = {
41 | EVENT_ID,
42 | subscribe,
43 | unsubscribe,
44 | dispatch
45 | };
46 |
--------------------------------------------------------------------------------
/src/util/util.js:
--------------------------------------------------------------------------------
1 | export default class Util {
2 | static getDragEvents(group) {
3 | return {
4 | id: group,
5 | moveEvent: group + '-MOVE',
6 | resetEvent: group + '-RESET',
7 | startEvent: group + '-START',
8 | endEvent: group + '-END',
9 | scrollEvent: group + '-SCROLL',
10 | placeholderEvent: group + '-PLACEHOLDER'
11 | };
12 | }
13 |
14 | static getDroppableParentElement(element, dragAndDropGroup) {
15 | let count = 0;
16 | let maxTries = 15;
17 | let droppableParentElem = null;
18 | while (element && element.parentNode && !droppableParentElem && element.tagName !== 'body' && count <= maxTries) {
19 | const foundDragAndDropGroup = element.getAttribute('droppablegroup');
20 | if (foundDragAndDropGroup && foundDragAndDropGroup === dragAndDropGroup) {
21 | droppableParentElem = element;
22 | }
23 | element = element.parentNode;
24 | count++;
25 | }
26 | return droppableParentElem;
27 | }
28 | static getDraggableParentElement(element) {
29 | let count = 0;
30 | let maxTries = 10;
31 | let draggableParentElem = null;
32 | while (element && element.parentNode && !draggableParentElem && element.tagName !== 'body' && count <= maxTries) {
33 | if (element.getAttribute('draggableid')) {
34 | draggableParentElem = element;
35 | break;
36 | }
37 | element = element.parentNode;
38 | count++;
39 | }
40 | return draggableParentElem;
41 | }
42 | static logUpdateReason(props, state, prevProps, prevState) {
43 | Object.entries(props).forEach(([key, val]) => prevProps[key] !== val && console.log(`Prop '${key}' changed`));
44 | Object.entries(state).forEach(([key, val]) => prevState[key] !== val && console.log(`State '${key}' changed`));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------