├── .watchmanconfig
├── .gitignore
├── app.json
├── demo.gif
├── demo.png
├── .babelrc
├── App.test.js
├── README.md
├── package.json
├── src
├── types.js
├── components
│ ├── NewTagModal.js
│ ├── TagsArea.js
│ ├── Tag.js
│ ├── InputField.js
│ └── Tags.js
└── helpers.js
├── .flowconfig
└── App.js
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules/
3 | .expo/
4 | npm-debug.*
5 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "sdkVersion": "21.0.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rationalappdev/react-native-drag-and-drop-tags-tutorial/HEAD/demo.gif
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rationalappdev/react-native-drag-and-drop-tags-tutorial/HEAD/demo.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-expo"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from './App';
3 |
4 | import renderer from 'react-test-renderer';
5 |
6 | it('renders without crashing', () => {
7 | const rendered = renderer.create().toJSON();
8 | expect(rendered).toBeTruthy();
9 | });
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Drag and Drop Tags in React Native
2 |
3 | Learn how to reorder tags, using animated drag and drop, remove them and add new tags using a modal with a text input.
4 |
5 | ## Blog Tutorial Series
6 |
7 | - [Part 1 of 2: The Basics and Removing the Tags](http://rationalappdev.com/drag-and-drop-tags-in-react-native-part-1-of-2)
8 | - [Part 2 of 2: Reordering and Adding New Tags](http://rationalappdev.com/drag-and-drop-tags-in-react-native-part-2-of-2)
9 |
10 | ## Demo
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-drag-and-drop-tags-tutorial",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "flow-bin": "^0.49.1",
7 | "jest-expo": "^21.0.2",
8 | "react-native-scripts": "1.5.0",
9 | "react-test-renderer": "16.0.0-alpha.12"
10 | },
11 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
12 | "scripts": {
13 | "start": "react-native-scripts start",
14 | "eject": "react-native-scripts eject",
15 | "android": "react-native-scripts android",
16 | "ios": "react-native-scripts ios",
17 | "test": "node node_modules/jest/bin/jest.js --watch",
18 | "flow": "flow"
19 | },
20 | "jest": {
21 | "preset": "jest-expo"
22 | },
23 | "dependencies": {
24 | "expo": "^21.0.0",
25 | "react": "16.0.0-alpha.12",
26 | "react-native": "^0.48.4",
27 | "react-native-vector-icons": "^4.4.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | // Tag object type
2 | export type TagObject = {
3 | title: string, // tag title
4 | tlX?: number, // top left x coordinate
5 | tlY?: number, // top left y coordinate
6 | brX?: number, // bottom right x coordinate
7 | brY?: number, // bottom right y coordinate
8 | isBeingDragged?: boolean, // whether the tag is currently being dragged or not
9 | };
10 |
11 | // PanResponder's gesture state type
12 | export type GestureState = {
13 | dx: number, // accumulated distance of the gesture since the touch started
14 | dy: number, // accumulated distance of the gesture since the touch started
15 | moveX: number, // the latest screen coordinates of the recently-moved touch
16 | moveY: number, // the latest screen coordinates of the recently-moved touch
17 | numberActiveTouches: number, // Number of touches currently on screen
18 | stateID: number, // ID of the gestureState- persisted as long as there at least one touch on screen
19 | vx: number, // current velocity of the gesture
20 | vy: number, // current velocity of the gesture
21 | x0: number, // the screen coordinates of the responder grant
22 | y0: number, // the screen coordinates of the responder grant
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/NewTagModal.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | Modal,
6 | StyleSheet,
7 | TouchableWithoutFeedback,
8 | View
9 | } from 'react-native';
10 | import InputField from './InputField';
11 |
12 | type Props = {
13 | visible: boolean,
14 | onClose: () => void,
15 | onSubmit: (tag: string) => void,
16 | };
17 |
18 | export default class NewTagModal extends PureComponent {
19 |
20 | props: Props;
21 |
22 | onSubmit = (tag: string): void => {
23 | this.props.onClose();
24 | this.props.onSubmit(tag);
25 | };
26 |
27 | render() {
28 | const { visible, onClose } = this.props;
29 | return (
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | container: {
49 | flex: 1, // take up the whole screen
50 | justifyContent: 'flex-end', // position input at the bottom
51 | backgroundColor: 'rgba(0,0,0,0.33)', // semi transparent background
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | // Calculates whether a given point is within a given area
4 | export const isPointWithinArea = (pointX: number, // x coordinate
5 | pointY: number, // y coordinate
6 | areaTlX: number, // top left x coordinate
7 | areaTlY: number, // top left y coordinate
8 | areaBrX: number, // bottom right x coordinate
9 | areaBrY: number // bottom right y coordinate
10 | ): boolean => {
11 | return areaTlX <= pointX && pointX <= areaBrX // is within horizontal axis
12 | && areaTlY <= pointY && pointY <= areaBrY; // is within vertical axis
13 | };
14 |
15 | // Moves an object within a given array from one position to another
16 | export const moveArrayElement = (array: {}[], // array of objects
17 | from: number, // element to move index
18 | to: number, // index where to move
19 | mergeProps?: {} = {} // merge additional props into the object
20 | ): {}[] => {
21 |
22 | if (to > array.length)
23 | return array;
24 |
25 | // Remove the element we need to move
26 | const arr = [
27 | ...array.slice(0, from),
28 | ...array.slice(from + 1),
29 | ];
30 |
31 | // And add it back at a new position
32 | return [
33 | ...arr.slice(0, to),
34 | {
35 | ...array[from],
36 | ...mergeProps, // merge passed props if any or nothing (empty object) by default
37 | },
38 | ...arr.slice(to),
39 | ];
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/TagsArea.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | Text,
6 | View
7 | } from 'react-native';
8 | import Tag from './Tag';
9 | import type { TagObject } from '../types';
10 |
11 | type Props = {
12 | tags: TagObject[],
13 | // Called when user taps 'Add new' button
14 | onPressAddNew: () => void,
15 | // Passes these two callbacks down to Tag component
16 | onPress: (tag: TagObject) => void,
17 | onRenderTag: (tag: TagObject, screenX: number, screenY: number, width: number, height: number) => void,
18 | };
19 |
20 | export default class TagsArea extends PureComponent {
21 |
22 | props: Props;
23 |
24 | render() {
25 | const {
26 | tags,
27 | onPress,
28 | onPressAddNew,
29 | onRenderTag,
30 | } = this.props;
31 |
32 | return (
33 |
34 |
35 | {tags.map(tag =>
36 |
42 | )}
43 |
44 |
48 | Add new
49 |
50 |
51 |
53 | );
54 | }
55 |
56 | }
57 |
58 | const styles = {
59 | container: {
60 | flexDirection: 'row',
61 | flexWrap: 'wrap',
62 | borderColor: 'rgba(255,255,255,0.5)',
63 | borderRadius: 5,
64 | borderWidth: 2,
65 | paddingBottom: 10,
66 | paddingHorizontal: 15,
67 | paddingTop: 15,
68 | },
69 | add: {
70 | backgroundColor: 'transparent',
71 | color: '#FFFFFF',
72 | paddingHorizontal: 5,
73 | paddingVertical: 5,
74 | textDecorationLine: 'underline',
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore "BUCK" generated dirs
6 | /\.buckd/
7 |
8 | ; Ignore unexpected extra "@providesModule"
9 | .*/node_modules/.*/node_modules/fbjs/.*
10 |
11 | ; Ignore duplicate module providers
12 | ; For RN Apps installed via npm, "Libraries" folder is inside
13 | ; "node_modules/react-native" but in the source repo it is in the root
14 | .*/Libraries/react-native/React.js
15 | .*/Libraries/react-native/ReactNative.js
16 |
17 | ; Additional create-react-native-app ignores
18 |
19 | ; Ignore duplicate module providers
20 | .*/node_modules/fbemitter/lib/*
21 |
22 | ; Ignore misbehaving dev-dependencies
23 | .*/node_modules/xdl/build/*
24 | .*/node_modules/reqwest/tests/*
25 |
26 | ; Ignore missing expo-sdk dependencies (temporarily)
27 | ; https://github.com/expo/expo/issues/162
28 | .*/node_modules/expo/src/*
29 |
30 | ; Ignore react-native-fbads dependency of the expo sdk
31 | .*/node_modules/react-native-fbads/*
32 |
33 | [include]
34 |
35 | [libs]
36 | node_modules/react-native/Libraries/react-native/react-native-interface.js
37 | node_modules/react-native/flow
38 | flow/
39 |
40 | [options]
41 | module.system=haste
42 |
43 | emoji=true
44 |
45 | experimental.strict_type_args=true
46 |
47 | munge_underscores=true
48 |
49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
50 |
51 | suppress_type=$FlowIssue
52 | suppress_type=$FlowFixMe
53 | suppress_type=$FixMe
54 |
55 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
56 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
57 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
58 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
59 |
60 | unsafe.enable_getters_and_setters=true
61 |
62 | [version]
63 | ^0.49.1
64 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | StatusBar,
6 | StyleSheet,
7 | Text,
8 | View,
9 | } from 'react-native';
10 | import Tags from './src/components/Tags';
11 | import NewTagModal from './src/components/NewTagModal';
12 |
13 | const TAGS = [
14 | '#love',
15 | '#instagood',
16 | '#photooftheday',
17 | '#beautiful',
18 | '#fashion',
19 | '#happy',
20 | '#tbt',
21 | '#cute',
22 | '#followme',
23 | '#like4like',
24 | '#follow',
25 | '#followme',
26 | '#picoftheday',
27 | '#me',
28 | '#selfie',
29 | '#summer',
30 | '#instadaily',
31 | '#photooftheday',
32 | '#friends',
33 | '#girl',
34 | '#fun',
35 | '#style',
36 | '#instalike',
37 | '#food',
38 | '#family',
39 | '#tagsforlikes',
40 | '#igers',
41 | ];
42 |
43 | type State = {
44 | modalVisible: boolean,
45 | };
46 |
47 | export default class Main extends PureComponent {
48 |
49 | state: State = {
50 | modalVisible: false,
51 | };
52 |
53 | // Reference Tags component
54 | _tagsComponent: ?Tags;
55 |
56 | openModal = (): void => {
57 | this.setState({ modalVisible: true });
58 | };
59 |
60 | closeModal = (): void => {
61 | this.setState({ modalVisible: false });
62 | };
63 |
64 | onSubmitNewTag = (tag: string) => {
65 | this._tagsComponent && this._tagsComponent.onSubmitNewTag(tag);
66 | };
67 |
68 | render() {
69 | const { modalVisible } = this.state;
70 | return (
71 |
72 |
73 |
74 |
79 |
80 |
81 |
82 | Let's drag and drop some tags!
83 |
84 |
85 | Drag and drop tags to reorder, tap to remove or press Add New to add new tags.
86 |
87 |
88 |
89 | this._tagsComponent = component }
91 | tags={TAGS}
92 | onPressAddNewTag={this.openModal}
93 | />
94 |
95 |
96 | );
97 | }
98 |
99 | }
100 |
101 | const styles = StyleSheet.create({
102 | container: {
103 | flex: 1,
104 | backgroundColor: '#2196F3',
105 | },
106 | header: {
107 | marginHorizontal: 20,
108 | marginVertical: 50,
109 | },
110 | title: {
111 | fontSize: 22,
112 | fontWeight: 'bold',
113 | marginBottom: 10,
114 | },
115 | text: {
116 | color: '#FFFFFF',
117 | fontSize: 16,
118 | textAlign: 'center',
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/src/components/Tag.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | Text,
6 | TouchableOpacity,
7 | View,
8 | } from 'react-native';
9 | import Icon from 'react-native-vector-icons/Ionicons';
10 | import type { NativeMethodsMixinType } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes';
11 | import type { TagObject } from '../types';
12 |
13 | type Props = {
14 | tag: TagObject,
15 | // Called when user taps on a tag
16 | onPress: (tag: TagObject) => void,
17 | // Called after a tag is rendered
18 | onRender: (tag: TagObject, screenX: number, screenY: number, width: number, height: number) => void,
19 | };
20 |
21 | export default class Tag extends PureComponent {
22 |
23 | props: Props;
24 |
25 | container: ?NativeMethodsMixinType;
26 |
27 | // Append styles.tagBeingDragged style if tag is being dragged
28 | getTagStyle = (): {} => ({
29 | ...styles.tag,
30 | ...(this.props.tag.isBeingDragged ? styles.tagBeingDragged : {}),
31 | });
32 |
33 | // Call view container's measure function to measure tag position on the screen
34 | onLayout = (): void => {
35 | this.container && this.container.measure(this.onMeasure);
36 | };
37 |
38 | // Pass tag coordinates up to the parent component
39 | onMeasure = (x: number,
40 | y: number,
41 | width: number,
42 | height: number,
43 | screenX: number,
44 | screenY: number): void => {
45 | this.props.onRender(this.props.tag, screenX, screenY, width, height);
46 | };
47 |
48 | // Handle tag taps
49 | onPress = (): void => {
50 | this.props.onPress(this.props.tag);
51 | };
52 |
53 | render() {
54 | const { tag: { title } } = this.props;
55 | return (
56 | this.container = el}
58 | style={styles.container}
59 | onLayout={this.onLayout}
60 | >
61 |
65 |
66 | {' '}
67 | {title}
68 |
69 |
70 | );
71 | }
72 |
73 | }
74 |
75 | const styles = {
76 | container: {
77 | marginBottom: 8,
78 | marginRight: 6,
79 | },
80 | tag: {
81 | flexDirection: 'row',
82 | alignItems: 'center',
83 | backgroundColor: 'rgba(255, 255, 255, .33)',
84 | borderColor: 'rgba(255, 255, 255, .25)',
85 | borderRadius: 20,
86 | borderWidth: 1,
87 | paddingHorizontal: 10,
88 | paddingVertical: 3,
89 | },
90 | tagBeingDragged: {
91 | backgroundColor: 'rgba(255, 255, 255, .01)',
92 | borderStyle: 'dashed',
93 | },
94 | title: {
95 | color: '#FFFFFF',
96 | fontSize: 15,
97 | fontWeight: 'normal',
98 | },
99 | };
100 |
--------------------------------------------------------------------------------
/src/components/InputField.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | KeyboardAvoidingView,
6 | TextInput,
7 | Text,
8 | TouchableOpacity,
9 | View,
10 | } from 'react-native';
11 |
12 | type Props = {
13 | onSubmit: (text: string) => void,
14 | };
15 |
16 | type State = {
17 | text: ?string,
18 | };
19 |
20 | export default class InputField extends PureComponent {
21 |
22 | props: Props;
23 |
24 | state: State = {
25 | text: undefined, // user's input
26 | };
27 |
28 | getButtonTextStyles = (): {} => ({
29 | ...styles.text,
30 | ...(!this.state.text ? styles.inactive : {}),
31 | });
32 |
33 | // Call this.props.onSubmit handler and pass the input
34 | submit = (): void => {
35 | const { text } = this.state;
36 | if (text) {
37 | this.setState({ text: undefined }, () => this.props.onSubmit(text));
38 | } else {
39 | alert('Please enter new tag first');
40 | }
41 | };
42 |
43 | // Update state when input changes
44 | onChangeText = (text: string): void => this.setState({ text });
45 |
46 | // Handle return press on the keyboard
47 | onSubmitEditing = (event: { nativeEvent: { text: ?string } }): void => {
48 | const { nativeEvent: { text } } = event;
49 | this.setState({ text }, this.submit);
50 | };
51 |
52 | render() {
53 | return (
54 | // This component moves children view with the text input above the keyboard
55 | // when the text input gets the focus and the keyboard appears
56 |
57 |
58 |
59 |
67 |
68 |
72 | Add
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | }
81 |
82 | const styles = {
83 | container: {
84 | flexDirection: 'row',
85 | alignItems: 'center',
86 | backgroundColor: '#FFF',
87 | borderColor: '#EEE',
88 | borderTopWidth: 1,
89 | paddingLeft: 15,
90 | },
91 | input: {
92 | flex: 1,
93 | fontSize: 15,
94 | height: 40,
95 | },
96 | button: {
97 | alignItems: 'center',
98 | justifyContent: 'center',
99 | height: 40,
100 | paddingHorizontal: 20,
101 | },
102 | inactive: {
103 | color: '#CCC',
104 | },
105 | text: {
106 | color: '#3F51B5',
107 | fontSize: 15,
108 | fontWeight: 'bold',
109 | textAlign: 'center',
110 | },
111 | };
112 |
--------------------------------------------------------------------------------
/src/components/Tags.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PureComponent } from 'react';
4 | import {
5 | LayoutAnimation,
6 | PanResponder,
7 | StyleSheet,
8 | View
9 | } from 'react-native';
10 | import { isPointWithinArea, moveArrayElement } from '../helpers';
11 | import TagsArea from './TagsArea';
12 | import type { TagObject, GestureState } from '../types';
13 |
14 | type Props = {
15 | // Array of tag titles
16 | tags: string[],
17 | // Tag swapping animation duration in ms
18 | animationDuration: number,
19 | // Passes onPressAddNewTag callback down to TagsArea component
20 | onPressAddNewTag: () => void,
21 | };
22 |
23 | type State = {
24 | tags: TagObject[],
25 | // Used to temporarily disable tag swapping while moving tag to the new position
26 | // to avoid unwanted tag swaps while the animation is happening
27 | dndEnabled: boolean,
28 | };
29 |
30 | export default class Tags extends PureComponent {
31 |
32 | props: Props;
33 |
34 | static defaultProps = {
35 | animationDuration: 250
36 | };
37 |
38 | state: State = {
39 | // Convert passed array of tag titles to array of objects of TagObject type,
40 | // so ['tag', 'another'] becomes [{ title: 'tag' }, { title: 'another' }]
41 | tags: [...new Set(this.props.tags)] // remove duplicates
42 | .map((title: string) => ({ title })), // convert to objects
43 | dndEnabled: true, // drag and drop enabled
44 | };
45 |
46 | // PanResponder to handle drag and drop gesture
47 | panResponder: PanResponder;
48 |
49 | // Tag that is currently being dragged
50 | tagBeingDragged: ?TagObject;
51 |
52 | // Initialize PanResponder
53 | componentWillMount() {
54 | this.panResponder = this.createPanResponder();
55 | }
56 |
57 | // Animate layout changes when dragging or removing a tag
58 | componentWillUpdate() {
59 | LayoutAnimation.configureNext({
60 | ...LayoutAnimation.Presets.easeInEaseOut,
61 | duration: this.props.animationDuration
62 | });
63 | }
64 |
65 | // Create PanResponder
66 | createPanResponder = (): PanResponder => PanResponder.create({
67 | // Handle drag gesture
68 | onMoveShouldSetPanResponder: (_, gestureState: GestureState) => this.onMoveShouldSetPanResponder(gestureState),
69 | onPanResponderGrant: (_, gestureState: GestureState) => this.onPanResponderGrant(),
70 | onPanResponderMove: (_, gestureState: GestureState) => this.onPanResponderMove(gestureState),
71 | // Handle drop gesture
72 | onPanResponderRelease: (_, gestureState: GestureState) => this.onPanResponderEnd(),
73 | onPanResponderTerminate: (_, gestureState: GestureState) => this.onPanResponderEnd(),
74 | });
75 |
76 | // Find out if we need to start handling tag dragging gesture
77 | onMoveShouldSetPanResponder = (gestureState: GestureState): boolean => {
78 | const { dx, dy, moveX, moveY, numberActiveTouches } = gestureState;
79 |
80 | // Do not set pan responder if a multi touch gesture is occurring
81 | if (numberActiveTouches !== 1) {
82 | return false;
83 | }
84 |
85 | // or if there was no movement since the gesture started
86 | if (dx === 0 && dy === 0) {
87 | return false;
88 | }
89 |
90 | // Find the tag below user's finger at given coordinates
91 | const tag = this.findTagAtCoordinates(moveX, moveY);
92 | if (tag) {
93 | // assign it to `this.tagBeingDragged` while dragging
94 | this.tagBeingDragged = tag;
95 | // and tell PanResponder to start handling the gesture by calling `onPanResponderMove`
96 | return true;
97 | }
98 |
99 | return false;
100 | };
101 |
102 | // Called when gesture is granted
103 | onPanResponderGrant = (): void => {
104 | this.updateTagState(this.tagBeingDragged, { isBeingDragged: true });
105 | };
106 |
107 | // Handle drag gesture
108 | onPanResponderMove = (gestureState: GestureState): void => {
109 | const { moveX, moveY } = gestureState;
110 | // Do nothing if dnd is disabled
111 | if (!this.state.dndEnabled) {
112 | return;
113 | }
114 | // Find the tag we're dragging the current tag over
115 | const draggedOverTag = this.findTagAtCoordinates(moveX, moveY, this.tagBeingDragged);
116 | if (draggedOverTag) {
117 | this.swapTags(this.tagBeingDragged, draggedOverTag);
118 | }
119 | };
120 |
121 | // Called after gesture ends
122 | onPanResponderEnd = (): void => {
123 | this.updateTagState(this.tagBeingDragged, { isBeingDragged: false });
124 | this.tagBeingDragged = undefined;
125 | };
126 |
127 | // Enable dnd back after the animation is over
128 | enableDndAfterAnimating = (): void => {
129 | setTimeout(this.enableDnd, this.props.animationDuration)
130 | };
131 |
132 | enableDnd = (): void => {
133 | this.setState({ dndEnabled: true });
134 | };
135 |
136 | // Find the tag at given coordinates
137 | findTagAtCoordinates = (x: number, y: number, exceptTag?: TagObject): ?TagObject => {
138 | return this.state.tags.find((tag) =>
139 | tag.tlX && tag.tlY && tag.brX && tag.brY
140 | && isPointWithinArea(x, y, tag.tlX, tag.tlY, tag.brX, tag.brY)
141 | && (!exceptTag || exceptTag.title !== tag.title)
142 | );
143 | };
144 |
145 | // Remove tag
146 | removeTag = (tag: TagObject): void => {
147 | this.setState((state: State) => {
148 | const index = state.tags.findIndex(({ title }) => title === tag.title);
149 | return {
150 | tags: [
151 | // Remove the tag
152 | ...state.tags.slice(0, index),
153 | ...state.tags.slice(index + 1),
154 | ]
155 | }
156 | });
157 | };
158 |
159 | // Swap two tags
160 | swapTags = (draggedTag: TagObject, anotherTag: TagObject): void => {
161 | this.setState((state: State) => {
162 | const draggedTagIndex = state.tags.findIndex(({ title }) => title === draggedTag.title);
163 | const anotherTagIndex = state.tags.findIndex(({ title }) => title === anotherTag.title);
164 | return {
165 | tags: moveArrayElement(
166 | state.tags,
167 | draggedTagIndex,
168 | anotherTagIndex,
169 | ),
170 | dndEnabled: false,
171 | }
172 | }, this.enableDndAfterAnimating);
173 | };
174 |
175 | // Update the tag in the state with given props
176 | updateTagState = (tag: TagObject, props: Object): void => {
177 | this.setState((state: State) => {
178 | const index = state.tags.findIndex(({ title }) => title === tag.title);
179 | return {
180 | tags: [
181 | ...state.tags.slice(0, index),
182 | {
183 | ...state.tags[index],
184 | ...props,
185 | },
186 | ...state.tags.slice(index + 1),
187 | ],
188 | }
189 | });
190 | };
191 |
192 | // Update tag coordinates in the state
193 | onRenderTag = (tag: TagObject,
194 | screenX: number,
195 | screenY: number,
196 | width: number,
197 | height: number): void => {
198 | this.updateTagState(tag, {
199 | tlX: screenX,
200 | tlY: screenY,
201 | brX: screenX + width,
202 | brY: screenY + height,
203 | });
204 | };
205 |
206 | // Add new tag to the state
207 | onSubmitNewTag = (title: string): void => {
208 | // Remove tag if it already exists to re-add it to the bottom of the list
209 | const existingTag = this.state.tags.find((tag: TagObject) => tag.title === title);
210 | if (existingTag) {
211 | this.removeTag(existingTag);
212 | }
213 | // Add new tag to the state
214 | this.setState((state: State) => {
215 | return {
216 | tags: [
217 | ...state.tags,
218 | { title },
219 | ],
220 | }
221 | });
222 | };
223 |
224 | render() {
225 | const { tags } = this.state;
226 | return (
227 |
231 |
232 |
238 |
239 |
240 | );
241 | }
242 |
243 | }
244 |
245 | const styles = StyleSheet.create({
246 | container: {
247 | flex: 1,
248 | paddingHorizontal: 15,
249 | },
250 | });
251 |
--------------------------------------------------------------------------------