├── .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 | [![Version](https://img.shields.io/npm/v/react-draggable-tags?logo=npm&style=flat-square&color=blue)](https://www.npmjs.com/package/react-draggable-tags) 3 | [![Downloads](https://img.shields.io/npm/dm/react-draggable-tags.svg?logo=npm&style=flat-square&color=blue)](https://www.npmjs.com/package/react-draggable-tags) 4 | [![License](https://img.shields.io/github/license/YGYOOO/react-draggable-tags.svg?style=flat-square)](LICENSE) 5 | 6 | [![](https://img.shields.io/github/followers/YGYOOO.svg?label=Follow&style=social)](https://github.com/YGYOOO) 7 | [![](https://img.shields.io/badge/Follow%20@卧槽竟然是YGY的微博--brightgreen.svg?logo=Sina%20Weibo&style=social)](https://weibo.com/u/5352731024) 8 | [![](https://img.shields.io/badge/Follow%20@YGYOOO--brightgreen.svg?logo=Twitter&style=social)](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 | 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 | 66 | 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 |
57 |
58 | {wrong} 59 |
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 | 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 | 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 | 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 | 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 |

332 | Nested Tags: 333 |

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 | // }; --------------------------------------------------------------------------------