├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── example
├── .babelrc
├── .gitignore
├── AddAndDelete
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── ControlledTags
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── CrossArea-restriction
│ ├── icons.js
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── CrossArea
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── Customized
│ ├── index.js
│ └── style.less
├── Draggable.js
├── List
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── NestedTags
│ ├── Tag
│ │ ├── index.js
│ │ └── style.less
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── README.md
├── Simple
│ ├── index.js
│ └── style.less
├── addAndDelete
│ ├── index.js
│ ├── mock.js
│ └── style.less
├── bundle.js
├── imgs
│ ├── delete.png
│ └── delete@2x.png
├── index.html
├── index.js
├── package-lock.json
├── package.json
├── simple
│ ├── index.js
│ └── style.less
├── style.less
└── webpack.config.js
├── imgs
├── .gitignore
├── AddAddDelete.gif
├── CrossAreaDrag.gif
├── DraggableList.gif
└── TagsInTags.gif
├── index.d.ts
├── index.html
├── lib
└── bundle.js
├── package.json
├── src
├── DraggableAreaBuilder.js
├── DraggableAreasGroup.js
├── index.js
└── style.less
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["transform-react-jsx", "transform-object-rest-spread"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 YGY
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-draggable-tags
2 | [](https://www.npmjs.com/package/react-draggable-tags)
3 | [](https://www.npmjs.com/package/react-draggable-tags)
4 | [](LICENSE)
5 |
6 | [](https://github.com/YGYOOO)
7 | [](https://weibo.com/u/5352731024)
8 | [](https://twitter.com/YGYOOO)
9 |
10 |
11 | A flexible, lightweight(<20kb) and easy-to-use draggable component. It also works on mobile now :)
12 |
13 | 一个轻量级的拖拽排序组件。该组件封装了一系列拖拽功能,可以灵活使用,也未提供任何样式,完全由你来控制(不一定是“tag”,你可以放入任意组件来拖拽排序)。支持移动端。
14 |
15 | ## Installation
16 | ```sh
17 | npm install --save react-draggable-tags
18 | ```
19 |
20 | ```js
21 | import {DraggableArea} from 'react-draggable-tags';
22 | ```
23 |
24 | ## Documentation and Demo
25 | https://ygyooo.github.io/react-draggable-tags
26 |
27 | ## Demo Code
28 | https://github.com/YGYOOO/react-draggable-tags/tree/master/example
29 |
30 | ## Screenshots
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
--------------------------------------------------------------------------------
/example/AddAndDelete/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 | import styles from './style.less';
7 |
8 | import mock from './mock.js';
9 |
10 | export default class AddAndDelete extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | tags: mock.tags
15 | };
16 |
17 | this.handleClickAdd = this.handleClickAdd.bind(this);
18 | }
19 |
20 | handleClickAdd() {
21 | if (this.input.value.length < 1) return;
22 | const tags = this.state.tags.slice();
23 | tags.push({id: Math.random() , content: this.input.value});
24 | this.setState({tags});
25 | this.input.value = '';
26 | }
27 |
28 | handleClickDelete(tag) {
29 | const tags = this.state.tags.filter(t => tag.id !== t.id);
30 | this.setState({tags});
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
(
40 |
41 |
this.handleClickDelete(tag)}
46 | />
47 | {tag.content}
48 |
49 | )}
50 | onChange={tags => this.setState({tags})}
51 | />
52 |
53 |
54 | this.input = r} />
55 | Add
56 |
57 |
58 | );
59 | }
60 | }
--------------------------------------------------------------------------------
/example/AddAndDelete/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [{
3 | id: '1',
4 | content: 'apple',
5 | },{
6 | id: '2',
7 | content: 'watermelon',
8 | },{
9 | id: '3',
10 | content: 'banana',
11 | },{
12 | id: '4',
13 | content: 'lemon',
14 | },{
15 | id: '5',
16 | content: 'orange',
17 | },{
18 | id: '6',
19 | content: 'grape',
20 | },{
21 | id: '7',
22 | content: 'strawberry',
23 | },{
24 | id: '8',
25 | content: 'cherry',
26 | },{
27 | id: '9',
28 | content: 'peach',
29 | }],
30 | }
--------------------------------------------------------------------------------
/example/AddAndDelete/style.less:
--------------------------------------------------------------------------------
1 | .AddAndDelete {
2 | .main {
3 | border: 1px solid #E9E9E9;
4 | border-radius: 4px;
5 | width: 294px;
6 | height: 220px;
7 | padding: 5px;
8 | }
9 | .tag {
10 | position: relative;
11 | margin: 3px;
12 | font-size: 13px;
13 | border: 1px dashed #3b9de9;
14 | border-radius: 4px;
15 | padding: 0 8px;
16 | line-height: 30px;
17 | color: #666666;
18 | background: rgba(255, 255, 255, 0.7);
19 | }
20 | .delete {
21 | position: absolute;
22 | top: -1px;
23 | right: -1px;
24 | width: 16px;
25 | height: 16px;
26 | cursor: pointer;
27 | user-drag: none;
28 | user-select: none;
29 | -moz-user-select: none;
30 | -webkit-user-drag: none;
31 | -webkit-user-select: none;
32 | -ms-user-select: none;
33 | }
34 | .inputs {
35 | margin-top: 10px;
36 | }
37 | input {
38 | position: relative;
39 | top: -1px;
40 | height: 23px;
41 | }
42 | button {
43 | margin-left: 10px;
44 | background-color: #3789c7;
45 | border: none;
46 | color: white;
47 | padding: 5px 15px;
48 | text-align: center;
49 | text-decoration: none;
50 | display: inline-block;
51 | font-size: 16px;
52 | border-radius: 2px;
53 | }
54 | }
--------------------------------------------------------------------------------
/example/ControlledTags/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea, DraggableAreasGroup} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 | import styles from './style.less';
7 |
8 | import mock from './mock.js';
9 |
10 | export default class ControlledTags extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | tags: mock.tags,
15 | };
16 | this.onChange = this.onChange.bind(this);
17 | this.handleClickDelete = this.handleClickDelete.bind(this);
18 | this.handleClickAdd = this.handleClickAdd.bind(this);
19 | this.handleClickSort = this.handleClickSort.bind(this);
20 | }
21 |
22 | onChange(tags) {
23 | this.setState({tags});
24 | }
25 |
26 | handleClickDelete(tag) {
27 | const tags = this.state.tags.filter(t => tag.id !== t.id);
28 | this.setState({tags});
29 | }
30 |
31 | handleClickAdd() {
32 | const tags = this.state.tags.slice();
33 | tags.push({id: Math.random() , content: this.input.value});
34 | this.setState({tags});
35 | this.input.value = '';
36 | }
37 |
38 | handleClickSort() {
39 | const tags = this.state.tags.sort(() => Math.random() - .5);
40 | this.setState({tags});
41 | }
42 |
43 | render() {
44 | return (
45 |
46 |
47 |
(
50 |
51 |
this.handleClickDelete(tag)}
56 | />
57 | {tag.content}
58 |
59 | )}
60 | onChange={this.onChange}
61 | />
62 |
63 |
64 | this.input = r} />
65 | Add
66 | Random Sort
67 |
68 |
69 | );
70 | }
71 | }
--------------------------------------------------------------------------------
/example/ControlledTags/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [{
3 | id: '1',
4 | content: 'apple',
5 | positionChangedTimes: 0,
6 | },{
7 | id: '2',
8 | content: 'watermelon',
9 | positionChangedTimes: 0,
10 | },{
11 | id: '3',
12 | content: 'banana',
13 | positionChangedTimes: 0,
14 | },{
15 | id: '4',
16 | content: 'lemon',
17 | positionChangedTimes: 0,
18 | },{
19 | id: '5',
20 | content: 'orange',
21 | positionChangedTimes: 0,
22 | },{
23 | id: '6',
24 | content: 'grape',
25 | positionChangedTimes: 0,
26 | },{
27 | id: '7',
28 | content: 'strawberry',
29 | positionChangedTimes: 0,
30 | },{
31 | id: '8',
32 | content: 'cherry',
33 | positionChangedTimes: 0,
34 | },{
35 | id: '9',
36 | content: 'peach',
37 | positionChangedTimes: 0,
38 | }],
39 | }
--------------------------------------------------------------------------------
/example/ControlledTags/style.less:
--------------------------------------------------------------------------------
1 | .ControlledTags {
2 | .main {
3 | position: relative;
4 | border: 1px solid #E9E9E9;
5 | border-radius: 4px;
6 | width: 294px;
7 | height: 220px;
8 | padding: 5px;
9 | }
10 | .tag {
11 | position: relative;
12 | margin: 3px;
13 | font-size: 13px;
14 | border: 1px dashed #3b9de9;
15 | border-radius: 4px;
16 | padding: 0 8px;
17 | line-height: 30px;
18 | color: #666666;
19 | background: rgba(255, 255, 255, 0.7);
20 | }
21 | .delete {
22 | position: absolute;
23 | top: -1px;
24 | right: -1px;
25 | width: 16px;
26 | height: 16px;
27 | cursor: pointer;
28 | user-drag: none;
29 | user-select: none;
30 | -moz-user-select: none;
31 | -webkit-user-drag: none;
32 | -webkit-user-select: none;
33 | -ms-user-select: none;
34 | }
35 | .inputs {
36 | margin-top: 10px;
37 | }
38 | input {
39 | position: relative;
40 | top: -1px;
41 | height: 23px;
42 | }
43 | button {
44 | margin-left: 10px;
45 | background-color: #3789c7;
46 | border: none;
47 | color: white;
48 | padding: 5px 15px;
49 | text-align: center;
50 | text-decoration: none;
51 | display: inline-block;
52 | font-size: 16px;
53 | border-radius: 2px;
54 | }
55 | }
--------------------------------------------------------------------------------
/example/CrossArea-restriction/icons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const correct = (
4 |
7 |
9 |
18 |
19 |
20 | );
21 |
22 | export const wrong = (
23 |
26 |
28 |
41 |
42 |
43 | );
--------------------------------------------------------------------------------
/example/CrossArea-restriction/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableAreasGroup} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 |
7 | import styles from './style.less';
8 |
9 | import {correct, wrong} from './icons';
10 |
11 | import mock from './mock.js';
12 |
13 | const group = new DraggableAreasGroup();
14 | const DraggableArea1 = group.addArea('area1');
15 | const DraggableArea2 = group.addArea('area2');
16 |
17 |
18 | export default class CrossAreaRestriction extends Component {
19 | constructor() {
20 | super();
21 | this.state = {
22 | leftTags: mock.leftTags,
23 | rightTags: mock.rightTags,
24 | }
25 | }
26 |
27 |
28 | render() {
29 | return (
30 |
31 |
32 |
(
35 |
36 | {tag.content}
37 |
38 | )}
39 | onChange={(leftTags, {fromArea, toArea}) => {
40 | console.log(fromArea); // {id: "area2", tag: {…}}
41 | if (fromArea.id === 'area2') {
42 | this.setState({leftTags: this.state.leftTags});
43 | } else {
44 | this.setState({leftTags});
45 | }
46 | }}
47 | />
48 |
49 |
50 |
51 |
52 | →
53 |
54 | {/* {correct} */}
55 |
56 |
60 |
61 |
62 |
(
65 |
66 | {tag.content}
67 |
68 | )}
69 | onChange={(rightTags, {fromArea, toArea}) => {
70 | if (toArea.id === 'area1') {
71 | this.setState({rightTags: this.state.rightTags});
72 | } else {
73 | this.setState({rightTags});
74 | }
75 | }}
76 | />
77 |
78 |
79 | );
80 | }
81 | }
--------------------------------------------------------------------------------
/example/CrossArea-restriction/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | leftTags: [{
3 | id: 'apple',
4 | content: 'apple',
5 | },{
6 | id: 'watermelon',
7 | content: 'watermelon',
8 | },{
9 | id: 'banana',
10 | content: 'banana',
11 | },{
12 | id: 'lemon',
13 | content: 'lemon',
14 | },{
15 | id: 'orange',
16 | content: 'orange',
17 | },{
18 | id: 'grape',
19 | content: 'grape',
20 | },{
21 | id: 'strawberry',
22 | content: 'strawberry',
23 | },{
24 | id: 'cherry',
25 | content: 'cherry',
26 | }],
27 | rightTags: [{
28 | id: 'tomato',
29 | content: 'tomato',
30 | },{
31 | id: 'cucumber',
32 | content: 'cucumber',
33 | },{
34 | id: 'mushroom',
35 | content: 'mushroom',
36 | },{
37 | id: 'pea',
38 | content: 'pea',
39 | },{
40 | id: 'carrot',
41 | content: 'carrot',
42 | },{
43 | id: 'cabbage',
44 | content: 'cabbage',
45 | },{
46 | id: 'Chinese cabbage',
47 | content: 'Chinese cabbage',
48 | },{
49 | id: 'melon',
50 | content: 'melon',
51 | },{
52 | id: 'celery',
53 | content: 'celery',
54 | },{
55 | id: 'pepper',
56 | content: 'pepper',
57 | },{
58 | id: 'eggplant',
59 | content: 'eggplant',
60 | },{
61 | id: 'beet',
62 | content: 'beet',
63 | },{
64 | id: 'been',
65 | content: 'been',
66 | }],
67 | }
--------------------------------------------------------------------------------
/example/CrossArea-restriction/style.less:
--------------------------------------------------------------------------------
1 | .CrossArea-restriction{
2 | &::after {
3 | content: '';
4 | display: block;
5 | clear: both;
6 | }
7 | .square {
8 | border: 1px solid #E9E9E9;
9 | border-radius: 4px;
10 | width: 294px;
11 | height: 220px;
12 | padding: 5px;
13 | float: left;
14 | margin-right: 10px;
15 | .tag {
16 | position: relative;
17 | margin: 3px;
18 | font-size: 13px;
19 | border: 1px dashed #cccccc;
20 | border-radius: 4px;
21 | padding: 0 8px;
22 | line-height: 30px;
23 | color: #666666;
24 | background: rgba(255, 255, 255, 0.7);
25 | }
26 | }
27 | .CTags {
28 | margin-top: 10px;
29 | }
30 | .middle {
31 | float: left;
32 | .arrow-right {
33 | position: relative;
34 | top: 30px;
35 | color: rgb(180, 180, 180);
36 | > div {
37 | font-size: 90px;
38 | transform: scaleY(.6);
39 | }
40 | > svg {
41 | position: absolute;
42 | top: 20%;
43 | left: 50%;
44 | transform: translateX(-50%);
45 | }
46 | }
47 | .arrow-left {
48 | position: relative;
49 | bottom: 30px;
50 | color: rgb(180, 180, 180);
51 | > div {
52 | font-size: 90px;
53 | transform: scaleY(.6) translateX(-5%);
54 | }
55 | > svg {
56 | position: absolute;
57 | top: 50%;
58 | left: 50%;
59 | transform: translate(-50%, -50%);
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/example/CrossArea/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableAreasGroup} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 |
7 | import styles from './style.less';
8 |
9 | import mock from './mock.js';
10 |
11 | const group = new DraggableAreasGroup();
12 | const DraggableArea1 = group.addArea('area1');
13 | const DraggableArea2 = group.addArea('area2');
14 |
15 |
16 | export default class CrossArea extends Component {
17 | constructor() {
18 | super();
19 | this.state = {
20 | leftTags: mock.leftTags,
21 | rightTags: mock.rightTags
22 | }
23 | }
24 |
25 | handleClickDelete(tag) {
26 | const rightTags = this.state.rightTags.filter(t => tag.id !== t.id);
27 | this.setState({rightTags});
28 | }
29 |
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
(
38 |
39 | {tag.content}
40 |
41 | )}
42 | onChange={leftTags => {
43 | this.setState({leftTags});
44 | }}
45 | />
46 |
47 |
48 |
(
51 |
52 |
this.handleClickDelete(tag)}
57 | />
58 | {tag.content}
59 |
60 | )}
61 | onChange={rightTags => {
62 | this.setState({rightTags});
63 | }}
64 | />
65 |
66 |
67 | );
68 | }
69 | }
--------------------------------------------------------------------------------
/example/CrossArea/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | leftTags: [{
3 | id: 'apple',
4 | content: 'apple',
5 | },{
6 | id: 'watermelon',
7 | content: 'watermelon',
8 | },{
9 | id: 'banana',
10 | content: 'banana',
11 | },{
12 | id: 'lemon',
13 | content: 'lemon',
14 | },{
15 | id: 'orange',
16 | content: 'orange',
17 | },{
18 | id: 'grape',
19 | content: 'grape',
20 | },{
21 | id: 'strawberry',
22 | content: 'strawberry',
23 | },{
24 | id: 'cherry',
25 | content: 'cherry',
26 | }],
27 | rightTags: [{
28 | id: 'tomato',
29 | content: 'tomato',
30 | },{
31 | id: 'cucumber',
32 | content: 'cucumber',
33 | },{
34 | id: 'mushroom',
35 | content: 'mushroom',
36 | },{
37 | id: 'pea',
38 | content: 'pea',
39 | },{
40 | id: 'carrot',
41 | content: 'carrot',
42 | },{
43 | id: 'cabbage',
44 | content: 'cabbage',
45 | },{
46 | id: 'Chinese cabbage',
47 | content: 'Chinese cabbage',
48 | },{
49 | id: 'melon',
50 | content: 'melon',
51 | },{
52 | id: 'celery',
53 | content: 'celery',
54 | },{
55 | id: 'pepper',
56 | content: 'pepper',
57 | },{
58 | id: 'eggplant',
59 | content: 'eggplant',
60 | },{
61 | id: 'beet',
62 | content: 'beet',
63 | },{
64 | id: 'been',
65 | content: 'been',
66 | }],
67 | }
--------------------------------------------------------------------------------
/example/CrossArea/style.less:
--------------------------------------------------------------------------------
1 | .CrossArea{
2 | &::after {
3 | content: '';
4 | display: block;
5 | clear: both;
6 | }
7 | .square {
8 | border: 1px solid #E9E9E9;
9 | border-radius: 4px;
10 | width: 294px;
11 | height: 220px;
12 | padding: 5px;
13 | }
14 | .left {
15 | float: left;
16 | margin-right: 10px;
17 | .tag {
18 | position: relative;
19 | margin: 3px;
20 | font-size: 13px;
21 | border: 1px dashed #cccccc;
22 | border-radius: 4px;
23 | padding: 0 8px;
24 | line-height: 30px;
25 | color: #666666;
26 | background: rgba(255, 255, 255, 0.7);
27 | }
28 | }
29 | .right {
30 | float: left;
31 | .tag {
32 | position: relative;
33 | margin: 3px;
34 | font-size: 13px;
35 | border: 1px dashed #9cc6f3;
36 | border-radius: 4px;
37 | padding: 0 8px;
38 | line-height: 30px;
39 | color: #666666;
40 | background: rgba(255, 255, 255, 0.7);
41 | }
42 | .delete {
43 | position: absolute;
44 | top: -1px;
45 | right: -1px;
46 | width: 16px;
47 | height: 16px;
48 | cursor: pointer;
49 | user-drag: none;
50 | user-select: none;
51 | -moz-user-select: none;
52 | -webkit-user-drag: none;
53 | -webkit-user-select: none;
54 | -ms-user-select: none;
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/example/Customized/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 |
5 | import styles from './style.less';
6 |
7 | const fruits = ['apple', 'olive', 'banana', 'lemon', 'orange', 'grape'];
8 | class Fruit extends Component {
9 | constructor() {
10 | super();
11 | this.state = {
12 | fruitIndex: 0,
13 | }
14 |
15 | this.changeFruit = this.changeFruit.bind(this);
16 | }
17 |
18 | changeFruit() {
19 | this.setState({fruitIndex: ++this.state.fruitIndex % 6})
20 | }
21 |
22 | render() {
23 | return (
24 |
25 |
Change Fruit
26 |
{fruits[this.state.fruitIndex]}
27 |
28 | );
29 | }
30 | }
31 |
32 | const initialTags = [
33 | {id: 1, content: apple
}, {id: 2, content:
},
34 | {id: 3, content: banana
}, {id: 4, content: lemon
},
35 | {id: 5, content: }, {id: 6, content: grape
}
36 | ];
37 |
38 | export default class Main extends Component {
39 | render() {
40 | return (
41 |
42 | tag.content}
45 | onChange={tags => console.log(tags)}
46 | />
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/example/Customized/style.less:
--------------------------------------------------------------------------------
1 | .Customized {
2 | border: 1px solid #E9E9E9;
3 | border-radius: 4px;
4 | width: 290px;
5 | height: 220px;
6 | padding: 5px;
7 | .tag {
8 | margin: 5px;
9 | font-size: 13px;
10 | border: 1px dashed #cccccc;
11 | border-radius: 4px;
12 | padding: 0 8px;
13 | line-height: 40px;
14 | color: #666666;
15 | background: rgba(255, 255, 255, 0.7);
16 | }
17 | .tag2 {
18 | margin: 5px;
19 | font-size: 18px;
20 | padding: 0 8px;
21 | line-height: 40px;
22 | color: white;
23 | background: #2db7f5;
24 | }
25 | .tag3 {
26 | margin: 5px;
27 | font-size: 13px;
28 | padding: 0 8px;
29 | line-height: 20px;
30 | color: white;
31 | background: #87d068;
32 | box-shadow: 2px 2px 3px 2px rgb(194, 194, 194);
33 | }
34 | }
--------------------------------------------------------------------------------
/example/Draggable.js:
--------------------------------------------------------------------------------
1 | // export {DraggableArea, DraggableAreasGroup} from '../src';
2 | // export {DraggableArea, DraggableAreasGroup} from '../lib/bundle.js';
3 | export {DraggableArea, DraggableAreasGroup} from 'react-draggable-tags';
--------------------------------------------------------------------------------
/example/List/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 | import styles from './style.less';
7 |
8 | import mock from './mock.js';
9 |
10 | export default class List extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | tags: mock.tags
15 | }
16 |
17 | this.handleClickAdd = this.handleClickAdd.bind(this);
18 | }
19 |
20 | handleClickAdd() {
21 | const tags = this.state.tags.slice();
22 | tags.push({id: Math.random() , content: this.input.value});
23 | this.setState({tags});
24 | this.input.value = '';
25 | }
26 |
27 | handleClickDelete(tag) {
28 | const tags = this.state.tags.filter(t => tag.id !== t.id);
29 | this.setState({tags});
30 | }
31 |
32 | render() {
33 | return (
34 |
35 |
36 |
(
40 |
41 |
this.handleClickDelete(tag)}
46 | />
47 | {tag.content}
48 |
49 | )}
50 | onChange={(tags) => this.setState({tags})}
51 | />
52 |
53 |
54 | this.input = r} />
55 | Add
56 |
57 |
58 | );
59 | }
60 | }
--------------------------------------------------------------------------------
/example/List/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [{
3 | id: '1',
4 | content: 'apple',
5 | },{
6 | id: '2',
7 | content: 'watermelon',
8 | },{
9 | id: '3',
10 | content: 'banana',
11 | },{
12 | id: '4',
13 | content: 'lemon',
14 | }],
15 | }
--------------------------------------------------------------------------------
/example/List/style.less:
--------------------------------------------------------------------------------
1 | .List {
2 | .main {
3 | border: 1px solid #E9E9E9;
4 | border-radius: 4px;
5 | width: 294px;
6 | height: 220px;
7 | }
8 | .row {
9 | position: relative;
10 | margin: 5px;
11 | font-size: 13px;
12 | border: 1px solid #3b9de9;
13 | border-radius: 4px;
14 | padding: 0 8px;
15 | line-height: 30px;
16 | color: #666666;
17 | background: rgba(255, 255, 255, 0.7);
18 | }
19 | .delete {
20 | position: absolute;
21 | top: -1px;
22 | right: -1px;
23 | width: 16px;
24 | height: 16px;
25 | cursor: pointer;
26 | user-drag: none;
27 | user-select: none;
28 | -moz-user-select: none;
29 | -webkit-user-drag: none;
30 | -webkit-user-select: none;
31 | -ms-user-select: none;
32 | }
33 | .inputs {
34 | margin-top: 10px;
35 | }
36 | input {
37 | position: relative;
38 | top: -1px;
39 | height: 23px;
40 | }
41 | button {
42 | margin-left: 10px;
43 | background-color: #3789c7;
44 | border: none;
45 | color: white;
46 | padding: 5px 15px;
47 | text-align: center;
48 | text-decoration: none;
49 | display: inline-block;
50 | font-size: 16px;
51 | border-radius: 2px;
52 | }
53 | }
--------------------------------------------------------------------------------
/example/NestedTags/Tag/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea, DraggableAreasGroup} from '../../Draggable';
4 | import deleteBtn from '../../imgs/delete.png';
5 | import deleteBtn2x from '../../imgs/delete@2x.png';
6 |
7 | import styles from './style.less';
8 |
9 |
10 | const group = new DraggableAreasGroup();
11 | const DraggableArea1 = group.addArea();
12 | const DraggableArea2 = group.addArea();
13 |
14 |
15 | export default class Tag extends Component {
16 | constructor({topTags, bottomTags}) {
17 | super();
18 |
19 | this.state = {
20 | topTags,
21 | bottomTags,
22 | };
23 | }
24 |
25 | handleClickDelete(tag) {
26 | const bottomTags = this.state.bottomTags.filter(t => tag.id !== t.id);
27 | this.setState({bottomTags});
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
(
37 |
38 | {tag.content}
39 |
40 | )}
41 | onChange={topTags => this.setState({topTags})}
42 | />
43 |
44 |
45 |
{
48 | return (
49 |
50 |
{
55 | this.clientX = e.clientX;
56 | this.clientY = e.clientY;
57 | }}
58 | onMouseUp={(e) => {
59 | if (this.clientX - e.clientX < 2 & this.clientY - e.clientY < 2) {
60 | // deleteThis();
61 | this.handleClickDelete(tag);
62 | }
63 | }}
64 | />
65 | {tag.content}
66 |
67 | )
68 | }}
69 | onChange={bottomTags => this.setState({bottomTags})}
70 | />
71 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/example/NestedTags/Tag/style.less:
--------------------------------------------------------------------------------
1 | .InnerTag {
2 | &::after {
3 | content: '';
4 | display: block;
5 | clear: both;
6 | }
7 | .inner-square {
8 | border: 1px solid #E9E9E9;
9 | border-radius: 4px;
10 | width: 140px;
11 | height: 65px;
12 | padding: 3px;
13 | margin: 5px;
14 | }
15 | .inner-left {
16 | float: left;
17 | margin-right: 10px;
18 | .inner-tag {
19 | margin: 2px;
20 | position: relative;
21 | font-size: 12px;
22 | border: 1px dashed #cccccc;
23 | border-radius: 4px;
24 | padding: 0 4px;
25 | line-height: 20px;
26 | color: #666666;
27 | background: rgba(255, 255, 255, 0.7);
28 | }
29 | }
30 | .inner-right {
31 | float: left;
32 | .inner-tag {
33 | margin: 2px;
34 | position: relative;
35 | font-size: 12px;
36 | border: 1px dashed #3b9de9;
37 | border-radius: 4px;
38 | padding: 0 4px;
39 | line-height: 20px;
40 | color: #666666;
41 | background: rgba(255, 255, 255, 0.7);
42 | }
43 | .inner-delete {
44 | position: absolute;
45 | top: -1px;
46 | right: -1px;
47 | width: 16px;
48 | height: 16px;
49 | cursor: pointer;
50 | user-drag: none;
51 | user-select: none;
52 | -moz-user-select: none;
53 | -webkit-user-drag: none;
54 | -webkit-user-select: none;
55 | -ms-user-select: none;
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/example/NestedTags/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {DraggableAreasGroup} from '../Draggable';
3 |
4 | import Tag from './Tag';
5 | import deleteBtn from '../imgs/delete.png';
6 | import deleteBtn2x from '../imgs/delete@2x.png';
7 |
8 | import styles from './style.less';
9 |
10 | import mock from './mock.js';
11 |
12 | const getRandomTags = function(tags) {
13 | return tags.sort((a,b) => Math.random() - 0.5) //random sort
14 | .slice(0, 1 + Math.floor(Math.random() * 3)) // random slice
15 | .map(tag => ({id: Math.random(), content: tag.content}));
16 | }
17 |
18 | const leftTags = [
19 | { id: "1", content: },
20 | { id: "2", content: },
21 | { id: "3", content: },
22 | { id: "4", content: }
23 | ];
24 | const rightTags = [
25 | { id: "10", content: },
26 | { id: "11", content: },
27 | { id: "12", content: }
28 | ];
29 |
30 | const group = new DraggableAreasGroup();
31 | const DraggableArea1 = group.addArea();
32 | const DraggableArea2 = group.addArea();
33 |
34 | export default class NestedTags extends Component {
35 | constructor() {
36 | super();
37 | this.state = {
38 | leftTags,
39 | rightTags
40 | };
41 | }
42 |
43 | handleClickDelete(tag) {
44 | const rightTags = this.state.rightTags.filter(t => tag.id !== t.id);
45 | this.setState({rightTags});
46 | }
47 |
48 | render() {
49 | return (
50 |
51 |
52 |
(
55 |
56 | {tag.content}
57 |
58 | )}
59 | onChange={leftTags => this.setState({leftTags})}
60 | />
61 |
62 |
63 |
{
66 | return (
67 |
68 |
this.handleClickDelete(tag)}
73 | />
74 | {tag.content}
75 |
76 | )
77 | }}
78 | onChange={rightTags => this.setState({rightTags})}
79 | />
80 |
81 |
82 | );
83 | }
84 | }
--------------------------------------------------------------------------------
/example/NestedTags/mock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tag from './Tag';
3 |
4 | export default {
5 | topTags: [{
6 | content: 'apple',
7 | },{
8 | content: 'banana',
9 | },{
10 | content: 'lemon',
11 | },{
12 | content: 'orange',
13 | },{
14 | content: 'grape',
15 | },{
16 | content: 'strawberry',
17 | },{
18 | content: 'cherry',
19 | }],
20 | bottomTags: [{
21 | content: 'tomato',
22 | },{
23 | content: 'mushroom',
24 | },{
25 | content: 'pea',
26 | },{
27 | content: 'carrot',
28 | },{
29 | content: 'cabbage',
30 | },{
31 | content: 'melon',
32 | },{
33 | content: 'celery',
34 | }],
35 | }
--------------------------------------------------------------------------------
/example/NestedTags/style.less:
--------------------------------------------------------------------------------
1 | .TagsInTags {
2 | &::after {
3 | content: '';
4 | display: block;
5 | clear: both;
6 | }
7 | .square {
8 | border: 1px solid #E9E9E9;
9 | border-radius: 4px;
10 | width: 345px;
11 | min-height: 200px;
12 | padding: 5px;
13 | }
14 | .left {
15 | float: left;
16 | margin-right: 10px;
17 | .tag {
18 | margin: 5px;
19 | position: relative;
20 | width: 155px;
21 | font-size: 13px;
22 | border: 1px dashed #979797;
23 | border-radius: 4px;
24 | padding: 4px;
25 | background: rgba(255, 255, 255, 0.7);
26 | }
27 | }
28 | .right {
29 | float: left;
30 | .tag {
31 | margin: 5px;
32 | position: relative;
33 | width: 155px;
34 | font-size: 13px;
35 | border: 1px dashed #979797;
36 | border-radius: 4px;
37 | padding: 4px;
38 | background: rgba(255, 255, 255, 0.7);
39 | }
40 | .delete {
41 | position: absolute;
42 | top: -1px;
43 | right: -1px;
44 | width: 16px;
45 | height: 16px;
46 | cursor: pointer;
47 | user-drag: none;
48 | user-select: none;
49 | -moz-user-select: none;
50 | -webkit-user-drag: none;
51 | -webkit-user-select: none;
52 | -ms-user-select: none;
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 |
2 | ```sh
3 | npm install
4 | ```
5 | Run examples at http://localhost:9000 with webpack dev server:
6 | ```sh
7 | npm start
8 | ```
9 |
--------------------------------------------------------------------------------
/example/Simple/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 |
5 | import styles from './style.less';
6 |
7 | const initialTags = [
8 | {id: 1, content: 'apple'}, {id: 2, content: 'olive'}, {id: 3, content: 'banana'},
9 | {id: 4, content: 'lemon'}, {id: 5, content: 'orange'}, {id: 6, content: 'grape'},
10 | {id: 7, content: 'strawberry'}, {id: 8, content: 'cherry'}, {id: 9, content: 'peach'}];
11 |
12 | export default class Main extends Component {
13 | render() {
14 | return (
15 |
16 |
(
19 |
20 | {tag.content}
21 |
22 | )}
23 | onChange={tags => console.log(tags)}
24 | />
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/example/Simple/style.less:
--------------------------------------------------------------------------------
1 | .Simple {
2 | border: 1px solid #E9E9E9;
3 | border-radius: 4px;
4 | width: 294px;
5 | height: 220px;
6 | padding: 5px;
7 | .tag {
8 | margin: 3px;
9 | font-size: 13px;
10 | border: 1px dashed #cccccc;
11 | border-radius: 4px;
12 | padding: 0 8px;
13 | line-height: 30px;
14 | color: #666666;
15 | background: rgba(255, 255, 255, 0.7);
16 | }
17 | }
--------------------------------------------------------------------------------
/example/addAndDelete/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 | import deleteBtn from '../imgs/delete.png';
5 | import deleteBtn2x from '../imgs/delete@2x.png';
6 | import styles from './style.less';
7 |
8 | import mock from './mock.js';
9 |
10 | export default class AddAndDelete extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | tags: mock.tags
15 | };
16 |
17 | this.handleClickAdd = this.handleClickAdd.bind(this);
18 | }
19 |
20 | handleClickAdd() {
21 | if (this.input.value.length < 1) return;
22 | const tags = this.state.tags.slice();
23 | tags.push({id: Math.random() , content: this.input.value});
24 | this.setState({tags});
25 | this.input.value = '';
26 | }
27 |
28 | handleClickDelete(tag) {
29 | const tags = this.state.tags.filter(t => tag.id !== t.id);
30 | this.setState({tags});
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
(
40 |
41 |
this.handleClickDelete(tag)}
46 | />
47 | {tag.content}
48 |
49 | )}
50 | onChange={tags => this.setState({tags})}
51 | />
52 |
53 |
54 | this.input = r} />
55 | Add
56 |
57 |
58 | );
59 | }
60 | }
--------------------------------------------------------------------------------
/example/addAndDelete/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [{
3 | id: '1',
4 | content: 'apple',
5 | },{
6 | id: '2',
7 | content: 'watermelon',
8 | },{
9 | id: '3',
10 | content: 'banana',
11 | },{
12 | id: '4',
13 | content: 'lemon',
14 | },{
15 | id: '5',
16 | content: 'orange',
17 | },{
18 | id: '6',
19 | content: 'grape',
20 | },{
21 | id: '7',
22 | content: 'strawberry',
23 | },{
24 | id: '8',
25 | content: 'cherry',
26 | },{
27 | id: '9',
28 | content: 'peach',
29 | }],
30 | }
--------------------------------------------------------------------------------
/example/addAndDelete/style.less:
--------------------------------------------------------------------------------
1 | .AddAndDelete {
2 | .main {
3 | border: 1px solid #E9E9E9;
4 | border-radius: 4px;
5 | width: 294px;
6 | height: 220px;
7 | padding: 5px;
8 | }
9 | .tag {
10 | position: relative;
11 | margin: 3px;
12 | font-size: 13px;
13 | border: 1px dashed #3b9de9;
14 | border-radius: 4px;
15 | padding: 0 8px;
16 | line-height: 30px;
17 | color: #666666;
18 | background: rgba(255, 255, 255, 0.7);
19 | }
20 | .delete {
21 | position: absolute;
22 | top: -1px;
23 | right: -1px;
24 | width: 16px;
25 | height: 16px;
26 | cursor: pointer;
27 | user-drag: none;
28 | user-select: none;
29 | -moz-user-select: none;
30 | -webkit-user-drag: none;
31 | -webkit-user-select: none;
32 | -ms-user-select: none;
33 | }
34 | .inputs {
35 | margin-top: 10px;
36 | }
37 | input {
38 | position: relative;
39 | top: -1px;
40 | height: 23px;
41 | }
42 | button {
43 | margin-left: 10px;
44 | background-color: #3789c7;
45 | border: none;
46 | color: white;
47 | padding: 5px 15px;
48 | text-align: center;
49 | text-decoration: none;
50 | display: inline-block;
51 | font-size: 16px;
52 | border-radius: 2px;
53 | }
54 | }
--------------------------------------------------------------------------------
/example/imgs/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/example/imgs/delete.png
--------------------------------------------------------------------------------
/example/imgs/delete@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/example/imgs/delete@2x.png
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | react-draggable-tags
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import { prism } from 'react-syntax-highlighter/dist/styles/prism';
5 |
6 |
7 | import Simple from './Simple';
8 | import Customized from './Customized';
9 | import AddAndDelete from './AddAndDelete';
10 | import CrossArea from './CrossArea';
11 | import CrossAreaRestriction from './CrossArea-restriction';
12 | import List from './List';
13 | import NestedTags from './NestedTags';
14 | import ControlledTags from './ControlledTags';
15 |
16 | import styles from './style.less';
17 |
18 | class Main extends Component {
19 | render() {
20 | return (
21 |
22 |
23 |
24 | React Draggable Tags
25 |
26 |
27 | An easy and flexible tag component
28 |
29 |
30 | View on Github
31 |
32 |
33 |
34 |
35 | Installation
36 |
37 |
38 | {`npm install react-draggable-tags --save`}
39 |
40 |
41 |
42 | Simple Usage:
43 |
44 |
45 | You need to pass an "tags" array and a "render" function to DraggableArea. Each tag should have a unique id.
46 | (React Draggable Tags do not have default styles, you could write any style for the tags as you want)
47 |
48 |
49 |
50 | {`import {DraggableArea} from 'react-draggable-tags';
51 |
52 | const initialTags = [
53 | {id: 1, content: 'apple'}, {id: 2, content: 'olive'}, {id: 3, content: 'banana'},
54 | {id: 4, content: 'lemon'}, {id: 5, content: 'orange'}, {id: 6, content: 'grape'},
55 | {id: 7, content: 'strawberry'}, {id: 8, content: 'cherry'}, {id: 9, content: 'peach'}
56 | ];`}
57 |
58 |
59 | {`
60 |
(
63 |
64 | {tag.content}
65 |
66 | )}
67 | onChange={tags => console.log(tags)}
68 | />
69 | `}
70 |
71 |
72 | {`.Simple {
73 | border: 1px solid #E9E9E9;
74 | border-radius: 4px;
75 | width: 294px;
76 | height: 220px;
77 | padding: 5px;
78 | }
79 | .tag {
80 | margin: 3px;
81 | font-size: 13px;
82 | border: 1px dashed #cccccc;
83 | border-radius: 4px;
84 | padding: 0 8px;
85 | line-height: 30px;
86 | color: #666666;
87 | background: rgba(255, 255, 255, 0.7);
88 | }`}
89 |
90 |
91 | View code on Github
92 |
93 |
94 |
95 |
96 | Highly Customized:
97 |
98 |
99 |
100 | {`class Fruit extends Component {
101 | ...
102 | }
103 | const initialTags = [
104 | {id: 1, content: apple
}, {id: 2, content:
},
105 | {id: 3, content: banana
}, {id: 4, content: lemon
},
106 | {id: 5, content: }, {id: 6, content: grape
}
107 | ];`}
108 |
109 |
110 | {`
111 | tag.content}
114 | onChange={tags => console.log(tags)}
115 | />
116 |
`}
117 |
118 |
119 | {`.Customized {
120 | ...
121 | }
122 | .tag {
123 | ...
124 | }
125 | .tag2 {
126 | ...
127 | }
128 | .tag3 {
129 | ...
130 | }`}
131 |
132 |
133 | View code on Github
134 |
135 |
136 |
137 |
138 | Add Add Delete:
139 |
140 |
141 |
142 | {`
143 |
144 |
(
147 |
148 |
this.handleClickDelete(tag)}
152 | />
153 | {tag.content}
154 |
155 | )}
156 | onChange={tags => this.setState({tags})}
157 | />
158 |
159 |
160 | this.input = r} />
161 | Add
162 |
163 |
`}
164 |
165 |
166 | {`handleClickAdd() {
167 | const tags = this.state.tags.slice();
168 | tags.push({id: Math.random() , content: this.input.value});
169 | this.setState({tags});
170 | this.input.value = '';
171 | }
172 |
173 | handleClickDelete(tag) {
174 | const tags = this.state.tags.filter(t => tag.id !== t.id);
175 | this.setState({tags});
176 | }`}
177 |
178 |
179 | View code on Github
180 |
181 |
182 |
183 |
184 | Cross-Area Drag:
185 |
186 |
187 |
188 | {`import {DraggableAreasGroup} from 'react-draggable-tags';
189 |
190 | const group = new DraggableAreasGroup();
191 | const DraggableArea1 = group.addArea();
192 | const DraggableArea2 = group.addArea();`}
193 |
194 |
195 | {`
196 |
(
199 |
200 | {tag.content}
201 |
202 | )}
203 | onChange={leftTags => this.setState({leftTags})}
204 | />
205 |
206 |
207 |
(
210 |
211 |
this.handleClickDelete(tag)}
215 | />
216 | {tag.content}
217 |
218 | )}
219 | onChange={rightTags => this.setState({rightTags})}
220 | />
221 | `}
222 |
223 |
224 | {`handleClickDelete(tag) {
225 | const tags = this.state.tags.filter(t => tag.id !== t.id);
226 | this.setState({tags});
227 | }`}
228 |
229 |
230 | View code on Github
231 |
232 |
233 |
234 |
235 |
236 | Cross-Area Drag with Restrictions:
237 |
238 |
239 | For the following example, you can only drag tags from the left area to the right area.
240 |
241 |
242 |
243 | {`import {DraggableAreasGroup} from 'react-draggable-tags';
244 |
245 | const group = new DraggableAreasGroup();
246 | const DraggableArea1 = group.addArea('area1');
247 | const DraggableArea2 = group.addArea('area2');`}
248 |
249 |
250 | {`
251 |
(
254 |
255 | {tag.content}
256 |
257 | )}
258 | onChange={(leftTags, {fromArea, toArea}) => {
259 | console.log(fromArea); // {id: "area2", tag: {…}}
260 | if (fromArea.id === 'area2') {
261 | this.setState({leftTags: this.state.leftTags});
262 | } else {
263 | this.setState({leftTags});
264 | }
265 | }}
266 | />
267 |
268 | ....
269 |
270 |
(
273 |
274 | {tag.content}
275 |
276 | )}
277 | onChange={(rightTags, {fromArea, toArea}) => {
278 | if (toArea.id === 'area1') {
279 | this.setState({rightTags: this.state.rightTags});
280 | } else {
281 | this.setState({rightTags});
282 | }
283 | }}
284 | />
285 | `}
286 |
287 |
288 | View code on Github
289 |
290 |
291 |
292 |
293 | Draggable List:
294 |
295 |
296 |
297 | {` (
301 |
302 |
this.handleClickDelete(tag)}
306 | />
307 | {tag.content}
308 |
309 | )}
310 | onChange={(tags) => this.setState({tags})}
311 | />`}
312 |
313 |
314 | {`handleClickAdd() {
315 | const tags = this.state.tags.slice();
316 | tags.push({id: Math.random() , content: this.input.value});
317 | this.setState({tags});
318 | this.input.value = '';
319 | }
320 |
321 | handleClickDelete(tag) {
322 | const tags = this.state.tags.filter(t => tag.id !== t.id);
323 | this.setState({tags});
324 | }`}
325 |
326 |
327 | View code on Github
328 |
329 |
330 |
331 |
334 |
335 | React Draggable Tags is quite flexible, you can put anything in a tag. So you could even build "nested tags" like this:
336 |
337 |
338 |
339 | {`export default class Tag extends Component {
340 | render() {
341 | return (
342 |
343 |
344 |
347 |
348 |
349 |
352 |
353 |
354 | );
355 | }
356 | }`}
357 |
358 |
359 | {`constructor() {
360 | ...
361 | this.state = {
362 | leftTags: [
363 | {id: '1', content: }, {id: '2', content: },
364 | {id: '3', content: }, {id: '4', content: }
365 | ],
366 | rightTags: ...
367 | };
368 | }
369 | ...
370 |
371 |
372 |
(
375 |
376 | {tag.content}
377 |
378 | )}
379 | ...
380 | />
381 |
382 |
383 |
387 |
388 | `}
389 |
390 |
391 | View code on Github
392 |
393 |
394 |
395 | );
396 | }
397 | }
398 |
399 | const root = document.createElement('div');
400 | document.body.appendChild(root);
401 | ReactDOM.render(
402 | ,
403 | root
404 | );
405 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.2",
4 | "main": "./bundle.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "build": "webpack",
8 | "start": "webpack-dev-server"
9 | },
10 | "author": "Yifan Gu",
11 | "devDependencies": {
12 | "@babel/cli": "^7.2.3",
13 | "@babel/core": "^7.2.2",
14 | "@babel/plugin-transform-react-jsx": "^7.2.0",
15 | "@babel/preset-env": "^7.2.3",
16 | "@babel/preset-react": "^7.0.0",
17 | "babel-core": "^7.0.0-bridge.0",
18 | "babel-loader": "^7.1.5",
19 | "babel-minify-webpack-plugin": "^0.3.1",
20 | "babel-plugin-transform-object-rest-spread": "^7.0.0-beta.3",
21 | "babel-plugin-transform-react-jsx": "^6.24.1",
22 | "babel-preset-env": "^1.7.0",
23 | "babel-preset-es2017": "^6.24.1",
24 | "babel-preset-react": "^6.24.1",
25 | "clean-webpack-plugin": "^0.1.19",
26 | "css-loader": "^0.28.11",
27 | "file-loader": "^1.1.11",
28 | "html-webpack-plugin": "^3.2.0",
29 | "less": "^3.0.4",
30 | "less-loader": "^4.1.0",
31 | "react-prism": "^4.3.2",
32 | "style-loader": "^0.21.0",
33 | "url-loader": "^1.0.1",
34 | "webpack": "^4.8.3",
35 | "webpack-cli": "^3.1.1",
36 | "webpack-dev-server": "^3.1.14"
37 | },
38 | "dependencies": {
39 | "react": "^16.13.1",
40 | "react-dom": "^16.13.1",
41 | "react-draggable-tags": "^1.0.4",
42 | "react-syntax-highlighter": "^10.1.2"
43 | },
44 | "keywords": [
45 | "react",
46 | "tags",
47 | "tag",
48 | "draggable",
49 | "drag"
50 | ],
51 | "description": "",
52 | "files": [
53 | "lib"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/example/simple/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import {DraggableArea} from '../Draggable';
4 |
5 | import styles from './style.less';
6 |
7 | const initialTags = [
8 | {id: 1, content: 'apple'}, {id: 2, content: 'olive'}, {id: 3, content: 'banana'},
9 | {id: 4, content: 'lemon'}, {id: 5, content: 'orange'}, {id: 6, content: 'grape'},
10 | {id: 7, content: 'strawberry'}, {id: 8, content: 'cherry'}, {id: 9, content: 'peach'}];
11 |
12 | export default class Main extends Component {
13 | render() {
14 | return (
15 |
16 |
(
19 |
20 | {tag.content}
21 |
22 | )}
23 | onChange={tags => console.log(tags)}
24 | />
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/example/simple/style.less:
--------------------------------------------------------------------------------
1 | .Simple {
2 | border: 1px solid #E9E9E9;
3 | border-radius: 4px;
4 | width: 294px;
5 | height: 220px;
6 | padding: 5px;
7 | .tag {
8 | margin: 3px;
9 | font-size: 13px;
10 | border: 1px dashed #cccccc;
11 | border-radius: 4px;
12 | padding: 0 8px;
13 | line-height: 30px;
14 | color: #666666;
15 | background: rgba(255, 255, 255, 0.7);
16 | }
17 | }
--------------------------------------------------------------------------------
/example/style.less:
--------------------------------------------------------------------------------
1 | html, body, div, span {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
6 | }
7 | pre {
8 | font-size: 13px;
9 | }
10 | .head {
11 | background-color: #32589e;
12 | text-align: center;
13 | overflow: hidden;
14 | margin-bottom: 10px;
15 | padding-bottom: 50px;
16 | > h1 {
17 | font-size: 50px;
18 | color: white;
19 | font-weight: bold;
20 | margin: 0;
21 | margin-top: 55px;
22 | }
23 | > h2 {
24 | font-size: 25px;
25 | color: #d3e9ff;
26 | margin: 0;
27 | margin-top: 20px;
28 | font-weight: 200;
29 | }
30 | > a {
31 | display: inline-block;
32 | font-weight: 400;
33 | margin-top: 25px;
34 | line-height: 50px;
35 | padding: 0 15px;
36 | border-radius: 4px;
37 | font-size: 20px;
38 | color: white;
39 | background: rgba(255, 255, 255, 0.103);
40 | border: 1px solid white;
41 | cursor: pointer;
42 | text-decoration: none;
43 | transition: backgroud .5s;
44 | &:hover {
45 | background: rgba(255, 255, 255, 0.336);
46 | }
47 | }
48 | }
49 | .content {
50 | width: 770px;
51 | margin: 0 auto;
52 | margin-bottom: 30px;
53 | > a {
54 | color: #2c5fbd;
55 | font-size: 14px;
56 | }
57 | }
58 | .section-title {
59 | font-size: 25px;
60 | font-weight: normal;
61 | line-height: 60px;
62 | margin: 0;
63 | margin-bottom: 10px;
64 | border-bottom: 1px solid rgba(0, 0, 0, 0.103);
65 | &:not(:first-of-type) {
66 | margin-top: 15px;
67 | }
68 | }
69 | .des {
70 | margin-bottom: 15px;
71 | color: #666666;
72 | font-weight: 200;
73 | font-size: 16px;
74 | line-height: 22px;
75 | }
76 | @media (max-width: 800px) {
77 | .content {
78 | width: auto;
79 | margin: 0 10px;
80 | }
81 | }
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const webpack = require('webpack');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | entry: './index.js',
8 | output: {
9 | filename: 'bundle.js',
10 | path: __dirname,
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.css$/,
16 | use: [
17 | 'style-loader',
18 | 'css-loader'
19 | ]
20 | },
21 | {
22 | test: /\.less$/,
23 | use: [{
24 | loader: 'style-loader' // creates style nodes from JS strings
25 | }, {
26 | loader: 'css-loader' // translates CSS into CommonJS
27 | }, {
28 | loader: 'less-loader' // compiles Less to CSS
29 | }]
30 | },
31 | { test: /\.js$/, exclude: /node_modules/,
32 | use: [
33 | {
34 | loader: 'babel-loader',
35 | options: {
36 | presets: ['@babel/preset-react']
37 | }
38 | }
39 | ],
40 | },
41 | {
42 | test: /\.(png|jpg|gif)$/,
43 | use: [
44 | {
45 | loader: 'url-loader',
46 | options: {
47 | limit: 25000
48 | }
49 | }
50 | ]
51 | }
52 | ]
53 | },
54 | devServer: {
55 | contentBase: __dirname,
56 | compress: true,
57 | port: 9000,
58 | host: 'localhost',
59 | }
60 | };
--------------------------------------------------------------------------------
/imgs/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
--------------------------------------------------------------------------------
/imgs/AddAddDelete.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/imgs/AddAddDelete.gif
--------------------------------------------------------------------------------
/imgs/CrossAreaDrag.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/imgs/CrossAreaDrag.gif
--------------------------------------------------------------------------------
/imgs/DraggableList.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/imgs/DraggableList.gif
--------------------------------------------------------------------------------
/imgs/TagsInTags.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YGYOOO/react-draggable-tags/73a3b95d8ec30b4b57a8bea25902899ed1bca924/imgs/TagsInTags.gif
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-draggable-tags' {
2 | import * as React from 'react';
3 |
4 | export interface DraggableProps {
5 | tags: Array;
6 | render: (arg: { tag: T; index: number }) => JSX.Element;
7 | onChange?: (tags: Array) => void;
8 | isList?: boolean;
9 | }
10 |
11 | export class DraggableArea extends React.Component, unknown> { }
12 | }
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | react-draggable-tags
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lib/bundle.js:
--------------------------------------------------------------------------------
1 | module.exports=function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(o,r,function(e){return t[e]}.bind(null,r));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=7)}([function(t,e){t.exports=require("react")},function(t,e){t.exports=require("immutable")},function(t,e,n){var o=n(3);"string"==typeof o&&(o=[[t.i,o,""]]);var r={hmr:!0,transform:void 0,insertInto:void 0};n(5)(o,r);o.locals&&(t.exports=o.locals)},function(t,e,n){(t.exports=n(4)(!1)).push([t.i,".DraggableTags {\n position: relative;\n height: 100%;\n touch-action: none;\n}\n.DraggableTags::after {\n content: '';\n display: block;\n clear: both;\n}\n.DraggableTags-tag {\n display: inline-block;\n position: relative;\n color: transparent;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.DraggableTags-undraggable {\n cursor: no-drop;\n}\n.DraggableTags-tag-drag {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n z-index: 1;\n}\n.hotspot-9485743 {\n cursor: move;\n}\n.excludedInHotspot-9485743 {\n cursor: default;\n}\n",""])},function(t,e){t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var n=function(t,e){var n=t[1]||"",o=t[3];if(!o)return n;if(e&&"function"==typeof btoa){var r=(a=o,"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(a))))+" */"),i=o.sources.map((function(t){return"/*# sourceURL="+o.sourceRoot+t+" */"}));return[n].concat(i).concat([r]).join("\n")}var a;return[n].join("\n")}(e,t);return e[2]?"@media "+e[2]+"{"+n+"}":n})).join("")},e.i=function(t,n){"string"==typeof t&&(t=[[null,t,""]]);for(var o={},r=0;r=0&&l.splice(e,1)}function b(t){var e=document.createElement("style");return void 0===t.attrs.type&&(t.attrs.type="text/css"),m(e,t.attrs),h(t,e),e}function m(t,e){Object.keys(e).forEach((function(n){t.setAttribute(n,e[n])}))}function y(t,e){var n,o,r,i;if(e.transform&&t.css){if(!(i=e.transform(t.css)))return function(){};t.css=i}if(e.singleton){var a=f++;n=u||(u=b(e)),o=E.bind(null,n,a,!1),r=E.bind(null,n,a,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(t){var e=document.createElement("link");return void 0===t.attrs.type&&(t.attrs.type="text/css"),t.attrs.rel="stylesheet",m(e,t.attrs),h(t,e),e}(e),o=j.bind(null,n,e),r=function(){v(n),n.href&&URL.revokeObjectURL(n.href)}):(n=b(e),o=O.bind(null,n),r=function(){v(n)});return o(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;o(t=e)}else r()}}t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=a()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var n=g(t,e);return d(n,e),function(t){for(var o=[],r=0;r0&&void 0!==arguments[0]?arguments[0]:{},e=t.isInAnotherArea,n=void 0===e?function(){return{}}:e,o=t.passAddFunc,a=void 0===o?function(){}:o,s=function(t){var e=t.children;return r.a.createElement("div",{className:"hotspot-9485743"},e)},u=function(t){var e=t.children;return r.a.createElement("div",{className:"excludedInHotspot-9485743"},e)},l=function(t){d(o,t);var e=h(o);function o(){var t;return f(this,o),(t=e.call(this)).state={tags:Object(i.List)([])},t.draggableTagEles={},t.tagEles={},t.positions=[],t.rect={},t.dragStart={},t.tagChanged=!1,t.tagsElesWhichBindedDrag=new WeakSet,t}return p(o,[{key:"componentDidMount",value:function(){this.props.initialTags?this.setTags(Object(i.List)(this.props.initialTags)):this.setTags(Object(i.List)(this.props.tags)),a(this.container,this.addTag.bind(this)),this.props.getAddTagFunc&&this.props.getAddTagFunc(this.addTag.bind(this))}},{key:"UNSAFE_componentWillReceiveProps",value:function(t){var e=this,n=t.tags;n&&(n.length===this.props.tags.length&&n.length===this.state.tags.size&&!n.some((function(t,n){return!e.state.tags.get(n)||t.id!==e.state.tags.get(n).id}))||this.forbitSetTagsState||this.setTags(Object(i.List)(n)))}},{key:"componentDidUpdate",value:function(t,e){var n=e.tags;this.tagChanged=this.tagChanged||n.size!==this.state.tags.size||this.state.tags.some((function(t,e){return!n.get(e)||t.id!==n.get(e).id}))}},{key:"dragElement",value:function(t,e,o){var r,i=this,a=this.props.isList,s=0,c=0,u=0,f=0,l={},p=!1;this.positions.forEach((function(t,n){t.id===e&&(r=n)}));var d=function(n){if(p=!1,i.props.withHotspot){var o=n.target.closest(".".concat("hotspot-9485743")),a=n.target.closest(".".concat("excludedInHotspot-9485743"));if(!o)return;if(o.contains(a))return}if(i.tagChanged=!1,!window.dragMouseDown){for(window.dragMouseDown=!0,l=i.container.getBoundingClientRect(),n=n||window.event,s=u=n.clientX||n.touches[0].clientX,c=f=n.clientY||n.touches[0].clientY,t.style.zIndex=2,window.parentDragTag=t.parentElement;window.parentDragTag&&!window.parentDragTag.classList.contains("DraggableTags-tag-drag");)window.parentDragTag=window.parentDragTag.parentElement;window.parentDragTag&&(window.parentDragTag.style.zIndex=2),document.addEventListener("mouseup",h,!1),document.addEventListener("mousemove",g,!1),t.addEventListener("touchend",h,!1),t.addEventListener("touchcancel",h,!1),t.addEventListener("touchmove",g,!1),i.positions.forEach((function(t,n){t.id===e&&(r=n)}))}},g=function(e){m&&(i.container.style.overflowY="visible"),"touchmove"===e.type&&e.preventDefault();var d=void 0===(e=e||window.event).clientX?e.touches[0].clientX:e.clientX,g=void 0===e.clientY?e.touches[0].clientY:e.clientY;if(d!==u||g!==f){p=!0;var h=d-s,v=g-c;s=d,c=g;var b=t.offsetTop+v,y=t.offsetLeft+h;t.style.top=b+"px",t.style.left=y+"px";var w,T=o.offsetTop+t.offsetHeight/2,E=o.offsetLeft+t.offsetWidth/2,O=T+b,j=E+y,L=t.getBoundingClientRect(),x=L.left+L.width/2,S=L.top+L.height/2,C=!1;if(xl.right||Sl.bottom)C=n(L,i.state.tags.get(r),!1).isIn;for(w=0;wD.bottom-4&&OA.bottom-4&&(I=!0)):(0===w&&O>D.top&&OA.top&&j>A.left-8||O>A.bottom)&&(I=!0),O>D.top&&OD.right-8&&jA.top&&OD.top&&OD.right-8&&D.topa?y-(s-a):y+(a-s),"px"),t.style.top="".concat(o>e?b+(o-e):b-(e-o),"px")})),"break"}())break}}else p=!1},h=function e(o){if(p){var a=function t(e){e.stopPropagation(),document.removeEventListener("click",t,!0)};document.addEventListener("click",a,!0),setTimeout((function(){document.removeEventListener("click",a,!0)}),500)}m&&(i.container.style.overflowY="auto"),window.dragMouseDown=!1,document.removeEventListener("mouseup",e,!1),document.removeEventListener("mousemove",g,!1),t.removeEventListener("touchend",e,!1),t.removeEventListener("touchcancel",e,!1),t.removeEventListener("touchmove",g,!1),window.parentDragTag&&(window.parentDragTag.style.zIndex=1);var s=t.getBoundingClientRect(),c=s.left+s.width/2,u=s.top+s.height/2;if(cl.right||ul.bottom){i.forbitSetTagsState=!0;var f=n(t.getBoundingClientRect(),i.state.tags.get(r));if(f&&f.isIn){i.positions.splice(r,1);var d=i.state.tags.get(r);return void i.setState({tags:i.state.tags.splice(r,1)},(function(){i.props.onChange&&i.props.onChange(i.state.tags.toJS(),i.buildOnChangeObj({toArea:{id:f.id,tag:d}})),i.forbitSetTagsState=!1}))}i.forbitSetTagsState=!1}t.style.top=0,t.style.left=0,t.style.zIndex=1,i.tagChanged&&i.props.onChange&&(i.tagChanged=!1,i.props.onChange(i.state.tags.toJS(),i.buildOnChangeObj()))};t.removeEventListener("mousedown",d),t.removeEventListener("touchstart",d),t.addEventListener("mousedown",d,!1),t.addEventListener("touchstart",d,!1)}},{key:"setTags",value:function(t,e){var n=this;this.setState({tags:t},(function(){e&&e(),n.positions=[],n.state.tags.forEach((function(t,e){var o=n.draggableTagEles[t.id],r=n.tagEles[t.id];if(n.positions.push({id:t.id,top:r.offsetTop,left:r.offsetLeft,bottom:r.offsetTop+r.offsetHeight,right:r.offsetLeft+r.offsetWidth,width:r.offsetWidth,height:r.offsetHeight}),!t.undraggable){if(n.tagsElesWhichBindedDrag.has(o))return;n.tagsElesWhichBindedDrag.add(o),n.dragElement(o,t.id,r)}}))}))}},{key:"addTag",value:function(t){var e,n=this,o=t.tag,r=t.fromAreaId,i=t.x,a=t.y,s=this.container.getBoundingClientRect(),c=a-s.top,u=i-s.left,f=!1,l=!1,p=!1,d=!1,g=!1;for(e=0;ev&&ch.top&&ch.top&&cb&&um.top&&ch.top&&cb&&h.top0&&void 0!==arguments[0]?arguments[0]:{},e=t.fromArea,n=void 0===e?{}:e,o=t.toArea,r=void 0===o?{}:o;return{fromArea:n,toArea:r}}},{key:"render",value:function(){var t=this,e=this.props,n=e.render,o=e.build,i=e.style,a=e.className,s=e.isList,u=(e.tagMargin,e.tagStyle),f=e.withHotspot;n||(n=o);var l=this.state.tags.toJS().map((function(e,o){return r.a.createElement("div",{key:e.id,className:"DraggableTags-tag ".concat(e.undraggable?"DraggableTags-undraggable":""," ").concat(f?"":"hotspot-9485743"),ref:function(n){t.tagEles[e.id]=n},style:s?c({display:"block"},u):u},r.a.createElement("div",{className:"DraggableTags-tag-drag",ref:function(n){return t.draggableTagEles[e.id]=n}},n({tag:e,index:o,deleteThis:t.buildDeleteTagFunc(e)})),r.a.createElement("div",{style:{opacity:0,overflow:"hidden"}},n({tag:e,index:o,deleteThis:t.buildDeleteTagFunc(e)})))}));return r.a.createElement("div",{ref:function(e){return t.container=e},className:"DraggableTags ".concat(a||""),style:m?c({overflowY:"auto"},i):i},m?r.a.createElement("div",{style:{height:"101%"}},l):l)}}]),o}(r.a.Component);return l.Hotspot=s,l.ExcludedInHotspot=u,l}function w(t,e){for(var n=0;n2&&void 0!==arguments[2])||arguments[2],i=n.left+n.width/2,a=n.top+n.height/2,s={isIn:!1};return e.isInAreas.forEach((function(e){var n=e({tag:o,x:i,y:a,areaId:t},r);n.isIn&&(s=n)})),s},passAddFunc:function(n,o){e.isInAreas.push((function(e,r){var i=e.tag,a=e.x,s=e.y,c=e.areaId,u=n.getBoundingClientRect();return a>u.left&&au.top&&s= 16.0",
17 | "react-dom": ">= 16.0"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/YGYOOO/react-draggable-tags"
22 | },
23 | "homepage": "https://ygyooo.github.io/react-draggable-tags/",
24 | "bugs": {
25 | "url": "https://github.com/YGYOOO/react-draggable-tags/issues"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.2.2",
29 | "@babel/preset-env": "^7.2.3",
30 | "babel-cli": "^6.26.0",
31 | "babel-core": "^6.26.3",
32 | "babel-loader": "^8.0.0-beta.6",
33 | "babel-minify-webpack-plugin": "^0.3.1",
34 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
35 | "babel-plugin-transform-react-jsx": "^6.24.1",
36 | "babel-preset-env": "^1.7.0",
37 | "babel-preset-es2017": "^6.24.1",
38 | "css-loader": "^0.28.11",
39 | "file-loader": "^1.1.11",
40 | "less": "^3.0.4",
41 | "less-loader": "^4.1.0",
42 | "react": "^16.9.1",
43 | "react-dom": "^16.9.1",
44 | "style-loader": "^0.21.0",
45 | "uglifyjs-webpack-plugin": "^1.2.5",
46 | "url-loader": "^1.0.1",
47 | "webpack": "^4.8.3",
48 | "webpack-cli": "^3.1.1",
49 | "webpack-dev-server": "^3.1.4",
50 | "webpack-node-externals": "^1.7.2"
51 | },
52 | "keywords": [
53 | "tags",
54 | "tag",
55 | "draggable",
56 | "drag",
57 | "react",
58 | "react draggable",
59 | "draggable tag",
60 | "react draggable tag"
61 | ],
62 | "description": "",
63 | "files": [
64 | "lib",
65 | "index.d.ts",
66 | "README.md"
67 | ],
68 | "types": "index.d.ts"
69 | }
70 |
--------------------------------------------------------------------------------
/src/DraggableAreaBuilder.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'immutable';
3 |
4 | import styles from './style.less';
5 |
6 | const isMobile = (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
7 |
8 | const hotspotClassName = 'hotspot-9485743';
9 | const excludedInHotspotClassName = 'excludedInHotspot-9485743';
10 |
11 | export default function buildDraggableArea({isInAnotherArea = () => ({}), passAddFunc = () => {}} = {}) {
12 | const Hotspot = ({children}) => (
13 |
14 | {children}
15 |
16 | );
17 | const ExcludedInHotspot = ({children}) => (
18 |
19 | {children}
20 |
21 | );
22 |
23 | class DraggableArea extends React.Component {
24 | constructor() {
25 | super();
26 | this.state = {
27 | tags: List([]),
28 | }
29 |
30 | this.draggableTagEles = {};
31 | this.tagEles = {};
32 | this.positions = [];
33 | this.rect = {};
34 | this.dragStart = {};
35 | this.tagChanged = false;
36 |
37 | this.tagsElesWhichBindedDrag = new WeakSet();
38 | }
39 |
40 | componentDidMount() {
41 | if (this.props.initialTags) {
42 | this.setTags(List(this.props.initialTags));
43 | } else {
44 | this.setTags(List(this.props.tags));
45 | }
46 |
47 | passAddFunc(this.container, this.addTag.bind(this));
48 | this.props.getAddTagFunc && this.props.getAddTagFunc(this.addTag.bind(this));
49 | }
50 |
51 | UNSAFE_componentWillReceiveProps({tags}) {
52 | if (!tags) return;
53 | if ((
54 | tags.length !== this.props.tags.length ||
55 | tags.length !== this.state.tags.size ||
56 | tags.some((tag, i) => !this.state.tags.get(i) || tag.id !== this.state.tags.get(i).id)
57 | ) && !this.forbitSetTagsState
58 | ) {
59 | this.setTags(List(tags));
60 | }
61 | }
62 |
63 | componentDidUpdate(prevProps, {tags}) {
64 | this.tagChanged = this.tagChanged ||
65 | tags.size !== this.state.tags.size ||
66 | this.state.tags.some((tag, i) => !tags.get(i) || tag.id !== tags.get(i).id);
67 | }
68 |
69 | dragElement(elmnt, id, parent) {
70 | const isList = this.props.isList;
71 | let prevX = 0, prevY = 0;
72 | // 记录 dragStart 起始位置,用于判断是否发生了位移
73 | let dragStartX = 0, dragStartY = 0;
74 | let rect = {};
75 | let dragged = false;
76 |
77 | let index;
78 | this.positions.forEach((p, i) => {
79 | if (p.id === id) index = i;
80 | });
81 |
82 | const dragStart = (e) => {
83 | dragged = false;
84 | if (this.props.withHotspot) {
85 | const closestHotspot = e.target.closest(`.${hotspotClassName}`);
86 | const closestExcludedInHotspot = e.target.closest(`.${excludedInHotspotClassName}`);
87 |
88 | if (!closestHotspot) return;
89 | if (closestHotspot.contains(closestExcludedInHotspot)) return;
90 | }
91 | // e.preventDefault();
92 | this.tagChanged = false;
93 |
94 | if (window.dragMouseDown) return;
95 | window.dragMouseDown = true;
96 |
97 | rect = this.container.getBoundingClientRect();
98 | e = e || window.event;
99 | // 保存 dragStart 起始位置
100 | prevX = dragStartX = e.clientX || e.touches[0].clientX;
101 | prevY = dragStartY = e.clientY || e.touches[0].clientY;
102 | elmnt.style.zIndex = 2;
103 | window.parentDragTag = elmnt.parentElement;
104 | while (window.parentDragTag && !window.parentDragTag.classList.contains('DraggableTags-tag-drag')) {
105 | window.parentDragTag = window.parentDragTag.parentElement;
106 | }
107 | if (window.parentDragTag) window.parentDragTag.style.zIndex = 2;
108 | document.addEventListener("mouseup", closeDragElement, false);
109 | document.addEventListener("mousemove", elementDrag, false);
110 | elmnt.addEventListener("touchend", closeDragElement, false);
111 | elmnt.addEventListener("touchcancel", closeDragElement, false);
112 | elmnt.addEventListener("touchmove", elementDrag, false);
113 |
114 | this.positions.forEach((p, i) => {
115 | if (p.id === id) index = i;
116 | });
117 | }
118 |
119 | const elementDrag = (e) => {
120 | if (isMobile) this.container.style.overflowY = 'visible';
121 | // Prevent scrolling on mobile devices
122 | e.type === 'touchmove' && e.preventDefault();
123 |
124 | // Figure out the new position of tag
125 | e = e || window.event;
126 | let clientX = e.clientX === undefined ? e.touches[0].clientX : e.clientX;
127 | let clientY = e.clientY === undefined ? e.touches[0].clientY : e.clientY;
128 |
129 | // 判断是否真正发生了位移
130 | if (clientX === dragStartX && clientY === dragStartY) {
131 | dragged = false;
132 | return;
133 | }
134 | dragged = true;
135 |
136 | let movedX = clientX - prevX;
137 | let movedY = clientY - prevY;
138 | prevX = clientX;
139 | prevY = clientY;
140 | let t = elmnt.offsetTop + movedY;
141 | let l = elmnt.offsetLeft + movedX;
142 | elmnt.style.top = t + "px";
143 | elmnt.style.left = l + "px";
144 |
145 | let baseCenterTop = parent.offsetTop + elmnt.offsetHeight / 2;
146 | let baseCenterLeft = parent.offsetLeft + elmnt.offsetWidth / 2;
147 | // The center position of the tag
148 | let ctop = baseCenterTop + t;
149 | let cleft = baseCenterLeft + l;
150 |
151 | let eRect = elmnt.getBoundingClientRect();
152 | let x = eRect.left + eRect.width / 2;
153 | let y = eRect.top + eRect.height / 2;
154 | let isInAnother = false;
155 | if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
156 | const { isIn } = isInAnotherArea(eRect, this.state.tags.get(index), false);
157 | isInAnother = isIn;
158 | }
159 |
160 | let i; // safari 10 bug
161 | // Check if the tag could be put into a new position
162 | for (i = 0; i < this.positions.length - 1; i++) {
163 | if (isInAnother && i !== this.positions.length - 2) continue;
164 |
165 | // Do not check its left-side space and right-side space
166 | if ((index !== i || (index === this.positions.length - 2 && i === this.positions.length - 2)) && !(index - 1 === i && i !== 0)) {
167 | const p1 = this.positions[i];
168 | const p2 = this.positions[i+1];
169 |
170 | let isHead = false;
171 | let isTail = false;
172 | let between2Tags = false;
173 | let endOfLine = false;
174 | let startOfLine = false;
175 |
176 | if (!isList) {
177 | // Is not "list view"
178 | if (
179 | // Head of tag list
180 | i === 0 &&
181 | ctop > p1.top &&
182 | ctop < p1.bottom &&
183 | cleft < p1.left + 8
184 | ) isHead = true;
185 |
186 | if (
187 | // Tail of tag list
188 | i === this.positions.length - 2 && ((
189 | ctop > p2.top &&
190 | cleft > p2.left - 8) || ctop > p2.bottom)
191 | ) isTail = true;
192 |
193 | if (
194 | // Between two tags
195 | ctop > p1.top &&
196 | ctop < p1.bottom &&
197 | cleft > p1.right - 8 &&
198 | cleft < p2.left + 8
199 | ) between2Tags = true;
200 |
201 | if (
202 | // Start of line
203 | ctop > p2.top &&
204 | ctop < p2.bottom &&
205 | cleft < p2.left + 8 &&
206 | p1.top < p2.top
207 | ) startOfLine = true;
208 |
209 | if (
210 | // End of line
211 | ctop > p1.top &&
212 | ctop < p1.bottom &&
213 | cleft > p1.right - 8 &&
214 | p1.top < p2.top
215 | ) endOfLine = true;
216 | } else {
217 | // Is "list view"
218 | if (
219 | // Between two tags
220 | ctop > p1.bottom - 4 &&
221 | ctop < p2.top + 4
222 | ) between2Tags = true;
223 |
224 | if (
225 | // Head of tag list
226 | i === 0 &&
227 | ctop < p1.top + 4
228 | ) isHead = true;
229 |
230 | if (
231 | // Tail of tag list
232 | i === this.positions.length - 2 &&
233 | ctop > p2.bottom - 4
234 | ) isTail = true;
235 | }
236 |
237 | if (
238 | isInAnother
239 | ||
240 | (!isList && (isHead || isTail || between2Tags || startOfLine || endOfLine))
241 | ||
242 | (isList && (isHead || isTail || between2Tags))
243 | ) {
244 | let cur = this.state.tags.get(index);
245 | let tags = this.state.tags.splice(index, 1);
246 | if ((index < i || isHead) && !isTail) {
247 | tags = tags.splice(i, 0, cur);
248 | index = i;
249 | } else {
250 | tags = tags.splice(i+1, 0, cur);
251 | index = i + 1;
252 | }
253 | this.positions = [];
254 | const prevBaseTop = this.tagEles[cur.id].offsetTop;
255 | const prevBaseLeft = this.tagEles[cur.id].offsetLeft;
256 |
257 | this.setState({tags}, () => {
258 | let curBaseTop;
259 | let curBaseLeft;
260 | tags.forEach((t, i) => {
261 | const tag = this.tagEles[t.id];
262 | if (i === index) {
263 | curBaseLeft = tag.offsetLeft;
264 | curBaseTop= tag.offsetTop;
265 | }
266 | this.positions.push({
267 | id: t.id,
268 | top: tag.offsetTop,
269 | left: tag.offsetLeft,
270 | bottom: tag.offsetTop + tag.offsetHeight,
271 | right: tag.offsetLeft + tag.offsetWidth,
272 | width: tag.offsetWidth,
273 | height: tag.offsetHeight,
274 | });
275 | });
276 |
277 | // Calculate the new position
278 | if (curBaseLeft > prevBaseLeft) {
279 | elmnt.style.left = `${l - (curBaseLeft - prevBaseLeft)}px`;
280 | } else {
281 | elmnt.style.left = `${l + (prevBaseLeft - curBaseLeft)}px`;
282 | }
283 | if (prevBaseTop > curBaseTop) {
284 | elmnt.style.top = `${t + (prevBaseTop - curBaseTop)}px`;
285 | } else {
286 | elmnt.style.top = `${t - (curBaseTop - prevBaseTop)}px`;
287 | }
288 | });
289 | break;
290 | }
291 | }
292 | }
293 | }
294 |
295 | const closeDragElement = (e) => {
296 | if (dragged) {
297 | function captureClick(e) {
298 | e.stopPropagation();
299 | document.removeEventListener('click', captureClick, true);
300 | }
301 | document.addEventListener('click', captureClick, true);
302 | setTimeout(() => {
303 | document.removeEventListener('click', captureClick, true);
304 | }, 500);
305 | }
306 |
307 | if (isMobile) this.container.style.overflowY = 'auto';
308 |
309 | window.dragMouseDown = false;
310 |
311 | document.removeEventListener("mouseup", closeDragElement, false);
312 | document.removeEventListener("mousemove", elementDrag, false);
313 | elmnt.removeEventListener("touchend", closeDragElement, false);
314 | elmnt.removeEventListener("touchcancel", closeDragElement, false);
315 | elmnt.removeEventListener("touchmove", elementDrag, false);
316 |
317 | if (window.parentDragTag) window.parentDragTag.style.zIndex = 1;
318 |
319 | let eRect = elmnt.getBoundingClientRect();
320 | let x = eRect.left + eRect.width / 2;
321 | let y = eRect.top + eRect.height / 2;
322 | if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
323 | this.forbitSetTagsState = true;
324 | const result = isInAnotherArea(elmnt.getBoundingClientRect(), this.state.tags.get(index));
325 | if (result && result.isIn) {
326 | this.positions.splice(index, 1);
327 | const tagDraggedOut = this.state.tags.get(index);
328 | this.setState({tags: this.state.tags.splice(index, 1)}, () => {
329 | this.props.onChange && this.props.onChange(this.state.tags.toJS(), this.buildOnChangeObj({
330 | toArea: {
331 | id: result.id,
332 | tag: tagDraggedOut
333 | }
334 | }));
335 | this.forbitSetTagsState = false;
336 | });
337 | return;
338 | } else {
339 | this.forbitSetTagsState = false;
340 | }
341 | }
342 | elmnt.style.top = 0;
343 | elmnt.style.left = 0;
344 | elmnt.style.zIndex = 1;
345 | if (this.tagChanged && this.props.onChange) {
346 | this.tagChanged = false;
347 | this.props.onChange(this.state.tags.toJS(), this.buildOnChangeObj());
348 | }
349 | }
350 |
351 | elmnt.removeEventListener("mousedown", dragStart);
352 | elmnt.removeEventListener("touchstart", dragStart);
353 |
354 | elmnt.addEventListener("mousedown", dragStart, false);
355 | elmnt.addEventListener("touchstart", dragStart, false);
356 | }
357 |
358 | setTags(tags, callback) {
359 | this.setState({tags}, () => {
360 | callback && callback();
361 | this.positions = [];
362 | this.state.tags.forEach((t, i) => {
363 | const draggableTag = this.draggableTagEles[t.id];
364 | const tag = this.tagEles[t.id];
365 | this.positions.push({
366 | id: t.id,
367 | top: tag.offsetTop,
368 | left: tag.offsetLeft,
369 | bottom: tag.offsetTop + tag.offsetHeight,
370 | right: tag.offsetLeft + tag.offsetWidth,
371 | width: tag.offsetWidth,
372 | height: tag.offsetHeight,
373 | });
374 | if (!t.undraggable) {
375 | if (this.tagsElesWhichBindedDrag.has(draggableTag)) return;
376 | this.tagsElesWhichBindedDrag.add(draggableTag);
377 | this.dragElement(draggableTag, t.id, tag);
378 | }
379 | });
380 | });
381 | }
382 |
383 | addTag({tag, fromAreaId, x, y}) {
384 | const rect = this.container.getBoundingClientRect();
385 | // The center position of the tag
386 | let ctop = y - rect.top;
387 | let cleft = x - rect.left;
388 | let i; // safari 10 bug
389 |
390 | let isHead = false;
391 | let isTail = false;
392 | let between2Tags = false;
393 | let endOfLine = false;
394 | let startOfLine = false;
395 |
396 | // Check if the tag could be put into a new position
397 | for (i = 0; i < this.positions.length - 1; i++) {
398 | // Do not check its left-side space and right-side space
399 | const p1 = this.positions[i];
400 | const p1Ctop = p1.top + p1.height / 2;
401 | const p1Cleft = p1.left + p1.width / 2;
402 | const p2 = this.positions[i+1];
403 | const p2Ctop = p2.top + p2.height / 2;
404 | const p2Cleft = p2.left + p2.width / 2;
405 |
406 | isHead = false;
407 | isTail = false;
408 | between2Tags = false;
409 | endOfLine = false;
410 | startOfLine = false;
411 |
412 | if (!this.props.isList) {
413 | // Is not "list view"
414 | if (
415 | // Head of tag list
416 | i === 0 &&
417 | ctop > p1.top &&
418 | ctop < p1.bottom &&
419 | cleft < p1Cleft
420 | ) isHead = true;
421 |
422 |
423 | if (
424 | // Between two tags
425 | ctop > p1.top &&
426 | ctop < p1.bottom &&
427 | cleft > p1Cleft &&
428 | cleft < p2Cleft
429 | ) between2Tags = true;
430 |
431 | if (
432 | // Start of line
433 | ctop > p2.top &&
434 | ctop < p2.bottom &&
435 | cleft < p2Cleft &&
436 | p1.top < p2.top
437 | ) startOfLine = true;
438 |
439 | if (
440 | // End of line
441 | ctop > p1.top &&
442 | ctop < p1.bottom &&
443 | cleft > p1Cleft &&
444 | p1.top < p2.top
445 | ) endOfLine = true;
446 |
447 | if (
448 | // Tail of tag list
449 | i === this.positions.length - 2 &&
450 | !(isHead || between2Tags || startOfLine || endOfLine)
451 | ) isTail = true;
452 |
453 | if (isHead || isTail || between2Tags || startOfLine || endOfLine) break;
454 |
455 | } else {
456 | // Is "list view"
457 | if (
458 | // Between two tags
459 | ctop > p1Ctop &&
460 | ctop < p2Ctop
461 | ) between2Tags = true;
462 |
463 | if (
464 | // Head of tag list
465 | i === 0 &&
466 | ctop < p1Ctop
467 | ) isHead = true;
468 |
469 | if (
470 | // Tail of tag list
471 | i === this.positions.length - 2 &&
472 | !(between2Tags || isHead)
473 | ) isTail = true;
474 |
475 | if (isHead || isTail || between2Tags) break;
476 | }
477 | }
478 |
479 | let tags = this.state.tags;
480 | if (isTail) {
481 | tags = tags.push(tag);
482 | } else if (isHead) {
483 | tags = tags.unshift(tag);
484 | } else {
485 | tags = tags.splice(i+1, 0, tag);
486 | }
487 | this.positions = [];
488 |
489 | this.setState({tags}, () => {
490 | this.props.onChange && this.props.onChange(this.state.tags.toJS(), this.buildOnChangeObj({
491 | fromArea: {
492 | id: fromAreaId,
493 | tag,
494 | }
495 | }));
496 | });
497 | }
498 |
499 | buildDeleteTagFunc(tag) {
500 | return () => {
501 | const tags = this.state.tags.filter(t => tag.id !== t.id);
502 | this.setTags(tags, () => {
503 | this.props.onChange && this.props.onChange(this.state.tags.toJS(), this.buildOnChangeObj());
504 | });
505 | }
506 | }
507 |
508 | buildOnChangeObj({fromArea = {}, toArea = {}} = {}) {
509 | return {
510 | fromArea,
511 | toArea
512 | };
513 | }
514 |
515 |
516 | render() {
517 | let {render, build, style, className, isList, tagMargin = '5px', tagStyle, withHotspot} = this.props;
518 | if (!render) render = build;
519 | const tags = this.state.tags.toJS().map((tag, index) => (
520 | {
524 | this.tagEles[tag.id] = target;
525 | }}
526 | style={isList ? {display: 'block', ...tagStyle} : tagStyle}
527 | >
528 |
this.draggableTagEles[tag.id] = target}
531 | >
532 | {render({tag, index, deleteThis: this.buildDeleteTagFunc(tag)})}
533 |
534 |
535 | {render({tag, index, deleteThis: this.buildDeleteTagFunc(tag)})}
536 |
537 |
538 | ))
539 | return (
540 | this.container = r}
542 | className={`DraggableTags ${className || ''}`}
543 | style={isMobile ? { overflowY: 'auto', ...style} : style}
544 | >
545 | {
546 | // To prevent body scroll on mobile device when dragging tags
547 | isMobile ? (
{tags}
) : tags
548 | }
549 |
550 | );
551 | }
552 | }
553 |
554 | DraggableArea.Hotspot = Hotspot;
555 | DraggableArea.ExcludedInHotspot = ExcludedInHotspot;
556 |
557 | return DraggableArea;
558 | }
--------------------------------------------------------------------------------
/src/DraggableAreasGroup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fromJS, List, is } from 'immutable';
3 | import buildDraggableArea from './DraggableAreaBuilder';
4 |
5 |
6 |
7 | export default class DraggableTagsGroup {
8 | constructor() {
9 | this.isInAreas = [];
10 | }
11 |
12 | addArea(areaId) {
13 | return buildDraggableArea({
14 | isInAnotherArea: (tagRect, tag, shouldAdd = true) => {
15 | let x = tagRect.left + tagRect.width / 2;
16 | let y = tagRect.top + tagRect.height / 2;
17 |
18 | let result = {isIn: false};
19 | this.isInAreas.forEach(isInArea => {
20 | const r = isInArea({tag, x, y, areaId}, shouldAdd);
21 | if (r.isIn) {
22 | result = r;
23 | }
24 | });
25 |
26 | return result
27 | },
28 | passAddFunc: (ele, addTag) => {
29 | this.isInAreas.push(function({tag, x, y, areaId: fromAreaId}, shouldAdd) {
30 |
31 | const rect = ele.getBoundingClientRect();
32 | if (x > rect.left && x < rect.right && y > rect.top && y < rect.bottom) {
33 | shouldAdd && addTag({tag, fromAreaId, x, y});
34 | return {
35 | isIn: true,
36 | id: areaId
37 | };
38 | }
39 |
40 | return {
41 | isIn: false,
42 | };
43 | });
44 | }
45 | });
46 | }
47 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // import './style.css';
2 |
3 | // import React from 'react';
4 | // import ReactDOM from 'react-dom';
5 |
6 | // ReactDOM.render(
7 | // Hello, world! ,
8 | // document.getElementById('root')
9 | // );
10 | import React from 'react';
11 |
12 | import buildDraggableArea from './DraggableAreaBuilder';
13 | import DraggableAreasGroup from './DraggableAreasGroup';
14 |
15 | const DraggableArea = buildDraggableArea();
16 | export {DraggableArea, DraggableAreasGroup};
--------------------------------------------------------------------------------
/src/style.less:
--------------------------------------------------------------------------------
1 | .DraggableTags {
2 | position: relative;
3 | height: 100%;
4 | touch-action: none;
5 | &::after {
6 | content: '';
7 | display: block;
8 | clear: both;
9 | }
10 |
11 | &-tag {
12 | display: inline-block;
13 | position: relative;
14 | color: transparent;
15 | -webkit-touch-callout: none;
16 | -webkit-user-select: none;
17 | -khtml-user-select: none;
18 | -moz-user-select: none;
19 | -ms-user-select: none;
20 | user-select: none;
21 | }
22 | &-undraggable {
23 | cursor: no-drop;
24 | }
25 | &-tag-drag {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | width: 100%;
30 | z-index: 1;
31 | }
32 | }
33 |
34 | .hotspot-9485743 {
35 | cursor: move;
36 | }
37 | .excludedInHotspot-9485743 {
38 | cursor: default;
39 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const nodeExternals = require('webpack-node-externals');
3 |
4 | // const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
5 |
6 | module.exports = {
7 | entry: './src/index.js',
8 | output: {
9 | filename: 'bundle.js',
10 | path: path.resolve(__dirname, 'lib'),
11 | libraryTarget: 'commonjs2'
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: [
18 | 'style-loader',
19 | 'css-loader'
20 | ]
21 | },
22 | {
23 | test: /\.less$/,
24 | use: [{
25 | loader: 'style-loader' // creates style nodes from JS strings
26 | }, {
27 | loader: 'css-loader' // translates CSS into CommonJS
28 | }, {
29 | loader: 'less-loader' // compiles Less to CSS
30 | }]
31 | },
32 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
33 | {
34 | test: /\.(png|jpg|gif)$/,
35 | use: [
36 | {
37 | loader: 'url-loader',
38 | options: {
39 | limit: 8192
40 | }
41 | }
42 | ]
43 | }
44 | ]
45 | },
46 | externals: [nodeExternals()]
47 | };
48 |
49 |
50 | // module.exports = {
51 | // entry: './example/index.js',
52 | // output: {
53 | // filename: 'bundle.js',
54 | // path: path.resolve(__dirname, 'example'),
55 | // },
56 | // module: {
57 | // rules: [
58 | // {
59 | // test: /\.css$/,
60 | // use: [
61 | // 'style-loader',
62 | // 'css-loader'
63 | // ]
64 | // },
65 | // {
66 | // test: /\.less$/,
67 | // use: [{
68 | // loader: 'style-loader' // creates style nodes from JS strings
69 | // }, {
70 | // loader: 'css-loader' // translates CSS into CommonJS
71 | // }, {
72 | // loader: 'less-loader' // compiles Less to CSS
73 | // }]
74 | // },
75 | // { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
76 | // {
77 | // test: /\.(png|jpg|gif)$/,
78 | // use: [
79 | // {
80 | // loader: 'url-loader',
81 | // options: {
82 | // limit: 8192
83 | // }
84 | // }
85 | // ]
86 | // }
87 | // ]
88 | // },
89 | // // plugins: [
90 | // // new UglifyJsPlugin({
91 | // // uglifyOptions: {
92 | // // safari10: true,
93 | // // }
94 | // // }),
95 | // // ],
96 | // devServer: {
97 | // contentBase: path.join(__dirname, "example"),
98 | // compress: true,
99 | // port: 9000
100 | // }
101 | // };
--------------------------------------------------------------------------------