react-crud-mobile 1.3.369 → 1.3.373

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.
@@ -1,194 +1,255 @@
1
- import React, { useState } from 'react';
2
- import { ChildType, ComponentUtils, ScopeUtils, Utils } from 'react-crud-utils';
3
- import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4
- import UI from '../UI';
5
- import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
6
- import DraggableFlatList, {
7
- ScaleDecorator,
8
- } from 'react-native-draggable-flatlist';
9
- import { GestureHandlerRootView } from 'react-native-gesture-handler';
10
- import UIHeader from './UIHeader';
11
- import UIChildren from '../UIChildren';
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+ import { ChildType, OptionType } from 'react-crud-utils';
3
+ import {
4
+ View,
5
+ Text,
6
+ StyleSheet,
7
+ Animated,
8
+ PanResponder,
9
+ TouchableOpacity,
10
+ } from 'react-native';
11
+
12
+ const ITEM_HEIGHT = 70;
12
13
 
13
14
  export default function UIOrder(props: ChildType) {
14
15
  const scope = props.scope;
15
- const crud = scope.crud;
16
- const original = scope.original;
17
- const cols = Utils.nvl(scope.getPart('cols', undefined, 1));
18
-
19
- const getStyle = (key: string, extra?: any) => {
20
- return scope.getStyle(key, { ...extra, ...styles[key] });
21
- };
22
-
23
- const getContainerStyle = (extra?: any) => {
24
- let row = getStyle('container', {});
25
-
26
- if (cols > 1) {
27
- row = { ...row, flexDirection: 'row', flexWrap: 'wrap' };
28
- }
29
-
30
- return row;
31
- };
32
-
33
- const items = Utils.call(() => {
34
- let list = Utils.nvl(scope.getItems(), []);
35
-
36
- if (original.search && !original.list?.url) {
37
- let query = crud
38
- .get('query', '')
39
- .toLowerCase()
40
- .trim() as string;
41
-
42
- if (query.length > 1) {
43
- let filters: any[] = [];
44
- let filterBy = Utils.nvl(original.filterBy, 'label');
45
-
46
- Utils.each(list, o => {
47
- let label = o[filterBy];
16
+ const initial: OptionType[] = scope.getOptions();
17
+
18
+ const [items, setItems] = useState<OptionType[]>(initial);
19
+ const [draggingId, setDraggingId] = useState<string | null>(null);
20
+
21
+ // O tipo Animated.Value em um ambiente TypeScript puro não tem o método ._value,
22
+ // mas é a propriedade interna usada para acessar o valor diretamente.
23
+ // Usamos 'any' no acesso para suprimir o erro TS.
24
+ const positions = useRef<Record<string, Animated.Value>>(
25
+ Object.fromEntries(
26
+ initial.map((it, i) => [it.value, new Animated.Value(i * ITEM_HEIGHT)])
27
+ )
28
+ ).current;
29
+
30
+ const panRespondersRef = useRef<Record<string, any>>({});
31
+
32
+ const activeItem = useRef<OptionType | null>(null);
33
+ const activeIndex = useRef<number>(-1);
34
+ const offsetY = useRef(0);
35
+
36
+ // Armazena a lista mais recente em uma ref para uso em funções de PanResponder
37
+ const itemsRef = useRef(items);
38
+ useEffect(() => {
39
+ itemsRef.current = items;
40
+ }, [items]);
41
+
42
+ // 1. Atualiza posições apenas para itens existentes
43
+ useEffect(() => {
44
+ items.forEach((it, idx) => {
45
+ if (!positions[it.value])
46
+ positions[it.value] = new Animated.Value(idx * ITEM_HEIGHT);
47
+
48
+ Animated.spring(positions[it.value], {
49
+ toValue: idx * ITEM_HEIGHT,
50
+ useNativeDriver: false,
51
+ }).start();
52
+ });
53
+ }, [items]);
54
+
55
+ const createPanResponder = (item: OptionType) =>
56
+ PanResponder.create({
57
+ onStartShouldSetPanResponder: () => true,
58
+ onPanResponderGrant: () => {
59
+ // Use itemsRef.current para obter a lista mais recente no momento do arrasto.
60
+ const currentItems = itemsRef.current;
61
+ if (
62
+ !positions[item.value] ||
63
+ !currentItems.find(it => it.value === item.value)
64
+ )
65
+ return;
66
+
67
+ activeItem.current = item;
68
+ activeIndex.current = currentItems.findIndex(
69
+ it => it.value === item.value
70
+ );
71
+ // CORREÇÃO: Usa a propriedade interna _value para acessar o valor atual
72
+ offsetY.current = (positions[item.value] as any)._value; // 👈 CORREÇÃO APLICADA
73
+ setDraggingId(item.value);
74
+ },
75
+ onPanResponderMove: (_, gesture) => {
76
+ const currentItems = itemsRef.current;
77
+ if (
78
+ !positions[item.value] ||
79
+ !currentItems.find(it => it.value === item.value)
80
+ )
81
+ return;
82
+
83
+ const y = offsetY.current + gesture.dy;
84
+ positions[item.value].setValue(y);
85
+
86
+ const targetIndex = Math.max(
87
+ 0,
88
+ Math.min(
89
+ currentItems.length - 1,
90
+ Math.floor((y + ITEM_HEIGHT / 2) / ITEM_HEIGHT)
91
+ )
92
+ );
48
93
 
49
- if (label) {
50
- if (label.includes(query)) {
51
- filters.push(o);
52
- }
53
- }
94
+ currentItems.forEach((other, idx) => {
95
+ if (other.value === item.value || !positions[other.value]) return;
96
+ let targetY = idx * ITEM_HEIGHT;
97
+ if (idx > activeIndex.current && idx <= targetIndex)
98
+ targetY = (idx - 1) * ITEM_HEIGHT;
99
+ else if (idx < activeIndex.current && idx >= targetIndex)
100
+ targetY = (idx + 1) * ITEM_HEIGHT;
101
+
102
+ Animated.timing(positions[other.value], {
103
+ toValue: targetY,
104
+ duration: 120,
105
+ useNativeDriver: false,
106
+ }).start();
54
107
  });
108
+ },
109
+ onPanResponderRelease: () => {
110
+ const moved = activeItem.current;
111
+ const currentItems = itemsRef.current;
112
+
113
+ if (
114
+ !moved ||
115
+ !positions[moved.value] ||
116
+ !currentItems.find(it => it.value === moved.value)
117
+ )
118
+ return;
119
+
120
+ // CORREÇÃO: Usa a propriedade interna _value para obter a posição final
121
+ const finalY = (positions[moved.value] as any)._value; // 👈 CORREÇÃO APLICADA
122
+ const newIndex = Math.max(
123
+ 0,
124
+ Math.min(
125
+ currentItems.length - 1,
126
+ Math.floor((finalY + ITEM_HEIGHT / 2) / ITEM_HEIGHT)
127
+ )
128
+ );
55
129
 
56
- return filters;
57
- }
130
+ const oldIndex = activeIndex.current;
131
+ const updated = [...currentItems];
132
+ const [removed] = updated.splice(oldIndex, 1);
133
+ updated.splice(newIndex, 0, removed);
134
+
135
+ // Atualiza o state com a nova ordem.
136
+ setItems(updated);
137
+
138
+ // A animação final agora está no useEffect, mas fazemos a limpeza das refs.
139
+ setDraggingId(null);
140
+ activeItem.current = null;
141
+ activeIndex.current = -1;
142
+ },
143
+ });
144
+
145
+ // Cria/Reusa os PanResponders
146
+ items.forEach(it => {
147
+ if (!panRespondersRef.current[it.value]) {
148
+ panRespondersRef.current[it.value] = createPanResponder(it);
58
149
  }
59
- return list;
60
150
  });
61
151
 
62
- //v2
63
-
64
- const renderItem = ({ item, drag, isActive, getIndex }) => {
65
- const index = getIndex();
66
- const name = `${scope.key('row', index, '')}`;
67
- const [row] = useState(
68
- ScopeUtils.create({
69
- ...original,
70
- parent: scope,
71
- name,
72
- crud: scope.crud,
73
- index,
74
- type: 'row',
75
- data: item,
76
- })
77
- );
78
-
79
- let css = { ...scope.getStyle('row') };
80
-
81
- if (isActive) {
82
- css = { ...css, ...scope.getStyle('active') };
152
+ const removeItem = (id: string) => {
153
+ if (draggingId === id) {
154
+ setDraggingId(null);
155
+ activeItem.current = null;
156
+ activeIndex.current = -1;
83
157
  }
84
158
 
85
- if (isActive) {
86
- css = { ...css, ...row.getStyle('rowSelected', {}) };
87
- } else {
88
- css = { ...css, ...row.getStyle('rowUnSelected', {}) };
159
+ // CORREÇÃO: Delete a referência Animated.Value *e* a referência PanResponder
160
+ if (positions[id]) {
161
+ delete positions[id];
89
162
  }
90
- const Child = () => {
91
- if (Utils.isEmpty(props.children)) {
92
- return (
93
- <>
94
- <MaterialCommunityIcons name="drag" size={24} color="black" />
95
- <Text style={styles.text}>{scope.getItemLabel(item)}</Text>
96
- </>
97
- );
98
- }
99
-
100
- return (
101
- <>
102
- <UIChildren transient scope={row} crud={row.crud}>
103
- {props.children}
104
- </UIChildren>
105
- </>
106
- );
107
- };
108
- return (
109
- <ScaleDecorator>
110
- <TouchableOpacity
111
- style={[
112
- styles.row,
113
- { backgroundColor: isActive ? 'lightblue' : 'white' },
114
- { ...css },
115
- ]}
116
- delayLongPress={100}
117
- onLongPress={drag} // Initiate drag on long press
118
- >
119
- <Child />
120
- </TouchableOpacity>
121
- </ScaleDecorator>
122
- );
123
- };
124
-
125
- const OrderView = ({ children }) => {
126
- if (original.gesture !== false) {
127
- return (
128
- <GestureHandlerRootView
129
- style={{
130
- flex: 1,
131
- width: '100%',
132
- ...scope.getStyle('order', { justifyContent: 'flex-start' }),
133
- }}
134
- >
135
- {children}
136
- </GestureHandlerRootView>
137
- );
163
+ if (panRespondersRef.current[id]) {
164
+ delete panRespondersRef.current[id];
138
165
  }
139
- return <>{children}</>;
140
- };
141
166
 
142
- const Header = ({ pos }) => {
143
- const hPos = Utils.nvl(original.headerPos, 'outer');
167
+ setItems(prev => {
168
+ const updated = prev.filter(it => it.value !== id);
144
169
 
145
- if (hPos !== pos) return <></>;
170
+ // Anima os itens restantes para seus novos índices
171
+ updated.forEach((it, idx) => {
172
+ if (positions[it.value])
173
+ Animated.spring(positions[it.value], {
174
+ toValue: idx * ITEM_HEIGHT,
175
+ useNativeDriver: false,
176
+ }).start();
177
+ });
146
178
 
147
- return <UIHeader scope={scope} />;
179
+ return updated;
180
+ });
148
181
  };
149
182
 
150
- let OrderData = () => {
151
- const [data, setData] = useState(items);
152
-
153
- return (
154
- <DraggableFlatList
155
- data={data}
156
- ListHeaderComponent={<Header pos="inner" />}
157
- ListFooterComponent={<>{scope.getPart('footer')}</>}
158
- renderItem={renderItem}
159
- containerStyle={{ ...scope.getStyle('list') }}
160
- keyExtractor={item => {
161
- let key = scope.getItemValue(item);
162
-
163
- if (original?.debug) {
164
- console.log(key);
165
- }
166
- return key;
167
- }}
168
- onDragEnd={({ data }) => {
169
- setData(data);
170
- scope.changeValue(data);
171
- }}
172
- />
173
- );
174
- };
175
183
  return (
176
- <OrderView>
177
- <Header pos="outer" />
178
- <OrderData />
179
- </OrderView>
184
+ <View style={styles.container}>
185
+ <Text style={styles.title}>
186
+ Lista Dragável (Esperando que a correção final funcione 🙏)
187
+ </Text>
188
+ <View style={{ height: items.length * ITEM_HEIGHT, width: '100%' }}>
189
+ {items.map(item => {
190
+ const y = positions[item.value];
191
+ if (!y) return null;
192
+
193
+ const isDragging = draggingId === item.value;
194
+ return (
195
+ <Animated.View
196
+ key={item.value}
197
+ {...(panRespondersRef.current[item.value]?.panHandlers ?? {})}
198
+ style={[
199
+ styles.item,
200
+ {
201
+ position: 'absolute',
202
+ left: 0,
203
+ right: 0,
204
+ height: ITEM_HEIGHT - 8,
205
+ transform: [{ translateY: y }],
206
+ zIndex: isDragging ? 999 : 0,
207
+ elevation: isDragging ? 999 : 0,
208
+ },
209
+ ]}
210
+ >
211
+ <View style={styles.handle}>
212
+ <Text style={styles.handleText}>≡</Text>
213
+ </View>
214
+ <Text style={styles.itemText}>{item.label}</Text>
215
+ <TouchableOpacity
216
+ onPress={() => removeItem(item.value)}
217
+ style={styles.del}
218
+ >
219
+ <Text style={{ color: '#fff' }}>X</Text>
220
+ </TouchableOpacity>
221
+ </Animated.View>
222
+ );
223
+ })}
224
+ </View>
225
+ </View>
180
226
  );
181
227
  }
182
228
 
183
229
  const styles = StyleSheet.create({
184
- row: {
230
+ container: { flex: 1, paddingHorizontal: 20, backgroundColor: '#f5f7fb' },
231
+ title: { fontSize: 18, fontWeight: '600', marginBottom: 12 },
232
+ item: {
233
+ backgroundColor: '#fff',
234
+ borderRadius: 10,
235
+ marginVertical: 4,
236
+ paddingHorizontal: 12,
237
+ alignItems: 'center',
185
238
  flexDirection: 'row',
186
- gap: 10,
187
- padding: 15,
188
- borderBottomWidth: 1,
189
- borderBottomColor: '#ccc',
239
+ elevation: 3,
240
+ shadowColor: '#000',
241
+ shadowOpacity: 0.05,
242
+ shadowRadius: 6,
190
243
  },
191
- text: {
192
- fontSize: 18,
244
+ handle: { width: 40, alignItems: 'center', justifyContent: 'center' },
245
+ handleText: { fontSize: 22, color: '#666' },
246
+ itemText: { fontSize: 16, flex: 1 },
247
+ del: {
248
+ backgroundColor: '#e74c3c',
249
+ height: 34,
250
+ width: 34,
251
+ borderRadius: 6,
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
193
254
  },
194
255
  });