diff --git a/react-dnd/react-dnd-test.ts b/react-dnd/react-dnd-test.ts new file mode 100644 index 000000000..244144029 --- /dev/null +++ b/react-dnd/react-dnd-test.ts @@ -0,0 +1,260 @@ +/// +"use strict"; + +// Test adapted from the ReactDnD chess game tutorial: +// http://gaearon.github.io/react-dnd/docs-tutorial.html + +import React = require("react"); +import ReactDnd = require("react-dnd"); + +var r = React.DOM; + +import DragSource = ReactDnd.DragSource; +import DropTarget = ReactDnd.DropTarget; +import DragDropContext = ReactDnd.DragDropContext; +import HTML5Backend = require('react-dnd/modules/backends/HTML5'); + +// Game Component +// ---------------------------------------------------------------------- + +module Game { + var knightPosition = [0, 0]; + var observer: any = null; + + function emitChange() { + observer(knightPosition); + } + + export function observe(o: any) { + if (observer) { + throw new Error("Multiple observers not implemented."); + } + + observer = o; + emitChange(); + } + + export function moveKnight(toX: number, toY: number) { + knightPosition = [toX, toY]; + emitChange(); + } + + export function canMoveKnight(toX: number, toY: number) { + const x = knightPosition[0]; + const y = knightPosition[1]; + const dx = toX - x; + const dy = toY - y; + + return (Math.abs(dx) === 2 && Math.abs(dy) === 1) || + (Math.abs(dx) === 1 && Math.abs(dy) === 2); + } +} + +var ItemTypes = { + KNIGHT: "knight" +}; + +// Knight Component +// ---------------------------------------------------------------------- + +module Knight { + interface KnightP extends React.Props { + connectDragSource: ReactDnd.ConnectDragSource; + connectDragPreview: ReactDnd.ConnectDragPreview; + isDragging: boolean; + } + + var knightSource: ReactDnd.DragSourceSpec = { + beginDrag: (props) => { + return {}; + } + }; + + function knightCollect(connect: ReactDnd.DragSourceConnector, monitor: ReactDnd.DragSourceMonitor) { + return { + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging() + }; + } + + export class Knight extends React.Component { + static create = React.createFactory(Knight); + + componentDidMount() { + var img = HTML5Backend.getEmptyImage(); + img.onload = () => this.props.connectDragPreview(img); + } + + render() { + return this.props.connectDragSource( + r.div({ + style: { + opacity: this.props.isDragging ? 0.5 : 1, + fontSize: 25, + fontWeight: 'bold', + cursor: 'move' + } + }, "♘") + ); + } + } + + export var DndKnight = DragSource(ItemTypes.KNIGHT, knightSource, knightCollect)(Knight); + export var create = React.createFactory(DndKnight); +} + +// Square Component +// ---------------------------------------------------------------------- + +module Square { + interface SquareP extends React.Props { + black: boolean; + } + + export class Square extends React.Component { + render() { + var fill = this.props.black ? 'black' : 'white'; + return r.div({ + style: { + backgroundColor: fill + } + }) + } + } + + export var create = React.createFactory(Square); +} + +// BoardSquare Component +// ---------------------------------------------------------------------- + +module BoardSquare { + interface BoardSquareP extends React.Props { + x: number; + y: number; + connectDropTarget?: ReactDnd.ConnectDropTarget; + isOver?: boolean; + canDrop?: boolean; + } + + var boardSquareTarget: ReactDnd.DropTargetSpec = { + canDrop: (props) => Game.canMoveKnight(props.x, props.y), + drop: (props) => Game.moveKnight(props.x, props.y) + }; + + function boardSquareCollect(connect: ReactDnd.DropTargetConnector, monitor: ReactDnd.DropTargetMonitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }; + } + + export class BoardSquare extends React.Component { + private _renderOverlay = (color: string) => { + return r.div({ + style: { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + width: '100%', + zIndex: 1, + opacity: 0.5, + backgroundColor: color + } + }); + }; + + render() { + var black = (this.props.x + this.props.y) % 2 === 1; + var isOver = this.props.isOver; + var canDrop = this.props.canDrop; + + return this.props.connectDropTarget( + r.div({ + style: { + position: 'relative', + width: '100%', + height: '100%' + }, + children: [ + Square.create({ + black: black + }), + isOver && !canDrop ? this._renderOverlay('red') : null, + !isOver && canDrop ? this._renderOverlay('yellow') : null, + isOver && canDrop ? this._renderOverlay('green') : null + ] + }) + ); + } + } + + export var DndBoardSquare = DropTarget(ItemTypes.KNIGHT, boardSquareTarget, boardSquareCollect)(BoardSquare); + export var create = React.createFactory(DndBoardSquare); +} + +// Board Component +// ---------------------------------------------------------------------- + +module Board { + interface BoardP extends React.Props { + knightPosition: number[]; + } + + export class Board extends React.Component { + private _renderPiece = (x: number, y: number) => { + var knightX = this.props.knightPosition[0]; + var knightY = this.props.knightPosition[1]; + return x === knightX && y === knightY ? + Knight.create() : + null; + }; + + private _renderSquare = (i: number) => { + var x = i % 8; + var y = Math.floor(i / 8); + + return r.div({ + key: i, + style: { + width: '12.5%', + height: '12.5%' + } + }, BoardSquare.create({ + x: x, + y: y + }, this._renderPiece(x, y))); + }; + + render() { + var squares: React.DOMElement[] = []; + for (let i = 0; i < 64; i++) { + squares.push(this._renderSquare(i)); + } + + return r.div({ + style: { + width: '100%', + height: '100%', + display: 'flex', + flexWrap: 'wrap' + }, + children: squares + }); + } + } + + var DndBoard = DragDropContext(HTML5Backend)(Board); + export var create = React.createFactory(DndBoard); +} + + +// Render the Board Component +// ---------------------------------------------------------------------- + +Board.create({ + knightPosition: [0, 0] +}); diff --git a/react-dnd/react-dnd.d.ts b/react-dnd/react-dnd.d.ts new file mode 100644 index 000000000..86eab01cd --- /dev/null +++ b/react-dnd/react-dnd.d.ts @@ -0,0 +1,172 @@ +// Type definitions for React DnD v1.1.4 +// Project: https://github.com/gaearon/react-dnd +// Definitions by: Asana +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare module __ReactDnd { + import React = __React; + + // Decorated React Components + // ---------------------------------------------------------------------- + + class ContextComponent extends React.Component { + getDecoratedComponentInstance(): React.Component; + // Note: getManager is not yet documented on the React DnD docs. + getManager(): any; + } + + class DndComponent extends React.Component { + getDecoratedComponentInstance(): React.Component; + getHandlerId(): Identifier; + } + + interface ContextComponentClass

