├── .eslintrc.json
├── .github
└── workflows
│ └── dependency-review.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── demo
├── index.html
└── index.jsx
├── dist
├── react-lineto.js
└── react-lineto.min.js
├── package.json
├── preview.png
├── scripts
├── build.sh
├── docker-build.sh
├── server.sh
└── test.sh
├── src
├── index.d.ts
└── index.jsx
├── webpack.config.js
├── webpack.demo.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:react/recommended"],
7 | "parser": "@babel/eslint-parser",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | },
12 | "sourceType": "module"
13 | },
14 | "plugins": [
15 | "react"
16 | ],
17 | "settings": {
18 | "react": {
19 | "version": "detect"
20 | }
21 | },
22 | "rules": {
23 | "accessor-pairs": "error",
24 | "array-bracket-spacing": "error",
25 | "array-callback-return": "error",
26 | "arrow-body-style": "error",
27 | "arrow-parens": "error",
28 | "arrow-spacing": "error",
29 | "block-scoped-var": "error",
30 | "block-spacing": "error",
31 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
32 | "callback-return": "error",
33 | "camelcase": "error",
34 | "capitalized-comments": "off",
35 | "class-methods-use-this": "off",
36 | "comma-dangle": "off",
37 | "comma-spacing": [
38 | "error",
39 | {
40 | "after": true,
41 | "before": false
42 | }
43 | ],
44 | "comma-style": [
45 | "error",
46 | "last"
47 | ],
48 | "complexity": "error",
49 | "computed-property-spacing": [
50 | "error",
51 | "never"
52 | ],
53 | "consistent-return": "error",
54 | "consistent-this": "error",
55 | "curly": "error",
56 | "default-case": "off",
57 | "dot-location": "error",
58 | "dot-notation": "error",
59 | "eol-last": "error",
60 | "eqeqeq": "error",
61 | "func-call-spacing": "error",
62 | "func-name-matching": "error",
63 | "func-names": "error",
64 | "func-style": "off",
65 | "generator-star-spacing": "error",
66 | "global-require": "error",
67 | "guard-for-in": "error",
68 | "handle-callback-err": "error",
69 | "id-blacklist": "error",
70 | "id-length": "off",
71 | "id-match": "error",
72 | "indent": "off",
73 | "init-declarations": "error",
74 | "jsx-quotes": "error",
75 | "key-spacing": "error",
76 | "keyword-spacing": [
77 | "error",
78 | {
79 | "after": true,
80 | "before": true
81 | }
82 | ],
83 | "line-comment-position": "error",
84 | "linebreak-style": [
85 | "error",
86 | "unix"
87 | ],
88 | "lines-around-comment": "error",
89 | "lines-around-directive": "error",
90 | "max-depth": "error",
91 | "max-len": "off",
92 | "max-lines": "off",
93 | "max-nested-callbacks": "error",
94 | "max-params": "off",
95 | "max-statements": "off",
96 | "max-statements-per-line": "error",
97 | "multiline-ternary": "off",
98 | "new-cap": "error",
99 | "new-parens": "error",
100 | "newline-after-var": "off",
101 | "newline-before-return": "off",
102 | "newline-per-chained-call": "error",
103 | "no-alert": "error",
104 | "no-array-constructor": "error",
105 | "no-await-in-loop": "error",
106 | "no-bitwise": "error",
107 | "no-caller": "error",
108 | "no-catch-shadow": "error",
109 | "no-confusing-arrow": "error",
110 | "no-continue": "error",
111 | "no-div-regex": "error",
112 | "no-duplicate-imports": "error",
113 | "no-else-return": "error",
114 | "no-empty-function": "error",
115 | "no-eq-null": "error",
116 | "no-eval": "error",
117 | "no-extend-native": "error",
118 | "no-extra-bind": "error",
119 | "no-extra-label": "error",
120 | "no-extra-parens": "off",
121 | "no-floating-decimal": "error",
122 | "no-implicit-coercion": "error",
123 | "no-implicit-globals": "error",
124 | "no-implied-eval": "error",
125 | "no-inline-comments": "error",
126 | "no-invalid-this": "error",
127 | "no-iterator": "error",
128 | "no-label-var": "error",
129 | "no-labels": "error",
130 | "no-lone-blocks": "error",
131 | "no-lonely-if": "error",
132 | "no-loop-func": "error",
133 | "no-magic-numbers": "off",
134 | "no-mixed-operators": "off",
135 | "no-mixed-requires": "error",
136 | "no-multi-assign": "error",
137 | "no-multi-spaces": "off",
138 | "no-multi-str": "error",
139 | "no-multiple-empty-lines": "error",
140 | "no-native-reassign": "error",
141 | "no-negated-condition": "error",
142 | "no-negated-in-lhs": "error",
143 | "no-nested-ternary": "error",
144 | "no-new": "error",
145 | "no-new-func": "error",
146 | "no-new-object": "error",
147 | "no-new-require": "error",
148 | "no-new-wrappers": "error",
149 | "no-octal-escape": "error",
150 | "no-param-reassign": "error",
151 | "no-path-concat": "error",
152 | "no-plusplus": "error",
153 | "no-process-env": "error",
154 | "no-process-exit": "error",
155 | "no-proto": "error",
156 | "no-prototype-builtins": "error",
157 | "no-restricted-globals": "error",
158 | "no-restricted-imports": "error",
159 | "no-restricted-modules": "error",
160 | "no-restricted-properties": "error",
161 | "no-restricted-syntax": "error",
162 | "no-return-assign": "error",
163 | "no-return-await": "error",
164 | "no-script-url": "error",
165 | "no-self-compare": "error",
166 | "no-sequences": "error",
167 | "no-shadow": "error",
168 | "no-shadow-restricted-names": "error",
169 | "no-spaced-func": "error",
170 | "no-sync": "error",
171 | "no-tabs": "error",
172 | "no-template-curly-in-string": "error",
173 | "no-ternary": "off",
174 | "no-throw-literal": "error",
175 | "no-trailing-spaces": "error",
176 | "no-undef-init": "error",
177 | "no-undefined": "error",
178 | "no-underscore-dangle": "error",
179 | "no-unmodified-loop-condition": "error",
180 | "no-unneeded-ternary": "error",
181 | "no-unused-expressions": "error",
182 | "no-use-before-define": "error",
183 | "no-useless-call": "error",
184 | "no-useless-computed-key": "error",
185 | "no-useless-concat": "error",
186 | "no-useless-constructor": "error",
187 | "no-useless-escape": "error",
188 | "no-useless-rename": "error",
189 | "no-useless-return": "error",
190 | "no-var": "error",
191 | "no-void": "error",
192 | "no-warning-comments": "error",
193 | "no-whitespace-before-property": "error",
194 | "no-with": "error",
195 | "object-curly-newline": "off",
196 | "object-curly-spacing": [
197 | "error",
198 | "always"
199 | ],
200 | "object-property-newline": [
201 | "error",
202 | {
203 | "allowMultiplePropertiesPerLine": true
204 | }
205 | ],
206 | "object-shorthand": "off",
207 | "one-var": "off",
208 | "one-var-declaration-per-line": "error",
209 | "operator-assignment": [
210 | "error",
211 | "always"
212 | ],
213 | "operator-linebreak": "error",
214 | "padded-blocks": "off",
215 | "prefer-arrow-callback": "error",
216 | "prefer-const": "error",
217 | "prefer-destructuring": "error",
218 | "prefer-numeric-literals": "error",
219 | "prefer-promise-reject-errors": "error",
220 | "prefer-reflect": "error",
221 | "prefer-rest-params": "error",
222 | "prefer-spread": "error",
223 | "prefer-template": "error",
224 | "quote-props": "off",
225 | "quotes": [
226 | "error",
227 | "single"
228 | ],
229 | "radix": "error",
230 | "require-await": "error",
231 | "require-jsdoc": "off",
232 | "rest-spread-spacing": "error",
233 | "semi": "off",
234 | "semi-spacing": "error",
235 | "sort-imports": "off",
236 | "sort-keys": "off",
237 | "sort-vars": "error",
238 | "space-before-blocks": "error",
239 | "space-before-function-paren": "off",
240 | "space-in-parens": [
241 | "error",
242 | "never"
243 | ],
244 | "space-infix-ops": "error",
245 | "space-unary-ops": "error",
246 | "spaced-comment": [
247 | "error",
248 | "always"
249 | ],
250 | "strict": "error",
251 | "symbol-description": "error",
252 | "template-curly-spacing": [
253 | "error",
254 | "never"
255 | ],
256 | "template-tag-spacing": "error",
257 | "unicode-bom": [
258 | "error",
259 | "never"
260 | ],
261 | "valid-jsdoc": "error",
262 | "vars-on-top": "error",
263 | "wrap-iife": "error",
264 | "wrap-regex": "error",
265 | "yield-star-spacing": "error",
266 | "yoda": [
267 | "error",
268 | "never"
269 | ]
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | # Dependency Review Action
2 | #
3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
4 | #
5 | # Source repository: https://github.com/actions/dependency-review-action
6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
7 | name: 'Dependency Review'
8 | on: [pull_request]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | dependency-review:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: 'Checkout Repository'
18 | uses: actions/checkout@v3
19 | - name: 'Dependency Review'
20 | uses: actions/dependency-review-action@v3
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | yarn-error.log
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 | install:
5 | - yarn install
6 | script:
7 | - yarn build
8 | - yarn test
9 | deploy:
10 | provider: npm
11 | email: kdeloach@gmail.com
12 | api_key:
13 | secure: UAbDC994fYrXh+jLfb2HH9mtwJ8+Yae8ncCQOUCxHwGZwZIW8Ek4Ltwk01i94QRbujcl4+o93oJg0Ddj6RaC1jYnhNUbS6g8HvoRo3taFSvOt+Kh7zU2a7aoDjv8803kkh8tugWmOCfE75CfYrDXeAt+olQDo0tqtTXhqMWYFXKirdHkNHgblobOK605/vebUf8Dh5ZZGHqVyNVsPQ9LUMaO/81CmWO9SCkRe4JwO9ric8cp8tr9fkddoHtir1Iv6u745WFeKoFedQSU47sHCKD4pTGf9n6D46cyDwkIq2Euyg9tQIdsYNpL0cfLZWrql7IQLZA3y0/ENZhEvjE2og2XoLB8Z3rjpXryY8AUenrFiO3dfAFWYzE5RZli962avlaJZ5LUF4iSyz13n1Zh7S5cOnH1CytrE/18GUYPFoSw+eXvLi0XtJlI6RZtEdYyq0AcG8GzkyOh3vg9Fqhdi2rQGbsyVxdfawIdmguUR3evPMRMSeJr7XBlI3vvbelc6dNSdfIGOUViqlBijiLYLs+5nk1Pi+3UXYjr+q60Ic3G8kGHaNWhHDdjlPgg1kYWRgxc8ssd1t7e2bwyN4V5pSwASrKVQYYkGQGfqXRBNuOls/d3RVi5d37GHrwx3dRzfrZB7084VPPyX34THegjHNEugFvjTVOnhWSJ6fjr2z8=
14 | on:
15 | tags: true
16 | repo: kdeloach/react-lineto
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 3.3.0
2 | * Fix subpixel render issues of SteppedLine component
3 |
4 | ## 3.2.1
5 | * Fix vulnerabilities, update to react-17, babel-7, eslint-7, webpack-5, node lts
6 |
7 | ## 3.2.0
8 | * Add TypeScript type definitions
9 |
10 | ## 3.1.3
11 | * Include scroll position in line offset calculation
12 |
13 | ## 3.1.2
14 | * Clear timeout set by deferUpdate when unmounting component
15 |
16 | ## 3.1.1
17 | * Fix bug where `zIndex` defaults to 1 when prop value is 0
18 |
19 | ## 3.1.0
20 | * Set `NODE_ENV` to production for published bundle
21 | * Exclude `prop-types` from published bundle
22 |
23 | ## 3.0.0
24 | * Add `SteppedLine` and `SteppedLineTo` components
25 | * Remove `style` and `border` properties
26 | * Add `borderWidth`, `borderStyle`, and `borderColor` properties
27 | * More flexible anchor parsing
28 | * Support boolean or number value for `delay` property
29 | * Add more demos
30 |
31 | ## 2.1.0
32 | * Add `within` property to support mounting inside specific elements
33 |
34 | ## 2.0.0
35 | * Compatible with React 16
36 |
37 | ## 1.2.0
38 | * Fix demos in IE
39 | * Exclude React library from output bundle
40 |
41 | ## 1.1.1
42 | * Fix broken NPM deployment
43 |
44 | ## 1.1.0
45 | * Remove element offset calculation
46 | * Attach line element to document body
47 | * Improve anchor parsing
48 | * Create Line component which accepts X & Y pairs
49 | * Add "delay" property for deferred render
50 |
51 | ## 1.0.1
52 | * Output minified and non-minified bundles
53 |
54 | ## 1.0.0
55 | * Initial release
56 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Please do not check in any `dist/*` files. They will be regenerated
4 | during the release process.
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-buster-slim
2 |
3 | WORKDIR /usr/src
4 |
5 | COPY package.json yarn.lock ./
6 | RUN yarn install
7 |
8 | ENV PATH="${PATH}:/usr/src/node_modules/.bin"
9 |
10 | WORKDIR /usr/src/app
11 |
12 | ENTRYPOINT ["yarn"]
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Kevin DeLoach
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 | # react-lineto
2 |
3 | Draw a line between two elements in React.
4 |
5 | [](https://app.travis-ci.com/kdeloach/react-lineto)
6 |
7 | ## Getting Started
8 |
9 | ```
10 | yarn install
11 | yarn run demo
12 | ```
13 |
14 | Browse to [localhost:4567](http://localhost:4567).
15 |
16 | ## Demo
17 |
18 | 
19 |
20 | ## Components
21 |
22 | * [LineTo](#lineto)
23 | * [SteppedLineTo](#steppedlineto)
24 | * [Line](#line)
25 |
26 | ### LineTo
27 |
28 | Draw line between two DOM elements.
29 |
30 | #### Example
31 |
32 | ```
33 | import LineTo from 'react-lineto';
34 |
35 | function render() {
36 | return (
37 |
38 |
Element A
39 |
Element B
40 |
41 |
42 | );
43 | }
44 | ```
45 | If using multiple instances of `` inside separate components, you must provide a unique key for each of the container divs.
46 |
47 |
48 | #### Properties
49 |
50 | | Name | Type | Description | Example Values
51 | | ----------- | ------ | ---------------------------------------------- | --------------
52 | | borderColor | string | Border color | `#f00`, `red`, etc.
53 | | borderStyle | string | Border style | `solid`, `dashed`, etc.
54 | | borderWidth | number | Border width (px) |
55 | | className | string | Desired CSS className for the rendered element |
56 | | delay | number or bool | Force render after delay (ms) | `0`, `1`, `100`, `true`
57 | | fromAnchor | string | Anchor for starting point (Format: "x y") | `top right`, `bottom center`, `left`, `100% 0`
58 | | from\* | string | CSS class name of the first element |
59 | | toAnchor | string | Anchor for ending point (Format: "x y") | `top right`, `bottom center`, `left`, `100% 0`
60 | | to\* | string | CSS class name of the second element |
61 | | within | string | CSS class name of the desired container |
62 | | zIndex | number | Z-index offset |
63 |
64 | \* Required
65 |
66 | ### SteppedLineTo
67 |
68 | Draw stepped line between two DOM elements.
69 |
70 | #### Example
71 |
72 | ```
73 | import { SteppedLineTo } from 'react-lineto';
74 |
75 | function render() {
76 | return (
77 |
78 |
Element A
79 |
Element B
80 |
81 |
82 | );
83 | }
84 | ```
85 |
86 | #### Properties
87 |
88 | | Name | Type | Description | Example Values
89 | | ----------- | ------ | ---------------------------------------------- | --------------
90 | | borderColor | string | Border color | `#f00`, `red`, etc.
91 | | borderStyle | string | Border style | `solid`, `dashed`, etc.
92 | | borderWidth | number | Border width (px) |
93 | | className | string | Desired CSS className for the rendered element |
94 | | delay | number or bool | Force render after delay (ms) | `0`, `1`, `100`, `true`
95 | | fromAnchor | string | Anchor for starting point (Format: "x y") | `top right`, `bottom center`, `left`, `100% 0`
96 | | from\* | string | CSS class name of the first element |
97 | | orientation | enum | "h" for horizonal, "v" for vertical | `h` or `v`
98 | | toAnchor | string | Anchor for ending point (Format: "x y") | `top right`, `bottom center`, `left`, `100% 0`
99 | | to\* | string | CSS class name of the second element |
100 | | within | string | CSS class name of the desired container |
101 | | zIndex | number | Z-index offset |
102 |
103 | \* Required
104 |
105 | ### Line
106 |
107 | Draw line using pixel coordinates (relative to viewport).
108 |
109 | #### Example
110 |
111 | ```
112 | import { Line } from 'react-lineto';
113 |
114 | function render() {
115 | return (
116 |
117 | );
118 | }
119 | ```
120 |
121 | #### Properties
122 |
123 | | Name | Type | Description | Example Values
124 | | ----------- | ------ | ---------------------------------------------- | --------------
125 | | borderColor | string | Border color | `#f00`, `red`, etc.
126 | | borderStyle | string | Border style | `solid`, `dashed`, etc.
127 | | borderWidth | number | Border width (px) |
128 | | className | string | Desired CSS className for the rendered element |
129 | | within | string | CSS class name of the desired container |
130 | | x0\* | number | First X coordinate |
131 | | x1\* | number | Second X coordinate |
132 | | y0\* | number | First Y coordinate |
133 | | y1\* | number | Second Y coordinate |
134 | | zIndex | number | Z-index offset |
135 |
136 | \* Required
137 |
138 | ## Release Checklist
139 |
140 | 1. Bump version in `package.json`
141 | 1. Update `CHANGELOG.md`
142 | 1. Run `yarn build` or `./scripts/update`
143 | 1. Create version commit (ex. "2.0.0")
144 | 1. Create matching tag (ex. "2.0.0")
145 | 1. Push `master` branch and tags to origin
146 | 1. Verify Travis CI published NPM package
147 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | react-lineto demo
6 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/demo/index.jsx:
--------------------------------------------------------------------------------
1 | import 'core-js/stable';
2 | import 'regenerator-runtime/runtime';
3 |
4 | import React, { Component } from 'react';
5 | import PropTypes from 'prop-types';
6 | import { render } from 'react-dom';
7 |
8 | import LineTo, { SteppedLineTo, Line } from '../src/index.jsx';
9 |
10 | function Demo() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | class Block extends Component {
23 | render() {
24 | const { top, left, color, className } = this.props;
25 | const style = { top, left, backgroundColor: color };
26 | return (
27 |
33 | {this.props.children}
34 |
35 | );
36 | }
37 | }
38 |
39 | Block.propTypes = {
40 | children: PropTypes.any,
41 | onMouseOver: PropTypes.func,
42 | onMouseOut: PropTypes.func,
43 | top: PropTypes.string,
44 | left: PropTypes.string,
45 | color: PropTypes.string,
46 | className: PropTypes.string,
47 | };
48 |
49 | class PolygonTest extends Component {
50 | makeShape(x, y, n, initialAngle) {
51 | const elems = [];
52 | const lineLength = 100;
53 | const angle = Math.PI - Math.PI / n;
54 |
55 | let x0 = x;
56 | let y0 = y;
57 |
58 | for (let i = 0, theta = initialAngle; i < n; i += 1, theta += angle) {
59 | const x1 = x0 + lineLength * Math.cos(theta);
60 | const y1 = y0 + lineLength * Math.sin(theta);
61 | elems.push();
62 | x0 = x1;
63 | y0 = y1;
64 | }
65 |
66 | return elems;
67 | }
68 |
69 | render() {
70 | const triangle = this.makeShape(80, 75, 3, Math.PI / 3);
71 | const star = this.makeShape(150, 105, 5, 0);
72 | const ngon = this.makeShape(280, 85, 7, Math.PI / 7);
73 |
74 | return (
75 |
84 | );
85 | }
86 | }
87 |
88 | class SteppedTest extends Component {
89 | render() {
90 | const style = {
91 | delay: true,
92 | borderColor: '#ddd',
93 | borderStyle: 'solid',
94 | borderWidth: 3,
95 | };
96 | return (
97 |
149 | );
150 | }
151 | }
152 |
153 | class HoverTest extends Component {
154 | constructor(props) {
155 | super(props);
156 | this.initialState = {
157 | from: null,
158 | to: null,
159 | };
160 | this.state = this.initialState;
161 | this.clearLine = this.clearLine.bind(this);
162 | this.drawLine = this.drawLine.bind(this);
163 | }
164 | clearLine() {
165 | this.setState(this.initialState);
166 | }
167 |
168 | drawLine(from, to) {
169 | this.setState({ from, to });
170 | }
171 |
172 | render() {
173 | const { from, to } = this.state;
174 | const line = from && to ? (
175 |
182 | ) : null;
183 | return (
184 |
240 | );
241 | }
242 | }
243 |
244 | class DelayTest extends Component {
245 | constructor(props) {
246 | super(props);
247 | this.state = {
248 | targetVisible: false,
249 | };
250 | }
251 |
252 | render() {
253 | const target = this.state.targetVisible ? (
254 | F
260 | ) : null;
261 | return (
262 |
287 | );
288 | }
289 | }
290 |
291 | class TreeTest extends Component {
292 | render() {
293 | return (
294 |
300 | );
301 | }
302 | }
303 |
304 | class TreeItem extends Component {
305 | render() {
306 | const style = {
307 | delay: true,
308 | borderColor: '#ddd',
309 | borderStyle: 'solid',
310 | borderWidth: 3
311 | };
312 | const h = ({ _: 20, A: 120, B: 100, C: 200, D: 50 })[this.props.name[0] || '_'];
313 | const l = Math.ceil(((this.props.index + 2) / 20) * 100) + 10 * (this.props.depth + 1);
314 | return (
315 |
316 |
317 |
318 | {this.props.name || 'X'}
319 |
320 |
321 | {this.props.depth < 2 ? (
322 |
323 | {Array(Math.ceil(Math.random() * 3) + 1).fill(null).
324 | map((_, i) => (
325 |
332 | ))
333 | }
334 |
335 | ) : null}
336 | {this.props.parent ? (
337 |
345 | ) : null}
346 |
347 | );
348 | }
349 | }
350 |
351 | TreeItem.propTypes = {
352 | depth: PropTypes.number,
353 | index: PropTypes.number,
354 | parent: PropTypes.instanceOf(TreeItem),
355 | name: PropTypes.string
356 | };
357 |
358 | function createRootElement() {
359 | const root = document.createElement('div');
360 | root.setAttribute('id', 'root');
361 | document.body.appendChild(root);
362 | return root;
363 | }
364 |
365 | function getRootElement() {
366 | return document.getElementById('root') ||
367 | createRootElement();
368 | }
369 |
370 | render(
371 | ,
372 | getRootElement()
373 | );
374 |
--------------------------------------------------------------------------------
/dist/react-lineto.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory(require("prop-types"), require("react"));
4 | else if(typeof define === 'function' && define.amd)
5 | define("react-lineto", ["prop-types", "react"], factory);
6 | else if(typeof exports === 'object')
7 | exports["react-lineto"] = factory(require("prop-types"), require("react"));
8 | else
9 | root["react-lineto"] = factory(root["prop-types"], root["react"]);
10 | })(self, function(__WEBPACK_EXTERNAL_MODULE__229__, __WEBPACK_EXTERNAL_MODULE__297__) {
11 | return /******/ (() => { // webpackBootstrap
12 | /******/ "use strict";
13 | /******/ var __webpack_modules__ = ({
14 |
15 | /***/ 229:
16 | /***/ ((module) => {
17 |
18 | module.exports = __WEBPACK_EXTERNAL_MODULE__229__;
19 |
20 | /***/ }),
21 |
22 | /***/ 297:
23 | /***/ ((module) => {
24 |
25 | module.exports = __WEBPACK_EXTERNAL_MODULE__297__;
26 |
27 | /***/ })
28 |
29 | /******/ });
30 | /************************************************************************/
31 | /******/ // The module cache
32 | /******/ var __webpack_module_cache__ = {};
33 | /******/
34 | /******/ // The require function
35 | /******/ function __webpack_require__(moduleId) {
36 | /******/ // Check if module is in cache
37 | /******/ var cachedModule = __webpack_module_cache__[moduleId];
38 | /******/ if (cachedModule !== undefined) {
39 | /******/ return cachedModule.exports;
40 | /******/ }
41 | /******/ // Create a new module (and put it into the cache)
42 | /******/ var module = __webpack_module_cache__[moduleId] = {
43 | /******/ // no module.id needed
44 | /******/ // no module.loaded needed
45 | /******/ exports: {}
46 | /******/ };
47 | /******/
48 | /******/ // Execute the module function
49 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
50 | /******/
51 | /******/ // Return the exports of the module
52 | /******/ return module.exports;
53 | /******/ }
54 | /******/
55 | /************************************************************************/
56 | /******/ /* webpack/runtime/compat get default export */
57 | /******/ (() => {
58 | /******/ // getDefaultExport function for compatibility with non-harmony modules
59 | /******/ __webpack_require__.n = (module) => {
60 | /******/ var getter = module && module.__esModule ?
61 | /******/ () => (module['default']) :
62 | /******/ () => (module);
63 | /******/ __webpack_require__.d(getter, { a: getter });
64 | /******/ return getter;
65 | /******/ };
66 | /******/ })();
67 | /******/
68 | /******/ /* webpack/runtime/define property getters */
69 | /******/ (() => {
70 | /******/ // define getter functions for harmony exports
71 | /******/ __webpack_require__.d = (exports, definition) => {
72 | /******/ for(var key in definition) {
73 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
74 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
75 | /******/ }
76 | /******/ }
77 | /******/ };
78 | /******/ })();
79 | /******/
80 | /******/ /* webpack/runtime/hasOwnProperty shorthand */
81 | /******/ (() => {
82 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
83 | /******/ })();
84 | /******/
85 | /******/ /* webpack/runtime/make namespace object */
86 | /******/ (() => {
87 | /******/ // define __esModule on exports
88 | /******/ __webpack_require__.r = (exports) => {
89 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
90 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
91 | /******/ }
92 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
93 | /******/ };
94 | /******/ })();
95 | /******/
96 | /************************************************************************/
97 | var __webpack_exports__ = {};
98 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
99 | (() => {
100 | __webpack_require__.r(__webpack_exports__);
101 | /* harmony export */ __webpack_require__.d(__webpack_exports__, {
102 | /* harmony export */ "default": () => (/* binding */ LineTo),
103 | /* harmony export */ "SteppedLineTo": () => (/* binding */ SteppedLineTo),
104 | /* harmony export */ "Line": () => (/* binding */ Line),
105 | /* harmony export */ "SteppedLine": () => (/* binding */ SteppedLine)
106 | /* harmony export */ });
107 | /* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(229);
108 | /* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_0__);
109 | /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(297);
110 | /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
111 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
112 |
113 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
114 |
115 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
116 |
117 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
118 |
119 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
120 |
121 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
122 |
123 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
124 |
125 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
126 |
127 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
128 |
129 | function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
130 |
131 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
132 |
133 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
134 |
135 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
136 |
137 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
138 |
139 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
140 |
141 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
142 |
143 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
144 |
145 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
146 |
147 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
148 |
149 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
150 |
151 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
152 |
153 |
154 |
155 | var defaultAnchor = {
156 | x: 0.5,
157 | y: 0.5
158 | };
159 | var defaultBorderColor = '#f00';
160 | var defaultBorderStyle = 'solid';
161 | var defaultBorderWidth = 1;
162 | var optionalStyleProps = {
163 | borderColor: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
164 | borderStyle: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
165 | borderWidth: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number),
166 | className: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
167 | zIndex: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number)
168 | };
169 |
170 | var LineTo = /*#__PURE__*/function (_Component) {
171 | _inherits(LineTo, _Component);
172 |
173 | var _super = _createSuper(LineTo);
174 |
175 | function LineTo() {
176 | _classCallCheck(this, LineTo);
177 |
178 | return _super.apply(this, arguments);
179 | }
180 |
181 | _createClass(LineTo, [{
182 | key: "UNSAFE_componentWillMount",
183 | value: // eslint-disable-next-line camelcase
184 | function UNSAFE_componentWillMount() {
185 | this.fromAnchor = this.parseAnchor(this.props.fromAnchor);
186 | this.toAnchor = this.parseAnchor(this.props.toAnchor);
187 | this.delay = this.parseDelay(this.props.delay);
188 | }
189 | }, {
190 | key: "componentDidMount",
191 | value: function componentDidMount() {
192 | this.delay = this.parseDelay(this.props.delay);
193 |
194 | if (typeof this.delay !== 'undefined') {
195 | this.deferUpdate(this.delay);
196 | }
197 | } // eslint-disable-next-line camelcase
198 |
199 | }, {
200 | key: "UNSAFE_componentWillReceiveProps",
201 | value: function UNSAFE_componentWillReceiveProps(nextProps) {
202 | if (nextProps.fromAnchor !== this.props.fromAnchor) {
203 | this.fromAnchor = this.parseAnchor(this.props.fromAnchor);
204 | }
205 |
206 | if (nextProps.toAnchor !== this.props.toAnchor) {
207 | this.toAnchor = this.parseAnchor(this.props.toAnchor);
208 | }
209 |
210 | this.delay = this.parseDelay(nextProps.delay);
211 |
212 | if (typeof this.delay !== 'undefined') {
213 | this.deferUpdate(this.delay);
214 | }
215 | }
216 | }, {
217 | key: "componentWillUnmount",
218 | value: function componentWillUnmount() {
219 | if (this.t) {
220 | clearTimeout(this.t);
221 | this.t = null;
222 | }
223 | }
224 | }, {
225 | key: "shouldComponentUpdate",
226 | value: function shouldComponentUpdate() {
227 | // Always update component if the parent component has been updated.
228 | // The reason for this is that we would not only like to update
229 | // this component when the props have changed, but also when
230 | // the position of our target elements has changed.
231 | // We could return true only if the positions of `from` and `to` have
232 | // changed, but that may be expensive and unnecessary.
233 | return true;
234 | } // Forced update after delay (MS)
235 |
236 | }, {
237 | key: "deferUpdate",
238 | value: function deferUpdate(delay) {
239 | var _this = this;
240 |
241 | if (this.t) {
242 | clearTimeout(this.t);
243 | }
244 |
245 | this.t = setTimeout(function () {
246 | return _this.forceUpdate();
247 | }, delay);
248 | }
249 | }, {
250 | key: "parseDelay",
251 | value: function parseDelay(value) {
252 | if (typeof value === 'undefined') {
253 | return value;
254 | } else if (typeof value === 'boolean' && value) {
255 | return 0;
256 | }
257 |
258 | var delay = parseInt(value, 10);
259 |
260 | if (isNaN(delay) || !isFinite(delay)) {
261 | throw new Error("LinkTo could not parse delay attribute \"".concat(value, "\""));
262 | }
263 |
264 | return delay;
265 | }
266 | }, {
267 | key: "parseAnchorPercent",
268 | value: function parseAnchorPercent(value) {
269 | var percent = parseFloat(value) / 100;
270 |
271 | if (isNaN(percent) || !isFinite(percent)) {
272 | throw new Error("LinkTo could not parse percent value \"".concat(value, "\""));
273 | }
274 |
275 | return percent;
276 | }
277 | }, {
278 | key: "parseAnchorText",
279 | value: function parseAnchorText(value) {
280 | // Try to infer the relevant axis.
281 | switch (value) {
282 | case 'top':
283 | return {
284 | y: 0
285 | };
286 |
287 | case 'left':
288 | return {
289 | x: 0
290 | };
291 |
292 | case 'middle':
293 | return {
294 | y: 0.5
295 | };
296 |
297 | case 'center':
298 | return {
299 | x: 0.5
300 | };
301 |
302 | case 'bottom':
303 | return {
304 | y: 1
305 | };
306 |
307 | case 'right':
308 | return {
309 | x: 1
310 | };
311 | }
312 |
313 | return null;
314 | }
315 | }, {
316 | key: "parseAnchor",
317 | value: function parseAnchor(value) {
318 | if (!value) {
319 | return defaultAnchor;
320 | }
321 |
322 | var parts = value.split(' ');
323 |
324 | if (parts.length > 2) {
325 | throw new Error('LinkTo anchor format is " "');
326 | }
327 |
328 | var _parts = _slicedToArray(parts, 2),
329 | x = _parts[0],
330 | y = _parts[1];
331 |
332 | return Object.assign({}, defaultAnchor, x ? this.parseAnchorText(x) || {
333 | x: this.parseAnchorPercent(x)
334 | } : {}, y ? this.parseAnchorText(y) || {
335 | y: this.parseAnchorPercent(y)
336 | } : {});
337 | }
338 | }, {
339 | key: "findElement",
340 | value: function findElement(className) {
341 | return document.getElementsByClassName(className)[0];
342 | }
343 | }, {
344 | key: "detect",
345 | value: function detect() {
346 | var _this$props = this.props,
347 | from = _this$props.from,
348 | to = _this$props.to,
349 | _this$props$within = _this$props.within,
350 | within = _this$props$within === void 0 ? '' : _this$props$within;
351 | var a = this.findElement(from);
352 | var b = this.findElement(to);
353 |
354 | if (!a || !b) {
355 | return false;
356 | }
357 |
358 | var anchor0 = this.fromAnchor;
359 | var anchor1 = this.toAnchor;
360 | var box0 = a.getBoundingClientRect();
361 | var box1 = b.getBoundingClientRect();
362 | var offsetX = window.pageXOffset;
363 | var offsetY = window.pageYOffset;
364 |
365 | if (within) {
366 | var p = this.findElement(within);
367 | var boxp = p.getBoundingClientRect();
368 | offsetX -= boxp.left + (window.pageXOffset || document.documentElement.scrollLeft) - p.scrollLeft;
369 | offsetY -= boxp.top + (window.pageYOffset || document.documentElement.scrollTop) - p.scrollTop;
370 | }
371 |
372 | var x0 = box0.left + box0.width * anchor0.x + offsetX;
373 | var x1 = box1.left + box1.width * anchor1.x + offsetX;
374 | var y0 = box0.top + box0.height * anchor0.y + offsetY;
375 | var y1 = box1.top + box1.height * anchor1.y + offsetY;
376 | return {
377 | x0: x0,
378 | y0: y0,
379 | x1: x1,
380 | y1: y1
381 | };
382 | }
383 | }, {
384 | key: "render",
385 | value: function render() {
386 | var points = this.detect();
387 | return points ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, points, this.props)) : null;
388 | }
389 | }]);
390 |
391 | return LineTo;
392 | }(react__WEBPACK_IMPORTED_MODULE_1__.Component);
393 |
394 |
395 | LineTo.propTypes = _objectSpread({
396 | from: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string.isRequired),
397 | to: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string.isRequired),
398 | within: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
399 | fromAnchor: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
400 | toAnchor: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string),
401 | delay: prop_types__WEBPACK_IMPORTED_MODULE_0___default().oneOfType([(prop_types__WEBPACK_IMPORTED_MODULE_0___default().number), (prop_types__WEBPACK_IMPORTED_MODULE_0___default().bool)])
402 | }, optionalStyleProps);
403 | var SteppedLineTo = /*#__PURE__*/function (_LineTo) {
404 | _inherits(SteppedLineTo, _LineTo);
405 |
406 | var _super2 = _createSuper(SteppedLineTo);
407 |
408 | function SteppedLineTo() {
409 | _classCallCheck(this, SteppedLineTo);
410 |
411 | return _super2.apply(this, arguments);
412 | }
413 |
414 | _createClass(SteppedLineTo, [{
415 | key: "render",
416 | value: function render() {
417 | var points = this.detect();
418 | return points ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(SteppedLine, _extends({}, points, this.props)) : null;
419 | }
420 | }]);
421 |
422 | return SteppedLineTo;
423 | }(LineTo);
424 | var Line = /*#__PURE__*/function (_PureComponent) {
425 | _inherits(Line, _PureComponent);
426 |
427 | var _super3 = _createSuper(Line);
428 |
429 | function Line() {
430 | _classCallCheck(this, Line);
431 |
432 | return _super3.apply(this, arguments);
433 | }
434 |
435 | _createClass(Line, [{
436 | key: "componentDidMount",
437 | value: function componentDidMount() {
438 | // Append rendered DOM element to the container the
439 | // offsets were calculated for
440 | this.within.appendChild(this.el);
441 | }
442 | }, {
443 | key: "componentWillUnmount",
444 | value: function componentWillUnmount() {
445 | this.within.removeChild(this.el);
446 | }
447 | }, {
448 | key: "findElement",
449 | value: function findElement(className) {
450 | return document.getElementsByClassName(className)[0];
451 | }
452 | }, {
453 | key: "render",
454 | value: function render() {
455 | var _this2 = this;
456 |
457 | var _this$props2 = this.props,
458 | x0 = _this$props2.x0,
459 | y0 = _this$props2.y0,
460 | x1 = _this$props2.x1,
461 | y1 = _this$props2.y1,
462 | _this$props2$within = _this$props2.within,
463 | within = _this$props2$within === void 0 ? '' : _this$props2$within;
464 | this.within = within ? this.findElement(within) : document.body;
465 | var dy = y1 - y0;
466 | var dx = x1 - x0;
467 | var angle = Math.atan2(dy, dx) * 180 / Math.PI;
468 | var length = Math.sqrt(dx * dx + dy * dy);
469 | var positionStyle = {
470 | position: 'absolute',
471 | top: "".concat(y0, "px"),
472 | left: "".concat(x0, "px"),
473 | width: "".concat(length, "px"),
474 | zIndex: Number.isFinite(this.props.zIndex) ? String(this.props.zIndex) : '1',
475 | transform: "rotate(".concat(angle, "deg)"),
476 | // Rotate around (x0, y0)
477 | transformOrigin: '0 0'
478 | };
479 | var defaultStyle = {
480 | borderTopColor: this.props.borderColor || defaultBorderColor,
481 | borderTopStyle: this.props.borderStyle || defaultBorderStyle,
482 | borderTopWidth: this.props.borderWidth || defaultBorderWidth
483 | };
484 | var props = {
485 | className: this.props.className,
486 | style: Object.assign({}, defaultStyle, positionStyle)
487 | }; // We need a wrapper element to prevent an exception when then
488 | // React component is removed. This is because we manually
489 | // move the rendered DOM element after creation.
490 |
491 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("div", {
492 | className: "react-lineto-placeholder"
493 | }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("div", _extends({
494 | ref: function ref(el) {
495 | _this2.el = el;
496 | }
497 | }, props)));
498 | }
499 | }]);
500 |
501 | return Line;
502 | }(react__WEBPACK_IMPORTED_MODULE_1__.PureComponent);
503 | Line.propTypes = _objectSpread({
504 | x0: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
505 | y0: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
506 | x1: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
507 | y1: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired)
508 | }, optionalStyleProps);
509 | var SteppedLine = /*#__PURE__*/function (_PureComponent2) {
510 | _inherits(SteppedLine, _PureComponent2);
511 |
512 | var _super4 = _createSuper(SteppedLine);
513 |
514 | function SteppedLine() {
515 | _classCallCheck(this, SteppedLine);
516 |
517 | return _super4.apply(this, arguments);
518 | }
519 |
520 | _createClass(SteppedLine, [{
521 | key: "render",
522 | value: function render() {
523 | if (this.props.orientation === 'h') {
524 | return this.renderHorizontal();
525 | }
526 |
527 | return this.renderVertical();
528 | }
529 | }, {
530 | key: "renderVertical",
531 | value: function renderVertical() {
532 | var x0 = Math.round(this.props.x0);
533 | var y0 = Math.round(this.props.y0);
534 | var x1 = Math.round(this.props.x1);
535 | var y1 = Math.round(this.props.y1);
536 | var dx = x1 - x0;
537 |
538 | if (Math.abs(dx) <= 1) {
539 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
540 | x0: x0,
541 | y0: y0,
542 | x1: x0,
543 | y1: y1
544 | }));
545 | }
546 |
547 | if (dx === 0) {
548 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, this.props);
549 | }
550 |
551 | var borderWidth = this.props.borderWidth || defaultBorderWidth;
552 | var y2 = Math.round((y0 + y1) / 2);
553 | var xOffset = dx > 0 ? borderWidth : 0;
554 | var minX = Math.min(x0, x1) - xOffset;
555 | var maxX = Math.max(x0, x1);
556 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("div", {
557 | className: "react-steppedlineto"
558 | }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
559 | x0: x0,
560 | y0: y0,
561 | x1: x0,
562 | y1: y2
563 | })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
564 | x0: x1,
565 | y0: y1,
566 | x1: x1,
567 | y1: y2
568 | })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
569 | x0: minX,
570 | y0: y2,
571 | x1: maxX,
572 | y1: y2
573 | })));
574 | }
575 | }, {
576 | key: "renderHorizontal",
577 | value: function renderHorizontal() {
578 | var x0 = Math.round(this.props.x0);
579 | var y0 = Math.round(this.props.y0);
580 | var x1 = Math.round(this.props.x1);
581 | var y1 = Math.round(this.props.y1);
582 | var dy = y1 - y0;
583 |
584 | if (Math.abs(dy) <= 1) {
585 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
586 | x0: x0,
587 | y0: y0,
588 | x1: x1,
589 | y1: y0
590 | }));
591 | }
592 |
593 | if (dy === 0) {
594 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, this.props);
595 | }
596 |
597 | var borderWidth = this.props.borderWidth || defaultBorderWidth;
598 | var x2 = Math.round((x0 + x1) / 2);
599 | var yOffset = dy < 0 ? borderWidth : 0;
600 | var minY = Math.min(y0, y1) - yOffset;
601 | var maxY = Math.max(y0, y1);
602 | return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("div", {
603 | className: "react-steppedlineto"
604 | }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
605 | x0: x0,
606 | y0: y0,
607 | x1: x2,
608 | y1: y0
609 | })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
610 | x0: x1,
611 | y0: y1,
612 | x1: x2,
613 | y1: y1
614 | })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(Line, _extends({}, this.props, {
615 | x0: x2,
616 | y0: minY,
617 | x1: x2,
618 | y1: maxY
619 | })));
620 | }
621 | }]);
622 |
623 | return SteppedLine;
624 | }(react__WEBPACK_IMPORTED_MODULE_1__.PureComponent);
625 | SteppedLine.propTypes = _objectSpread({
626 | x0: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
627 | y0: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
628 | x1: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
629 | y1: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().number.isRequired),
630 | orientation: prop_types__WEBPACK_IMPORTED_MODULE_0___default().oneOf(['h', 'v'])
631 | }, optionalStyleProps);
632 | })();
633 |
634 | /******/ return __webpack_exports__;
635 | /******/ })()
636 | ;
637 | });
--------------------------------------------------------------------------------
/dist/react-lineto.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("prop-types"),require("react")):"function"==typeof define&&define.amd?define("react-lineto",["prop-types","react"],t):"object"==typeof exports?exports["react-lineto"]=t(require("prop-types"),require("react")):e["react-lineto"]=t(e["prop-types"],e.react)}(self,(function(e,t){return(()=>{"use strict";var r={229:t=>{t.exports=e},297:e=>{e.exports=t}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var i={};return(()=>{o.r(i),o.d(i,{default:()=>w,SteppedLineTo:()=>E,Line:()=>A,SteppedLine:()=>j});var e=o(229),t=o.n(e),r=o(297),n=o.n(r);function s(e){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function a(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function u(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r2)throw new Error('LinkTo anchor format is " "');var r,n,o=(n=2,function(e){if(Array.isArray(e))return e}(r=t)||function(e,t){var r=e&&("undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"]);if(null!=r){var n,o,i=[],s=!0,a=!1;try{for(r=r.call(e);!(s=(n=r.next()).done)&&(i.push(n.value),!t||i.length!==t);s=!0);}catch(e){a=!0,o=e}finally{try{s||null==r.return||r.return()}finally{if(a)throw o}}return i}}(r,n)||function(e,t){if(e){if("string"==typeof e)return l(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?l(e,t):void 0}}(r,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()),i=o[0],s=o[1];return Object.assign({},g,i?this.parseAnchorText(i)||{x:this.parseAnchorPercent(i)}:{},s?this.parseAnchorText(s)||{y:this.parseAnchorPercent(s)}:{})}},{key:"findElement",value:function(e){return document.getElementsByClassName(e)[0]}},{key:"detect",value:function(){var e=this.props,t=e.from,r=e.to,n=e.within,o=void 0===n?"":n,i=this.findElement(t),s=this.findElement(r);if(!i||!s)return!1;var a=this.fromAnchor,u=this.toAnchor,c=i.getBoundingClientRect(),p=s.getBoundingClientRect(),l=window.pageXOffset,f=window.pageYOffset;if(o){var h=this.findElement(o),y=h.getBoundingClientRect();l-=y.left+(window.pageXOffset||document.documentElement.scrollLeft)-h.scrollLeft,f-=y.top+(window.pageYOffset||document.documentElement.scrollTop)-h.scrollTop}var d=c.left+c.width*a.x+l,m=p.left+p.width*u.x+l;return{x0:d,y0:c.top+c.height*a.y+f,x1:m,y1:p.top+p.height*u.y+f}}},{key:"render",value:function(){var e=this.detect();return e?n().createElement(A,p({},e,this.props)):null}}]),r}(r.Component);w.propTypes=u({from:t().string.isRequired,to:t().string.isRequired,within:t().string,fromAnchor:t().string,toAnchor:t().string,delay:t().oneOfType([t().number,t().bool])},O);var E=function(e){d(r,e);var t=b(r);function r(){return f(this,r),t.apply(this,arguments)}return y(r,[{key:"render",value:function(){var e=this.detect();return e?n().createElement(j,p({},e,this.props)):null}}]),r}(w),A=function(e){d(r,e);var t=b(r);function r(){return f(this,r),t.apply(this,arguments)}return y(r,[{key:"componentDidMount",value:function(){this.within.appendChild(this.el)}},{key:"componentWillUnmount",value:function(){this.within.removeChild(this.el)}},{key:"findElement",value:function(e){return document.getElementsByClassName(e)[0]}},{key:"render",value:function(){var e=this,t=this.props,r=t.x0,o=t.y0,i=t.x1,s=t.y1,a=t.within,u=void 0===a?"":a;this.within=u?this.findElement(u):document.body;var c=s-o,l=i-r,f=180*Math.atan2(c,l)/Math.PI,h=Math.sqrt(l*l+c*c),y={position:"absolute",top:"".concat(o,"px"),left:"".concat(r,"px"),width:"".concat(h,"px"),zIndex:Number.isFinite(this.props.zIndex)?String(this.props.zIndex):"1",transform:"rotate(".concat(f,"deg)"),transformOrigin:"0 0"},d={borderTopColor:this.props.borderColor||"#f00",borderTopStyle:this.props.borderStyle||"solid",borderTopWidth:this.props.borderWidth||1},m={className:this.props.className,style:Object.assign({},d,y)};return n().createElement("div",{className:"react-lineto-placeholder"},n().createElement("div",p({ref:function(t){e.el=t}},m)))}}]),r}(r.PureComponent);A.propTypes=u({x0:t().number.isRequired,y0:t().number.isRequired,x1:t().number.isRequired,y1:t().number.isRequired},O);var j=function(e){d(r,e);var t=b(r);function r(){return f(this,r),t.apply(this,arguments)}return y(r,[{key:"render",value:function(){return"h"===this.props.orientation?this.renderHorizontal():this.renderVertical()}},{key:"renderVertical",value:function(){var e=Math.round(this.props.x0),t=Math.round(this.props.y0),r=Math.round(this.props.x1),o=Math.round(this.props.y1),i=r-e;if(Math.abs(i)<=1)return n().createElement(A,p({},this.props,{x0:e,y0:t,x1:e,y1:o}));if(0===i)return n().createElement(A,this.props);var s=this.props.borderWidth||1,a=Math.round((t+o)/2),u=i>0?s:0,c=Math.min(e,r)-u,l=Math.max(e,r);return n().createElement("div",{className:"react-steppedlineto"},n().createElement(A,p({},this.props,{x0:e,y0:t,x1:e,y1:a})),n().createElement(A,p({},this.props,{x0:r,y0:o,x1:r,y1:a})),n().createElement(A,p({},this.props,{x0:c,y0:a,x1:l,y1:a})))}},{key:"renderHorizontal",value:function(){var e=Math.round(this.props.x0),t=Math.round(this.props.y0),r=Math.round(this.props.x1),o=Math.round(this.props.y1),i=o-t;if(Math.abs(i)<=1)return n().createElement(A,p({},this.props,{x0:e,y0:t,x1:r,y1:t}));if(0===i)return n().createElement(A,this.props);var s=this.props.borderWidth||1,a=Math.round((e+r)/2),u=i<0?s:0,c=Math.min(t,o)-u,l=Math.max(t,o);return n().createElement("div",{className:"react-steppedlineto"},n().createElement(A,p({},this.props,{x0:e,y0:t,x1:a,y1:t})),n().createElement(A,p({},this.props,{x0:r,y0:o,x1:a,y1:o})),n().createElement(A,p({},this.props,{x0:a,y0:c,x1:a,y1:l})))}}]),r}(r.PureComponent);j.propTypes=u({x0:t().number.isRequired,y0:t().number.isRequired,x1:t().number.isRequired,y1:t().number.isRequired,orientation:t().oneOf(["h","v"])},O)})(),i})()}));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-lineto",
3 | "version": "3.3.0",
4 | "author": "Kevin DeLoach ",
5 | "license": "MIT",
6 | "main": "dist/react-lineto.js",
7 | "types": "src/index.d.ts",
8 | "scripts": {
9 | "build": "webpack --config webpack.config.js",
10 | "demo": "webpack serve --config webpack.demo.config.js --hot --inline",
11 | "lint": "eslint --ext js,jsx ./src",
12 | "test": "npm run lint"
13 | },
14 | "homepage": "https://github.com/kdeloach/react-lineto",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/kdeloach/react-lineto.git"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/kdeloach/react-lineto/issues"
21 | },
22 | "dependencies": {
23 | "prop-types": "15.7.2",
24 | "react": "17.0.2"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.14.3",
28 | "@babel/eslint-parser": "^7.14.4",
29 | "@babel/preset-env": "^7.14.4",
30 | "@babel/preset-react": "^7.13.13",
31 | "babel-loader": "^8.2.2",
32 | "core-js": "^3.13.1",
33 | "eslint": "^7.27.0",
34 | "eslint-plugin-react": "^7.24.0",
35 | "eslint-webpack-plugin": "^2.5.4",
36 | "react-dom": "17.0.2",
37 | "regenerator-runtime": "^0.13.7",
38 | "webpack": "^5.38.1",
39 | "webpack-cli": "^4.7.0",
40 | "webpack-dev-server": "^3.11.2"
41 | },
42 | "babel": {
43 | "presets": ["@babel/preset-env", "@babel/preset-react"]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdeloach/react-lineto/1a72d511f1701c01a6a2ac1ec28d79a556c1073a/preview.png
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run --rm \
4 | -v ${PWD}:/usr/src/app \
5 | react-lineto-yarn \
6 | build
7 |
--------------------------------------------------------------------------------
/scripts/docker-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker build -t react-lineto-yarn .
4 |
--------------------------------------------------------------------------------
/scripts/server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run --rm -ti \
4 | -v ${PWD}:/usr/src/app \
5 | -p "4567:4567" \
6 | react-lineto-yarn \
7 | run demo
8 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run --rm \
4 | -v ${PWD}:/usr/src/app \
5 | react-lineto-yarn \
6 | test
7 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-lineto' {
2 | import { Component, PureComponent } from 'react';
3 |
4 | /**
5 | * Orientation type for 'Stepped' lines
6 | */
7 | type Orientation = 'h' | 'v';
8 |
9 | /**
10 | * Delay
11 | */
12 | type Delay = number | boolean;
13 |
14 | /**
15 | * Anchor type
16 | */
17 | type Anchor = string;
18 |
19 | /**
20 | * Coordinate type
21 | */
22 | type Coordinate = { x: number} | { y: number};
23 |
24 | /**
25 | * Coordinates type
26 | */
27 | type Coordinates = {
28 | x: number;
29 | y: number;
30 | };
31 |
32 | /**
33 | * Line coordinates
34 | */
35 | interface LineCoordinates {
36 | /**
37 | * First X coordinate
38 | */
39 | x0: number;
40 | /**
41 | * Second X coordinate
42 | */
43 | x1: number;
44 | /**
45 | * First Y coordinate
46 | */
47 | y0: number;
48 | /**
49 | * Second Y coordinate
50 | */
51 | y1: number;
52 | }
53 |
54 | /**
55 | * Base props for all components
56 | */
57 | interface BaseProps {
58 | /**
59 | * Border color, Example: #f00, red, etc.
60 | */
61 | borderColor?: string;
62 | /**
63 | * Border style, Example: solid, dashed, etc.
64 | */
65 | borderStyle?: string;
66 | /**
67 | * Border width (px)
68 | */
69 | borderWidth?: number;
70 | /**
71 | * Desired CSS className for the rendered element
72 | */
73 | className?: string;
74 | /**
75 | * Z-index offset
76 | */
77 | zIndex?: number;
78 | /**
79 | * CSS class name of the desired container
80 | */
81 | within?: string;
82 | }
83 |
84 | /**
85 | * Common props for 'LineTo' and 'SteppedLineTo' components
86 | */
87 | interface LineToCommonProps extends BaseProps {
88 | /**
89 | * Force render after delay (ms)
90 | */
91 | delay?: Delay;
92 | /**
93 | * Anchor for starting point (Format: "x y")
94 | */
95 | fromAnchor?: Anchor;
96 | /**
97 | * CSS class name of the first element
98 | */
99 | from: string;
100 | /**
101 | * Anchor for ending point (Format: 'x y")
102 | */
103 | toAnchor?: Anchor;
104 | /**
105 | * CSS class name of the second element
106 | */
107 | to: string;
108 | }
109 |
110 | /**
111 | * Common props for 'Line' and 'SteppedLine' components
112 | */
113 | interface LineCommonProps extends BaseProps, LineCoordinates {}
114 |
115 | /**
116 | * Props for 'Stepped' components
117 | */
118 | interface SteppedProps {
119 | /**
120 | * "h" for horizontal, "v" for vertical
121 | */
122 | orientation?: Orientation;
123 | }
124 |
125 | /**
126 | * Props of 'LineTo' component
127 | */
128 | export interface LineToProps extends LineToCommonProps {}
129 |
130 | /**
131 | * Props of 'SteppedLineTo' component
132 | */
133 | export interface SteppedLineToProps extends LineToProps, SteppedProps {}
134 |
135 | /**
136 | * Props of 'Line' component
137 | */
138 | export interface LineProps extends LineCommonProps {}
139 |
140 | /**
141 | * Props of 'SteppedLine' component
142 | */
143 | export interface SteppedLineProps extends LineProps, SteppedProps {}
144 |
145 | /**
146 | * Draw line between two DOM elements.
147 | */
148 | export default class LineTo extends Component> {
149 | /**
150 | * Forced update after delay (MS)
151 | */
152 | deferUpdate: (delay: number) => void;
153 |
154 | /**
155 | * Parse delay prop
156 | */
157 | parseDelay: (delay?: Delay) => number;
158 |
159 | /**
160 | * Parse anchor given as percentage
161 | */
162 | parseAnchorPercent: (value: string) => number;
163 |
164 | /**
165 | * Parse anchor given as text
166 | */
167 | parseAnchorText: (value: string) => Coordinate;
168 |
169 | /**
170 | * Parse anchor prop
171 | */
172 | parseAnchor: (value?: Anchor) => Coordinates;
173 |
174 | /**
175 | * Detect coordinates
176 | */
177 | detect: () => LineCoordinates;
178 |
179 | /**
180 | * Find element by class
181 | */
182 | findElement: (className: string) => Element;
183 | }
184 |
185 | /**
186 | * Draw stepped line between two DOM elements.
187 | */
188 | export class SteppedLineTo extends LineTo {}
189 |
190 | /**
191 | * Draw line using pixel coordinates (relative to viewport).
192 | */
193 | export class Line extends PureComponent {
194 | /**
195 | * Find element by class
196 | */
197 | findElement: (className: string) => Element;
198 | }
199 |
200 | /**
201 | * Draw stepped line using pixel coordinates (relative to viewport).
202 | */
203 | export class SteppedLine extends PureComponent {
204 | /**
205 | * Render vertically
206 | */
207 | renderVertical: () => React.ReactNode;
208 |
209 | /**
210 | * Render horizontally
211 | */
212 | renderHorizontal: () => React.ReactNode;
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component, PureComponent } from 'react';
3 |
4 | const defaultAnchor = { x: 0.5, y: 0.5 };
5 | const defaultBorderColor = '#f00';
6 | const defaultBorderStyle = 'solid';
7 | const defaultBorderWidth = 1;
8 |
9 | const optionalStyleProps = {
10 | borderColor: PropTypes.string,
11 | borderStyle: PropTypes.string,
12 | borderWidth: PropTypes.number,
13 | className: PropTypes.string,
14 | zIndex: PropTypes.number,
15 | };
16 |
17 | export default class LineTo extends Component {
18 | // eslint-disable-next-line camelcase
19 | UNSAFE_componentWillMount() {
20 | this.fromAnchor = this.parseAnchor(this.props.fromAnchor);
21 | this.toAnchor = this.parseAnchor(this.props.toAnchor);
22 | this.delay = this.parseDelay(this.props.delay);
23 | }
24 |
25 | componentDidMount() {
26 | this.delay = this.parseDelay(this.props.delay);
27 | if (typeof this.delay !== 'undefined') {
28 | this.deferUpdate(this.delay);
29 | }
30 | }
31 |
32 | // eslint-disable-next-line camelcase
33 | UNSAFE_componentWillReceiveProps(nextProps) {
34 | if (nextProps.fromAnchor !== this.props.fromAnchor) {
35 | this.fromAnchor = this.parseAnchor(this.props.fromAnchor);
36 | }
37 | if (nextProps.toAnchor !== this.props.toAnchor) {
38 | this.toAnchor = this.parseAnchor(this.props.toAnchor);
39 | }
40 | this.delay = this.parseDelay(nextProps.delay);
41 | if (typeof this.delay !== 'undefined') {
42 | this.deferUpdate(this.delay);
43 | }
44 | }
45 |
46 | componentWillUnmount() {
47 | if (this.t) {
48 | clearTimeout(this.t);
49 | this.t = null;
50 | }
51 | }
52 |
53 | shouldComponentUpdate() {
54 | // Always update component if the parent component has been updated.
55 | // The reason for this is that we would not only like to update
56 | // this component when the props have changed, but also when
57 | // the position of our target elements has changed.
58 | // We could return true only if the positions of `from` and `to` have
59 | // changed, but that may be expensive and unnecessary.
60 | return true;
61 | }
62 |
63 | // Forced update after delay (MS)
64 | deferUpdate(delay) {
65 | if (this.t) {
66 | clearTimeout(this.t);
67 | }
68 | this.t = setTimeout(() => this.forceUpdate(), delay);
69 | }
70 |
71 | parseDelay(value) {
72 | if (typeof value === 'undefined') {
73 | return value;
74 | } else if (typeof value === 'boolean' && value) {
75 | return 0;
76 | }
77 | const delay = parseInt(value, 10);
78 | if (isNaN(delay) || !isFinite(delay)) {
79 | throw new Error(`LinkTo could not parse delay attribute "${value}"`);
80 | }
81 | return delay;
82 | }
83 |
84 | parseAnchorPercent(value) {
85 | const percent = parseFloat(value) / 100;
86 | if (isNaN(percent) || !isFinite(percent)) {
87 | throw new Error(`LinkTo could not parse percent value "${value}"`);
88 | }
89 | return percent;
90 | }
91 |
92 | parseAnchorText(value) {
93 | // Try to infer the relevant axis.
94 | switch (value) {
95 | case 'top':
96 | return { y: 0 };
97 | case 'left':
98 | return { x: 0 };
99 | case 'middle':
100 | return { y: 0.5 };
101 | case 'center':
102 | return { x: 0.5 };
103 | case 'bottom':
104 | return { y: 1 };
105 | case 'right':
106 | return { x: 1 };
107 | }
108 | return null;
109 | }
110 |
111 | parseAnchor(value) {
112 | if (!value) {
113 | return defaultAnchor;
114 | }
115 | const parts = value.split(' ');
116 | if (parts.length > 2) {
117 | throw new Error('LinkTo anchor format is " "');
118 | }
119 | const [x, y] = parts;
120 | return Object.assign({}, defaultAnchor,
121 | x ? this.parseAnchorText(x) || { x: this.parseAnchorPercent(x) } : {},
122 | y ? this.parseAnchorText(y) || { y: this.parseAnchorPercent(y) } : {}
123 | );
124 | }
125 |
126 | findElement(className) {
127 | return document.getElementsByClassName(className)[0];
128 | }
129 |
130 | detect() {
131 | const { from, to, within = '' } = this.props;
132 |
133 | const a = this.findElement(from);
134 | const b = this.findElement(to);
135 |
136 | if (!a || !b) {
137 | return false;
138 | }
139 |
140 | const anchor0 = this.fromAnchor;
141 | const anchor1 = this.toAnchor;
142 |
143 | const box0 = a.getBoundingClientRect();
144 | const box1 = b.getBoundingClientRect();
145 |
146 | let offsetX = window.pageXOffset;
147 | let offsetY = window.pageYOffset;
148 |
149 | if (within) {
150 | const p = this.findElement(within);
151 | const boxp = p.getBoundingClientRect();
152 |
153 | offsetX -= boxp.left + (window.pageXOffset || document.documentElement.scrollLeft) - p.scrollLeft;
154 | offsetY -= boxp.top + (window.pageYOffset || document.documentElement.scrollTop) - p.scrollTop;
155 | }
156 |
157 | const x0 = box0.left + box0.width * anchor0.x + offsetX;
158 | const x1 = box1.left + box1.width * anchor1.x + offsetX;
159 | const y0 = box0.top + box0.height * anchor0.y + offsetY;
160 | const y1 = box1.top + box1.height * anchor1.y + offsetY;
161 |
162 | return { x0, y0, x1, y1 };
163 | }
164 |
165 | render() {
166 | const points = this.detect();
167 | return points ? (
168 |
169 | ) : null;
170 | }
171 | }
172 |
173 | LineTo.propTypes = {
174 | from: PropTypes.string.isRequired,
175 | to: PropTypes.string.isRequired,
176 | within: PropTypes.string,
177 | fromAnchor: PropTypes.string,
178 | toAnchor: PropTypes.string,
179 | delay: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
180 | ...optionalStyleProps,
181 | };
182 |
183 | export class SteppedLineTo extends LineTo {
184 | render() {
185 | const points = this.detect();
186 | return points ? (
187 |
188 | ) : null;
189 | }
190 | }
191 |
192 | export class Line extends PureComponent {
193 | componentDidMount() {
194 | // Append rendered DOM element to the container the
195 | // offsets were calculated for
196 | this.within.appendChild(this.el);
197 | }
198 |
199 | componentWillUnmount() {
200 | this.within.removeChild(this.el);
201 | }
202 |
203 | findElement(className) {
204 | return document.getElementsByClassName(className)[0];
205 | }
206 |
207 | render() {
208 | const { x0, y0, x1, y1, within = '' } = this.props;
209 |
210 | this.within = within ? this.findElement(within) : document.body;
211 |
212 | const dy = y1 - y0;
213 | const dx = x1 - x0;
214 |
215 | const angle = Math.atan2(dy, dx) * 180 / Math.PI;
216 | const length = Math.sqrt(dx * dx + dy * dy);
217 |
218 | const positionStyle = {
219 | position: 'absolute',
220 | top: `${y0}px`,
221 | left: `${x0}px`,
222 | width: `${length}px`,
223 | zIndex: Number.isFinite(this.props.zIndex)
224 | ? String(this.props.zIndex)
225 | : '1',
226 | transform: `rotate(${angle}deg)`,
227 | // Rotate around (x0, y0)
228 | transformOrigin: '0 0',
229 | };
230 |
231 | const defaultStyle = {
232 | borderTopColor: this.props.borderColor || defaultBorderColor,
233 | borderTopStyle: this.props.borderStyle || defaultBorderStyle,
234 | borderTopWidth: this.props.borderWidth || defaultBorderWidth,
235 | };
236 |
237 | const props = {
238 | className: this.props.className,
239 | style: Object.assign({}, defaultStyle, positionStyle),
240 | }
241 |
242 | // We need a wrapper element to prevent an exception when then
243 | // React component is removed. This is because we manually
244 | // move the rendered DOM element after creation.
245 | return (
246 |
247 |
{ this.el = el; }}
249 | {...props}
250 | />
251 |
252 | );
253 | }
254 | }
255 |
256 | Line.propTypes = {
257 | x0: PropTypes.number.isRequired,
258 | y0: PropTypes.number.isRequired,
259 | x1: PropTypes.number.isRequired,
260 | y1: PropTypes.number.isRequired,
261 | ...optionalStyleProps,
262 | };
263 |
264 | export class SteppedLine extends PureComponent {
265 | render() {
266 | if (this.props.orientation === 'h') {
267 | return this.renderHorizontal();
268 | }
269 | return this.renderVertical();
270 | }
271 |
272 | renderVertical() {
273 | const x0 = Math.round(this.props.x0);
274 | const y0 = Math.round(this.props.y0);
275 | const x1 = Math.round(this.props.x1);
276 | const y1 = Math.round(this.props.y1);
277 |
278 | const dx = x1 - x0;
279 | if (Math.abs(dx) <= 1) {
280 | return
;
281 | }
282 |
283 | const borderWidth = this.props.borderWidth || defaultBorderWidth;
284 | const y2 = Math.round((y0 + y1) / 2);
285 |
286 | const xOffset = dx > 0 ? borderWidth : 0;
287 | const minX = Math.min(x0, x1) - xOffset;
288 | const maxX = Math.max(x0, x1);
289 |
290 | return (
291 |
292 |
293 |
294 |
295 |
296 | );
297 | }
298 |
299 | renderHorizontal() {
300 | const x0 = Math.round(this.props.x0);
301 | const y0 = Math.round(this.props.y0);
302 | const x1 = Math.round(this.props.x1);
303 | const y1 = Math.round(this.props.y1);
304 |
305 | const dy = y1 - y0;
306 | if (Math.abs(dy) <= 1) {
307 | return
;
308 | }
309 |
310 | const borderWidth = this.props.borderWidth || defaultBorderWidth;
311 | const x2 = Math.round((x0 + x1) / 2);
312 |
313 | const yOffset = dy < 0 ? borderWidth : 0;
314 | const minY = Math.min(y0, y1) - yOffset;
315 | const maxY = Math.max(y0, y1);
316 |
317 | return (
318 |
319 |
320 |
321 |
322 |
323 | );
324 | }
325 | }
326 |
327 | SteppedLine.propTypes = {
328 | x0: PropTypes.number.isRequired,
329 | y0: PropTypes.number.isRequired,
330 | x1: PropTypes.number.isRequired,
331 | y1: PropTypes.number.isRequired,
332 | orientation: PropTypes.oneOf(['h', 'v']),
333 | ...optionalStyleProps,
334 | };
335 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | const path = require('path')
3 | const ESLintPlugin = require('eslint-webpack-plugin')
4 |
5 | const outputPath = path.join(__dirname, 'dist')
6 |
7 | const config = {
8 | entry: './src/index.jsx',
9 |
10 | mode: 'production',
11 | plugins: [new ESLintPlugin({ extensions: '.jsx' })],
12 |
13 | module: {
14 | rules: [
15 | {
16 | test: /\.jsx?$/,
17 | use: ['babel-loader'],
18 | exclude: /node_modules/,
19 | },
20 | ]
21 | },
22 |
23 | externals: [
24 | 'prop-types',
25 | 'react'
26 | ]
27 | };
28 |
29 | module.exports = [
30 | Object.assign({}, config, {
31 | output: {
32 | path: outputPath,
33 | filename: 'react-lineto.js',
34 | library: 'react-lineto',
35 | libraryTarget: 'umd',
36 | umdNamedDefine: true,
37 | },
38 | optimization: {
39 | minimize: false,
40 | }
41 | }),
42 | Object.assign({}, config, {
43 | output: {
44 | path: outputPath,
45 | filename: 'react-lineto.min.js',
46 | library: 'react-lineto',
47 | libraryTarget: 'umd',
48 | umdNamedDefine: true,
49 | }
50 | })
51 | ];
52 |
--------------------------------------------------------------------------------
/webpack.demo.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | const path = require('path')
3 | const ESLintPlugin = require('eslint-webpack-plugin')
4 |
5 | module.exports = {
6 | entry: './demo/index.jsx',
7 |
8 | mode: 'development',
9 | plugins: [new ESLintPlugin({ extensions: '.jsx' })],
10 |
11 | module: {
12 | rules: [
13 | {
14 | test: /\.jsx?$/,
15 | use: ['babel-loader'],
16 | exclude: /node_modules/
17 | }
18 | ]
19 | },
20 |
21 | devtool: 'source-map',
22 |
23 | devServer: {
24 | host: '0.0.0.0',
25 | port: 4567,
26 | contentBase: path.join(__dirname, 'demo'),
27 | },
28 | };
29 |
--------------------------------------------------------------------------------