@bit.rhplus/ui.f7.detail-item 0.0.9 → 0.0.11

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.
@@ -17,6 +17,7 @@ import {
17
17
  } from 'framework7-react';
18
18
  import { Tag } from 'lucide-react';
19
19
  import CircleButton from '@bit.rhplus/ui.circle-button';
20
+ import CategoryItems from '@bit.rhplus/ui.f7.category';
20
21
 
21
22
  // Category komponenta s modální editací - zobrazuje výběr jedné kategorie jako avatar
22
23
  export const Category = ({
@@ -135,7 +136,7 @@ export const Category = ({
135
136
  >
136
137
  <div className="view view-init">
137
138
  <div className="page page-with-navbar-large">
138
- <Navbar large className="navbar-transparent">
139
+ <Navbar large>
139
140
  <NavLeft>
140
141
  <Link onClick={handleCancel}>
141
142
  <Icon f7="arrow_left" style={{ fontWeight: 'bold' }} />
@@ -146,44 +147,7 @@ export const Category = ({
146
147
  </Navbar>
147
148
 
148
149
  <div className="page-content">
149
- {/* Dostupné kategorie */}
150
- <Block>
151
- <div style={{
152
- display: 'grid',
153
- gridTemplateColumns: 'repeat(auto-fit, minmax(80px, 1fr))',
154
- gap: '15px',
155
- marginTop: '15px',
156
- padding: '10px 5px'
157
- }}>
158
- {categories.map((category, index) => {
159
- const isSelected = selectedCategory && selectedCategory.id === category.id;
160
- return (
161
- <div key={category.id} style={{
162
- display: 'flex',
163
- justifyContent: 'center',
164
- minWidth: '80px'
165
- }}>
166
- <CircleButton
167
- icon={category.icon || 'tag'}
168
- name={category.name}
169
- bgColor={isSelected ? '#28a745' : (category.color ? (category.color.startsWith('#') ? category.color : `#${category.color}`) : undefined)}
170
- colorIndex={index}
171
- iconColor="white"
172
- textColor="#333"
173
- onClick={() => selectCategory(category)}
174
- style={{
175
- opacity: isSelected ? 1 : 0.7,
176
- transform: isSelected ? 'scale(1.05)' : 'scale(1)',
177
- transition: 'all 0.2s ease',
178
- width: '100%',
179
- maxWidth: '100px'
180
- }}
181
- />
182
- </div>
183
- );
184
- })}
185
- </div>
186
- </Block>
150
+ <CategoryItems categories={categories} onCategorySelect={selectCategory} />
187
151
  </div>
188
152
  </div>
189
153
  </div>
@@ -174,7 +174,7 @@ export const Grid = ({
174
174
  >
175
175
  <div className="view view-init">
176
176
  <div className="page page-with-navbar-large">
177
- <Navbar large className="navbar-transparent">
177
+ <Navbar large>
178
178
  <NavLeft>
179
179
  <Link onClick={handleCancel}>
180
180
  <Icon f7="arrow_left" style={{ fontWeight: 'bold' }} />
@@ -51,29 +51,18 @@ export const InputText = ({
51
51
  };
52
52
 
53
53
  const handleSave = () => {
54
- console.log("🔥 InputText handleSave called!");
55
- console.log("🔥 inputValue:", inputValue);
56
- console.log("🔥 value prop:", value);
57
- console.log("🔥 children prop:", children);
58
- console.log("🔥 onChange exists:", !!onChange);
59
- console.log("🔥 onSave exists:", !!onSave);
60
-
54
+
61
55
  // Rozhodni jakou hodnotu vrátit podle field parametru (pro InputText nevyžíváme pole objektu)
62
56
  const returnValue = inputValue;
63
- console.log("🔥 returnValue:", returnValue);
64
-
57
+
65
58
  // Preferuj onChange pro Form.Item kompatibilitu, jinak použij onSave
66
59
  if (onChange) {
67
- console.log("🔥 Calling onChange with:", returnValue);
68
60
  onChange(returnValue);
69
61
  } else if (onSave) {
70
- console.log("🔥 Calling onSave with:", returnValue);
71
62
  onSave(returnValue);
72
63
  } else {
73
- console.log("❌ No onChange or onSave handler available!");
74
64
  }
75
65
 
76
- console.log("🔥 Closing popup");
77
66
  setPopupOpened(false);
78
67
  };
79
68
 
@@ -135,9 +124,6 @@ export const InputText = ({
135
124
  </Block>
136
125
  <SaveButton
137
126
  onClick={() => {
138
- console.log("🔥 SaveButton onClick triggered!");
139
- console.log("🔥 SaveButton - inputValue před handleSave:", inputValue);
140
- console.log("🔥 SaveButton - onChange existuje:", !!onChange);
141
127
  handleSave();
142
128
  }}
143
129
  variant="black"
@@ -1,33 +1,34 @@
1
1
  /* eslint-disable */
2
- import React, { useState, useEffect, useMemo } from 'react';
3
- import {
4
- Link,
5
- Icon,
6
- Popup,
2
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
3
+ import {
4
+ Link,
5
+ Icon,
6
+ Popup,
7
7
  Navbar,
8
- NavLeft,
9
- NavTitle,
8
+ NavLeft,
9
+ NavTitle,
10
10
  NavTitleLarge,
11
- NavRight,
12
- Page,
13
- Block,
14
- List as F7List,
15
- ListItem,
11
+ NavRight,
12
+ Page,
13
+ Block,
14
+ List as F7List,
15
+ ListItem,
16
16
  Card,
17
- Button
17
+ Button,
18
18
  } from 'framework7-react';
19
19
  import { List as ListIcon, Globe } from 'lucide-react';
20
20
  import SaveButton from '@bit.rhplus/ui.f7.save-button';
21
+ import Filter from '@bit.rhplus/ui.f7.filter';
21
22
 
22
23
  // List komponenta s modální editací - zobrazuje seznam jako ListItem komponenty
23
- export const List = ({
24
- children,
25
- value,
26
- onSave,
24
+ export const List = ({
25
+ children,
26
+ value,
27
+ onSave,
27
28
  onChange, // Přidáno pro Form.Item kompatibilitu
28
- title = 'Editace výběru',
29
+ title = 'Editace výběru',
29
30
  placeholder = 'Vyberte položku',
30
- color = '#6887d3',
31
+ color = '#6887d3',
31
32
  size = 16,
32
33
  lucideIcon, // Lucide React ikona (např. List)
33
34
  icon, // Jakákoliv React komponenta ikony
@@ -42,25 +43,45 @@ export const List = ({
42
43
  showCountryFlag = false, // Zda zobrazit vlajku země místo standardní ikony
43
44
  flagSize = 16, // Velikost vlajky
44
45
  ItemRenderer, // Custom komponenta pro renderování jednotlivých položek
45
- field = null // Pole pro výběr konkrétní vlastnosti objektu
46
+ field = null, // Pole pro výběr konkrétní vlastnosti objektu
47
+ filterMultiSelect = false,
48
+ filterPopupContent,
49
+ onFilterTextClear,
50
+ onFilter, // Filtrovací funkce
51
+ filterFunction // Funkce pro aplikování filtrů
46
52
  }) => {
47
53
  const [popupOpened, setPopupOpened] = useState(false);
48
-
54
+ const [activeFilters, setActiveFilters] = useState(null);
55
+
56
+ // Filtrovaná data na základě aktivních filtrů
57
+ const filteredOptions = useMemo(() => {
58
+ const baseOptions = Array.isArray(options) ? options : [];
59
+
60
+ // Pokud existuje filterFunction, použij ji
61
+ if (filterFunction && activeFilters) {
62
+ return filterFunction(baseOptions, activeFilters.filters, activeFilters.searchText);
63
+ }
64
+
65
+ return baseOptions;
66
+ }, [options, activeFilters, filterFunction]);
67
+
49
68
  // Najíst objekty na základě value (pokud je field nastaven, value může být jen pole konkrétních vlastností)
50
69
  const findItemsByValue = (val) => {
51
70
  if (!val || !options.length) return [];
52
-
71
+
53
72
  const valueArray = Array.isArray(val) ? val : [val];
54
-
73
+
55
74
  if (field) {
56
75
  // Pokud je field nastaven, hledej objekty podle této vlastnosti
57
- return valueArray.map(v => options.find(opt => opt[field] === v)).filter(Boolean);
76
+ return valueArray
77
+ .map((v) => options.find((opt) => opt[field] === v))
78
+ .filter(Boolean);
58
79
  } else {
59
80
  // Pokud field není nastaven, očekáváme objekty
60
81
  return valueArray;
61
82
  }
62
83
  };
63
-
84
+
64
85
  // Inicializuj selectedItems pouze jednou při mount
65
86
  const initialSelectedItems = useMemo(() => {
66
87
  return findItemsByValue(value);
@@ -68,12 +89,12 @@ export const List = ({
68
89
 
69
90
  const [selectedItems, setSelectedItems] = useState(initialSelectedItems);
70
91
 
71
- const linkStyle = {
72
- color,
92
+ const linkStyle = {
93
+ color,
73
94
  cursor: 'pointer',
74
95
  display: 'flex',
75
96
  alignItems: 'center',
76
- gap: '6px'
97
+ gap: '6px',
77
98
  };
78
99
 
79
100
  const handleCancel = () => {
@@ -88,22 +109,22 @@ export const List = ({
88
109
  };
89
110
 
90
111
  const isSelected = (item) => {
91
- return selectedItems.some(selected => selected.id === item.id);
112
+ return selectedItems.some((selected) => selected.id === item.id);
92
113
  };
93
114
 
94
115
  const handleItemClick = (item) => {
95
116
  if (selectionMode === 'single') {
96
117
  // Single selection - přímé uložení položky bez použití state
97
-
118
+
98
119
  // Rozhodni jakou hodnotu vrátit podle field parametru
99
120
  const returnValue = field && item ? item[field] : item;
100
-
121
+
101
122
  if (onChange) {
102
123
  onChange(returnValue);
103
124
  } else if (onSave) {
104
125
  onSave(returnValue);
105
126
  }
106
-
127
+
107
128
  // Zavři popup
108
129
  setPopupOpened(false);
109
130
  return;
@@ -111,11 +132,13 @@ export const List = ({
111
132
  // Multiple selection - toggle výběr
112
133
  let newSelection;
113
134
  if (isSelected(item)) {
114
- newSelection = selectedItems.filter(selected => selected.id !== item.id);
135
+ newSelection = selectedItems.filter(
136
+ (selected) => selected.id !== item.id
137
+ );
115
138
  } else {
116
139
  newSelection = [...selectedItems, item];
117
140
  }
118
-
141
+
119
142
  setSelectedItems(newSelection);
120
143
  }
121
144
  };
@@ -123,32 +146,58 @@ export const List = ({
123
146
  const handleSaveSelection = () => {
124
147
  // Uložení vybraných položek pro oba režimy
125
148
  let result;
126
-
149
+
127
150
  if (selectionMode === 'single') {
128
151
  result = selectedItems.length > 0 ? selectedItems[0] : null;
129
152
  } else {
130
153
  result = selectedItems.length === 1 ? selectedItems[0] : selectedItems;
131
154
  }
132
-
155
+
133
156
  // Rozhodni jakou hodnotu vrátit podle field parametru
134
- const returnValue = field && result ? (Array.isArray(result) ? result.map(item => item[field]) : result[field]) : result;
135
-
157
+ const returnValue =
158
+ field && result
159
+ ? Array.isArray(result)
160
+ ? result.map((item) => item[field])
161
+ : result[field]
162
+ : result;
163
+
136
164
  if (onChange) {
137
165
  onChange(returnValue);
138
166
  } else if (onSave) {
139
167
  onSave(returnValue);
140
168
  }
141
-
169
+
142
170
  setPopupOpened(false);
143
171
  };
144
172
 
173
+ // Handle filtrování - aplikuj filtry a informuj rodiče
174
+ const handleFilterApply = useCallback((searchText, filters) => {
175
+ setActiveFilters({ searchText, filters });
176
+
177
+ // Spočítej počet filtrovaných položek pomocí filterFunction
178
+ let filteredOptions = options;
179
+ if (filterFunction) {
180
+ filteredOptions = filterFunction(options, filters, searchText);
181
+ }
182
+
183
+ // Informuj rodiče o aplikovaných filtrech
184
+ if (onFilter) {
185
+ onFilter({
186
+ searchText: searchText,
187
+ filters: filters,
188
+ filteredCount: filteredOptions.length,
189
+ totalCount: options.length
190
+ });
191
+ }
192
+ }, [onFilter, options, filterFunction]);
193
+
145
194
  // Určí jakou ikonu použít - priorita: vlajka země > icon > lucideIcon > výchozí List
146
195
  const renderIcon = () => {
147
196
  // Pokud chceme zobrazit vlajku země a máme vybranou hodnotu
148
197
  if (showCountryFlag && value) {
149
198
  const currentValue = Array.isArray(value) ? value[0] : value;
150
199
  const country = currentValue?.[countryField];
151
-
200
+
152
201
  if (country?.code) {
153
202
  return (
154
203
  <div
@@ -183,7 +232,7 @@ export const List = ({
183
232
  );
184
233
  }
185
234
  }
186
-
235
+
187
236
  if (icon) {
188
237
  return React.cloneElement(icon, { size, color, ...icon.props });
189
238
  }
@@ -198,11 +247,11 @@ export const List = ({
198
247
  const renderItemIcon = (item) => {
199
248
  const itemIcon = item[iconField];
200
249
  const itemColor = item[colorField] || color;
201
-
250
+
202
251
  if (React.isValidElement(itemIcon)) {
203
252
  return React.cloneElement(itemIcon, { size: 20, color: itemColor });
204
253
  }
205
-
254
+
206
255
  return <Icon f7="circle_fill" color={itemColor} size="20" />;
207
256
  };
208
257
 
@@ -213,11 +262,17 @@ export const List = ({
213
262
  {(() => {
214
263
  // Použij objekty pro zobrazení (převedené z value)
215
264
  const currentItems = findItemsByValue(value);
216
-
265
+
217
266
  if (currentItems.length === 1) {
218
267
  // Pokud je vybraná jedna položka, zobraz hodnotu z displayField
219
268
  const item = currentItems[0];
220
- return item[displayField] || item.name || item.title || item.id || 'Vybraná položka';
269
+ return (
270
+ item[displayField] ||
271
+ item.name ||
272
+ item.title ||
273
+ item.id ||
274
+ 'Vybraná položka'
275
+ );
221
276
  } else if (currentItems.length > 1) {
222
277
  return `${currentItems.length} vybraných položek`;
223
278
  }
@@ -225,8 +280,8 @@ export const List = ({
225
280
  })()}
226
281
  </Link>
227
282
 
228
- <Popup
229
- opened={popupOpened}
283
+ <Popup
284
+ opened={popupOpened}
230
285
  onPopupClosed={() => setPopupOpened(false)}
231
286
  animate
232
287
  backdrop
@@ -234,131 +289,156 @@ export const List = ({
234
289
  className="f7-parallax list-popup"
235
290
  style={{
236
291
  '--f7-popup-tablet-width': '90vw',
237
- '--f7-popup-tablet-height': '90vh'
292
+ '--f7-popup-tablet-height': '90vh',
238
293
  }}
239
294
  >
240
295
  <div className="view view-init">
241
296
  <div className="page page-with-navbar-large">
242
- <Navbar large className="navbar-transparent">
297
+ <Navbar large>
243
298
  <NavLeft>
244
299
  <Link onClick={handleCancel}>
245
300
  <Icon f7="arrow_left" style={{ fontWeight: 'bold' }} />
246
301
  </Link>
247
302
  </NavLeft>
248
303
  <NavTitle>{title}</NavTitle>
249
- <NavTitleLarge>{title}</NavTitleLarge>
304
+ <NavTitleLarge>
305
+ <div>{title}</div>
306
+ <Filter
307
+ data={options}
308
+ onTextClear={onFilterTextClear}
309
+ popupContent={filterPopupContent}
310
+ multiSelect={filterMultiSelect}
311
+ onFilterApply={handleFilterApply}
312
+ />
313
+ </NavTitleLarge>
250
314
  </Navbar>
251
-
315
+
252
316
  <div className="page-content">
253
- <Block style={{
254
- padding: '10px 0'
255
- }}>
317
+ <Block
318
+ style={{
319
+ padding: '10px 0',
320
+ }}
321
+ >
256
322
  <Card>
257
- <F7List style={{
258
- marginTop: '0',
259
- marginBottom: '0'
260
- }}>
261
- {options.map((item, index) => {
262
- const selected = isSelected(item);
263
-
264
- // Pokud je předán custom ItemRenderer, použij ho
265
- if (ItemRenderer) {
266
- return (
267
- <ItemRenderer
268
- key={item.id || index}
269
- item={item}
270
- index={index}
271
- selected={selected}
272
- onItemClick={(e) => {
273
- e.preventDefault();
274
- e.stopPropagation();
275
- handleItemClick(item);
276
- }}
277
- titleField={titleField}
278
- subtitleField={subtitleField}
279
- iconField={iconField}
280
- colorField={colorField}
281
- color={color}
282
- />
283
- );
284
- }
285
-
286
- // Defaultní renderování
287
- const title = item[titleField] || item.name || item.title || `Položka ${index + 1}`;
288
- const subtitle = item[subtitleField] || item.description;
289
-
290
- return (
291
- <ListItem
292
- key={item.id || index}
293
- link="#"
294
- noChevron
295
- onClick={() => handleItemClick(item)}
296
- style={{
297
- '--f7-list-item-padding-horizontal': '15px',
298
- '--f7-list-item-padding-vertical': '12px',
299
- backgroundColor: selected ? '#f0f9ff' : 'transparent'
300
- }}
301
- >
302
- <div
303
- slot="media"
304
- style={{
305
- display: 'flex',
306
- alignItems: 'center',
307
- marginRight: '12px'
308
- }}
309
- >
310
- {renderItemIcon(item)}
311
- </div>
312
-
313
- <div
314
- slot="inner"
315
- style={{
316
- display: 'flex',
317
- flexDirection: 'column',
318
- width: '100%',
319
- minHeight: '50px'
320
- }}
321
- >
322
- {/* Title row */}
323
- <div style={{
324
- display: 'flex',
325
- justifyContent: 'space-between',
326
- alignItems: 'center',
327
- marginBottom: subtitle ? '4px' : '0'
328
- }}>
329
- <span style={{
330
- fontWeight: selected ? 600 : 500,
331
- fontSize: '16px',
332
- color: selected ? '#007aff' : '#000'
333
- }}>
334
- {title}
335
- </span>
336
- {selected && (
337
- <Icon
338
- f7="checkmark_circle_fill"
339
- color="#007aff"
340
- size="20"
341
- />
342
- )}
343
- </div>
344
-
345
- {/* Subtitle */}
346
- {subtitle && (
347
- <div style={{
348
- fontSize: '14px',
349
- color: '#666',
350
- overflow: 'hidden',
351
- textOverflow: 'ellipsis',
352
- whiteSpace: 'nowrap'
353
- }}>
354
- {subtitle}
323
+ <F7List
324
+ style={{
325
+ marginTop: '0',
326
+ marginBottom: '0',
327
+ }}
328
+ >
329
+ {filteredOptions.map((item, index) => {
330
+ const selected = isSelected(item);
331
+
332
+ // Pokud je předán custom ItemRenderer, použij ho
333
+ if (ItemRenderer) {
334
+ return (
335
+ <ItemRenderer
336
+ key={item.id || index}
337
+ item={item}
338
+ index={index}
339
+ selected={selected}
340
+ onItemClick={(e) => {
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ handleItemClick(item);
344
+ }}
345
+ titleField={titleField}
346
+ subtitleField={subtitleField}
347
+ iconField={iconField}
348
+ colorField={colorField}
349
+ color={color}
350
+ />
351
+ );
352
+ }
353
+
354
+ // Defaultní renderování
355
+ const title =
356
+ item[titleField] ||
357
+ item.name ||
358
+ item.title ||
359
+ `Položka ${index + 1}`;
360
+ const subtitle = item[subtitleField] || item.description;
361
+
362
+ return (
363
+ <ListItem
364
+ key={item.id || index}
365
+ link="#"
366
+ noChevron
367
+ onClick={() => handleItemClick(item)}
368
+ style={{
369
+ '--f7-list-item-padding-horizontal': '15px',
370
+ '--f7-list-item-padding-vertical': '12px',
371
+ backgroundColor: selected
372
+ ? '#f0f9ff'
373
+ : 'transparent',
374
+ }}
375
+ >
376
+ <div
377
+ slot="media"
378
+ style={{
379
+ display: 'flex',
380
+ alignItems: 'center',
381
+ marginRight: '12px',
382
+ }}
383
+ >
384
+ {renderItemIcon(item)}
385
+ </div>
386
+
387
+ <div
388
+ slot="inner"
389
+ style={{
390
+ display: 'flex',
391
+ flexDirection: 'column',
392
+ width: '100%',
393
+ minHeight: '50px',
394
+ }}
395
+ >
396
+ {/* Title row */}
397
+ <div
398
+ style={{
399
+ display: 'flex',
400
+ justifyContent: 'space-between',
401
+ alignItems: 'center',
402
+ marginBottom: subtitle ? '4px' : '0',
403
+ }}
404
+ >
405
+ <span
406
+ style={{
407
+ fontWeight: selected ? 600 : 500,
408
+ fontSize: '16px',
409
+ color: selected ? '#007aff' : '#000',
410
+ }}
411
+ >
412
+ {title}
413
+ </span>
414
+ {selected && (
415
+ <Icon
416
+ f7="checkmark_circle_fill"
417
+ color="#007aff"
418
+ size="20"
419
+ />
420
+ )}
421
+ </div>
422
+
423
+ {/* Subtitle */}
424
+ {subtitle && (
425
+ <div
426
+ style={{
427
+ fontSize: '14px',
428
+ color: '#666',
429
+ overflow: 'hidden',
430
+ textOverflow: 'ellipsis',
431
+ whiteSpace: 'nowrap',
432
+ }}
433
+ >
434
+ {subtitle}
435
+ </div>
436
+ )}
355
437
  </div>
356
- )}
357
- </div>
358
- </ListItem>
359
- );
360
- })}
361
- </F7List>
438
+ </ListItem>
439
+ );
440
+ })}
441
+ </F7List>
362
442
  </Card>
363
443
  </Block>
364
444
  </div>
@@ -367,4 +447,4 @@ export const List = ({
367
447
  </Popup>
368
448
  </>
369
449
  );
370
- };
450
+ };