extends React.ComponentClass

{ + new(props?: P, context?: any): ContextComponent; + DecoratedComponent: React.ComponentClass

; + } + + interface DndComponentClass

extends React.ComponentClass

{ + new(props?: P, context?: any): DndComponent; + DecoratedComponent: React.ComponentClass

; + } + + // Top-level API + // ---------------------------------------------------------------------- + + export function DragSource

( + type: Identifier | ((props: P) => Identifier), + spec: DragSourceSpec

, + collect: (connect: DragSourceConnector, monitor: DragSourceMonitor) => Object, + options?: DndOptions

+ ): (componentClass: React.ComponentClass

) => DndComponentClass

; + + export function DropTarget

( + types: Identifier | Identifier[] | ((props: P) => Identifier | Identifier[]), + spec: DropTargetSpec

, + collect: (connect: DropTargetConnector, monitor: DropTargetMonitor) => Object, + options?: DndOptions

+ ): (componentClass: React.ComponentClass

) => DndComponentClass

; + + export function DragDropContext

( + backend: Backend + ): (componentClass: React.ComponentClass

) => ContextComponentClass

; + + // TODO: Add exported function for DragLayer. + // The React DnD docs say that this is an advanced feature that is only + // necessary when performing custom rendering or when using a custom + // backend. + + // Shared + // ---------------------------------------------------------------------- + + // The React DnD docs say that this can also be the ES6 Symbol. + type Identifier = string; + + interface ClientOffset { + x: number; + y: number; + } + + interface DndOptions

{ + arePropsEqual?(props: P, otherProps: P): boolean; + } + + // DragSource + // ---------------------------------------------------------------------- + + interface DragSourceSpec

{ + beginDrag(props: P, monitor?: DragSourceMonitor, component?: React.Component): Object; + endDrag?(props: P, monitor?: DragSourceMonitor, component?: React.Component): void; + canDrag?(props: P, monitor?: DragSourceMonitor): boolean; + isDragging?(props: P, monitor?: DragSourceMonitor): boolean; + } + + class DragSourceMonitor { + canDrag(): boolean; + isDragging(): boolean; + getItemType(): Identifier; + getItem(): Object; + getDropResult(): Object; + didDrop(): boolean; + getInitialClientOffset(): ClientOffset; + getInitialSourceClientOffset(): ClientOffset; + getClientOffset(): ClientOffset; + getDifferenceFromInitialOffset(): ClientOffset; + getSourceClientOffset(): ClientOffset; + } + + class DragSourceConnector { + dragSource(): ConnectDragSource; + dragPreview(): ConnectDragPreview; + } + + interface DragElementWrapper { +

(elementOrNode: React.ReactElement

, options?: O): React.ReactElement

; + } + + interface DragSourceOptions { + dropEffect?: string; + } + + interface DragPreviewOptions { + captureDraggingState?: boolean; + anchorX?: number; + anchorY?: number; + } + + type ConnectDragSource = DragElementWrapper; + type ConnectDragPreview = DragElementWrapper; + + /// DropTarget + // ---------------------------------------------------------------------- + + interface DropTargetSpec

{ + drop?(props: P, monitor?: DropTargetMonitor, component?: React.Component): Object|void; + hover?(props: P, monitor?: DropTargetMonitor, component?: React.Component): void; + canDrop?(props: P, monitor?: DropTargetMonitor): boolean; + } + + class DropTargetMonitor { + canDrop(): boolean; + isOver(options?: { shallow: boolean }): boolean; + getItemType(): Identifier; + getItem(): Object; + getDropResult(): Object; + didDrop(): boolean; + getInitialClientOffset(): ClientOffset; + getInitialSourceClientOffset(): ClientOffset; + getClientOffset(): ClientOffset; + getDifferenceFromInitialOffset(): ClientOffset; + getSourceClientOffset(): ClientOffset; + } + + class DropTargetConnector { + dropTarget(): ConnectDropTarget; + } + + type ConnectDropTarget =

(elementOrNode: React.ReactElement

) => React.ReactElement

; + + /// Backend + /// --------------------------------------------------------------------- + + // TODO: Fill in the Backend interface. + // The React DnD docs do not cover this, and this is only needed for + // creating custom backends (i.e. not using the built-in HTML5Backend). + interface Backend {} +} + +declare module "react-dnd" { + export = __ReactDnd; +} + +declare module "react-dnd/modules/backends/HTML5" { + enum _NativeTypes { FILE, URL, TEXT } + class HTML5Backend implements __ReactDnd.Backend { + static getEmptyImage(): any; // Image + static NativeTypes: _NativeTypes; + } + + export = HTML5Backend; +}