Complete DragContainer and DraggableNode components + Helpers

This commit is contained in:
2018-04-08 12:12:10 +05:30
parent 72f38b59bc
commit b9279d4ff9
10 changed files with 254 additions and 19 deletions
@@ -20,6 +20,7 @@
.App-intro {
font-size: large;
background-color: gray;
}
@keyframes App-logo-spin {
@@ -1,19 +1,29 @@
import * as React from 'react';
import './CreateEditorApp.css';
import { DragContainer } from '../DragContainer';
import { DraggableNode } from '../DraggableNode';
const logo = require('./logo.svg');
export class CreateEditorApp extends React.Component {
logFn() {
// tslint:disable-next-line:no-console
console.log(arguments);
}
render() {
return (
<div className="App">
<DraggableNode>
<DragContainer id="test">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
<DraggableNode id="test-img" containerId="test">
<img src={logo} className="App-logo" alt="logo" draggable={false} />
</DraggableNode>
<DraggableNode id="test-text" containerId="test">
<h1 className="App-title">Welcome to React</h1>
</DraggableNode>
</header>
</DraggableNode>
</DragContainer>
<p className="App-intro">
To get started, edit <code>src/App.tsx</code> and save to reload.
</p>
@@ -0,0 +1,3 @@
.drag-container {
overflow: hidden;
}
@@ -0,0 +1,148 @@
import * as React from 'react';
import './DragContainer.css';
import { DragContextProvider, DragSharedInfo, XYPair } from '../../Helpers';
interface DragContainerProps {
id: string;
}
type TotalOffset = XYPair;
interface DragContainerState {
ongoing: {
dragging: boolean;
draggableId: string | undefined;
startMousePosition: XYPair | undefined;
currentMousePosition: XYPair | undefined;
};
pastCumulativeOffsetMap: {
[draggableId: string]: TotalOffset
};
}
export class DragContainer extends React.Component<DragContainerProps, DragContainerState> {
constructor(props: DragContainerProps) {
super(props);
this.state = {
ongoing: {
dragging: false,
draggableId: undefined,
startMousePosition: undefined,
currentMousePosition: undefined,
},
pastCumulativeOffsetMap: {
}
};
this.handleDragStartOnDraggableNode = this.handleDragStartOnDraggableNode.bind(this);
this.handleDragMove = this.handleDragMove.bind(this);
this.handleDragStop = this.handleDragStop.bind(this);
}
handleDragStartOnDraggableNode(draggableId: string, ev: React.MouseEvent<HTMLDivElement>): void {
this.setState({
ongoing: {
dragging: true,
draggableId: draggableId,
startMousePosition: { x: ev.clientX, y: ev.clientY },
currentMousePosition: { x: ev.clientX, y: ev.clientY },
}
});
}
handleDragMove(ev: React.MouseEvent<HTMLDivElement>) {
if (this.state.ongoing.dragging) {
const oldStateOngoing = this.state.ongoing;
let newStateOngoing = Object.assign({}, oldStateOngoing);
newStateOngoing.currentMousePosition = {
x: ev.clientX,
y: ev.clientY
};
this.setState({
ongoing: newStateOngoing
});
}
}
handleDragStop(ev: React.MouseEvent<HTMLDivElement>) {
if (this.state.ongoing.dragging) {
const oldState = this.state;
const draggableId = oldState.ongoing.draggableId as string;
const oldStateStartMousePosition = oldState.ongoing.startMousePosition as XYPair;
const pastCumulativeOffset = oldState.pastCumulativeOffsetMap[draggableId] || { x: 0, y: 0 };
let newState: DragContainerState = Object.assign({}, oldState);
newState.ongoing = {
dragging: false,
draggableId: undefined,
currentMousePosition: undefined,
startMousePosition: undefined
};
newState.pastCumulativeOffsetMap[draggableId] = {
x: pastCumulativeOffset.x + (ev.clientX - oldStateStartMousePosition.x),
y: pastCumulativeOffset.y + (ev.clientY - oldStateStartMousePosition.y),
};
this.setState(newState);
}
}
getContext(): React.Context<DragSharedInfo | undefined> {
return DragContextProvider.get(this.props.id);
}
// FIX DragSharedInfo providing by using draggableID map
getCurrentOffsetMap() {
const stateOngoing = this.state.ongoing;
const lastOffsetMap = this.state.pastCumulativeOffsetMap;
if (stateOngoing.dragging) {
const startMousePosition = stateOngoing.startMousePosition as XYPair;
const currentMousePosition = stateOngoing.currentMousePosition as XYPair;
const draggableId = stateOngoing.draggableId as string;
const lastOffsetForCurrentDraggableId = lastOffsetMap[draggableId] || { x: 0, y: 0 };
let newOffsetMap = Object.assign({}, lastOffsetMap);
newOffsetMap[draggableId] = {
x: lastOffsetForCurrentDraggableId.x + (currentMousePosition.x - startMousePosition.x),
y: lastOffsetForCurrentDraggableId.y + (currentMousePosition.y - startMousePosition.y),
};
return newOffsetMap;
} else {
return lastOffsetMap;
}
}
render() {
const CustomProvider = this.getContext().Provider;
const currentOffsetMap = this.getCurrentOffsetMap();
const providerValue: DragSharedInfo = {
dragStartHandler: this.handleDragStartOnDraggableNode,
offsetMap: currentOffsetMap
};
return (
<CustomProvider value={providerValue}>
<div
className="drag-container"
onMouseMove={this.handleDragMove}
onMouseUp={this.handleDragStop}
// tslint:disable-next-line:no-console
onFocus={() => { console.log('focus'); }}
// tslint:disable-next-line:no-console
onBlur={() => { console.log('blur'); }}
>
{this.props.children}
</div>
</CustomProvider >
);
}
}
+1
View File
@@ -0,0 +1 @@
export { DragContainer } from './DragContainer';
@@ -0,0 +1,4 @@
.draggable-node {
cursor: move;
user-select: none;
}
+41 -15
View File
@@ -1,25 +1,51 @@
import * as React from 'react';
import './DraggableNode.css';
import { DragContextProvider, DragSharedInfo, XYPair } from '../../Helpers';
interface OnClickProps {
onClick(event: React.MouseEvent<HTMLDivElement>): void;
interface DraggableNodeProps extends React.HTMLProps<HTMLDivElement> {
id: string;
containerId: string;
}
export class DraggableNode extends React.Component {
getModifiedChildrenWithAddedClickHandler(): React.ReactElement<OnClickProps>[] {
return React.Children.map(this.props.children, (eachChild) => {
// tslint:disable-next-line:no-console
return React.cloneElement(
eachChild as React.ReactElement<OnClickProps>,
{
onClick: function (event: React.MouseEvent<HTMLDivElement>) {
// tslint:disable-next-line:no-console
console.log(arguments);
}
});
export class DraggableNode extends React.Component<DraggableNodeProps, {}> {
calculateTransform(offset: XYPair) {
return { transform: `translate(${offset.x}px, ${offset.y}px)` };
}
getContext(): React.Context<DragSharedInfo | undefined> {
return DragContextProvider.get(this.props.containerId);
}
renderChildren(dragSharedInfo: DragSharedInfo | undefined): JSX.Element {
if (typeof dragSharedInfo === 'undefined') {
throw new TypeError('Context provider (DragContainer) absent in current JSX tree');
}
const wrappedDragStartHandler = ((ev: React.MouseEvent<HTMLDivElement>) => {
dragSharedInfo.dragStartHandler(this.props.id, ev);
});
const offsetForThisNode: XYPair = (dragSharedInfo.offsetMap[this.props.id] || { x: 0, y: 0 });
const tmpTransform = this.calculateTransform(offsetForThisNode);
return (
<div className="draggable-node" style={tmpTransform} onMouseDown={wrappedDragStartHandler}>
{this.props.children}
</div>
);
}
render() {
return this.getModifiedChildrenWithAddedClickHandler();
const CustomConsumer = this.getContext().Consumer;
return (
<CustomConsumer>
{(dragSharedInfo) => {
return this.renderChildren(dragSharedInfo);
}
}
</CustomConsumer>
);
}
}
+36
View File
@@ -0,0 +1,36 @@
import * as React from 'react';
import { XYPair } from './utils';
export interface DragSharedInfo {
dragStartHandler: DragStartFn;
offsetMap: {
[draggableId: string]: XYPair;
};
}
export type DragStartFn = (draggableId: string, ev: React.MouseEvent<HTMLDivElement>) => void;
interface StringContextMap {
[key: string]: React.Context<DragSharedInfo | undefined>;
}
export class DragContextProvider {
private static idContextMap: StringContextMap = {
};
public static get(id: string): React.Context<DragSharedInfo | undefined> {
if (this.has(id)) {
return this.idContextMap[id];
} else {
const newContext = React.createContext(undefined);
this.idContextMap[id] = newContext;
return newContext;
}
}
public static has(id: string): boolean {
return (id in this.idContextMap);
}
}
+2
View File
@@ -0,0 +1,2 @@
export { DragContextProvider, DragStartFn, DragSharedInfo } from './DragContextProvider';
export { XYPair } from './utils';
+4
View File
@@ -0,0 +1,4 @@
export interface XYPair {
x: number;
y: number;
}