├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .storybook
└── main.js
├── LICENSE
├── README.md
├── __tests__
└── index.test.tsx
├── jest.config.js
├── package.json
├── src
└── index.tsx
├── stories
├── Demo.tsx
├── Drag.stories.tsx
├── Resize.stories.tsx
├── Restrict.stories.tsx
└── Snap.stories.tsx
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | docs/
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | "plugin:react/recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:storybook/recommended"],
7 | parserOptions: {
8 | ecmaVersion: 13,
9 | sourceType: 'module',
10 | ecmaFeatures: {
11 | jsx: true,
12 | },
13 | },
14 | env: {
15 | browser: true,
16 | jest: true,
17 | es2021: true,
18 | node: true,
19 | },
20 | plugins: [
21 | 'react',
22 | '@typescript-eslint',
23 | 'react-hooks',
24 | ],
25 | rules: {
26 | "semi": ["error", "never"],
27 | "react/sort-comp": "off",
28 | "react/require-default-props": "warn",
29 | "@typescript-eslint/indent": ["error", 2],
30 | "react/prop-types": "off",
31 | "react/self-closing-comp": "error",
32 | "@typescript-eslint/explicit-member-accessibility": "off",
33 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
34 | "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
35 | "react/display-name": "off"
36 | },
37 | settings: {
38 | react: {
39 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
40 | },
41 | },
42 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # testing
5 | /coverage
6 |
7 | # production
8 | /build
9 | .cache
10 | /dist
11 | /lib
12 |
13 | #development
14 | /storybook-static
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 | .vscode
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .rts*
28 | .rpt*
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../stories/*.stories.mdx",
4 | "../stories/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/addon-storysource/register",
10 |
11 | ],
12 | "framework": "@storybook/react"
13 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Victor Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reactablejs = Reactjs + Interactjs
2 |
3 | A react high-order component for [interactjs](https://github.com/taye/interact.js).
4 |
5 | Current supported features:
6 | - drag
7 | - resize
8 | - drop
9 | - multi-touch
10 | - restrict
11 | - snap
12 | - modifiers
13 |
14 | Current supported props:
15 | - options: `draggable` `resizable` `gesturable` `dropzone`.
16 | - event handlers: `onDragStart` `onDragMove` `onDragInertiaStart` `onDragEnd` `onResizeStart` `onResizeMove` `onResizeInertiaStart` `onResizeEnd` `onGestureStart` `onGestureMove` `onGestureEnd` `onDropActivate` `onDropdEactivate` `onDragEnter` `onDragLeave` `onDropMove` `onDrop` `onDown` `onMove` `onUp` `onCancel` `onTap` `onDoubleTap` `onHold`.
17 |
18 |
19 | **api details, visit [interactjs' docs](http://interactjs.io/docs/)**
20 |
21 | ## Installation
22 | > `reactablejs` use `interactjs` as `peerDependencies`, you should also install interactjs.
23 | ```
24 | npm install reactablejs interactjs --save
25 | ```
26 |
27 | ## Usage
28 | ```js
29 | import React from 'react'
30 | import reactable from 'reactablejs'
31 |
32 | const MyComponent = (props) => {
33 | return
34 | hello, world!
35 |
36 | }
37 |
38 | // MyComponent will receive getRef in props, put getRef to the element you want interact, then you can use all options and event handlers on Reactable
39 |
40 | const Reactable = reactable(MyComponent)
41 |
42 | ```
43 | ## Example
44 | - visit [storybooks](https://beizhedenglong.github.io/reactablejs/)
--------------------------------------------------------------------------------
/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import reactable from "../src/index"
3 | import { render, fireEvent } from "@testing-library/react"
4 |
5 | interface Props {
6 | getRef: React.RefObject;
7 | }
8 |
9 |
10 | const MyComponent = (props: Props) => test
11 | const ReactableComponent = reactable(MyComponent)
12 |
13 | test("reactable", () => {
14 | const downMock = jest.fn()
15 | const { getByText } = render(
16 |
20 | )
21 |
22 | const div = getByText(/test/)
23 | fireEvent.mouseDown(div)
24 | expect(downMock).toBeCalled()
25 | })
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | modulePathIgnorePatterns: ["/dist/"]
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactablejs",
3 | "version": "0.2.0",
4 | "source": "src/index.tsx",
5 | "main": "dist/index.js",
6 | "umd:main": "dist/index.umd.js",
7 | "module": "dist/index.m.js",
8 | "types": "dist/src/index.d.ts",
9 | "repository": "https://github.com/beizhedenglong/reactablejs.git",
10 | "author": "Victor Wang(wang shaojie) ",
11 | "license": "MIT",
12 | "private": false,
13 | "scripts": {
14 | "start": "yarn storybook",
15 | "demo": "parcel build ./examples/index.html -d docs --public-url ./",
16 | "build": "microbundle --no-compress --jsx React.createElement",
17 | "test": "jest",
18 | "storybook": "start-storybook -p 6006",
19 | "build-storybook": "build-storybook",
20 | "deploy-storybook": "storybook-to-ghpages"
21 | },
22 | "keywords": [
23 | "react",
24 | "reactable",
25 | "interact",
26 | "drag",
27 | "resize"
28 | ],
29 | "devDependencies": {
30 | "@storybook/addon-actions": "^6.4.9",
31 | "@storybook/addon-essentials": "^6.4.9",
32 | "@storybook/addon-links": "^6.4.9",
33 | "@storybook/addon-storysource": "^6.4.9",
34 | "@storybook/addons": "^6.4.9",
35 | "@storybook/react": "^6.4.9",
36 | "@storybook/storybook-deployer": "^2.8.10",
37 | "@testing-library/react": "^12.1.2",
38 | "@types/jest": "^27.0.3",
39 | "@types/react": "^17.0.37",
40 | "@types/react-dom": "^17.0.11",
41 | "@types/storybook__react": "^4.0.2",
42 | "@typescript-eslint/eslint-plugin": "^5.7.0",
43 | "@typescript-eslint/parser": "^5.7.0",
44 | "babel-core": "^6.26.3",
45 | "babel-loader": "^8.2.3",
46 | "babel-plugin-transform-class-properties": "^6.24.1",
47 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
48 | "babel-preset-env": "^1.7.0",
49 | "babel-preset-react": "^6.24.1",
50 | "eslint": "^8.4.1",
51 | "eslint-plugin-react": "^7.27.1",
52 | "eslint-plugin-react-hooks": "^4.3.0",
53 | "eslint-plugin-storybook": "^0.5.5",
54 | "interactjs": "^1.10.11",
55 | "jest": "^27.4.5",
56 | "microbundle": "^0.14.2",
57 | "parcel-bundler": "^1.6.2",
58 | "react": "^17.0.2",
59 | "react-docgen-typescript-loader": "^3.7.2",
60 | "react-dom": "^17.0.2",
61 | "react-scripts": "^4.0.3",
62 | "rimraf": "^3.0.2",
63 | "ts-jest": "^27.1.2",
64 | "ts-loader": "^9.2.6",
65 | "typescript": "^4.5.4"
66 | },
67 | "peerDependencies": {
68 | "interactjs": "^1.10.11",
69 | "react": "^17.0.2"
70 | },
71 | "dependencies": {}
72 | }
73 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import interact from 'interactjs'
2 | import * as React from 'react'
3 | import { useEffect, useRef } from 'react'
4 |
5 | const options = [
6 | 'draggable',
7 | 'resizable',
8 | 'gesturable',
9 | 'dropzone',
10 | ]
11 | const events = [
12 | // Interact Events
13 | 'DragStart',
14 | 'DragMove',
15 | 'DragInertiaStart',
16 | 'DragEnd',
17 | 'ResizeStart',
18 | 'ResizeMove',
19 | 'ResizeInertiaStart',
20 | 'ResizeEnd',
21 | 'GestureStart',
22 | 'GestureMove',
23 | 'GestureEnd',
24 | // Drop Events
25 | 'DropActivate',
26 | 'DropdEactivate',
27 | 'DragEnter',
28 | 'DragLeave',
29 | 'DropMove',
30 | 'Drop',
31 | // Pointer Events
32 | 'Down',
33 | 'Move',
34 | 'Up',
35 | 'Cancel',
36 | 'Tap',
37 | 'DoubleTap',
38 | 'Hold',
39 | ]
40 |
41 | export interface InjectedProps {
42 | getRef: React.Ref | React.LegacyRef;
43 | }
44 | export interface InteractProps {
45 | draggable?: Interact.DraggableOptions | boolean;
46 | resizable?: Interact.ResizableOptions | boolean;
47 | gesturable?: Interact.ResizableOptions | boolean;
48 | dropzone?: Interact.DropzoneOptions | boolean;
49 | onDragStart?: Interact.ListenersArg;
50 | onDragMove?: Interact.ListenersArg;
51 | onDragEnd?: Interact.ListenersArg;
52 | onResizeStart?: Interact.ListenersArg;
53 | onResizeMove?: Interact.ListenersArg;
54 | onResizeInertiaStart?: Interact.ListenersArg;
55 | onResizeEnd?: Interact.ListenersArg;
56 | onGestureStart?: Interact.ListenersArg;
57 | onGestureMove?: Interact.ListenersArg;
58 | onGestureEnd?: Interact.ListenersArg;
59 | onDropActivate?: Interact.ListenersArg;
60 | onDropdEactivate?: Interact.ListenersArg;
61 | onDragEnter?: Interact.ListenersArg;
62 | onDragLeave?: Interact.ListenersArg;
63 | onDropMove?: Interact.ListenersArg;
64 | onDrop?: Interact.ListenersArg;
65 | onDown?: Interact.ListenersArg;
66 | onMove?: Interact.ListenersArg;
67 | onUp?: Interact.ListenersArg;
68 | onCancel?: Interact.ListenersArg;
69 | onTap?: Interact.ListenersArg;
70 | onDoubleTap?: Interact.ListenersArg;
71 | onHold?: Interact.ListenersArg;
72 | }
73 |
74 | const reactable = (
75 | BaseComponent: React.ComponentType
76 | ): React.FC & InteractProps> => (props) => {
77 | const interactable = useRef(null)
78 | const node = useRef()
79 |
80 | //Create interactable
81 | useEffect(() => {
82 | if (!node.current) {
83 | console.error(' you should apply getRef props in the dom element') // eslint-disable-line
84 | return
85 | }
86 | interactable.current = interact(node.current)
87 | return () => interactable.current.unset()
88 | }, [])
89 |
90 | //Set options
91 | useEffect(() => {
92 | if (interactable.current) {
93 | options.forEach((option) => {
94 | if (option in props) {
95 | interactable.current[option](props[option])
96 | }
97 | })
98 | }
99 | // eslint-disable-next-line react-hooks/exhaustive-deps
100 | }, []) // "props" should be added to the update array. However, this causes infinite rerenders.
101 |
102 | //Set handlers
103 | useEffect(() => {
104 | if (interactable.current) {
105 | events.forEach((event) => {
106 | const handler = props[`on${event}`]
107 | if (typeof handler === 'function') {
108 | interactable.current
109 | .on(event.toLowerCase(), handler)
110 | }
111 | })
112 | return () => {
113 | events.forEach((event) => {
114 | const handler = props[`on${event}`]
115 | if (typeof handler === 'function') {
116 | interactable.current
117 | .off(event.toLowerCase(), handler)
118 | }
119 | })
120 | }
121 | }
122 | }, [props])
123 |
124 | const baseProps = (props) => {
125 | const baseProps = { ...props }
126 | options.forEach(option => delete baseProps[option])
127 | events.forEach(event => delete baseProps[`on${event}`])
128 | return baseProps
129 | }
130 |
131 | return
135 | }
136 | export default reactable
137 |
--------------------------------------------------------------------------------
/stories/Demo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export interface DemoProps {
4 | x?: number;
5 | y?: number;
6 | width?: number;
7 | height?: number;
8 | angle?: number;
9 | getRef: React.Ref;
10 | }
11 |
12 | const Demo = (props: DemoProps) => {
13 | const { getRef, x, y, angle, width, height } = props
14 | return (
15 |
28 | Reactable is a react hight-order component for interactjs.
29 |
30 | - left: {x}
31 | - top: {y}
32 | - width: {width}
33 | - height: {height}
34 |
35 |
36 | )
37 | }
38 |
39 | Demo.defaultProps = {
40 | x: 0,
41 | y: 0,
42 | width: 200,
43 | height: 200,
44 | angle: 0
45 | }
46 |
47 | export default Demo
48 |
--------------------------------------------------------------------------------
/stories/Drag.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 | import reactable from "../src"
5 | import Demo from "./Demo"
6 |
7 | const Reactable = reactable(Demo)
8 |
9 | const BasicDemo = () => {
10 | const [coordinate, setCoordinate] = React.useState({ x: 0, y: 0 })
11 | return (
12 | {
15 | const { dx, dy } = event
16 | setCoordinate(prev => ({
17 | x: prev.x + dx,
18 | y: prev.y + dy
19 | }))
20 | action("DragMove")(event)
21 | }}
22 | x={coordinate.x}
23 | y={coordinate.y}
24 | />
25 | )
26 | }
27 | const DraggableOptionsDemo = () => {
28 | const [coordinate, setCoordinate] = React.useState({ x: 0, y: 0 })
29 | return (
30 | setCoordinate(prev => ({
34 | x: prev.x + event.dx,
35 | y: prev.y + event.dy
36 | })),
37 | onend: action("DragEnd"),
38 | }}
39 | {...coordinate}
40 | />
41 | )
42 | }
43 |
44 | storiesOf('Drag', module)
45 | .add('basic', () => )
46 | .add("with options", () => )
47 |
--------------------------------------------------------------------------------
/stories/Resize.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 | import reactable from "../src"
5 | import Demo from "./Demo"
6 |
7 | const Reactable = reactable(Demo)
8 |
9 |
10 | const ResizeDemo = () => {
11 | const [coordinate, setCoordinate] = React.useState({ x: 0, y: 0,width: 300, height: 200 })
12 | return (
13 | {
18 | const { width, height } = e.rect
19 | const { left, top } = e.deltaRect
20 | setCoordinate(prev => {
21 | return {
22 | x: prev.x + left,
23 | y: prev.y + top,
24 | width,
25 | height
26 | }
27 | })
28 | }}
29 | {...coordinate}
30 | />
31 | )
32 | }
33 |
34 | storiesOf('Resize', module)
35 | .add('basic', () => )
36 |
--------------------------------------------------------------------------------
/stories/Restrict.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import interact from 'interactjs'
4 | import reactable from "../src"
5 | import Demo from "./Demo"
6 |
7 | const Reactable = reactable(Demo)
8 |
9 | const RestrictDemo = () => {
10 | const [coordinate, setCoordinate] = React.useState({ x: 0, y: 0, width: 300, height: 200 })
11 | return (
12 |
20 | {
23 | setCoordinate(prev => ({
24 | ...prev,
25 | x: prev.x + event.dx,
26 | y: prev.y + event.dy,
27 | }))
28 | },
29 | modifiers: [
30 | interact.modifiers.restrictRect({
31 | restriction: 'parent',
32 | endOnly: true
33 | })
34 | ],
35 | }}
36 | resizable={{
37 | edges: { left: true, right: true, bottom: true, top: true },
38 | onmove: (e) => {
39 | const { width, height } = e.rect
40 | const { left, top } = e.deltaRect
41 | setCoordinate(prev => {
42 | return {
43 | x: prev.x + left,
44 | y: prev.y + top,
45 | width,
46 | height
47 | }
48 | })
49 | },
50 | modifiers: [
51 | interact.modifiers.restrictEdges({
52 | outer: 'parent',
53 | endOnly: true
54 | }),
55 | ]
56 | }}
57 | {...coordinate}
58 | />
59 |
60 | )
61 | }
62 |
63 | storiesOf('Restrict', module)
64 | .add('basic', () => )
65 |
--------------------------------------------------------------------------------
/stories/Snap.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import interact from "interactjs"
4 | import reactable from "../src"
5 | import Demo from "./Demo"
6 |
7 | const Reactable = reactable(Demo)
8 |
9 | const SnapDemo = () => {
10 | const [coordinate, setCoordinate] = React.useState({ x: 0, y: 0 })
11 | return (
12 | setCoordinate(prev => ({
15 | x: prev.x + event.dx,
16 | y: prev.y + event.dy
17 | })),
18 | modifiers: [
19 | interact.modifiers.snap({
20 | targets: [
21 | interact.createSnapGrid({ x: 40, y: 40 } as any) as any
22 | ],
23 | range: Infinity,
24 | relativePoints: [{ x: 0, y: 0 }]
25 | }),
26 | ]
27 | }}
28 | {...coordinate}
29 | />
30 | )
31 | }
32 |
33 | storiesOf('Snap', module)
34 | .add('basic', () => )
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react"
4 | }
5 | }
--------------------------------------------------------------------------------