Skip to content

Commit

Permalink
feat(react-grid): Implement DragDropContext plugin (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
kvet authored Jun 7, 2017
1 parent 3d9a00b commit 31f6b2d
Show file tree
Hide file tree
Showing 35 changed files with 1,111 additions and 63 deletions.
14 changes: 14 additions & 0 deletions packages/dx-core/src/event-emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class EventEmitter {
constructor() {
this.handlers = [];
}
emit(e) {
this.handlers.forEach(handler => handler(e));
}
subscribe(handler) {
this.handlers.push(handler);
}
unsubscribe(handler) {
this.handlers.splice(this.handlers.indexOf(handler), 1);
}
}
22 changes: 22 additions & 0 deletions packages/dx-core/src/event-emitter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EventEmitter } from './event-emitter';

describe('EventEmitter', () => {
it('should work', () => {
const handler = jest.fn();
const emitter = new EventEmitter();

emitter.subscribe(handler);
emitter.emit('test');

expect(handler.mock.calls)
.toEqual([['test']]);

handler.mockReset();

emitter.unsubscribe(handler);
emitter.emit('test');

expect(handler.mock.calls)
.toEqual([]);
});
});
1 change: 1 addition & 0 deletions packages/dx-core/src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { PluginHost } from './plugin-host';
export { EventEmitter } from './event-emitter';
61 changes: 61 additions & 0 deletions packages/dx-react-core/src/drag-drop/context.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';

import { EventEmitter } from '@devexpress/dx-core';

class DragDropContextCore {
constructor() {
this.payload = null;
this.dragEmitter = new EventEmitter();
}
start(payload, clientOffset) {
this.payload = payload;
this.dragEmitter.emit({ payload: this.payload, clientOffset });
}
update(clientOffset) {
this.dragEmitter.emit({ payload: this.payload, clientOffset });
}
end(clientOffset) {
this.dragEmitter.emit({ payload: this.payload, clientOffset, end: true });
this.payload = null;
}
}

export class DragDropContext extends React.Component {
constructor(props) {
super(props);

this.dragDropContext = new DragDropContextCore();

this.dragDropContext.dragEmitter.subscribe(({ payload, clientOffset, end }) => {
this.props.onChange({
payload: end ? null : payload,
clientOffset: end ? null : clientOffset,
});
});
}
getChildContext() {
return {
dragDropContext: this.dragDropContext,
};
}
shouldComponentUpdate(nextProps) {
return nextProps.children !== this.props.children;
}
render() {
return this.props.children;
}
}

DragDropContext.childContextTypes = {
dragDropContext: PropTypes.shape().isRequired,
};

DragDropContext.propTypes = {
children: PropTypes.node.isRequired,
onChange: PropTypes.func,
};

DragDropContext.defaultProps = {
onChange: () => {},
};
44 changes: 44 additions & 0 deletions packages/dx-react-core/src/drag-drop/context.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { mount } from 'enzyme';

import { Draggable } from '../draggable';
import { DragDropContext } from './context';
import { DragSource } from './source';

describe('DragDropContext', () => {
it('should fire the "onChange" callback while dragging a source', () => {
const onChange = jest.fn();
const tree = mount(
<DragDropContext
onChange={onChange}
>
<div>
<DragSource
getPayload={() => 'data'}
>
<div className="source" />
</DragSource>
</div>
</DragDropContext>,
);

const draggable = tree.find(Draggable);

draggable.prop('onStart')({ x: 50, y: 50 });

expect(onChange.mock.calls[0][0])
.toEqual({ payload: 'data', clientOffset: { x: 50, y: 50 } });

onChange.mockReset();
draggable.prop('onUpdate')({ x: 100, y: 100 });

expect(onChange.mock.calls[0][0])
.toEqual({ payload: 'data', clientOffset: { x: 100, y: 100 } });

onChange.mockReset();
draggable.prop('onEnd')({ x: 100, y: 100 });

expect(onChange.mock.calls[0][0])
.toEqual({ payload: null, clientOffset: null });
});
});
32 changes: 32 additions & 0 deletions packages/dx-react-core/src/drag-drop/source.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Draggable } from '../draggable';

export class DragSource extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.children !== this.props.children;
}
render() {
const { dragDropContext } = this.context;
return (
<Draggable
onStart={({ x, y }) => dragDropContext.start(this.props.getPayload(), { x, y })}
onUpdate={({ x, y }) => dragDropContext.update({ x, y })}
onEnd={({ x, y }) => dragDropContext.end({ x, y })}
>
{this.props.children}
</Draggable>
);
}
}

DragSource.contextTypes = {
dragDropContext: PropTypes.shape().isRequired,
};

DragSource.propTypes = {
children: PropTypes.node.isRequired,
getPayload: PropTypes.func.isRequired,
};

DragSource.defaultProps = {};
70 changes: 70 additions & 0 deletions packages/dx-react-core/src/drag-drop/target.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';

const clamp = (value, min, max) => Math.max(Math.min(value, max), min);

export class DropTarget extends React.Component {
constructor(props, context) {
super(props, context);

this.node = null;
this.isOver = false;

this.handleDrag = this.handleDrag.bind(this);
}
componentWillMount() {
const { dragDropContext: { dragEmitter } } = this.context;
dragEmitter.subscribe(this.handleDrag);
}
shouldComponentUpdate(nextProps) {
return nextProps.children !== this.props.children;
}
componentWillUnmount() {
const { dragDropContext: { dragEmitter } } = this.context;
dragEmitter.unsubscribe(this.handleDrag);
}
handleDrag({ payload, clientOffset, end }) {
const { left, top, right, bottom } = this.node.getBoundingClientRect();
const isOver = clientOffset
&& clamp(clientOffset.x, left, right) === clientOffset.x
&& clamp(clientOffset.y, top, bottom) === clientOffset.y;

if (!this.isOver && isOver) this.props.onEnter({ payload, clientOffset });
if (this.isOver && isOver) this.props.onOver({ payload, clientOffset });
if (this.isOver && !isOver) this.props.onLeave({ payload, clientOffset });
if (isOver && end) this.props.onDrop({ payload, clientOffset });

this.isOver = isOver && !end;
}
render() {
const { children } = this.props;
return React.cloneElement(
React.Children.only(children),
{
ref: (node) => {
if (node) this.node = node;
if (typeof children.ref === 'function') children.ref(node);
},
},
);
}
}

DropTarget.contextTypes = {
dragDropContext: PropTypes.shape().isRequired,
};

DropTarget.propTypes = {
children: PropTypes.node.isRequired,
onEnter: PropTypes.func,
onOver: PropTypes.func,
onLeave: PropTypes.func,
onDrop: PropTypes.func,
};

DropTarget.defaultProps = {
onEnter: () => {},
onOver: () => {},
onLeave: () => {},
onDrop: () => {},
};
Loading

0 comments on commit 31f6b2d

Please sign in to comment.