diff --git a/lib/Infrastructure/list_extensions.dart b/lib/Infrastructure/list_extensions.dart new file mode 100644 index 0000000..b6b5b3a --- /dev/null +++ b/lib/Infrastructure/list_extensions.dart @@ -0,0 +1,20 @@ +extension ListExtension on List { + bool containsAll(List otherList) { + List checks = []; + // + for (E thisElement in otherList) { + checks.add(contains(thisElement)); + } + return !checks.contains(false); + } + + List getUnique() { + List uniqueItems = []; + for (E thisElement in this) { + if (!uniqueItems.contains(thisElement)) { + uniqueItems.add(thisElement); + } + } + return uniqueItems; + } +} diff --git a/lib/src/Selectors/selectors.astromic.dart b/lib/src/Selectors/selectors.astromic.dart index 3e87527..dca4e32 100644 --- a/lib/src/Selectors/selectors.astromic.dart +++ b/lib/src/Selectors/selectors.astromic.dart @@ -1,7 +1,8 @@ //s2 Core Packages Imports import 'package:flutter/widgets.dart'; -import 'src/configuration.dart'; import 'src/radio.selector.dart'; +import 'src/chip.selector.dart'; +import 'src/configuration.dart'; class AstromicSelectors { //S1 -- Radio @@ -21,7 +22,7 @@ class AstromicSelectors { initialSelectedValue: initialSelectedValue, onChanged: onChanged, // - configurations: configurations ?? const AstromicSelectorConfiguration(), + configuration: configurations ?? const AstromicSelectorConfiguration(), // itemSpacing: itemSpacing ?? 8.0, // @@ -29,4 +30,37 @@ class AstromicSelectors { disabledItemBuilder: disabledItemBuilder, items: items, ); + + //S1 -- Chip + static Widget chip({ + List? initialSelectedValues, + void Function(List selectedItems)? onChanged, + // + AstromicSelectorConfiguration? configuration, + // + double? itemSpacing = 4, + double? runSpacing = 8, + double? clearSpacing = 4, + // + TextStyle? labelStyle, + TextStyle? selectedLabelStyle, + TextStyle? disabledLabelStyle, + // + required List<(T item, bool isEnabled)> items, + required Widget Function(T item, {bool isSelected, VoidCallback? onTap, VoidCallback? onClearTapped}) itemBuilder, + required Widget Function(T item) disabledItemBuilder, + }) => + AstromicChipSelector( + initialSelectedValues: initialSelectedValues, + onChanged: onChanged, + // + configuration: configuration, + // + itemSpacing: itemSpacing, + runSpacing: runSpacing, + // + items: items, + itemBuilder: itemBuilder, + disabledItemBuilder: disabledItemBuilder, + ); } diff --git a/lib/src/Selectors/src/chip.selector.dart b/lib/src/Selectors/src/chip.selector.dart index e69de29..d6adba4 100644 --- a/lib/src/Selectors/src/chip.selector.dart +++ b/lib/src/Selectors/src/chip.selector.dart @@ -0,0 +1,212 @@ +//SECTION - Imports +// +//s1 PACKAGES +//--------------- +//s2 CORE +import 'package:flutter/material.dart'; +//s2 3RD-PARTY +// +//s1 DEPENDENCIES +//--------------- +//s2 SERVICES +//s2 MODELS +import '../../../Infrastructure/list_extensions.dart'; +import 'configuration.dart'; + +//s2 MISC +//!SECTION - Imports +// +//SECTION - Exports +//!SECTION - Exports +// +class AstromicChipSelector extends StatefulWidget { + //SECTION - Widget Arguments + //s1 -- Functionality + final List? initialSelectedValues; + final Function(List selectedItems)? onChanged; + //s1 -- Configuration + final AstromicSelectorConfiguration? configuration; + //s1 -- Style + final double? itemSpacing; + final double? runSpacing; + // + //s1 -- Content + final List<(T item, bool isEnabled)> items; + // + final Widget Function(T item, {bool isSelected, VoidCallback? onTap, VoidCallback? onClearTapped}) itemBuilder; + final Widget Function(T item) disabledItemBuilder; + //!SECTION + // + AstromicChipSelector({ + Key? key, + //s1 -- Functionality + this.initialSelectedValues, + this.onChanged, + //s1 -- Configuration + this.configuration, + //s1 -- Style + this.itemSpacing = 8, + this.runSpacing = 8, + //s1 -- Content + required this.items, + required this.itemBuilder, + required this.disabledItemBuilder, + }) : assert( + (configuration?.isNullable ?? true) || (initialSelectedValues != null && items.map((i) => i.$1).toList().containsAll(initialSelectedValues)), + "Initial values are not all present in the items!", + ), + super( + key: key, + ); + + @override + State> createState() => _AstromicChipSelectorState(); +} + +class _AstromicChipSelectorState extends State> { + // + //SECTION - State Variables + //s1 --Controllers + late ScrollController _scrollController; + //s1 --Controllers + // + //s1 --State + late AstromicSelectorConfiguration _configuration; + late List selectedItems; + //s1 --State + // + //s1 --Constants + //s1 --Constants + //!SECTION + + @override + void initState() { + super.initState(); + // + //SECTION - State Variables initializations & Listeners + //s1 --Controllers & Listeners + _scrollController = ScrollController(); + //s1 --Controllers & Listeners + // + //s1 --State + _configuration = widget.configuration ?? const AstromicSelectorConfiguration(); + selectedItems = widget.initialSelectedValues ?? []; + //s1 --State + // + //s1 --Late & Async Initializers + //s1 --Late & Async Initializers + //!SECTION + } + + //SECTION - Stateless functions + //!SECTION + + //SECTION - Action Callbacks + _onTap(T value) { + setState(() { + if (selectedItems.contains(value)) { + //---- if item selected + selectedItems.remove(value); + //---- !if item selected + } else { + //---- if item Not selected + selectedItems.add(value); + //---- !if item Not selected + } + }); + if (widget.onChanged != null) { + widget.onChanged!(selectedItems); + } + } + + // ---- + _onTapClear(T value) { + setState(() { + if (selectedItems.contains(value)) { + selectedItems.remove(value); + } + }); + if (widget.onChanged != null) { + widget.onChanged!(selectedItems); + } + } + //!SECTION + + @override + Widget build(BuildContext context) { + //SECTION - Build Setup + //s1 -Values + //s1 -Values + // + //s1 -Widgets + List baseChildren = widget.items.map((currentItem) { + int currentIndex = widget.items.indexOf(currentItem); + T? previousItem = currentIndex == 0 ? null : widget.items[widget.items.indexOf(currentItem) - 1].$1; + T? nextItem = currentIndex == widget.items.length - 1 ? null : widget.items[widget.items.indexOf(currentItem) + 1].$1; + bool isConsequent = _configuration.isConsequent; + // + bool isEnabled = currentItem.$2; + bool isSelected = selectedItems.contains(currentItem); + bool isPreviousItemSelected = selectedItems.contains(previousItem); + bool isNextItemSelected = selectedItems.contains(nextItem); + // --- + bool canBeSelected = (isConsequent && isEnabled && !isSelected && selectedItems.length < (_configuration.maxSelectedItems) && (isPreviousItemSelected || currentIndex == 0)) || + (!isConsequent && isEnabled && !isSelected && selectedItems.length < (_configuration.maxSelectedItems)); + bool canbeUnselected = (isConsequent && isEnabled && isSelected && (!isNextItemSelected || currentIndex == widget.items.length - 1)) || (!isConsequent && isEnabled && isSelected); + // --- + bool tapable = (isSelected && canbeUnselected) || (!isSelected && canBeSelected); + // --- + bool canShowClear = _configuration.withClearButton && canbeUnselected; + // --- + // + return isEnabled + ? widget.itemBuilder( + currentItem.$1, + isSelected: isSelected, + onTap: tapable ? () => _onTap(currentItem.$1) : null, + onClearTapped: canShowClear ? _onTapClear(currentItem.$1) : null, + ) + : widget.disabledItemBuilder(currentItem.$1); + }).toList(); + //s1 -Widgets + //!SECTION + + //SECTION - Build Return + return _configuration.isWrap + ? Wrap( + spacing: widget.itemSpacing!, + runSpacing: widget.runSpacing!, + // + alignment: _configuration.wrapMainAllignment, + crossAxisAlignment: _configuration.wrapCrossAllignment, + // + children: baseChildren, + ) + : GridView.builder( + physics: const NeverScrollableScrollPhysics(), + controller: _scrollController, + shrinkWrap: true, + padding: EdgeInsets.zero, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + mainAxisExtent: _configuration.fixedRowHeight, + // number of items per row + crossAxisCount: widget.configuration?.crossAxisCount ?? 1, + // vertical spacing between the items + mainAxisSpacing: widget.runSpacing!, + // the horizontal spacing between the items + crossAxisSpacing: widget.itemSpacing!, + ), + // number of items in your list + itemCount: baseChildren.length, + itemBuilder: (BuildContext context, int index) => baseChildren[index], + ); + //!SECTION + } + + @override + void dispose() { + //SECTION - Disposable variables + //!SECTION + super.dispose(); + } +} diff --git a/lib/src/Selectors/src/configuration.dart b/lib/src/Selectors/src/configuration.dart index 8d6e9ae..c050348 100644 --- a/lib/src/Selectors/src/configuration.dart +++ b/lib/src/Selectors/src/configuration.dart @@ -5,20 +5,28 @@ class AstromicSelectorConfiguration { final Axis axis; final bool isNullable; //s1 Chip Specific + final bool isWrap; + final WrapAlignment wrapMainAllignment; + final WrapCrossAlignment wrapCrossAllignment; final bool isConsequent; final bool withClearButton; final int maxSelectedItems; final int crossAxisCount; + final double? fixedRowHeight; //s1 Radio Specific final bool withExpandedSpace; const AstromicSelectorConfiguration({ this.axis = Axis.horizontal, this.isNullable = true, // + this.isWrap = true, + this.wrapMainAllignment = WrapAlignment.start, + this.wrapCrossAllignment = WrapCrossAlignment.center, this.isConsequent = false, this.withClearButton = false, this.maxSelectedItems = 10000, this.crossAxisCount = 3, + this.fixedRowHeight, // this.withExpandedSpace = false, }); @@ -26,20 +34,28 @@ class AstromicSelectorConfiguration { AstromicSelectorConfiguration copyWith({ Axis? axis, bool? isNullable, + bool? isWrap, + WrapAlignment? wrapMainAllignment, + WrapCrossAlignment? wrapCrossAllignment, bool? isConsequent, bool? withClearButton, int? maxSelectedItems, int? crossAxisCount, bool? withExpandedSpace, + double? fixedRowHeight, }) { return AstromicSelectorConfiguration( axis: axis ?? this.axis, isNullable: isNullable ?? this.isNullable, + isWrap: isWrap ?? this.isWrap, + wrapMainAllignment: wrapMainAllignment ?? this.wrapMainAllignment, + wrapCrossAllignment: wrapCrossAllignment ?? this.wrapCrossAllignment, isConsequent: isConsequent ?? this.isConsequent, withClearButton: withClearButton ?? this.withClearButton, maxSelectedItems: maxSelectedItems ?? this.maxSelectedItems, crossAxisCount: crossAxisCount ?? this.crossAxisCount, withExpandedSpace: withExpandedSpace ?? this.withExpandedSpace, + fixedRowHeight: fixedRowHeight ?? this.fixedRowHeight, ); } } diff --git a/lib/src/Selectors/src/radio.selector.dart b/lib/src/Selectors/src/radio.selector.dart index 8d4ab6d..e956aa7 100644 --- a/lib/src/Selectors/src/radio.selector.dart +++ b/lib/src/Selectors/src/radio.selector.dart @@ -26,7 +26,7 @@ class AstromicRadioSelector extends StatefulWidget { final T? initialSelectedValue; final Function(T selectedItem)? onChanged; //s1 -- Configuration - final AstromicSelectorConfiguration configurations; + final AstromicSelectorConfiguration configuration; //s1 -- Style final double itemSpacing; //s1 -- Content @@ -43,15 +43,15 @@ class AstromicRadioSelector extends StatefulWidget { required this.items, this.onChanged, //s1 -- Configuration - required this.configurations, + required this.configuration, //s1 -- Style required this.itemSpacing, //s1 -- Content required this.itemBuilder, this.disabledItemBuilder, - }) : assert(configurations.isNullable || initialSelectedValue != null, 'You need to supply an initial value if not nullable!'), + }) : assert(configuration.isNullable || initialSelectedValue != null, 'You need to supply an initial value if not nullable!'), assert( - configurations.isNullable || + configuration.isNullable || (items .map( (e) => e.$1, @@ -135,10 +135,10 @@ class _AstromicRadioSelectorState extends State> { bool isSelected = currentItem.$1 == selectedItem; // return !isEnabled && widget.disabledItemBuilder != null - ? widget.configurations.withExpandedSpace && widget.configurations.axis == Axis.horizontal + ? widget.configuration.withExpandedSpace && widget.configuration.axis == Axis.horizontal ? Expanded(child: widget.disabledItemBuilder!(currentItem.$1)) : widget.disabledItemBuilder!(currentItem.$1) - : widget.configurations.withExpandedSpace && widget.configurations.axis == Axis.horizontal + : widget.configuration.withExpandedSpace && widget.configuration.axis == Axis.horizontal ? Expanded(child: widget.itemBuilder(currentItem.$1, onTap: isEnabled ? () => _onTap(currentItem.$1) : null, isSelected: isSelected)) : widget.itemBuilder(currentItem.$1, onTap: isEnabled ? () => _onTap(currentItem.$1) : null, isSelected: isSelected); }).toList(); @@ -149,7 +149,7 @@ class _AstromicRadioSelectorState extends State> { return // widget.configurations.withExpandedSpace // ? - widget.configurations.axis == Axis.horizontal + widget.configuration.axis == Axis.horizontal ? separatedRow( baseChildren, AstromicSpacing.hsb(widget.itemSpacing), @@ -157,7 +157,7 @@ class _AstromicRadioSelectorState extends State> { : separatedColumn( baseChildren, AstromicSpacing.vsb(widget.itemSpacing), - widget.configurations.withExpandedSpace, + widget.configuration.withExpandedSpace, ); // : Wrap( // direction: widget.configurations.axis,