react-native-sortable-dynamic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +189 -0
- package/lib/commonjs/Config.js +101 -0
- package/lib/commonjs/Config.js.map +1 -0
- package/lib/commonjs/Item.js +188 -0
- package/lib/commonjs/Item.js.map +1 -0
- package/lib/commonjs/SortableList.js +91 -0
- package/lib/commonjs/SortableList.js.map +1 -0
- package/lib/commonjs/Tile.js +56 -0
- package/lib/commonjs/Tile.js.map +1 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/Config.js +93 -0
- package/lib/module/Config.js.map +1 -0
- package/lib/module/Item.js +185 -0
- package/lib/module/Item.js.map +1 -0
- package/lib/module/SortableList.js +86 -0
- package/lib/module/SortableList.js.map +1 -0
- package/lib/module/Tile.js +53 -0
- package/lib/module/Tile.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/package.json +175 -0
- package/src/Config.js +81 -0
- package/src/Item.js +196 -0
- package/src/SortableList.js +86 -0
- package/src/Tile.js +47 -0
- package/src/index.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-sortable-dynamic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "This package provides a highly customizable, animated, and performant sortable grid list component for React Native. It allows users to easily reorder grid items through drag-and-drop gestures, with smooth animations powered by react-native-reanimated and gesture handling by react-native-gesture-handler.",
|
|
5
|
+
"source": "./src/index.js",
|
|
6
|
+
"main": "./lib/commonjs/index.js",
|
|
7
|
+
"module": "./lib/module/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./lib/module/index.js",
|
|
11
|
+
"require": "./lib/commonjs/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"lib",
|
|
17
|
+
"android",
|
|
18
|
+
"ios",
|
|
19
|
+
"cpp",
|
|
20
|
+
"*.podspec",
|
|
21
|
+
"!ios/build",
|
|
22
|
+
"!android/build",
|
|
23
|
+
"!android/gradle",
|
|
24
|
+
"!android/gradlew",
|
|
25
|
+
"!android/gradlew.bat",
|
|
26
|
+
"!android/local.properties",
|
|
27
|
+
"!**/__tests__",
|
|
28
|
+
"!**/__fixtures__",
|
|
29
|
+
"!**/__mocks__",
|
|
30
|
+
"!**/.*"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"example": "yarn workspace react-native-sortable-dynamic-example",
|
|
34
|
+
"test": "jest",
|
|
35
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"release": "release-it"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react-native",
|
|
42
|
+
"ios",
|
|
43
|
+
"android"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/AdamLee321/react-native-sortable-dynamic.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "Adam Lee <adam@hkc.ie> (https://github.com/AdamLee321)",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/AdamLee321/react-native-sortable-dynamic/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/AdamLee321/react-native-sortable-dynamic#readme",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@commitlint/config-conventional": "^17.0.2",
|
|
60
|
+
"@evilmartians/lefthook": "^1.5.0",
|
|
61
|
+
"@react-native/eslint-config": "^0.73.1",
|
|
62
|
+
"@release-it/conventional-changelog": "^5.0.0",
|
|
63
|
+
"@types/jest": "^29.5.5",
|
|
64
|
+
"@types/react": "^18.2.44",
|
|
65
|
+
"commitlint": "^17.0.2",
|
|
66
|
+
"del-cli": "^5.1.0",
|
|
67
|
+
"eslint": "^8.51.0",
|
|
68
|
+
"eslint-config-prettier": "^9.0.0",
|
|
69
|
+
"eslint-plugin-prettier": "^5.0.1",
|
|
70
|
+
"jest": "^29.7.0",
|
|
71
|
+
"prettier": "^3.0.3",
|
|
72
|
+
"react": "18.2.0",
|
|
73
|
+
"react-native": "0.74.5",
|
|
74
|
+
"react-native-builder-bob": "^0.30.2",
|
|
75
|
+
"release-it": "^15.0.0",
|
|
76
|
+
"typescript": "^5.2.2",
|
|
77
|
+
"react-native-reanimated": "^3.0.0",
|
|
78
|
+
"react-native-gesture-handler": "^2.0.0"
|
|
79
|
+
},
|
|
80
|
+
"resolutions": {
|
|
81
|
+
"@types/react": "^18.2.44"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"react": "*",
|
|
85
|
+
"react-native": "*",
|
|
86
|
+
"react-native-reanimated": "^2.0.0",
|
|
87
|
+
"react-native-gesture-handler": "^1.10.0"
|
|
88
|
+
},
|
|
89
|
+
"workspaces": [
|
|
90
|
+
"example"
|
|
91
|
+
],
|
|
92
|
+
"packageManager": "yarn@3.6.1",
|
|
93
|
+
"jest": {
|
|
94
|
+
"preset": "react-native",
|
|
95
|
+
"modulePathIgnorePatterns": [
|
|
96
|
+
"<rootDir>/example/node_modules",
|
|
97
|
+
"<rootDir>/lib/"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"commitlint": {
|
|
101
|
+
"extends": [
|
|
102
|
+
"@commitlint/config-conventional"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"release-it": {
|
|
106
|
+
"git": {
|
|
107
|
+
"commitMessage": "chore: release ${version}",
|
|
108
|
+
"tagName": "v${version}"
|
|
109
|
+
},
|
|
110
|
+
"npm": {
|
|
111
|
+
"publish": true
|
|
112
|
+
},
|
|
113
|
+
"github": {
|
|
114
|
+
"release": true
|
|
115
|
+
},
|
|
116
|
+
"plugins": {
|
|
117
|
+
"@release-it/conventional-changelog": {
|
|
118
|
+
"preset": "angular"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"eslintConfig": {
|
|
123
|
+
"root": true,
|
|
124
|
+
"extends": [
|
|
125
|
+
"@react-native",
|
|
126
|
+
"prettier"
|
|
127
|
+
],
|
|
128
|
+
"rules": {
|
|
129
|
+
"react/react-in-jsx-scope": "off",
|
|
130
|
+
"prettier/prettier": [
|
|
131
|
+
"error",
|
|
132
|
+
{
|
|
133
|
+
"quoteProps": "consistent",
|
|
134
|
+
"singleQuote": true,
|
|
135
|
+
"tabWidth": 2,
|
|
136
|
+
"trailingComma": "es5",
|
|
137
|
+
"useTabs": false
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"eslintIgnore": [
|
|
143
|
+
"node_modules/",
|
|
144
|
+
"lib/"
|
|
145
|
+
],
|
|
146
|
+
"prettier": {
|
|
147
|
+
"quoteProps": "consistent",
|
|
148
|
+
"singleQuote": true,
|
|
149
|
+
"tabWidth": 2,
|
|
150
|
+
"trailingComma": "es5",
|
|
151
|
+
"useTabs": false
|
|
152
|
+
},
|
|
153
|
+
"react-native-builder-bob": {
|
|
154
|
+
"source": "src",
|
|
155
|
+
"output": "lib",
|
|
156
|
+
"targets": [
|
|
157
|
+
[
|
|
158
|
+
"commonjs",
|
|
159
|
+
{
|
|
160
|
+
"esm": true
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
"module",
|
|
165
|
+
{
|
|
166
|
+
"esm": true
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
"create-react-native-library": {
|
|
172
|
+
"type": "library",
|
|
173
|
+
"version": "0.41.2"
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/Config.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Dimensions } from 'react-native';
|
|
2
|
+
import { Easing } from 'react-native-reanimated';
|
|
3
|
+
import React, { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
// Get screen width to calculate dynamic sizes
|
|
6
|
+
const { width } = Dimensions.get('window');
|
|
7
|
+
|
|
8
|
+
// Default configuration for the sortable list
|
|
9
|
+
const defaultConfig = {
|
|
10
|
+
MARGIN: 10, // Default margin between items
|
|
11
|
+
COL: 2, // Default number of columns
|
|
12
|
+
// Default size for each item, calculated based on the number of columns and margin
|
|
13
|
+
SIZE: width / 2 - 10, // (width / COL - MARGIN)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Create a Context for the sortable list configuration
|
|
17
|
+
const ConfigContext = createContext(defaultConfig);
|
|
18
|
+
|
|
19
|
+
// Custom hook to use the sortable configuration context
|
|
20
|
+
export const useSortableConfig = () => useContext(ConfigContext);
|
|
21
|
+
|
|
22
|
+
// Configuration for animation settings
|
|
23
|
+
export const animationConfig = {
|
|
24
|
+
easing: Easing.inOut(Easing.ease),
|
|
25
|
+
duration: 350,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Helper function to calculate the item's position based on its index
|
|
29
|
+
// This is used to position items in a grid layout
|
|
30
|
+
export const getPosition = (position, COL, SIZE) => {
|
|
31
|
+
'worklet'; // Necessary for Reanimated 2 to run this function on the UI thread
|
|
32
|
+
return {
|
|
33
|
+
x: position % COL === 0 ? 0 : SIZE * (position % COL),
|
|
34
|
+
y: Math.floor(position / COL) * SIZE,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Helper function to determine the new order of items during drag-and-drop
|
|
39
|
+
export const getOrder = (tx, ty, max, COL, SIZE) => {
|
|
40
|
+
'worklet'; // Necessary for Reanimated 2 to run this function on the UI thread
|
|
41
|
+
const x = Math.round(tx / SIZE) * SIZE;
|
|
42
|
+
const y = Math.round(ty / SIZE) * SIZE;
|
|
43
|
+
const row = Math.max(y, 0) / SIZE;
|
|
44
|
+
const col = Math.max(x, 0) / SIZE;
|
|
45
|
+
return Math.min(row * COL + col, max);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SortableListProvider component
|
|
50
|
+
*
|
|
51
|
+
* Wrap your sortable list components with this provider to set custom configuration.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} config - Custom configuration to override the default settings.
|
|
54
|
+
* @param {number} config.MARGIN - Margin between items.
|
|
55
|
+
* @param {number} config.COL - Number of columns in the grid.
|
|
56
|
+
* @param {React.ReactNode} children - Child components that will use this configuration.
|
|
57
|
+
*
|
|
58
|
+
* Usage:
|
|
59
|
+
*
|
|
60
|
+
* <SortableListProvider config={{ MARGIN: 15, COL: 3 }}>
|
|
61
|
+
* <YourSortableList />
|
|
62
|
+
* </SortableListProvider>
|
|
63
|
+
*/
|
|
64
|
+
const SortableListProvider = ({ children, config }) => {
|
|
65
|
+
// Merge custom config with the default configuration
|
|
66
|
+
const mergedConfig = {
|
|
67
|
+
...defaultConfig,
|
|
68
|
+
...config,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Recalculate SIZE based on COL and MARGIN
|
|
72
|
+
mergedConfig.SIZE = width / mergedConfig.COL - mergedConfig.MARGIN;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ConfigContext.Provider value={mergedConfig}>
|
|
76
|
+
{children}
|
|
77
|
+
</ConfigContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default SortableListProvider;
|
package/src/Item.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Dimensions, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedGestureHandler,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useAnimatedReaction,
|
|
7
|
+
withSpring,
|
|
8
|
+
scrollTo,
|
|
9
|
+
withTiming,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
runOnJS,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import { PanGestureHandler } from 'react-native-gesture-handler';
|
|
14
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
15
|
+
import { animationConfig, getOrder, getPosition } from './Config';
|
|
16
|
+
import { useSortableConfig } from './Config';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Item Component
|
|
20
|
+
*
|
|
21
|
+
* This component represents a draggable item in the sortable grid. It manages the gesture handling
|
|
22
|
+
* and animations needed to drag and reorder the item within the grid.
|
|
23
|
+
*
|
|
24
|
+
* Props:
|
|
25
|
+
* @param {React.ReactNode} children - The content to render inside the item.
|
|
26
|
+
* @param {object} positions - Shared value that contains the current positions of all items.
|
|
27
|
+
* @param {number} id - Unique identifier for the item.
|
|
28
|
+
* @param {function} onDragEnd - Callback function triggered when dragging ends.
|
|
29
|
+
* @param {object} scrollView - Reference to the scroll view for scrolling during drag.
|
|
30
|
+
* @param {object} scrollY - Shared value representing the current scroll position.
|
|
31
|
+
* @param {boolean} editing - Whether the list is in editing mode.
|
|
32
|
+
* @param {boolean} draggable - Whether the item is draggable (default: true).
|
|
33
|
+
* @param {Array} tiles - Array of tile items used to check reorderable state.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const Item = ({
|
|
37
|
+
children,
|
|
38
|
+
positions,
|
|
39
|
+
id,
|
|
40
|
+
onDragEnd,
|
|
41
|
+
scrollView,
|
|
42
|
+
scrollY,
|
|
43
|
+
editing,
|
|
44
|
+
draggable = true,
|
|
45
|
+
tiles,
|
|
46
|
+
}) => {
|
|
47
|
+
// Get safe area insets for accurate height calculation
|
|
48
|
+
const inset = useSafeAreaInsets();
|
|
49
|
+
const containerHeight =
|
|
50
|
+
Dimensions.get('window').height - inset.top - inset.bottom;
|
|
51
|
+
|
|
52
|
+
// Get the configuration for columns and size
|
|
53
|
+
const { COL, SIZE } = useSortableConfig();
|
|
54
|
+
|
|
55
|
+
// Calculate content height based on the number of items
|
|
56
|
+
const contentHeight = (Object.keys(positions.value).length / COL) * SIZE;
|
|
57
|
+
const isGestureActive = useSharedValue(false); // Whether the item is being actively dragged
|
|
58
|
+
|
|
59
|
+
// Calculate initial position of the item
|
|
60
|
+
const position = getPosition(positions.value[id], COL, SIZE);
|
|
61
|
+
const translateX = useSharedValue(position.x);
|
|
62
|
+
const translateY = useSharedValue(position.y);
|
|
63
|
+
|
|
64
|
+
// Effect to reset isGestureActive when not in editing mode
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!editing) {
|
|
67
|
+
isGestureActive.value = false;
|
|
68
|
+
}
|
|
69
|
+
}, [editing, isGestureActive]);
|
|
70
|
+
|
|
71
|
+
// React to changes in the positions object
|
|
72
|
+
useAnimatedReaction(
|
|
73
|
+
() => positions.value[id], // Track changes to this item's position
|
|
74
|
+
(newOrder) => {
|
|
75
|
+
if (!isGestureActive.value) {
|
|
76
|
+
const pos = getPosition(newOrder, COL, SIZE);
|
|
77
|
+
translateX.value = withTiming(pos.x, animationConfig);
|
|
78
|
+
translateY.value = withTiming(pos.y, animationConfig);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Gesture handler for dragging
|
|
84
|
+
const onGestureEvent = useAnimatedGestureHandler({
|
|
85
|
+
onStart: (_, ctx) => {
|
|
86
|
+
if (editing && draggable) {
|
|
87
|
+
// Store the starting position
|
|
88
|
+
ctx.x = translateX.value;
|
|
89
|
+
ctx.y = translateY.value;
|
|
90
|
+
isGestureActive.value = false; // TODO: Set to false when grouping is implemented
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
onActive: ({ translationX, translationY }, ctx) => {
|
|
94
|
+
if (editing && draggable) {
|
|
95
|
+
// Calculate new position
|
|
96
|
+
translateX.value = ctx.x + translationX;
|
|
97
|
+
translateY.value = ctx.y + translationY;
|
|
98
|
+
|
|
99
|
+
// Calculate new order based on position
|
|
100
|
+
const newOrder = getOrder(
|
|
101
|
+
translateX.value,
|
|
102
|
+
translateY.value,
|
|
103
|
+
Object.keys(positions.value).length - 1,
|
|
104
|
+
COL,
|
|
105
|
+
SIZE
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const oldOrder = positions.value[id];
|
|
109
|
+
if (newOrder !== oldOrder) {
|
|
110
|
+
// Find the item to swap positions with
|
|
111
|
+
const idToSwap = Object.keys(positions.value).find(
|
|
112
|
+
(key) => positions.value[key] === newOrder
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Only swap if the target item is reorderable
|
|
116
|
+
const targetItem = tiles.find((tile) => tile.id === Number(idToSwap));
|
|
117
|
+
if (idToSwap && targetItem?.reorderable !== false) {
|
|
118
|
+
const newPositions = { ...positions.value };
|
|
119
|
+
newPositions[id] = newOrder;
|
|
120
|
+
newPositions[idToSwap] = oldOrder;
|
|
121
|
+
positions.value = newPositions;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle scrolling during drag
|
|
126
|
+
const lowerBound = scrollY.value;
|
|
127
|
+
const upperBound = lowerBound + containerHeight - SIZE;
|
|
128
|
+
const maxScroll = contentHeight - containerHeight;
|
|
129
|
+
const leftToScrollDown = maxScroll - scrollY.value;
|
|
130
|
+
|
|
131
|
+
// Scroll up
|
|
132
|
+
if (translateY.value < lowerBound) {
|
|
133
|
+
const diff = Math.min(lowerBound - translateY.value, lowerBound);
|
|
134
|
+
scrollY.value -= diff;
|
|
135
|
+
scrollTo(scrollView, 0, scrollY.value, false);
|
|
136
|
+
ctx.y -= diff;
|
|
137
|
+
translateY.value = ctx.y + translationY;
|
|
138
|
+
}
|
|
139
|
+
// Scroll down
|
|
140
|
+
if (translateY.value > upperBound) {
|
|
141
|
+
const diff = Math.min(
|
|
142
|
+
translateY.value - upperBound,
|
|
143
|
+
leftToScrollDown
|
|
144
|
+
);
|
|
145
|
+
scrollY.value += diff;
|
|
146
|
+
scrollTo(scrollView, 0, scrollY.value, false);
|
|
147
|
+
ctx.y += diff;
|
|
148
|
+
translateY.value = ctx.y + translationY;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
onEnd: () => {
|
|
153
|
+
if (draggable) {
|
|
154
|
+
// Snap the item back into its place when the drag ends
|
|
155
|
+
const newPosition = getPosition(positions.value[id], COL, SIZE);
|
|
156
|
+
translateX.value = withTiming(newPosition.x, animationConfig, () => {
|
|
157
|
+
isGestureActive.value = false; // Set gesture to inactive
|
|
158
|
+
runOnJS(onDragEnd)(positions.value); // Call onDragEnd on the JS thread
|
|
159
|
+
});
|
|
160
|
+
translateY.value = withTiming(newPosition.y, animationConfig);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Animated style for the item
|
|
166
|
+
const style = useAnimatedStyle(() => {
|
|
167
|
+
const zIndex = isGestureActive.value ? 100 : 0; // Bring the item to front when active
|
|
168
|
+
const scale =
|
|
169
|
+
editing && isGestureActive.value ? withSpring(1.05) : withSpring(1); // Slightly enlarge the item when dragging
|
|
170
|
+
return {
|
|
171
|
+
position: 'absolute',
|
|
172
|
+
top: 0,
|
|
173
|
+
left: 0,
|
|
174
|
+
width: SIZE,
|
|
175
|
+
height: SIZE,
|
|
176
|
+
zIndex,
|
|
177
|
+
transform: [
|
|
178
|
+
{ translateX: translateX.value },
|
|
179
|
+
{ translateY: translateY.value },
|
|
180
|
+
{ scale },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Animated.View style={style}>
|
|
187
|
+
<PanGestureHandler enabled={editing} onGestureEvent={onGestureEvent}>
|
|
188
|
+
<Animated.View style={StyleSheet.absoluteFill}>
|
|
189
|
+
{children}
|
|
190
|
+
</Animated.View>
|
|
191
|
+
</PanGestureHandler>
|
|
192
|
+
</Animated.View>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default Item;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
useAnimatedRef,
|
|
4
|
+
useAnimatedScrollHandler,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
import Item from './Item';
|
|
8
|
+
import { useSortableConfig } from './Config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SortableList Component
|
|
12
|
+
*
|
|
13
|
+
* This component renders a scrollable list of items that can be reordered using drag-and-drop gestures.
|
|
14
|
+
* It manages the overall layout of the items and provides the necessary props to each child `Item` component
|
|
15
|
+
* to enable dragging, scrolling, and reordering functionality.
|
|
16
|
+
*
|
|
17
|
+
* Props:
|
|
18
|
+
* @param {React.ReactNode[]} children - The list of items to render inside the sortable list.
|
|
19
|
+
* @param {boolean} editing - Whether the list is in editing mode, enabling drag-and-drop.
|
|
20
|
+
* @param {Array} tiles - Array of tile data to manage the reordering state.
|
|
21
|
+
* @param {function} onDragEnd - Callback function called with the updated positions when the drag ends.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
*
|
|
25
|
+
* <SortableList editing={isEditing} tiles={tiles} onDragEnd={handleDragEnd}>
|
|
26
|
+
* {tiles.map((tile) => (
|
|
27
|
+
* <YourTileComponent key={tile.id} id={tile.id} />
|
|
28
|
+
* ))}
|
|
29
|
+
* </SortableList>
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const SortableList = ({ children, editing, tiles, onDragEnd }) => {
|
|
33
|
+
// Get the configuration for columns and size from context
|
|
34
|
+
const { COL, SIZE } = useSortableConfig();
|
|
35
|
+
|
|
36
|
+
// Shared values to track scrolling and item positions
|
|
37
|
+
const scrollY = useSharedValue(0); // Current scroll position
|
|
38
|
+
const scrollView = useAnimatedRef(); // Reference to the scroll view
|
|
39
|
+
const positions = useSharedValue(
|
|
40
|
+
children.reduce(
|
|
41
|
+
(acc, child, index) => ({ ...acc, [child.props.id]: index }),
|
|
42
|
+
{}
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Scroll event handler to update scrollY shared value
|
|
47
|
+
const onScroll = useAnimatedScrollHandler({
|
|
48
|
+
onScroll: ({ contentOffset: { y } }) => {
|
|
49
|
+
scrollY.value = y;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Animated.ScrollView
|
|
55
|
+
onScroll={onScroll}
|
|
56
|
+
ref={scrollView}
|
|
57
|
+
contentContainerStyle={{
|
|
58
|
+
// Calculate the total height needed for the scroll view content
|
|
59
|
+
height: Math.ceil(children.length / COL) * SIZE,
|
|
60
|
+
}}
|
|
61
|
+
showsVerticalScrollIndicator={false}
|
|
62
|
+
bounces={false}
|
|
63
|
+
scrollEventThrottle={16} // Control scroll event frequency
|
|
64
|
+
>
|
|
65
|
+
{/* Render each child wrapped in an Item component */}
|
|
66
|
+
{children.map((child) => (
|
|
67
|
+
<Item
|
|
68
|
+
key={child.props.id}
|
|
69
|
+
id={child.props.id}
|
|
70
|
+
positions={positions}
|
|
71
|
+
editing={editing}
|
|
72
|
+
draggable={child.props.draggable}
|
|
73
|
+
reorderable={child.props.reorderable}
|
|
74
|
+
tiles={tiles}
|
|
75
|
+
onDragEnd={onDragEnd}
|
|
76
|
+
scrollView={scrollView}
|
|
77
|
+
scrollY={scrollY}
|
|
78
|
+
>
|
|
79
|
+
{child}
|
|
80
|
+
</Item>
|
|
81
|
+
))}
|
|
82
|
+
</Animated.ScrollView>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default memo(SortableList);
|
package/src/Tile.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TouchableOpacity } from 'react-native';
|
|
3
|
+
import { useSortableConfig } from './Config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tile Component
|
|
7
|
+
*
|
|
8
|
+
* This component represents a single tile in the sortable grid. It is designed to be flexible and
|
|
9
|
+
* allows for user interactions such as press and long press. The size of each tile is dynamically
|
|
10
|
+
* adjusted based on the configuration provided by `SortableListProvider`.
|
|
11
|
+
*
|
|
12
|
+
* Props:
|
|
13
|
+
* @param {function} onPress - Callback function called when the tile is pressed.
|
|
14
|
+
* @param {function} onLongPress - Callback function called when the tile is long-pressed.
|
|
15
|
+
* @param {number} activeOpacity - The opacity of the tile when it is pressed. Default is 0.7.
|
|
16
|
+
* @param {React.ReactNode} children - The content to render inside the tile.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
*
|
|
20
|
+
* <Tile onPress={handlePress} onLongPress={handleLongPress}>
|
|
21
|
+
* <Text>Tile Content</Text>
|
|
22
|
+
* </Tile>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const Tile = ({ onPress, onLongPress, activeOpacity = 0.7, children }) => {
|
|
26
|
+
// Get the size of the tile from the sortable config context
|
|
27
|
+
const { SIZE } = useSortableConfig();
|
|
28
|
+
|
|
29
|
+
// Define the style for the tile, ensuring that its size is consistent across the grid
|
|
30
|
+
const containerStyle = {
|
|
31
|
+
width: SIZE,
|
|
32
|
+
height: SIZE,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<TouchableOpacity
|
|
37
|
+
style={containerStyle}
|
|
38
|
+
onPress={onPress}
|
|
39
|
+
onLongPress={onLongPress}
|
|
40
|
+
activeOpacity={activeOpacity} // Control the opacity when the tile is pressed
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</TouchableOpacity>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default Tile;
|
package/src/index.js
ADDED