mirror of
https://github.com/bendtherules/piktochart-create-editor.git
synced 2026-08-18 13:52:56 +00:00
Complete DragContainer and DraggableNode components + Helpers
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
.App-intro {
|
.App-intro {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
|
background-color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
@keyframes App-logo-spin {
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './CreateEditorApp.css';
|
import './CreateEditorApp.css';
|
||||||
|
import { DragContainer } from '../DragContainer';
|
||||||
import { DraggableNode } from '../DraggableNode';
|
import { DraggableNode } from '../DraggableNode';
|
||||||
|
|
||||||
const logo = require('./logo.svg');
|
const logo = require('./logo.svg');
|
||||||
|
|
||||||
export class CreateEditorApp extends React.Component {
|
export class CreateEditorApp extends React.Component {
|
||||||
|
logFn() {
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<DraggableNode>
|
<DragContainer id="test">
|
||||||
<header className="App-header">
|
<header className="App-header">
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<DraggableNode id="test-img" containerId="test">
|
||||||
<h1 className="App-title">Welcome to React</h1>
|
<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>
|
</header>
|
||||||
</DraggableNode>
|
</DragContainer>
|
||||||
<p className="App-intro">
|
<p className="App-intro">
|
||||||
To get started, edit <code>src/App.tsx</code> and save to reload.
|
To get started, edit <code>src/App.tsx</code> and save to reload.
|
||||||
</p>
|
</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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { DragContainer } from './DragContainer';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.draggable-node {
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -1,25 +1,51 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import './DraggableNode.css';
|
||||||
|
import { DragContextProvider, DragSharedInfo, XYPair } from '../../Helpers';
|
||||||
|
|
||||||
interface OnClickProps {
|
interface DraggableNodeProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
onClick(event: React.MouseEvent<HTMLDivElement>): void;
|
id: string;
|
||||||
|
containerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DraggableNode extends React.Component {
|
export class DraggableNode extends React.Component<DraggableNodeProps, {}> {
|
||||||
getModifiedChildrenWithAddedClickHandler(): React.ReactElement<OnClickProps>[] {
|
|
||||||
return React.Children.map(this.props.children, (eachChild) => {
|
calculateTransform(offset: XYPair) {
|
||||||
// tslint:disable-next-line:no-console
|
return { transform: `translate(${offset.x}px, ${offset.y}px)` };
|
||||||
return React.cloneElement(
|
}
|
||||||
eachChild as React.ReactElement<OnClickProps>,
|
|
||||||
{
|
getContext(): React.Context<DragSharedInfo | undefined> {
|
||||||
onClick: function (event: React.MouseEvent<HTMLDivElement>) {
|
return DragContextProvider.get(this.props.containerId);
|
||||||
// tslint:disable-next-line:no-console
|
}
|
||||||
console.log(arguments);
|
|
||||||
}
|
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() {
|
render() {
|
||||||
return this.getModifiedChildrenWithAddedClickHandler();
|
const CustomConsumer = this.getContext().Consumer;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomConsumer>
|
||||||
|
{(dragSharedInfo) => {
|
||||||
|
return this.renderChildren(dragSharedInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</CustomConsumer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { DragContextProvider, DragStartFn, DragSharedInfo } from './DragContextProvider';
|
||||||
|
export { XYPair } from './utils';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface XYPair {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user