├── .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 | [![Build Status](https://app.travis-ci.com/kdeloach/react-lineto.svg?branch=master)](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 | ![Demo](https://github.com/kdeloach/react-lineto/raw/master/preview.png) 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 |
76 | Polygon Test 77 | 78 | Demonstrate how to draw absolutely positioned line segments. 79 | 80 | {triangle} 81 | {star} 82 | {ngon} 83 |
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 |
98 | Stepped Test 99 | 100 | Demonstrate how to draw stepped lines. 101 | 102 | A 108 | B 114 | C 120 | D 126 | E 132 | F 138 | 140 | 142 | 144 | 146 | 148 |
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 |
185 | Hover Test 186 | 187 | Demonstrate how to draw a line from one component to another 188 | in response to a hover event. 189 | 190 | this.drawLine('hover-A', 'hover-F')} 196 | onMouseOut={this.clearLine} 197 | >A 198 | this.drawLine('hover-B', 'hover-E')} 204 | onMouseOut={this.clearLine} 205 | >B 206 | this.drawLine('hover-C', 'hover-D')} 212 | onMouseOut={this.clearLine} 213 | >C 214 | this.drawLine('hover-D', 'hover-C')} 220 | onMouseOut={this.clearLine} 221 | >D 222 | this.drawLine('hover-E', 'hover-B')} 228 | onMouseOut={this.clearLine} 229 | >E 230 | this.drawLine('hover-F', 'hover-A')} 236 | onMouseOut={this.clearLine} 237 | >F 238 | {line} 239 |
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 |
263 | Delay Test 264 | 265 | Demonstrate how to draw a line to a component which does not 266 | exist at the moment that the LineTo component has been mounted. 267 | 268 | this.setState({ targetVisible: true })} 274 | onMouseOut={() => this.setState({ targetVisible: false })} 275 | >E 276 | {target} 277 | 286 |
287 | ); 288 | } 289 | } 290 | 291 | class TreeTest extends Component { 292 | render() { 293 | return ( 294 |
295 | Tree Test 296 |
297 | 298 |
299 |
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 | --------------------------------------------------------------------------------