diff --git a/lib/bottom_sheet/multi_select_bottom_sheet.dart b/lib/bottom_sheet/multi_select_bottom_sheet.dart index 26d797f..df50abe 100644 --- a/lib/bottom_sheet/multi_select_bottom_sheet.dart +++ b/lib/bottom_sheet/multi_select_bottom_sheet.dart @@ -1,375 +1,382 @@ -import 'package:flutter/material.dart'; -import '../util/multi_select_item.dart'; -import '../util/multi_select_actions.dart'; -import '../util/multi_select_list_type.dart'; - -/// A bottom sheet widget containing either a classic checkbox style list, or a chip style list. -class MultiSelectBottomSheet extends StatefulWidget - with MultiSelectActions { - /// List of items to select from. - final List> items; - - /// The list of selected values before interaction. - final List initialValue; - - /// The text at the top of the BottomSheet. - final Widget? title; - - /// Fires when the an item is selected / unselected. - final void Function(List)? onSelectionChanged; - - /// Fires when confirm is tapped. - final void Function(List)? onConfirm; - - /// Toggles search functionality. - final bool searchable; - - /// Text on the confirm button. - final Text? confirmText; - - /// Text on the cancel button. - final Text? cancelText; - - /// An enum that determines which type of list to render. - final MultiSelectListType? listType; - - /// Sets the color of the checkbox or chip when it's selected. - final Color? selectedColor; - - /// Set the initial height of the BottomSheet. - final double? initialChildSize; - - /// Set the minimum height threshold of the BottomSheet before it closes. - final double? minChildSize; - - /// Set the maximum height of the BottomSheet. - final double? maxChildSize; - - /// Set the placeholder text of the search field. - final String? searchHint; - - /// A function that sets the color of selected items based on their value. - /// It will either set the chip color, or the checkbox color depending on the list type. - final Color? Function(T)? colorator; - - /// Color of the chip body or checkbox border while not selected. - final Color? unselectedColor; - - /// Icon button that shows the search field. - final Icon? searchIcon; - - /// Icon button that hides the search field - final Icon? closeSearchIcon; - - /// Style the text on the chips or list tiles. - final TextStyle? itemsTextStyle; - - /// Style the text on the selected chips or list tiles. - final TextStyle? selectedItemsTextStyle; - - /// Style the search text. - final TextStyle? searchTextStyle; - - /// Style the search hint. - final TextStyle? searchHintStyle; - - /// Moves the selected items to the top of the list. - final bool separateSelectedItems; - - /// Set the color of the check in the checkbox - final Color? checkColor; - - MultiSelectBottomSheet({ - required this.items, - required this.initialValue, - this.title, - this.onSelectionChanged, - this.onConfirm, - this.listType, - this.cancelText, - this.confirmText, - this.searchable = false, - this.selectedColor, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.colorator, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHint, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - }); - - @override - _MultiSelectBottomSheetState createState() => - _MultiSelectBottomSheetState(items); -} - -class _MultiSelectBottomSheetState extends State> { - List _selectedValues = []; - bool _showSearch = false; - List> _items; - - _MultiSelectBottomSheetState(this._items); - - @override - void initState() { - super.initState(); - _selectedValues.addAll(widget.initialValue); - - for (int i = 0; i < _items.length; i++) { - if (_selectedValues.contains(_items[i].value)) { - _items[i].selected = true; - } - } - - if (widget.separateSelectedItems) { - _items = widget.separateSelected(_items); - } - } - - /// Returns a CheckboxListTile - Widget _buildListItem(MultiSelectItem item) { - return Theme( - data: ThemeData( - unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, - ), - child: CheckboxListTile( - checkColor: widget.checkColor, - value: item.selected, - activeColor: widget.colorator != null - ? widget.colorator!(item.value) ?? widget.selectedColor - : widget.selectedColor, - title: Text( - item.label, - style: item.selected - ? widget.selectedItemsTextStyle - : widget.itemsTextStyle, - ), - controlAffinity: ListTileControlAffinity.leading, - onChanged: (checked) { - setState(() { - _selectedValues = widget.onItemCheckedChange( - _selectedValues, item.value, checked!); - - if (checked) { - item.selected = true; - } else { - item.selected = false; - } - if (widget.separateSelectedItems) { - _items = widget.separateSelected(_items); - } - }); - if (widget.onSelectionChanged != null) { - widget.onSelectionChanged!(_selectedValues); - } - }, - ), - ); - } - - /// Returns a ChoiceChip - Widget _buildChipItem(MultiSelectItem item) { - return Container( - padding: const EdgeInsets.all(2.0), - child: ChoiceChip( - backgroundColor: widget.unselectedColor, - selectedColor: - widget.colorator != null && widget.colorator!(item.value) != null - ? widget.colorator!(item.value) - : widget.selectedColor != null - ? widget.selectedColor - : Theme.of(context).primaryColor.withOpacity(0.35), - label: Text( - item.label, - style: _selectedValues.contains(item.value) - ? TextStyle( - color: widget.selectedItemsTextStyle?.color ?? - widget.colorator?.call(item.value) ?? - widget.selectedColor?.withOpacity(1) ?? - Theme.of(context).primaryColor, - fontSize: widget.selectedItemsTextStyle != null - ? widget.selectedItemsTextStyle!.fontSize - : null, - ) - : widget.itemsTextStyle, - ), - selected: item.selected, - onSelected: (checked) { - if (checked) { - item.selected = true; - } else { - item.selected = false; - } - setState(() { - _selectedValues = widget.onItemCheckedChange( - _selectedValues, item.value, checked); - }); - if (widget.onSelectionChanged != null) { - widget.onSelectionChanged!(_selectedValues); - } - }, - ), - ); - } - - @override - Widget build(BuildContext context) { - return Container( - padding: - EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: DraggableScrollableSheet( - initialChildSize: widget.initialChildSize ?? 0.3, - minChildSize: widget.minChildSize ?? 0.3, - maxChildSize: widget.maxChildSize ?? 0.6, - expand: false, - builder: (BuildContext context, ScrollController scrollController) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _showSearch - ? Expanded( - child: Container( - padding: const EdgeInsets.only(left: 10), - child: TextField( - autofocus: true, - style: widget.searchTextStyle, - decoration: InputDecoration( - hintStyle: widget.searchHintStyle, - hintText: widget.searchHint ?? "Search", - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: widget.selectedColor ?? - Theme.of(context).primaryColor), - ), - ), - onChanged: (val) { - List> filteredList = []; - filteredList = widget.updateSearchQuery( - val, widget.items); - setState(() { - if (widget.separateSelectedItems) { - _items = - widget.separateSelected(filteredList); - } else { - _items = filteredList; - } - }); - }, - ), - ), - ) - : widget.title ?? - Text( - "Select", - style: TextStyle(fontSize: 18), - ), - widget.searchable - ? IconButton( - icon: _showSearch - ? widget.closeSearchIcon ?? Icon(Icons.close) - : widget.searchIcon ?? Icon(Icons.search), - onPressed: () { - setState(() { - _showSearch = !_showSearch; - if (!_showSearch) { - if (widget.separateSelectedItems) { - _items = - widget.separateSelected(widget.items); - } else { - _items = widget.items; - } - } - }); - }, - ) - : Padding( - padding: EdgeInsets.all(15), - ), - ], - ), - ), - Expanded( - child: widget.listType == null || - widget.listType == MultiSelectListType.LIST - ? ListView.builder( - controller: scrollController, - itemCount: _items.length, - itemBuilder: (context, index) { - return _buildListItem(_items[index]); - }, - ) - : SingleChildScrollView( - controller: scrollController, - child: Container( - padding: EdgeInsets.all(10), - child: Wrap( - children: _items.map(_buildChipItem).toList(), - ), - ), - ), - ), - Container( - padding: EdgeInsets.all(2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextButton( - onPressed: () { - widget.onCancelTap(context, widget.initialValue); - }, - child: widget.cancelText ?? - Text( - "CANCEL", - style: TextStyle( - color: (widget.selectedColor != null && - widget.selectedColor != - Colors.transparent) - ? widget.selectedColor!.withOpacity(1) - : Theme.of(context).primaryColor, - ), - ), - ), - ), - SizedBox(width: 10), - Expanded( - child: TextButton( - onPressed: () { - widget.onConfirmTap( - context, _selectedValues, widget.onConfirm); - }, - child: widget.confirmText ?? - Text( - "OK", - style: TextStyle( - color: (widget.selectedColor != null && - widget.selectedColor != - Colors.transparent) - ? widget.selectedColor!.withOpacity(1) - : Theme.of(context).primaryColor, - ), - ), - ), - ), - ], - ), - ), - ], - ); - }, - ), - ); - } -} +import 'package:flutter/material.dart'; +import '../util/multi_select_item.dart'; +import '../util/multi_select_actions.dart'; +import '../util/multi_select_list_type.dart'; + +/// A bottom sheet widget containing either a classic checkbox style list, or a chip style list. +class MultiSelectBottomSheet extends StatefulWidget + with MultiSelectActions { + /// List of items to select from. + final List> items; + + /// The list of selected values before interaction. + final List initialValue; + + /// The text at the top of the BottomSheet. + final Widget? title; + + /// Fires when the an item is selected / unselected. + final void Function(List)? onSelectionChanged; + + /// Fires when confirm is tapped. + final void Function(List)? onConfirm; + + /// Toggles search functionality. + final bool searchable; + + /// Text on the confirm button. + final Text? confirmText; + + /// Text on the cancel button. + final Text? cancelText; + + /// An enum that determines which type of list to render. + final MultiSelectListType? listType; + + /// Sets the color of the checkbox or chip when it's selected. + final Color? selectedColor; + + /// Set the initial height of the BottomSheet. + final double? initialChildSize; + + /// Set the minimum height threshold of the BottomSheet before it closes. + final double? minChildSize; + + /// Set the maximum height of the BottomSheet. + final double? maxChildSize; + + /// Set the placeholder text of the search field. + final String? searchHint; + + /// A function that sets the color of selected items based on their value. + /// It will either set the chip color, or the checkbox color depending on the list type. + final Color? Function(T)? colorator; + + /// Color of the chip body or checkbox border while not selected. + final Color? unselectedColor; + + /// Icon button that shows the search field. + final Icon? searchIcon; + + /// Icon button that hides the search field + final Icon? closeSearchIcon; + + /// Style the text on the chips or list tiles. + final TextStyle? itemsTextStyle; + + /// Style the text on the selected chips or list tiles. + final TextStyle? selectedItemsTextStyle; + + /// Style the search text. + final TextStyle? searchTextStyle; + + /// Style the search hint. + final TextStyle? searchHintStyle; + + /// Moves the selected items to the top of the list. + final bool separateSelectedItems; + + /// Set the color of the check in the checkbox + final Color? checkColor; + + MultiSelectBottomSheet({ + required this.items, + required this.initialValue, + this.title, + this.onSelectionChanged, + this.onConfirm, + this.listType, + this.cancelText, + this.confirmText, + this.searchable = false, + this.selectedColor, + this.initialChildSize, + this.minChildSize, + this.maxChildSize, + this.colorator, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHint, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + }); + + @override + _MultiSelectBottomSheetState createState() => + _MultiSelectBottomSheetState(items); +} + +class _MultiSelectBottomSheetState extends State> { + List _selectedValues = []; + bool _showSearch = false; + List> _items; + + _MultiSelectBottomSheetState(this._items); + + @override + void initState() { + super.initState(); + _selectedValues.addAll(widget.initialValue); + + for (int i = 0; i < _items.length; i++) { + if (_selectedValues.contains(_items[i].value)) { + _items[i].selected = true; + } + } + + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); + } + } + + /// Returns a CheckboxListTile + Widget _buildListItem(MultiSelectItem item) { + return Theme( + data: ThemeData( + unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, + ), + child: CheckboxListTile( + checkColor: widget.checkColor, + value: item.selected, + activeColor: widget.colorator != null + ? widget.colorator!(item.value) ?? widget.selectedColor + : widget.selectedColor, + title: Text( + item.label, + style: item.selected + ? widget.selectedItemsTextStyle + : widget.itemsTextStyle, + ), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (checked) { + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked!); + + if (checked) { + item.selected = true; + } else { + item.selected = false; + } + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); + } + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); + } + + /// Returns a ChoiceChip + Widget _buildChipItem(MultiSelectItem item) { + return Container( + padding: const EdgeInsets.all(2.0), + child: ChoiceChip( + backgroundColor: widget.unselectedColor, + selectedColor: + widget.colorator != null && widget.colorator!(item.value) != null + ? widget.colorator!(item.value) + : widget.selectedColor != null + ? widget.selectedColor + : Theme.of(context).primaryColor.withOpacity(0.35), + label: Text( + item.label, + style: _selectedValues.contains(item.value) + ? TextStyle( + color: widget.selectedItemsTextStyle?.color ?? + widget.colorator?.call(item.value) ?? + widget.selectedColor?.withOpacity(1) ?? + Theme.of(context).primaryColor, + fontSize: widget.selectedItemsTextStyle != null + ? widget.selectedItemsTextStyle!.fontSize + : null, + ) + : widget.itemsTextStyle, + ), + selected: item.selected, + onSelected: (checked) { + if (checked) { + item.selected = true; + } else { + item.selected = false; + } + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked); + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + MediaQueryData mData = MediaQuery.of(context); + return Container( + padding: + EdgeInsets.only(bottom: mData.viewInsets.bottom + (mData.viewPadding.bottom * 0.3)), + child: DraggableScrollableSheet( + initialChildSize: widget.initialChildSize ?? 0.3, + minChildSize: widget.minChildSize ?? 0.3, + maxChildSize: widget.maxChildSize ?? 0.6, + expand: false, + builder: (BuildContext context, ScrollController scrollController) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _showSearch + ? Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: TextField( + autofocus: true, + style: widget.searchTextStyle, + decoration: InputDecoration( + hintStyle: widget.searchHintStyle, + hintText: widget.searchHint ?? "Search", + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: widget.selectedColor ?? + Theme.of(context).primaryColor), + ), + ), + onChanged: (val) { + List> filteredList = []; + filteredList = widget.updateSearchQuery( + val, widget.items); + setState(() { + if (widget.separateSelectedItems) { + _items = + widget.separateSelected(filteredList); + } else { + _items = filteredList; + } + }); + }, + ), + ), + ) + : widget.title ?? + Text( + "Select", + style: TextStyle(fontSize: 18), + ), + widget.searchable + ? IconButton( + icon: _showSearch + ? widget.closeSearchIcon ?? Icon(Icons.close) + : widget.searchIcon ?? Icon(Icons.search), + onPressed: () { + setState(() { + _showSearch = !_showSearch; + if (!_showSearch) { + if (widget.separateSelectedItems) { + _items = + widget.separateSelected(widget.items); + } else { + _items = widget.items; + } + } + }); + }, + ) + : Padding( + padding: EdgeInsets.all(15), + ), + ], + ), + ), + Expanded( + child: widget.listType == null || + widget.listType == MultiSelectListType.LIST + ? ListView.builder( + controller: scrollController, + itemCount: _items.length, + itemBuilder: (context, index) { + return _buildListItem(_items[index]); + }, + ) + : SingleChildScrollView( + controller: scrollController, + child: Container( + padding: EdgeInsets.all(10), + child: Wrap( + children: _items.map(_buildChipItem).toList(), + ), + ), + ), + ), + Container( + padding: EdgeInsets.all(2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: TextButton( + onPressed: () { + /// Reset selection option to maintain UI + _items.forEach((e) { + if (e.selected && !widget.initialValue.contains(e.value)) { + e.selected = false; + } + }); + widget.onCancelTap(context, widget.initialValue); + }, + child: widget.cancelText ?? + Text( + "CANCEL", + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), + ), + ), + SizedBox(width: 10), + Expanded( + child: TextButton( + onPressed: () { + widget.onConfirmTap( + context, _selectedValues, widget.onConfirm); + }, + child: widget.confirmText ?? + Text( + "OK", + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/bottom_sheet/multi_select_bottom_sheet_field.dart b/lib/bottom_sheet/multi_select_bottom_sheet_field.dart index 8a1cf46..395dbde 100644 --- a/lib/bottom_sheet/multi_select_bottom_sheet_field.dart +++ b/lib/bottom_sheet/multi_select_bottom_sheet_field.dart @@ -1,485 +1,499 @@ -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import '../util/multi_select_list_type.dart'; -import '../chip_display/multi_select_chip_display.dart'; -import '../util/multi_select_item.dart'; -import 'multi_select_bottom_sheet.dart'; - -/// A customizable InkWell widget that opens the MultiSelectBottomSheet -// ignore: must_be_immutable -class MultiSelectBottomSheetField extends FormField> { - /// Style the Container that makes up the field. - final BoxDecoration? decoration; - - /// Set text that is displayed on the button. - final Text? buttonText; - - /// Specify the button icon. - final Icon? buttonIcon; - - /// List of items to select from. - final List> items; - - /// The list of selected values before interaction. - final List? initialValue; - - /// The text at the top of the dialog. - final Widget? title; - - /// Fires when the an item is selected / unselected. - final void Function(List)? onSelectionChanged; - - /// Fires when confirm is tapped. - final void Function(List) onConfirm; - - /// Toggles search functionality. - final bool searchable; - - /// Text on the confirm button. - final Text? confirmText; - - /// Text on the cancel button. - final Text? cancelText; - - /// An enum that determines which type of list to render. - final MultiSelectListType? listType; - - /// Sets the color of the checkbox or chip body when selected. - final Color? selectedColor; - - /// Set the hint text of the search field. - final String? searchHint; - - /// Set the initial height of the BottomSheet. - final double? initialChildSize; - - /// Set the minimum height threshold of the BottomSheet before it closes. - final double? minChildSize; - - /// Set the maximum height of the BottomSheet. - final double? maxChildSize; - - /// Apply a ShapeBorder to alter the edges of the BottomSheet. - final ShapeBorder? shape; - - /// Set the color of the space outside the BottomSheet. - final Color? barrierColor; - - /// Overrides the default MultiSelectChipDisplay attached to this field. - /// If you want to remove it, use MultiSelectChipDisplay.none(). - final MultiSelectChipDisplay? chipDisplay; - - /// A function that sets the color of selected items based on their value. - /// It will either set the chip color, or the checkbox color depending on the list type. - final Color Function(V)? colorator; - - /// Set the background color of the bottom sheet. - final Color? backgroundColor; - - /// Color of the chip body or checkbox border while not selected. - final Color? unselectedColor; - - /// Replaces the deafult search icon when searchable is true. - final Icon? searchIcon; - - /// Replaces the default close search icon when searchable is true. - final Icon? closeSearchIcon; - - /// The TextStyle of the items within the BottomSheet. - final TextStyle? itemsTextStyle; - - /// Style the text on the selected chips or list tiles. - final TextStyle? selectedItemsTextStyle; - - /// Moves the selected items to the top of the list. - final bool separateSelectedItems; - - /// Style the text that is typed into the search field. - final TextStyle? searchTextStyle; - - /// Style the search hint. - final TextStyle? searchHintStyle; - - /// Set the color of the check in the checkbox - final Color? checkColor; - - final AutovalidateMode autovalidateMode; - final FormFieldValidator>? validator; - final FormFieldSetter>? onSaved; - final GlobalKey? key; - FormFieldState>? state; - - MultiSelectBottomSheetField({ - required this.items, - required this.onConfirm, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.chipDisplay, - this.initialValue, - this.searchable = false, - this.confirmText, - this.cancelText, - this.selectedColor, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.shape, - this.barrierColor, - this.searchHint, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - this.key, - this.onSaved, - this.validator, - this.autovalidateMode = AutovalidateMode.disabled, - }) : super( - key: key, - onSaved: onSaved, - validator: validator, - autovalidateMode: autovalidateMode, - initialValue: initialValue ?? [], - builder: (FormFieldState> state) { - _MultiSelectBottomSheetFieldView view = - _MultiSelectBottomSheetFieldView( - items: items, - decoration: decoration, - unselectedColor: unselectedColor, - colorator: colorator, - itemsTextStyle: itemsTextStyle, - selectedItemsTextStyle: selectedItemsTextStyle, - backgroundColor: backgroundColor, - title: title, - initialValue: initialValue, - barrierColor: barrierColor, - buttonIcon: buttonIcon, - buttonText: buttonText, - cancelText: cancelText, - chipDisplay: chipDisplay, - closeSearchIcon: closeSearchIcon, - confirmText: confirmText, - initialChildSize: initialChildSize, - listType: listType, - maxChildSize: maxChildSize, - minChildSize: minChildSize, - onConfirm: onConfirm, - onSelectionChanged: onSelectionChanged, - searchHintStyle: searchHintStyle, - searchIcon: searchIcon, - searchHint: searchHint, - searchTextStyle: searchTextStyle, - searchable: searchable, - selectedColor: selectedColor, - separateSelectedItems: separateSelectedItems, - shape: shape, - checkColor: checkColor, - ); - return _MultiSelectBottomSheetFieldView._withState( - view as _MultiSelectBottomSheetFieldView, state); - }); -} - -// ignore: must_be_immutable -class _MultiSelectBottomSheetFieldView extends StatefulWidget { - final BoxDecoration? decoration; - final Text? buttonText; - final Icon? buttonIcon; - final List> items; - final List? initialValue; - final Widget? title; - final void Function(List)? onSelectionChanged; - final void Function(List)? onConfirm; - final bool searchable; - final Text? confirmText; - final Text? cancelText; - final MultiSelectListType? listType; - final Color? selectedColor; - final String? searchHint; - final double? initialChildSize; - final double? minChildSize; - final double? maxChildSize; - final ShapeBorder? shape; - final Color? barrierColor; - final MultiSelectChipDisplay? chipDisplay; - final Color Function(V)? colorator; - final Color? backgroundColor; - final Color? unselectedColor; - final Icon? searchIcon; - final Icon? closeSearchIcon; - final TextStyle? itemsTextStyle; - final TextStyle? selectedItemsTextStyle; - final TextStyle? searchTextStyle; - final TextStyle? searchHintStyle; - final bool separateSelectedItems; - final Color? checkColor; - FormFieldState>? state; - - _MultiSelectBottomSheetFieldView({ - required this.items, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.onConfirm, - this.chipDisplay, - this.initialValue, - this.searchable = false, - this.confirmText, - this.cancelText, - this.selectedColor, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.shape, - this.barrierColor, - this.searchHint, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - }); - - /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectBottomSheetField. - _MultiSelectBottomSheetFieldView._withState( - _MultiSelectBottomSheetFieldView field, FormFieldState> state) - : items = field.items, - title = field.title, - buttonText = field.buttonText, - buttonIcon = field.buttonIcon, - listType = field.listType, - decoration = field.decoration, - onSelectionChanged = field.onSelectionChanged, - onConfirm = field.onConfirm, - chipDisplay = field.chipDisplay, - initialValue = field.initialValue, - searchable = field.searchable, - confirmText = field.confirmText, - cancelText = field.cancelText, - selectedColor = field.selectedColor, - initialChildSize = field.initialChildSize, - minChildSize = field.minChildSize, - maxChildSize = field.maxChildSize, - shape = field.shape, - barrierColor = field.barrierColor, - searchHint = field.searchHint, - colorator = field.colorator, - backgroundColor = field.backgroundColor, - unselectedColor = field.unselectedColor, - searchIcon = field.searchIcon, - closeSearchIcon = field.closeSearchIcon, - itemsTextStyle = field.itemsTextStyle, - searchHintStyle = field.searchHintStyle, - searchTextStyle = field.searchTextStyle, - selectedItemsTextStyle = field.selectedItemsTextStyle, - separateSelectedItems = field.separateSelectedItems, - checkColor = field.checkColor, - state = state; - - @override - __MultiSelectBottomSheetFieldViewState createState() => - __MultiSelectBottomSheetFieldViewState(); -} - -class __MultiSelectBottomSheetFieldViewState - extends State<_MultiSelectBottomSheetFieldView> { - List _selectedItems = []; - - @override - void initState() { - super.initState(); - if (widget.initialValue != null) { - _selectedItems.addAll(widget.initialValue!); - } - } - - Widget _buildInheritedChipDisplay() { - List?> chipDisplayItems = []; - chipDisplayItems = _selectedItems - .map((e) => - widget.items.firstWhereOrNull((element) => e == element.value)) - .toList(); - chipDisplayItems.removeWhere((element) => element == null); - if (widget.chipDisplay != null) { - // if user has specified a chipDisplay, use its params - if (widget.chipDisplay!.disabled!) { - return Container(); - } else { - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.chipDisplay!.colorator ?? widget.colorator, - onTap: (item) { - List? newValues; - if (widget.chipDisplay!.onTap != null) { - dynamic result = widget.chipDisplay!.onTap!(item); - if (result is List) newValues = result; - } - if (newValues != null) { - _selectedItems = newValues; - if (widget.state != null) { - widget.state!.didChange(_selectedItems); - } - } - }, - decoration: widget.chipDisplay!.decoration, - chipColor: widget.chipDisplay!.chipColor ?? - ((widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null), - alignment: widget.chipDisplay!.alignment, - textStyle: widget.chipDisplay!.textStyle, - icon: widget.chipDisplay!.icon, - shape: widget.chipDisplay!.shape, - scroll: widget.chipDisplay!.scroll, - scrollBar: widget.chipDisplay!.scrollBar, - height: widget.chipDisplay!.height, - chipWidth: widget.chipDisplay!.chipWidth, - ); - } - } else { - // user didn't specify a chipDisplay, build the default - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.colorator, - chipColor: (widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null, - ); - } - } - - _showBottomSheet(BuildContext ctx) async { - await showModalBottomSheet( - backgroundColor: widget.backgroundColor, - barrierColor: widget.barrierColor, - shape: widget.shape ?? - RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), - ), - isScrollControlled: true, - context: context, - builder: (context) { - return MultiSelectBottomSheet( - checkColor: widget.checkColor, - selectedItemsTextStyle: widget.selectedItemsTextStyle, - searchTextStyle: widget.searchTextStyle, - searchHintStyle: widget.searchHintStyle, - itemsTextStyle: widget.itemsTextStyle, - searchIcon: widget.searchIcon, - closeSearchIcon: widget.closeSearchIcon, - unselectedColor: widget.unselectedColor, - colorator: widget.colorator, - searchHint: widget.searchHint, - selectedColor: widget.selectedColor, - listType: widget.listType, - items: widget.items, - cancelText: widget.cancelText, - confirmText: widget.confirmText, - separateSelectedItems: widget.separateSelectedItems, - initialValue: _selectedItems, - onConfirm: (selected) { - if (widget.state != null) { - widget.state!.didChange(selected); - } - _selectedItems = selected; - if (widget.onConfirm != null) widget.onConfirm!(selected); - }, - onSelectionChanged: widget.onSelectionChanged, - searchable: widget.searchable, - title: widget.title, - initialChildSize: widget.initialChildSize, - minChildSize: widget.minChildSize, - maxChildSize: widget.maxChildSize, - ); - }); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - InkWell( - onTap: () { - _showBottomSheet(context); - }, - child: Container( - decoration: widget.state != null - ? widget.decoration ?? - BoxDecoration( - border: Border( - bottom: BorderSide( - color: widget.state != null && widget.state!.hasError - ? Colors.red.shade800.withOpacity(0.6) - : _selectedItems.isNotEmpty - ? (widget.selectedColor != null && - widget.selectedColor != - Colors.transparent) - ? widget.selectedColor! - : Theme.of(context).primaryColor - : Colors.black45, - width: _selectedItems.isNotEmpty - ? (widget.state != null && widget.state!.hasError) - ? 1.4 - : 1.8 - : 1.2, - ), - ), - ) - : widget.decoration, - padding: EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - widget.buttonText ?? Text("Select"), - widget.buttonIcon ?? Icon(Icons.arrow_downward), - ], - ), - ), - ), - _buildInheritedChipDisplay(), - widget.state != null && widget.state!.hasError - ? SizedBox(height: 5) - : Container(), - widget.state != null && widget.state!.hasError - ? Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 4), - child: Text( - widget.state!.errorText!, - style: TextStyle( - color: Colors.red[800], - fontSize: 12.5, - ), - ), - ), - ], - ) - : Container(), - ], - ); - } -} +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import '../util/multi_select_list_type.dart'; +import '../chip_display/multi_select_chip_display.dart'; +import '../util/multi_select_item.dart'; +import 'multi_select_bottom_sheet.dart'; + +/// A customizable InkWell widget that opens the MultiSelectBottomSheet +// ignore: must_be_immutable +class MultiSelectBottomSheetField extends FormField> { + /// Style the Container that makes up the field. + final BoxDecoration? decoration; + + /// Set text that is displayed on the button. + final Text? buttonText; + + /// Specify the button icon. + final Icon? buttonIcon; + + /// List of items to select from. + final List> items; + + /// The list of selected values before interaction. + final List? initialValue; + + /// The text at the top of the dialog. + final Widget? title; + + /// Fires when the an item is selected / unselected. + final void Function(List)? onSelectionChanged; + + /// Fires when confirm is tapped. + final void Function(List) onConfirm; + + /// Toggles search functionality. + final bool searchable; + + /// Text on the confirm button. + final Text? confirmText; + + /// Text on the cancel button. + final Text? cancelText; + + /// An enum that determines which type of list to render. + final MultiSelectListType? listType; + + /// Sets the color of the checkbox or chip body when selected. + final Color? selectedColor; + + /// Set the hint text of the search field. + final String? searchHint; + + /// Set the initial height of the BottomSheet. + final double? initialChildSize; + + /// Set the minimum height threshold of the BottomSheet before it closes. + final double? minChildSize; + + /// Set the maximum height of the BottomSheet. + final double? maxChildSize; + + /// Apply a ShapeBorder to alter the edges of the BottomSheet. + final ShapeBorder? shape; + + /// Set the color of the space outside the BottomSheet. + final Color? barrierColor; + + /// Overrides the default MultiSelectChipDisplay attached to this field. + /// If you want to remove it, use MultiSelectChipDisplay.none(). + final MultiSelectChipDisplay? chipDisplay; + + /// A function that sets the color of selected items based on their value. + /// It will either set the chip color, or the checkbox color depending on the list type. + final Color Function(V)? colorator; + + /// Set the background color of the bottom sheet. + final Color? backgroundColor; + + /// Color of the chip body or checkbox border while not selected. + final Color? unselectedColor; + + /// Replaces the deafult search icon when searchable is true. + final Icon? searchIcon; + + /// Replaces the default close search icon when searchable is true. + final Icon? closeSearchIcon; + + /// The TextStyle of the items within the BottomSheet. + final TextStyle? itemsTextStyle; + + /// Style the text on the selected chips or list tiles. + final TextStyle? selectedItemsTextStyle; + + /// Moves the selected items to the top of the list. + final bool separateSelectedItems; + + /// Style the text that is typed into the search field. + final TextStyle? searchTextStyle; + + /// Style the search hint. + final TextStyle? searchHintStyle; + + /// Set the color of the check in the checkbox + final Color? checkColor; + + final AutovalidateMode autovalidateMode; + final FormFieldValidator>? validator; + final FormFieldSetter>? onSaved; + final GlobalKey? key; + FormFieldState>? state; + + MultiSelectBottomSheetField({ + required this.items, + required this.onConfirm, + this.title, + this.buttonText, + this.buttonIcon, + this.listType, + this.decoration, + this.onSelectionChanged, + this.chipDisplay, + this.initialValue, + this.searchable = false, + this.confirmText, + this.cancelText, + this.selectedColor, + this.initialChildSize, + this.minChildSize, + this.maxChildSize, + this.shape, + this.barrierColor, + this.searchHint, + this.colorator, + this.backgroundColor, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + this.key, + this.onSaved, + this.validator, + this.autovalidateMode = AutovalidateMode.disabled, + }) : super( + key: key, + onSaved: onSaved, + validator: validator, + autovalidateMode: autovalidateMode, + initialValue: initialValue ?? [], + builder: (FormFieldState> state) { + _MultiSelectBottomSheetFieldView view = + _MultiSelectBottomSheetFieldView( + items: items, + decoration: decoration, + unselectedColor: unselectedColor, + colorator: colorator, + itemsTextStyle: itemsTextStyle, + selectedItemsTextStyle: selectedItemsTextStyle, + backgroundColor: backgroundColor, + title: title, + initialValue: initialValue, + barrierColor: barrierColor, + buttonIcon: buttonIcon, + buttonText: buttonText, + cancelText: cancelText, + chipDisplay: chipDisplay, + closeSearchIcon: closeSearchIcon, + confirmText: confirmText, + initialChildSize: initialChildSize, + listType: listType, + maxChildSize: maxChildSize, + minChildSize: minChildSize, + onConfirm: onConfirm, + onSelectionChanged: onSelectionChanged, + searchHintStyle: searchHintStyle, + searchIcon: searchIcon, + searchHint: searchHint, + searchTextStyle: searchTextStyle, + searchable: searchable, + selectedColor: selectedColor, + separateSelectedItems: separateSelectedItems, + shape: shape, + checkColor: checkColor, + ); + return _MultiSelectBottomSheetFieldView._withState( + view as _MultiSelectBottomSheetFieldView, state); + }); +} + +// ignore: must_be_immutable +class _MultiSelectBottomSheetFieldView extends StatefulWidget { + final BoxDecoration? decoration; + final Text? buttonText; + final Icon? buttonIcon; + final List> items; + final List? initialValue; + final Widget? title; + final void Function(List)? onSelectionChanged; + final void Function(List)? onConfirm; + final bool searchable; + final Text? confirmText; + final Text? cancelText; + final MultiSelectListType? listType; + final Color? selectedColor; + final String? searchHint; + final double? initialChildSize; + final double? minChildSize; + final double? maxChildSize; + final ShapeBorder? shape; + final Color? barrierColor; + final MultiSelectChipDisplay? chipDisplay; + final Color Function(V)? colorator; + final Color? backgroundColor; + final Color? unselectedColor; + final Icon? searchIcon; + final Icon? closeSearchIcon; + final TextStyle? itemsTextStyle; + final TextStyle? selectedItemsTextStyle; + final TextStyle? searchTextStyle; + final TextStyle? searchHintStyle; + final bool separateSelectedItems; + final Color? checkColor; + FormFieldState>? state; + + _MultiSelectBottomSheetFieldView({ + required this.items, + this.title, + this.buttonText, + this.buttonIcon, + this.listType, + this.decoration, + this.onSelectionChanged, + this.onConfirm, + this.chipDisplay, + this.initialValue, + this.searchable = false, + this.confirmText, + this.cancelText, + this.selectedColor, + this.initialChildSize, + this.minChildSize, + this.maxChildSize, + this.shape, + this.barrierColor, + this.searchHint, + this.colorator, + this.backgroundColor, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + }); + + /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectBottomSheetField. + _MultiSelectBottomSheetFieldView._withState( + _MultiSelectBottomSheetFieldView field, FormFieldState> state) + : items = field.items, + title = field.title, + buttonText = field.buttonText, + buttonIcon = field.buttonIcon, + listType = field.listType, + decoration = field.decoration, + onSelectionChanged = field.onSelectionChanged, + onConfirm = field.onConfirm, + chipDisplay = field.chipDisplay, + initialValue = field.initialValue, + searchable = field.searchable, + confirmText = field.confirmText, + cancelText = field.cancelText, + selectedColor = field.selectedColor, + initialChildSize = field.initialChildSize, + minChildSize = field.minChildSize, + maxChildSize = field.maxChildSize, + shape = field.shape, + barrierColor = field.barrierColor, + searchHint = field.searchHint, + colorator = field.colorator, + backgroundColor = field.backgroundColor, + unselectedColor = field.unselectedColor, + searchIcon = field.searchIcon, + closeSearchIcon = field.closeSearchIcon, + itemsTextStyle = field.itemsTextStyle, + searchHintStyle = field.searchHintStyle, + searchTextStyle = field.searchTextStyle, + selectedItemsTextStyle = field.selectedItemsTextStyle, + separateSelectedItems = field.separateSelectedItems, + checkColor = field.checkColor, + state = state; + + @override + __MultiSelectBottomSheetFieldViewState createState() => + __MultiSelectBottomSheetFieldViewState(); +} + +class __MultiSelectBottomSheetFieldViewState + extends State<_MultiSelectBottomSheetFieldView> { + List _selectedItems = []; + + @override + void initState() { + super.initState(); + if (widget.initialValue != null) { + _selectedItems.addAll(widget.initialValue!); + } + } + + Widget _buildInheritedChipDisplay() { + List?> chipDisplayItems = []; + chipDisplayItems = _selectedItems + .map((e) => + widget.items.firstWhereOrNull((element) => e == element.value)) + .toList(); + chipDisplayItems.removeWhere((element) => element == null); + if (widget.chipDisplay != null) { + // if user has specified a chipDisplay, use its params + if (widget.chipDisplay!.disabled!) { + return Container(); + } else { + return MultiSelectChipDisplay( + items: chipDisplayItems, + colorator: widget.chipDisplay!.colorator ?? widget.colorator, + onTap: (item) { + List? newValues; + if (widget.chipDisplay!.onTap != null) { + dynamic result = widget.chipDisplay!.onTap!(item); + if (result is List) newValues = result; + } + if (newValues != null) { + _selectedItems = newValues; + if (widget.state != null) { + widget.state!.didChange(_selectedItems); + } + } + }, + decoration: widget.chipDisplay!.decoration, + chipColor: widget.chipDisplay!.chipColor ?? + ((widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(0.35) + : null), + alignment: widget.chipDisplay!.alignment, + textStyle: widget.chipDisplay!.textStyle, + icon: widget.chipDisplay!.icon, + shape: widget.chipDisplay!.shape, + scroll: widget.chipDisplay!.scroll, + scrollBar: widget.chipDisplay!.scrollBar, + height: widget.chipDisplay!.height, + chipWidth: widget.chipDisplay!.chipWidth, + ); + } + } else { + // user didn't specify a chipDisplay, build the default + return MultiSelectChipDisplay( + items: chipDisplayItems, + colorator: widget.colorator, + chipColor: (widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(0.35) + : null, + ); + } + } + + _showBottomSheet(BuildContext ctx) async { + await showModalBottomSheet( + backgroundColor: widget.backgroundColor, + barrierColor: widget.barrierColor, + shape: widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), + ), + isScrollControlled: true, + context: context, + builder: (context) { + return MultiSelectBottomSheet( + checkColor: widget.checkColor, + selectedItemsTextStyle: widget.selectedItemsTextStyle, + searchTextStyle: widget.searchTextStyle, + searchHintStyle: widget.searchHintStyle, + itemsTextStyle: widget.itemsTextStyle, + searchIcon: widget.searchIcon, + closeSearchIcon: widget.closeSearchIcon, + unselectedColor: widget.unselectedColor, + colorator: widget.colorator, + searchHint: widget.searchHint, + selectedColor: widget.selectedColor, + listType: widget.listType, + items: widget.items, + cancelText: widget.cancelText, + confirmText: widget.confirmText, + separateSelectedItems: widget.separateSelectedItems, + initialValue: _selectedItems, + onConfirm: (selected) { + if (widget.state != null) { + widget.state!.didChange(selected); + } + _selectedItems = selected; + if (widget.onConfirm != null) widget.onConfirm!(selected); + }, + onSelectionChanged: widget.onSelectionChanged, + searchable: widget.searchable, + title: widget.title, + initialChildSize: widget.initialChildSize, + minChildSize: widget.minChildSize, + maxChildSize: widget.maxChildSize, + ); + }); + } + + @override + Widget build(BuildContext context) { + /// Check for fix for changing objects + if (widget.initialValue != null) { + if ((widget.initialValue ?? []).isNotEmpty) { + final arrList = widget.items.map((e) => e.value); + final arr = _selectedItems.where((e) => !arrList.contains(e)).toList(); + if (arr.isNotEmpty || (_selectedItems.length != (widget.initialValue ?? []).length)) { + _selectedItems.clear(); + _selectedItems.addAll(widget.initialValue!); + } + } else { + _selectedItems.clear(); + } + } + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + InkWell( + onTap: () { + _showBottomSheet(context); + }, + child: Container( + decoration: widget.state != null + ? widget.decoration ?? + BoxDecoration( + border: Border( + bottom: BorderSide( + color: widget.state != null && widget.state!.hasError + ? Colors.red.shade800.withOpacity(0.6) + : _selectedItems.isNotEmpty + ? (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor! + : Theme.of(context).primaryColor + : Colors.black45, + width: _selectedItems.isNotEmpty + ? (widget.state != null && widget.state!.hasError) + ? 1.4 + : 1.8 + : 1.2, + ), + ), + ) + : widget.decoration, + padding: EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + widget.buttonText ?? Text("Select"), + widget.buttonIcon ?? Icon(Icons.arrow_downward), + ], + ), + ), + ), + _buildInheritedChipDisplay(), + widget.state != null && widget.state!.hasError + ? SizedBox(height: 5) + : Container(), + widget.state != null && widget.state!.hasError + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 4), + child: Text( + widget.state!.errorText!, + style: TextStyle( + color: Colors.red[800], + fontSize: 12.5, + ), + ), + ), + ], + ) + : Container(), + ], + ); + } +} diff --git a/lib/chip_display/multi_select_chip_display.dart b/lib/chip_display/multi_select_chip_display.dart index a90a287..e60355e 100644 --- a/lib/chip_display/multi_select_chip_display.dart +++ b/lib/chip_display/multi_select_chip_display.dart @@ -1,173 +1,175 @@ -import 'package:flutter/material.dart'; -import '../util/horizontal_scrollbar.dart'; -import '../util/multi_select_item.dart'; - -/// A widget meant to display selected values as chips. -// ignore: must_be_immutable -class MultiSelectChipDisplay extends StatelessWidget { - /// The source list of selected items. - final List?>? items; - - /// Fires when a chip is tapped. - final Function(V)? onTap; - - /// Set the chip color. - final Color? chipColor; - - /// Change the alignment of the chips. - final Alignment? alignment; - - /// Style the Container that makes up the chip display. - final BoxDecoration? decoration; - - /// Style the text on the chips. - final TextStyle? textStyle; - - /// A function that sets the color of selected items based on their value. - final Color? Function(V)? colorator; - - /// An icon to display prior to the chip's label. - final Icon? icon; - - /// Set a ShapeBorder. Typically a RoundedRectangularBorder. - final ShapeBorder? shape; - - /// Enables horizontal scrolling. - final bool scroll; - - /// Enables the scrollbar when scroll is `true`. - final HorizontalScrollBar? scrollBar; - - final ScrollController _scrollController = ScrollController(); - - /// Set a fixed height. - final double? height; - - /// Set the width of the chips. - final double? chipWidth; - - bool? disabled; - - MultiSelectChipDisplay({ - this.items, - this.onTap, - this.chipColor, - this.alignment, - this.decoration, - this.textStyle, - this.colorator, - this.icon, - this.shape, - this.scroll = false, - this.scrollBar, - this.height, - this.chipWidth, - }) { - this.disabled = false; - } - - MultiSelectChipDisplay.none({ - this.items = const [], - this.disabled = true, - this.onTap, - this.chipColor, - this.alignment, - this.decoration, - this.textStyle, - this.colorator, - this.icon, - this.shape, - this.scroll = false, - this.scrollBar, - this.height, - this.chipWidth, - }); - - @override - Widget build(BuildContext context) { - if (items == null || items!.isEmpty) return Container(); - return Container( - decoration: decoration, - alignment: alignment ?? Alignment.centerLeft, - padding: EdgeInsets.symmetric(horizontal: scroll ? 0 : 10), - child: scroll - ? Container( - width: MediaQuery.of(context).size.width, - height: height ?? MediaQuery.of(context).size.height * 0.08, - child: scrollBar != null - ? Scrollbar( - isAlwaysShown: scrollBar!.isAlwaysShown, - controller: _scrollController, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: items!.length, - itemBuilder: (ctx, index) { - return _buildItem(items![index]!, context); - }, - ), - ) - : ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: items!.length, - itemBuilder: (ctx, index) { - return _buildItem(items![index]!, context); - }, - ), - ) - : Wrap( - children: items != null - ? items!.map((item) => _buildItem(item!, context)).toList() - : [ - Container(), - ], - ), - ); - } - - Widget _buildItem(MultiSelectItem item, BuildContext context) { - return Container( - padding: const EdgeInsets.all(2.0), - child: ChoiceChip( - shape: shape as OutlinedBorder?, - avatar: icon != null - ? Icon( - icon!.icon, - color: colorator != null && colorator!(item.value) != null - ? colorator!(item.value)!.withOpacity(1) - : icon!.color ?? Theme.of(context).primaryColor, - ) - : null, - label: Container( - width: chipWidth, - child: Text( - item.label, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: colorator != null && colorator!(item.value) != null - ? textStyle != null - ? textStyle!.color ?? colorator!(item.value) - : colorator!(item.value) - : textStyle != null && textStyle!.color != null - ? textStyle!.color - : chipColor != null - ? chipColor!.withOpacity(1) - : null, - fontSize: textStyle != null ? textStyle!.fontSize : null, - ), - ), - ), - selected: items!.contains(item), - selectedColor: colorator != null && colorator!(item.value) != null - ? colorator!(item.value) - : chipColor != null - ? chipColor - : Theme.of(context).primaryColor.withOpacity(0.33), - onSelected: (_) { - if (onTap != null) onTap!(item.value); - }, - ), - ); - } -} +import 'package:flutter/material.dart'; +import '../util/horizontal_scrollbar.dart'; +import '../util/multi_select_item.dart'; + +/// A widget meant to display selected values as chips. +// ignore: must_be_immutable +class MultiSelectChipDisplay extends StatelessWidget { + /// The source list of selected items. + final List?>? items; + + /// Fires when a chip is tapped. + final Function(V)? onTap; + + /// Set the chip color. + final Color? chipColor; + + /// Change the alignment of the chips. + final Alignment? alignment; + + /// Style the Container that makes up the chip display. + final BoxDecoration? decoration; + + /// Style the text on the chips. + final TextStyle? textStyle; + + /// A function that sets the color of selected items based on their value. + final Color? Function(V)? colorator; + + /// An icon to display prior to the chip's label. + final Icon? icon; + + /// Set a ShapeBorder. Typically a RoundedRectangularBorder. + final ShapeBorder? shape; + + /// Enables horizontal scrolling. + final bool scroll; + + /// Enables the scrollbar when scroll is `true`. + final HorizontalScrollBar? scrollBar; + + final ScrollController _scrollController = ScrollController(); + + /// Set a fixed height. + final double? height; + + /// Set the width of the chips. + final double? chipWidth; + + bool? disabled; + + MultiSelectChipDisplay({ + this.items, + this.onTap, + this.chipColor, + this.alignment, + this.decoration, + this.textStyle, + this.colorator, + this.icon, + this.shape, + this.scroll = false, + this.scrollBar, + this.height, + this.chipWidth, + }) { + this.disabled = false; + } + + MultiSelectChipDisplay.none({ + this.items = const [], + this.disabled = true, + this.onTap, + this.chipColor, + this.alignment, + this.decoration, + this.textStyle, + this.colorator, + this.icon, + this.shape, + this.scroll = false, + this.scrollBar, + this.height, + this.chipWidth, + }); + + @override + Widget build(BuildContext context) { + if (items == null || items!.isEmpty) return Container(); + return Container( + decoration: decoration, + alignment: alignment ?? Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: scroll ? 0 : 10), + child: scroll + ? Container( + width: MediaQuery.of(context).size.width, + height: height ?? MediaQuery.of(context).size.height * 0.08, + child: scrollBar != null + ? Scrollbar( + isAlwaysShown: scrollBar!.isAlwaysShown, + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: items!.length, + itemBuilder: (ctx, index) { + return _buildItem(items![index]!, context); + }, + ), + ) + : ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: items!.length, + itemBuilder: (ctx, index) { + return _buildItem(items![index]!, context); + }, + ), + ) + : Wrap( + children: items != null + ? items!.map((item) => _buildItem(item!, context)).toList() + : [ + Container(), + ], + ), + ); + } + + Widget _buildItem(MultiSelectItem item, BuildContext context) { + return Container( + padding: const EdgeInsets.all(2.0), + margin: const EdgeInsets.symmetric(vertical: 2.0), + child: ChoiceChip( + shape: shape as OutlinedBorder?, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + avatar: icon != null + ? Icon( + icon!.icon, + color: colorator != null && colorator!(item.value) != null + ? colorator!(item.value)!.withOpacity(1) + : icon!.color ?? Theme.of(context).primaryColor, + ) + : null, + label: Container( + width: chipWidth, + child: Text( + item.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: colorator != null && colorator!(item.value) != null + ? textStyle != null + ? textStyle!.color ?? colorator!(item.value) + : colorator!(item.value) + : textStyle != null && textStyle!.color != null + ? textStyle!.color + : chipColor != null + ? chipColor!.withOpacity(1) + : null, + fontSize: textStyle != null ? textStyle!.fontSize : null, + ), + ), + ), + selected: items!.contains(item), + selectedColor: colorator != null && colorator!(item.value) != null + ? colorator!(item.value) + : chipColor != null + ? chipColor + : Theme.of(context).primaryColor.withOpacity(0.33), + onSelected: (_) { + if (onTap != null) onTap!(item.value); + }, + ), + ); + } +} diff --git a/lib/chip_field/multi_select_chip_field.dart b/lib/chip_field/multi_select_chip_field.dart index 1f420ce..0257ebd 100644 --- a/lib/chip_field/multi_select_chip_field.dart +++ b/lib/chip_field/multi_select_chip_field.dart @@ -1,531 +1,531 @@ -import 'package:flutter/material.dart'; -import '../multi_select_flutter.dart'; - -class MultiSelectChipField extends FormField> { - /// Style the Container that makes up the field. - final BoxDecoration? decoration; - - /// List of items to select from. - final List> items; - - /// Color of the chip while not selected. - final Color? chipColor; - - /// Sets the color of the chip while selected. - final Color? selectedChipColor; - - /// Style the text of the chips. - final TextStyle? textStyle; - - /// Style the text of the selected chips. - final TextStyle? selectedTextStyle; - - /// The icon displayed in front of text on selected chips. - final Icon? icon; - - /// Replaces the deafult search icon when searchable is true. - final Icon? searchIcon; - - /// Replaces the default close search icon when searchable is true. - final Icon? closeSearchIcon; - - /// Set a ShapeBorder. Typically a RoundedRectangularBorder. - final ShapeBorder? chipShape; - - /// Defines the header text. - final Text? title; - - /// Enables horizontal scrolling. Default is true. - final bool scroll; - - /// A function that sets the color of selected items based on their value. - final Color Function(V)? colorator; - - /// Fires when a chip is tapped. A good time to store the selected values. - final Function(List)? onTap; - - /// Enables search functionality. - final bool? searchable; - - /// Set the search hint. - final String? searchHint; - - /// Set the TextStyle of the search hint. - final TextStyle? searchHintStyle; - - /// Set the TextStyle of the text that gets typed into the search bar. - final TextStyle? searchTextStyle; - - /// Set the header color. - final Color? headerColor; - - /// Build a custom widget that gets created dynamically for each item. - final Widget Function(MultiSelectItem, FormFieldState>)? - itemBuilder; - - /// Set the height of the selectable area. - final double? height; - - /// Make use of the ScrollController to automatically scroll through the list. - final Function(ScrollController)? scrollControl; - - /// Define a HorizontalScrollBar. - final HorizontalScrollBar? scrollBar; - - /// Determines whether to show the header. - final bool showHeader; - - /// Set the width of the chip. - final double? chipWidth; - - final List? initialValue; - final AutovalidateMode autovalidateMode; - final FormFieldValidator>? validator; - final FormFieldSetter>? onSaved; - final GlobalKey? key; - - MultiSelectChipField({ - required this.items, - this.decoration, - this.chipColor, - this.selectedChipColor, - this.colorator, - this.textStyle, - this.selectedTextStyle, - this.icon, - this.searchIcon, - this.closeSearchIcon, - this.chipShape, - this.onTap, - this.title, - this.scroll = true, - this.searchable, - this.searchHint, - this.searchHintStyle, - this.searchTextStyle, - this.headerColor, - this.key, - this.onSaved, - this.validator, - this.autovalidateMode = AutovalidateMode.disabled, - this.initialValue, - this.itemBuilder, - this.height, - this.scrollControl, - this.scrollBar, - this.showHeader = true, - this.chipWidth, - }) : super( - key: key, - onSaved: onSaved, - validator: validator, - autovalidateMode: autovalidateMode, - initialValue: initialValue ?? [], - builder: (FormFieldState> state) { - _MultiSelectChipFieldView view = _MultiSelectChipFieldView( - items: items, - decoration: decoration, - chipColor: chipColor, - selectedChipColor: selectedChipColor, - colorator: colorator, - textStyle: textStyle, - selectedTextStyle: selectedTextStyle, - icon: icon, - searchIcon: searchIcon, - closeSearchIcon: closeSearchIcon, - chipShape: chipShape, - onTap: onTap, - title: title, - scroll: scroll, - initialValue: initialValue, - searchable: searchable, - searchHint: searchHint, - searchHintStyle: searchHintStyle, - searchTextStyle: searchTextStyle, - headerColor: headerColor, - itemBuilder: itemBuilder, - height: height, - scrollControl: scrollControl, - scrollBar: scrollBar, - showHeader: showHeader, - chipWidth: chipWidth, - ); - return _MultiSelectChipFieldView.withState( - view as _MultiSelectChipFieldView, state); - }); -} - -// ignore: must_be_immutable -class _MultiSelectChipFieldView extends StatefulWidget - with MultiSelectActions { - final BoxDecoration? decoration; - final List> items; - final List>? selectedItems; - final Color? chipColor; - final Color? selectedChipColor; - final TextStyle? textStyle; - final TextStyle? selectedTextStyle; - final Icon? icon; - final Icon? searchIcon; - final Icon? closeSearchIcon; - final ShapeBorder? chipShape; - final Text? title; - final bool scroll; - final bool? searchable; - final String? searchHint; - final TextStyle? searchHintStyle; - final TextStyle? searchTextStyle; - final List? initialValue; - final Color? Function(V)? colorator; - final Function(List)? onTap; - final Color? headerColor; - final Widget Function(MultiSelectItem, FormFieldState>)? - itemBuilder; - final double? height; - FormFieldState>? state; - final Function(ScrollController)? scrollControl; - final HorizontalScrollBar? scrollBar; - final bool showHeader; - final double? chipWidth; - - _MultiSelectChipFieldView({ - required this.items, - this.selectedItems, - this.decoration, - this.chipColor, - this.selectedChipColor, - this.colorator, - this.textStyle, - this.selectedTextStyle, - this.icon, - this.chipShape, - this.onTap, - this.title, - this.scroll = true, - this.initialValue, - this.searchable, - this.searchHint, - this.searchIcon, - this.closeSearchIcon, - this.searchHintStyle, - this.searchTextStyle, - this.headerColor, - this.itemBuilder, - this.height, - this.scrollControl, - this.scrollBar, - this.showHeader = true, - this.chipWidth, - }); - - /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectChipField. - _MultiSelectChipFieldView.withState( - _MultiSelectChipFieldView field, FormFieldState> state) - : items = field.items, - title = field.title, - decoration = field.decoration, - initialValue = field.initialValue, - selectedChipColor = field.selectedChipColor, - chipShape = field.chipShape, - colorator = field.colorator, - chipColor = field.chipColor, - icon = field.icon, - closeSearchIcon = field.closeSearchIcon, - selectedItems = field.selectedItems, - textStyle = field.textStyle, - scroll = field.scroll, - selectedTextStyle = field.selectedTextStyle, - onTap = field.onTap, - searchable = field.searchable, - searchHint = field.searchHint, - searchIcon = field.searchIcon, - searchTextStyle = field.searchTextStyle, - searchHintStyle = field.searchHintStyle, - headerColor = field.headerColor, - itemBuilder = field.itemBuilder, - height = field.height, - scrollControl = field.scrollControl, - scrollBar = field.scrollBar, - showHeader = field.showHeader, - chipWidth = field.chipWidth, - state = state; - - @override - __MultiSelectChipFieldViewState createState() => - __MultiSelectChipFieldViewState(items); -} - -class __MultiSelectChipFieldViewState - extends State<_MultiSelectChipFieldView> { - List _selectedValues = []; - bool _showSearch = false; - List _items; - ScrollController _scrollController = ScrollController(); - - __MultiSelectChipFieldViewState(this._items); - - void initState() { - super.initState(); - if (widget.initialValue != null) { - _selectedValues.addAll(widget.initialValue!); - } - if (widget.scrollControl != null && widget.scroll) - WidgetsBinding.instance!.addPostFrameCallback((_) => _scrollToPosition()); - } - - _scrollToPosition() { - widget.scrollControl!(_scrollController); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - decoration: widget.decoration ?? - BoxDecoration( - border: - Border.all(width: 1, color: Theme.of(context).primaryColor), - ), - child: Column( - children: [ - widget.showHeader - ? Container( - color: - widget.headerColor ?? Theme.of(context).primaryColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _showSearch - ? Expanded( - child: Container( - padding: EdgeInsets.only(left: 10), - child: TextField( - style: widget.searchTextStyle, - decoration: InputDecoration( - hintStyle: widget.searchHintStyle, - hintText: widget.searchHint ?? "Search", - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: widget.selectedChipColor ?? - Theme.of(context).primaryColor, - ), - ), - ), - onChanged: (val) { - setState(() { - _items = widget.updateSearchQuery( - val, widget.items); - }); - }, - ), - ), - ) - : Padding( - padding: const EdgeInsets.only(left: 8.0), - child: widget.title != null - ? Text( - widget.title!.data!, - style: TextStyle( - color: widget.title!.style != null - ? widget.title!.style!.color - : null, - fontSize: - widget.title!.style != null - ? widget.title!.style! - .fontSize ?? - 18 - : 18), - ) - : Text( - "Select", - style: TextStyle(fontSize: 18), - ), - ), - widget.searchable != null && widget.searchable! - ? IconButton( - icon: _showSearch - ? widget.closeSearchIcon ?? - Icon( - Icons.close, - size: 22, - ) - : widget.searchIcon ?? - Icon( - Icons.search, - size: 22, - ), - onPressed: () { - setState(() { - _showSearch = !_showSearch; - if (!_showSearch) _items = widget.items; - }); - }, - ) - : Padding( - padding: EdgeInsets.all(18), - ), - ], - ), - ) - : Container(), - widget.scroll - ? Container( - padding: widget.itemBuilder == null - ? EdgeInsets.symmetric(horizontal: 5) - : null, - width: MediaQuery.of(context).size.width, - height: widget.height ?? - MediaQuery.of(context).size.height * 0.08, - child: widget.scrollBar != null - ? Scrollbar( - isAlwaysShown: widget.scrollBar!.isAlwaysShown, - controller: _scrollController, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: _items.length, - itemBuilder: (ctx, index) { - return widget.itemBuilder != null - ? widget.itemBuilder!( - _items[index] as MultiSelectItem, - widget.state!) - : _buildItem( - _items[index] as MultiSelectItem); - }, - ), - ) - : ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: _items.length, - itemBuilder: (ctx, index) { - return widget.itemBuilder != null - ? widget.itemBuilder!( - _items[index] as MultiSelectItem, - widget.state!) - : _buildItem( - _items[index] as MultiSelectItem); - }, - ), - ) - : Container( - height: widget.height, - alignment: Alignment.centerLeft, - padding: EdgeInsets.symmetric(horizontal: 10), - child: Wrap( - children: widget.itemBuilder != null - ? _items - .map((item) => widget.itemBuilder!( - item as MultiSelectItem, widget.state!)) - .toList() - : _items - .map((item) => - _buildItem(item as MultiSelectItem)) - .toList(), - ), - ), - ], - ), - ), - widget.state != null && widget.state!.hasError - ? Row( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(4, 4, 0, 0), - child: Text( - widget.state!.errorText!, - style: TextStyle( - color: Colors.red[800], - fontSize: 12.5, - ), - ), - ), - ], - ) - : Container(), - ], - ); - } - - Widget _buildItem(MultiSelectItem item) { - return Container( - margin: EdgeInsets.all(0), - padding: const EdgeInsets.all(2.0), - child: ChoiceChip( - shape: widget.chipShape as OutlinedBorder? ?? - RoundedRectangleBorder( - side: BorderSide( - color: widget.colorator != null && - widget.colorator!(item.value) != null && - _selectedValues.contains(item.value) - ? widget.colorator!(item.value)! - : widget.selectedChipColor ?? - Theme.of(context).primaryColor), - borderRadius: BorderRadius.vertical( - top: Radius.circular(15.0), - bottom: Radius.circular(15.0), - ), - ), - avatar: _selectedValues.contains(item.value) - ? widget.icon != null - ? Icon( - widget.icon!.icon, - color: widget.colorator != null && - widget.colorator!(item.value) != null - ? widget.colorator!(item.value)!.withOpacity(1) - : widget.icon!.color ?? - widget.selectedChipColor ?? - Theme.of(context).primaryColor, - ) - : null - : null, - label: Container( - width: widget.chipWidth, - child: Text( - item.label, - overflow: TextOverflow.ellipsis, - style: _selectedValues.contains(item.value) - ? TextStyle( - color: widget.colorator != null && - widget.colorator!(item.value) != null - ? widget.colorator!(item.value)!.withOpacity(1) - : widget.selectedTextStyle != null - ? widget.selectedTextStyle!.color - : null) - : TextStyle( - color: widget.textStyle != null - ? widget.textStyle!.color ?? widget.chipColor - : widget.chipColor, - fontSize: widget.textStyle != null - ? widget.textStyle!.fontSize - : null, - ), - ), - ), - selected: _selectedValues.contains(item.value), - backgroundColor: widget.chipColor ?? Colors.white70, - selectedColor: - widget.colorator != null && widget.colorator!(item.value) != null - ? widget.colorator!(item.value) - : widget.selectedChipColor != null - ? widget.selectedChipColor - : Theme.of(context).primaryColor.withOpacity(0.33), - onSelected: (_) { - if (_) { - _selectedValues.add(item.value); - if (widget.state != null) { - widget.state!.didChange(_selectedValues); - } - } else { - _selectedValues.remove(item.value); - if (widget.state != null) { - widget.state!.didChange(_selectedValues); - } - } - if (widget.onTap != null) widget.onTap!(_selectedValues); - }, - ), - ); - } -} +import 'package:flutter/material.dart'; +import '../multi_select_flutter.dart'; + +class MultiSelectChipField extends FormField> { + /// Style the Container that makes up the field. + final BoxDecoration? decoration; + + /// List of items to select from. + final List> items; + + /// Color of the chip while not selected. + final Color? chipColor; + + /// Sets the color of the chip while selected. + final Color? selectedChipColor; + + /// Style the text of the chips. + final TextStyle? textStyle; + + /// Style the text of the selected chips. + final TextStyle? selectedTextStyle; + + /// The icon displayed in front of text on selected chips. + final Icon? icon; + + /// Replaces the deafult search icon when searchable is true. + final Icon? searchIcon; + + /// Replaces the default close search icon when searchable is true. + final Icon? closeSearchIcon; + + /// Set a ShapeBorder. Typically a RoundedRectangularBorder. + final ShapeBorder? chipShape; + + /// Defines the header text. + final Text? title; + + /// Enables horizontal scrolling. Default is true. + final bool scroll; + + /// A function that sets the color of selected items based on their value. + final Color Function(V)? colorator; + + /// Fires when a chip is tapped. A good time to store the selected values. + final Function(List)? onTap; + + /// Enables search functionality. + final bool? searchable; + + /// Set the search hint. + final String? searchHint; + + /// Set the TextStyle of the search hint. + final TextStyle? searchHintStyle; + + /// Set the TextStyle of the text that gets typed into the search bar. + final TextStyle? searchTextStyle; + + /// Set the header color. + final Color? headerColor; + + /// Build a custom widget that gets created dynamically for each item. + final Widget Function(MultiSelectItem, FormFieldState>)? + itemBuilder; + + /// Set the height of the selectable area. + final double? height; + + /// Make use of the ScrollController to automatically scroll through the list. + final Function(ScrollController)? scrollControl; + + /// Define a HorizontalScrollBar. + final HorizontalScrollBar? scrollBar; + + /// Determines whether to show the header. + final bool showHeader; + + /// Set the width of the chip. + final double? chipWidth; + + final List? initialValue; + final AutovalidateMode autovalidateMode; + final FormFieldValidator>? validator; + final FormFieldSetter>? onSaved; + final GlobalKey? key; + + MultiSelectChipField({ + required this.items, + this.decoration, + this.chipColor, + this.selectedChipColor, + this.colorator, + this.textStyle, + this.selectedTextStyle, + this.icon, + this.searchIcon, + this.closeSearchIcon, + this.chipShape, + this.onTap, + this.title, + this.scroll = true, + this.searchable, + this.searchHint, + this.searchHintStyle, + this.searchTextStyle, + this.headerColor, + this.key, + this.onSaved, + this.validator, + this.autovalidateMode = AutovalidateMode.disabled, + this.initialValue, + this.itemBuilder, + this.height, + this.scrollControl, + this.scrollBar, + this.showHeader = true, + this.chipWidth, + }) : super( + key: key, + onSaved: onSaved, + validator: validator, + autovalidateMode: autovalidateMode, + initialValue: initialValue ?? [], + builder: (FormFieldState> state) { + _MultiSelectChipFieldView view = _MultiSelectChipFieldView( + items: items, + decoration: decoration, + chipColor: chipColor, + selectedChipColor: selectedChipColor, + colorator: colorator, + textStyle: textStyle, + selectedTextStyle: selectedTextStyle, + icon: icon, + searchIcon: searchIcon, + closeSearchIcon: closeSearchIcon, + chipShape: chipShape, + onTap: onTap, + title: title, + scroll: scroll, + initialValue: initialValue, + searchable: searchable, + searchHint: searchHint, + searchHintStyle: searchHintStyle, + searchTextStyle: searchTextStyle, + headerColor: headerColor, + itemBuilder: itemBuilder, + height: height, + scrollControl: scrollControl, + scrollBar: scrollBar, + showHeader: showHeader, + chipWidth: chipWidth, + ); + return _MultiSelectChipFieldView.withState( + view as _MultiSelectChipFieldView, state); + }); +} + +// ignore: must_be_immutable +class _MultiSelectChipFieldView extends StatefulWidget + with MultiSelectActions { + final BoxDecoration? decoration; + final List> items; + final List>? selectedItems; + final Color? chipColor; + final Color? selectedChipColor; + final TextStyle? textStyle; + final TextStyle? selectedTextStyle; + final Icon? icon; + final Icon? searchIcon; + final Icon? closeSearchIcon; + final ShapeBorder? chipShape; + final Text? title; + final bool scroll; + final bool? searchable; + final String? searchHint; + final TextStyle? searchHintStyle; + final TextStyle? searchTextStyle; + final List? initialValue; + final Color? Function(V)? colorator; + final Function(List)? onTap; + final Color? headerColor; + final Widget Function(MultiSelectItem, FormFieldState>)? + itemBuilder; + final double? height; + FormFieldState>? state; + final Function(ScrollController)? scrollControl; + final HorizontalScrollBar? scrollBar; + final bool showHeader; + final double? chipWidth; + + _MultiSelectChipFieldView({ + required this.items, + this.selectedItems, + this.decoration, + this.chipColor, + this.selectedChipColor, + this.colorator, + this.textStyle, + this.selectedTextStyle, + this.icon, + this.chipShape, + this.onTap, + this.title, + this.scroll = true, + this.initialValue, + this.searchable, + this.searchHint, + this.searchIcon, + this.closeSearchIcon, + this.searchHintStyle, + this.searchTextStyle, + this.headerColor, + this.itemBuilder, + this.height, + this.scrollControl, + this.scrollBar, + this.showHeader = true, + this.chipWidth, + }); + + /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectChipField. + _MultiSelectChipFieldView.withState( + _MultiSelectChipFieldView field, FormFieldState> state) + : items = field.items, + title = field.title, + decoration = field.decoration, + initialValue = field.initialValue, + selectedChipColor = field.selectedChipColor, + chipShape = field.chipShape, + colorator = field.colorator, + chipColor = field.chipColor, + icon = field.icon, + closeSearchIcon = field.closeSearchIcon, + selectedItems = field.selectedItems, + textStyle = field.textStyle, + scroll = field.scroll, + selectedTextStyle = field.selectedTextStyle, + onTap = field.onTap, + searchable = field.searchable, + searchHint = field.searchHint, + searchIcon = field.searchIcon, + searchTextStyle = field.searchTextStyle, + searchHintStyle = field.searchHintStyle, + headerColor = field.headerColor, + itemBuilder = field.itemBuilder, + height = field.height, + scrollControl = field.scrollControl, + scrollBar = field.scrollBar, + showHeader = field.showHeader, + chipWidth = field.chipWidth, + state = state; + + @override + __MultiSelectChipFieldViewState createState() => + __MultiSelectChipFieldViewState(items); +} + +class __MultiSelectChipFieldViewState + extends State<_MultiSelectChipFieldView> { + List _selectedValues = []; + bool _showSearch = false; + List _items; + ScrollController _scrollController = ScrollController(); + + __MultiSelectChipFieldViewState(this._items); + + void initState() { + super.initState(); + if (widget.initialValue != null) { + _selectedValues.addAll(widget.initialValue!); + } + if (widget.scrollControl != null && widget.scroll) + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToPosition()); + } + + _scrollToPosition() { + widget.scrollControl!(_scrollController); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: widget.decoration ?? + BoxDecoration( + border: + Border.all(width: 1, color: Theme.of(context).primaryColor), + ), + child: Column( + children: [ + widget.showHeader + ? Container( + color: + widget.headerColor ?? Theme.of(context).primaryColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _showSearch + ? Expanded( + child: Container( + padding: EdgeInsets.only(left: 10), + child: TextField( + style: widget.searchTextStyle, + decoration: InputDecoration( + hintStyle: widget.searchHintStyle, + hintText: widget.searchHint ?? "Search", + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: widget.selectedChipColor ?? + Theme.of(context).primaryColor, + ), + ), + ), + onChanged: (val) { + setState(() { + _items = widget.updateSearchQuery( + val, widget.items); + }); + }, + ), + ), + ) + : Padding( + padding: const EdgeInsets.only(left: 8.0), + child: widget.title != null + ? Text( + widget.title!.data!, + style: TextStyle( + color: widget.title!.style != null + ? widget.title!.style!.color + : null, + fontSize: + widget.title!.style != null + ? widget.title!.style! + .fontSize ?? + 18 + : 18), + ) + : Text( + "Select", + style: TextStyle(fontSize: 18), + ), + ), + widget.searchable != null && widget.searchable! + ? IconButton( + icon: _showSearch + ? widget.closeSearchIcon ?? + Icon( + Icons.close, + size: 22, + ) + : widget.searchIcon ?? + Icon( + Icons.search, + size: 22, + ), + onPressed: () { + setState(() { + _showSearch = !_showSearch; + if (!_showSearch) _items = widget.items; + }); + }, + ) + : Padding( + padding: EdgeInsets.all(18), + ), + ], + ), + ) + : Container(), + widget.scroll + ? Container( + padding: widget.itemBuilder == null + ? EdgeInsets.symmetric(horizontal: 5) + : null, + width: MediaQuery.of(context).size.width, + height: widget.height ?? + MediaQuery.of(context).size.height * 0.08, + child: widget.scrollBar != null + ? Scrollbar( + isAlwaysShown: widget.scrollBar!.isAlwaysShown, + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: _items.length, + itemBuilder: (ctx, index) { + return widget.itemBuilder != null + ? widget.itemBuilder!( + _items[index] as MultiSelectItem, + widget.state!) + : _buildItem( + _items[index] as MultiSelectItem); + }, + ), + ) + : ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: _items.length, + itemBuilder: (ctx, index) { + return widget.itemBuilder != null + ? widget.itemBuilder!( + _items[index] as MultiSelectItem, + widget.state!) + : _buildItem( + _items[index] as MultiSelectItem); + }, + ), + ) + : Container( + height: widget.height, + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 10), + child: Wrap( + children: widget.itemBuilder != null + ? _items + .map((item) => widget.itemBuilder!( + item as MultiSelectItem, widget.state!)) + .toList() + : _items + .map((item) => + _buildItem(item as MultiSelectItem)) + .toList(), + ), + ), + ], + ), + ), + widget.state != null && widget.state!.hasError + ? Row( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 0, 0), + child: Text( + widget.state!.errorText!, + style: TextStyle( + color: Colors.red[800], + fontSize: 12.5, + ), + ), + ), + ], + ) + : Container(), + ], + ); + } + + Widget _buildItem(MultiSelectItem item) { + return Container( + margin: EdgeInsets.all(0), + padding: const EdgeInsets.all(2.0), + child: ChoiceChip( + shape: widget.chipShape as OutlinedBorder? ?? + RoundedRectangleBorder( + side: BorderSide( + color: widget.colorator != null && + widget.colorator!(item.value) != null && + _selectedValues.contains(item.value) + ? widget.colorator!(item.value)! + : widget.selectedChipColor ?? + Theme.of(context).primaryColor), + borderRadius: BorderRadius.vertical( + top: Radius.circular(15.0), + bottom: Radius.circular(15.0), + ), + ), + avatar: _selectedValues.contains(item.value) + ? widget.icon != null + ? Icon( + widget.icon!.icon, + color: widget.colorator != null && + widget.colorator!(item.value) != null + ? widget.colorator!(item.value)!.withOpacity(1) + : widget.icon!.color ?? + widget.selectedChipColor ?? + Theme.of(context).primaryColor, + ) + : null + : null, + label: Container( + width: widget.chipWidth, + child: Text( + item.label, + overflow: TextOverflow.ellipsis, + style: _selectedValues.contains(item.value) + ? TextStyle( + color: widget.colorator != null && + widget.colorator!(item.value) != null + ? widget.colorator!(item.value)!.withOpacity(1) + : widget.selectedTextStyle != null + ? widget.selectedTextStyle!.color + : null) + : TextStyle( + color: widget.textStyle != null + ? widget.textStyle!.color ?? widget.chipColor + : widget.chipColor, + fontSize: widget.textStyle != null + ? widget.textStyle!.fontSize + : null, + ), + ), + ), + selected: _selectedValues.contains(item.value), + backgroundColor: widget.chipColor ?? Colors.white70, + selectedColor: + widget.colorator != null && widget.colorator!(item.value) != null + ? widget.colorator!(item.value) + : widget.selectedChipColor != null + ? widget.selectedChipColor + : Theme.of(context).primaryColor.withOpacity(0.33), + onSelected: (_) { + if (_) { + _selectedValues.add(item.value); + if (widget.state != null) { + widget.state!.didChange(_selectedValues); + } + } else { + _selectedValues.remove(item.value); + if (widget.state != null) { + widget.state!.didChange(_selectedValues); + } + } + if (widget.onTap != null) widget.onTap!(_selectedValues); + }, + ), + ); + } +} diff --git a/lib/dialog/mult_select_dialog.dart b/lib/dialog/mult_select_dialog.dart index 8d00c2b..6a0147d 100644 --- a/lib/dialog/mult_select_dialog.dart +++ b/lib/dialog/mult_select_dialog.dart @@ -1,332 +1,338 @@ -import 'package:flutter/material.dart'; -import '../util/multi_select_actions.dart'; -import '../util/multi_select_item.dart'; -import '../util/multi_select_list_type.dart'; - -/// A dialog containing either a classic checkbox style list, or a chip style list. -class MultiSelectDialog extends StatefulWidget with MultiSelectActions { - /// List of items to select from. - final List> items; - - /// The list of selected values before interaction. - final List initialValue; - - /// The text at the top of the dialog. - final Widget? title; - - /// Fires when the an item is selected / unselected. - final void Function(List)? onSelectionChanged; - - /// Fires when confirm is tapped. - final void Function(List)? onConfirm; - - /// Toggles search functionality. Default is false. - final bool searchable; - - /// Text on the confirm button. - final Text? confirmText; - - /// Text on the cancel button. - final Text? cancelText; - - /// An enum that determines which type of list to render. - final MultiSelectListType? listType; - - /// Sets the color of the checkbox or chip when it's selected. - final Color? selectedColor; - - /// Sets a fixed height on the dialog. - final double? height; - - /// Sets a fixed width on the dialog. - final double? width; - - /// Set the placeholder text of the search field. - final String? searchHint; - - /// A function that sets the color of selected items based on their value. - /// It will either set the chip color, or the checkbox color depending on the list type. - final Color? Function(T)? colorator; - - /// The background color of the dialog. - final Color? backgroundColor; - - /// The color of the chip body or checkbox border while not selected. - final Color? unselectedColor; - - /// Icon button that shows the search field. - final Icon? searchIcon; - - /// Icon button that hides the search field - final Icon? closeSearchIcon; - - /// Style the text on the chips or list tiles. - final TextStyle? itemsTextStyle; - - /// Style the text on the selected chips or list tiles. - final TextStyle? selectedItemsTextStyle; - - /// Style the search text. - final TextStyle? searchTextStyle; - - /// Style the search hint. - final TextStyle? searchHintStyle; - - /// Moves the selected items to the top of the list. - final bool separateSelectedItems; - - /// Set the color of the check in the checkbox - final Color? checkColor; - - MultiSelectDialog({ - required this.items, - required this.initialValue, - this.title, - this.onSelectionChanged, - this.onConfirm, - this.listType, - this.searchable = false, - this.confirmText, - this.cancelText, - this.selectedColor, - this.searchHint, - this.height, - this.width, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchHintStyle, - this.searchTextStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - }); - - @override - State createState() => _MultiSelectDialogState(items); -} - -class _MultiSelectDialogState extends State> { - List _selectedValues = []; - bool _showSearch = false; - List> _items; - - _MultiSelectDialogState(this._items); - - @override - void initState() { - super.initState(); - _selectedValues.addAll(widget.initialValue); - - for (int i = 0; i < _items.length; i++) { - if (_selectedValues.contains(_items[i].value)) { - _items[i].selected = true; - } - } - - if (widget.separateSelectedItems) { - _items = widget.separateSelected(_items); - } - } - - /// Returns a CheckboxListTile - Widget _buildListItem(MultiSelectItem item) { - return Theme( - data: ThemeData( - unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, - ), - child: CheckboxListTile( - checkColor: widget.checkColor, - value: item.selected, - activeColor: widget.colorator != null - ? widget.colorator!(item.value) ?? widget.selectedColor - : widget.selectedColor, - title: Text( - item.label, - style: item.selected - ? widget.selectedItemsTextStyle - : widget.itemsTextStyle, - ), - controlAffinity: ListTileControlAffinity.leading, - onChanged: (checked) { - setState(() { - _selectedValues = widget.onItemCheckedChange( - _selectedValues, item.value, checked!); - - if (checked) { - item.selected = true; - } else { - item.selected = false; - } - if (widget.separateSelectedItems) { - _items = widget.separateSelected(_items); - } - }); - if (widget.onSelectionChanged != null) { - widget.onSelectionChanged!(_selectedValues); - } - }, - ), - ); - } - - /// Returns a ChoiceChip - Widget _buildChipItem(MultiSelectItem item) { - return Container( - padding: const EdgeInsets.all(2.0), - child: ChoiceChip( - backgroundColor: widget.unselectedColor, - selectedColor: widget.colorator?.call(item.value) ?? - widget.selectedColor ?? - Theme.of(context).primaryColor.withOpacity(0.35), - label: Text( - item.label, - style: item.selected - ? TextStyle( - color: widget.selectedItemsTextStyle?.color ?? - widget.colorator?.call(item.value) ?? - widget.selectedColor?.withOpacity(1) ?? - Theme.of(context).primaryColor, - fontSize: widget.selectedItemsTextStyle?.fontSize, - ) - : widget.itemsTextStyle, - ), - selected: item.selected, - onSelected: (checked) { - if (checked) { - item.selected = true; - } else { - item.selected = false; - } - setState(() { - _selectedValues = widget.onItemCheckedChange( - _selectedValues, item.value, checked); - }); - if (widget.onSelectionChanged != null) { - widget.onSelectionChanged!(_selectedValues); - } - }, - ), - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: widget.backgroundColor, - title: widget.searchable == false - ? widget.title ?? const Text("Select") - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _showSearch - ? Expanded( - child: Container( - padding: EdgeInsets.only(left: 10), - child: TextField( - style: widget.searchTextStyle, - decoration: InputDecoration( - hintStyle: widget.searchHintStyle, - hintText: widget.searchHint ?? "Search", - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: widget.selectedColor ?? - Theme.of(context).primaryColor, - ), - ), - ), - onChanged: (val) { - List> filteredList = []; - filteredList = - widget.updateSearchQuery(val, widget.items); - setState(() { - if (widget.separateSelectedItems) { - _items = - widget.separateSelected(filteredList); - } else { - _items = filteredList; - } - }); - }, - ), - ), - ) - : widget.title ?? Text("Select"), - IconButton( - icon: _showSearch - ? widget.closeSearchIcon ?? Icon(Icons.close) - : widget.searchIcon ?? Icon(Icons.search), - onPressed: () { - setState(() { - _showSearch = !_showSearch; - if (!_showSearch) { - if (widget.separateSelectedItems) { - _items = widget.separateSelected(widget.items); - } else { - _items = widget.items; - } - } - }); - }, - ), - ], - ), - contentPadding: - widget.listType == null || widget.listType == MultiSelectListType.LIST - ? EdgeInsets.only(top: 12.0) - : EdgeInsets.all(20), - content: Container( - height: widget.height, - width: widget.width ?? MediaQuery.of(context).size.width * 0.73, - child: widget.listType == null || - widget.listType == MultiSelectListType.LIST - ? ListView.builder( - itemCount: _items.length, - itemBuilder: (context, index) { - return _buildListItem(_items[index]); - }, - ) - : SingleChildScrollView( - child: Wrap( - children: _items.map(_buildChipItem).toList(), - ), - ), - ), - actions: [ - TextButton( - child: widget.cancelText ?? - Text( - "CANCEL", - style: TextStyle( - color: (widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(1) - : Theme.of(context).primaryColor, - ), - ), - onPressed: () { - widget.onCancelTap(context, widget.initialValue); - }, - ), - TextButton( - child: widget.confirmText ?? - Text( - 'OK', - style: TextStyle( - color: (widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(1) - : Theme.of(context).primaryColor, - ), - ), - onPressed: () { - widget.onConfirmTap(context, _selectedValues, widget.onConfirm); - }, - ) - ], - ); - } -} +import 'package:flutter/material.dart'; +import '../util/multi_select_actions.dart'; +import '../util/multi_select_item.dart'; +import '../util/multi_select_list_type.dart'; + +/// A dialog containing either a classic checkbox style list, or a chip style list. +class MultiSelectDialog extends StatefulWidget with MultiSelectActions { + /// List of items to select from. + final List> items; + + /// The list of selected values before interaction. + final List initialValue; + + /// The text at the top of the dialog. + final Widget? title; + + /// Fires when the an item is selected / unselected. + final void Function(List)? onSelectionChanged; + + /// Fires when confirm is tapped. + final void Function(List)? onConfirm; + + /// Toggles search functionality. Default is false. + final bool searchable; + + /// Text on the confirm button. + final Text? confirmText; + + /// Text on the cancel button. + final Text? cancelText; + + /// An enum that determines which type of list to render. + final MultiSelectListType? listType; + + /// Sets the color of the checkbox or chip when it's selected. + final Color? selectedColor; + + /// Sets a fixed height on the dialog. + final double? height; + + /// Sets a fixed width on the dialog. + final double? width; + + /// Set the placeholder text of the search field. + final String? searchHint; + + /// A function that sets the color of selected items based on their value. + /// It will either set the chip color, or the checkbox color depending on the list type. + final Color? Function(T)? colorator; + + /// The background color of the dialog. + final Color? backgroundColor; + + /// The color of the chip body or checkbox border while not selected. + final Color? unselectedColor; + + /// Icon button that shows the search field. + final Icon? searchIcon; + + /// Icon button that hides the search field + final Icon? closeSearchIcon; + + /// Style the text on the chips or list tiles. + final TextStyle? itemsTextStyle; + + /// Style the text on the selected chips or list tiles. + final TextStyle? selectedItemsTextStyle; + + /// Style the search text. + final TextStyle? searchTextStyle; + + /// Style the search hint. + final TextStyle? searchHintStyle; + + /// Moves the selected items to the top of the list. + final bool separateSelectedItems; + + /// Set the color of the check in the checkbox + final Color? checkColor; + + MultiSelectDialog({ + required this.items, + required this.initialValue, + this.title, + this.onSelectionChanged, + this.onConfirm, + this.listType, + this.searchable = false, + this.confirmText, + this.cancelText, + this.selectedColor, + this.searchHint, + this.height, + this.width, + this.colorator, + this.backgroundColor, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchHintStyle, + this.searchTextStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + }); + + @override + State createState() => _MultiSelectDialogState(items); +} + +class _MultiSelectDialogState extends State> { + List _selectedValues = []; + bool _showSearch = false; + List> _items; + + _MultiSelectDialogState(this._items); + + @override + void initState() { + super.initState(); + _selectedValues.addAll(widget.initialValue); + + for (int i = 0; i < _items.length; i++) { + if (_selectedValues.contains(_items[i].value)) { + _items[i].selected = true; + } + } + + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); + } + } + + /// Returns a CheckboxListTile + Widget _buildListItem(MultiSelectItem item) { + return Theme( + data: ThemeData( + unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, + ), + child: CheckboxListTile( + checkColor: widget.checkColor, + value: item.selected, + activeColor: widget.colorator != null + ? widget.colorator!(item.value) ?? widget.selectedColor + : widget.selectedColor, + title: Text( + item.label, + style: item.selected + ? widget.selectedItemsTextStyle + : widget.itemsTextStyle, + ), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (checked) { + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked!); + + if (checked) { + item.selected = true; + } else { + item.selected = false; + } + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); + } + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); + } + + /// Returns a ChoiceChip + Widget _buildChipItem(MultiSelectItem item) { + return Container( + padding: const EdgeInsets.all(2.0), + child: ChoiceChip( + backgroundColor: widget.unselectedColor, + selectedColor: widget.colorator?.call(item.value) ?? + widget.selectedColor ?? + Theme.of(context).primaryColor.withOpacity(0.35), + label: Text( + item.label, + style: item.selected + ? TextStyle( + color: widget.selectedItemsTextStyle?.color ?? + widget.colorator?.call(item.value) ?? + widget.selectedColor?.withOpacity(1) ?? + Theme.of(context).primaryColor, + fontSize: widget.selectedItemsTextStyle?.fontSize, + ) + : widget.itemsTextStyle, + ), + selected: item.selected, + onSelected: (checked) { + if (checked) { + item.selected = true; + } else { + item.selected = false; + } + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked); + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: widget.backgroundColor, + title: widget.searchable == false + ? widget.title ?? const Text("Select") + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _showSearch + ? Expanded( + child: Container( + padding: EdgeInsets.only(left: 10), + child: TextField( + style: widget.searchTextStyle, + decoration: InputDecoration( + hintStyle: widget.searchHintStyle, + hintText: widget.searchHint ?? "Search", + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: widget.selectedColor ?? + Theme.of(context).primaryColor, + ), + ), + ), + onChanged: (val) { + List> filteredList = []; + filteredList = + widget.updateSearchQuery(val, widget.items); + setState(() { + if (widget.separateSelectedItems) { + _items = + widget.separateSelected(filteredList); + } else { + _items = filteredList; + } + }); + }, + ), + ), + ) + : widget.title ?? Text("Select"), + IconButton( + icon: _showSearch + ? widget.closeSearchIcon ?? Icon(Icons.close) + : widget.searchIcon ?? Icon(Icons.search), + onPressed: () { + setState(() { + _showSearch = !_showSearch; + if (!_showSearch) { + if (widget.separateSelectedItems) { + _items = widget.separateSelected(widget.items); + } else { + _items = widget.items; + } + } + }); + }, + ), + ], + ), + contentPadding: + widget.listType == null || widget.listType == MultiSelectListType.LIST + ? EdgeInsets.only(top: 12.0) + : EdgeInsets.all(20), + content: Container( + height: widget.height, + width: widget.width ?? MediaQuery.of(context).size.width * 0.73, + child: widget.listType == null || + widget.listType == MultiSelectListType.LIST + ? ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + return _buildListItem(_items[index]); + }, + ) + : SingleChildScrollView( + child: Wrap( + children: _items.map(_buildChipItem).toList(), + ), + ), + ), + actions: [ + TextButton( + child: widget.cancelText ?? + Text( + "CANCEL", + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), + onPressed: () { + /// Reset selection option to maintain UI + _items.forEach((e) { + if (e.selected && !widget.initialValue.contains(e.value)) { + e.selected = false; + } + }); + widget.onCancelTap(context, widget.initialValue); + }, + ), + TextButton( + child: widget.confirmText ?? + Text( + 'OK', + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), + onPressed: () { + widget.onConfirmTap(context, _selectedValues, widget.onConfirm); + }, + ) + ], + ); + } +} diff --git a/lib/dialog/multi_select_dialog_field.dart b/lib/dialog/multi_select_dialog_field.dart index ed72e6c..cfbe376 100644 --- a/lib/dialog/multi_select_dialog_field.dart +++ b/lib/dialog/multi_select_dialog_field.dart @@ -1,464 +1,464 @@ -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import '../util/multi_select_list_type.dart'; -import '../util/multi_select_item.dart'; -import '../chip_display/multi_select_chip_display.dart'; -import 'mult_select_dialog.dart'; - -/// A customizable InkWell widget that opens the MultiSelectDialog -// ignore: must_be_immutable -class MultiSelectDialogField extends FormField> { - /// An enum that determines which type of list to render. - final MultiSelectListType? listType; - - /// Style the Container that makes up the field. - final BoxDecoration? decoration; - - /// Set text that is displayed on the button. - final Text? buttonText; - - /// Specify the button icon. - final Icon? buttonIcon; - - /// The text at the top of the dialog. - final Widget? title; - - /// List of items to select from. - final List> items; - - /// Fires when the an item is selected / unselected. - final void Function(List)? onSelectionChanged; - - /// Overrides the default MultiSelectChipDisplay attached to this field. - /// If you want to remove it, use MultiSelectChipDisplay.none(). - final MultiSelectChipDisplay? chipDisplay; - - /// The list of selected values before interaction. - final List? initialValue; - - /// Fires when confirm is tapped. - final void Function(List) onConfirm; - - /// Toggles search functionality. - final bool searchable; - - /// Text on the confirm button. - final Text? confirmText; - - /// Text on the cancel button. - final Text? cancelText; - - /// Set the color of the space outside the BottomSheet. - final Color? barrierColor; - - /// Sets the color of the checkbox or chip when it's selected. - final Color? selectedColor; - - /// Sets a fixed height on the dialog. - final double? dialogHeight; - - /// Sets a fixed width on the dialog. - final double? dialogWidth; - - /// Set the placeholder text of the search field. - final String? searchHint; - - /// A function that sets the color of selected items based on their value. - /// It will either set the chip color, or the checkbox color depending on the list type. - final Color Function(V)? colorator; - - /// Set the background color of the dialog. - final Color? backgroundColor; - - /// Color of the chip body or checkbox border while not selected. - final Color? unselectedColor; - - /// Replaces the deafult search icon when searchable is true. - final Icon? searchIcon; - - /// Replaces the default close search icon when searchable is true. - final Icon? closeSearchIcon; - - /// Style the text on the chips or list tiles. - final TextStyle? itemsTextStyle; - - /// Style the text on the selected chips or list tiles. - final TextStyle? selectedItemsTextStyle; - - /// Style the text that is typed into the search field. - final TextStyle? searchTextStyle; - - /// Style the search hint. - final TextStyle? searchHintStyle; - - /// Moves the selected items to the top of the list. - final bool separateSelectedItems; - - /// Set the color of the check in the checkbox - final Color? checkColor; - - final AutovalidateMode autovalidateMode; - final FormFieldValidator>? validator; - final FormFieldSetter>? onSaved; - final GlobalKey? key; - FormFieldState>? state; - - MultiSelectDialogField({ - required this.items, - required this.onConfirm, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.chipDisplay, - this.searchable = false, - this.confirmText, - this.cancelText, - this.barrierColor, - this.selectedColor, - this.searchHint, - this.dialogHeight, - this.dialogWidth, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - this.onSaved, - this.validator, - this.initialValue, - this.autovalidateMode = AutovalidateMode.disabled, - this.key, - }) : super( - key: key, - onSaved: onSaved, - validator: validator, - autovalidateMode: autovalidateMode, - initialValue: initialValue ?? [], - builder: (FormFieldState> state) { - _MultiSelectDialogFieldView field = - _MultiSelectDialogFieldView( - title: title, - items: items, - buttonText: buttonText, - buttonIcon: buttonIcon, - chipDisplay: chipDisplay, - decoration: decoration, - listType: listType, - onConfirm: onConfirm, - onSelectionChanged: onSelectionChanged, - initialValue: initialValue, - searchable: searchable, - confirmText: confirmText, - cancelText: cancelText, - barrierColor: barrierColor, - selectedColor: selectedColor, - searchHint: searchHint, - dialogHeight: dialogHeight, - dialogWidth: dialogWidth, - colorator: colorator, - backgroundColor: backgroundColor, - unselectedColor: unselectedColor, - searchIcon: searchIcon, - closeSearchIcon: closeSearchIcon, - itemsTextStyle: itemsTextStyle, - searchTextStyle: searchTextStyle, - searchHintStyle: searchHintStyle, - selectedItemsTextStyle: selectedItemsTextStyle, - separateSelectedItems: separateSelectedItems, - checkColor: checkColor, - ); - return _MultiSelectDialogFieldView._withState(field, state); - }); -} - -// ignore: must_be_immutable -class _MultiSelectDialogFieldView extends StatefulWidget { - final MultiSelectListType? listType; - final BoxDecoration? decoration; - final Text? buttonText; - final Icon? buttonIcon; - final Widget? title; - final List> items; - final void Function(List)? onSelectionChanged; - final MultiSelectChipDisplay? chipDisplay; - final List? initialValue; - final void Function(List)? onConfirm; - final bool? searchable; - final Text? confirmText; - final Text? cancelText; - final Color? barrierColor; - final Color? selectedColor; - final double? dialogHeight; - final double? dialogWidth; - final String? searchHint; - final Color Function(V)? colorator; - final Color? backgroundColor; - final Color? unselectedColor; - final Icon? searchIcon; - final Icon? closeSearchIcon; - final TextStyle? itemsTextStyle; - final TextStyle? selectedItemsTextStyle; - final TextStyle? searchTextStyle; - final TextStyle? searchHintStyle; - final bool separateSelectedItems; - final Color? checkColor; - FormFieldState>? state; - - _MultiSelectDialogFieldView({ - required this.items, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.onConfirm, - this.chipDisplay, - this.initialValue, - this.searchable, - this.confirmText, - this.cancelText, - this.barrierColor, - this.selectedColor, - this.searchHint, - this.dialogHeight, - this.dialogWidth, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - }); - - /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectDialogField. - _MultiSelectDialogFieldView._withState( - _MultiSelectDialogFieldView field, FormFieldState> state) - : items = field.items, - title = field.title, - buttonText = field.buttonText, - buttonIcon = field.buttonIcon, - listType = field.listType, - decoration = field.decoration, - onSelectionChanged = field.onSelectionChanged, - onConfirm = field.onConfirm, - chipDisplay = field.chipDisplay, - initialValue = field.initialValue, - searchable = field.searchable, - confirmText = field.confirmText, - cancelText = field.cancelText, - barrierColor = field.barrierColor, - selectedColor = field.selectedColor, - dialogHeight = field.dialogHeight, - dialogWidth = field.dialogWidth, - searchHint = field.searchHint, - colorator = field.colorator, - backgroundColor = field.backgroundColor, - unselectedColor = field.unselectedColor, - searchIcon = field.searchIcon, - closeSearchIcon = field.closeSearchIcon, - itemsTextStyle = field.itemsTextStyle, - searchHintStyle = field.searchHintStyle, - searchTextStyle = field.searchTextStyle, - selectedItemsTextStyle = field.selectedItemsTextStyle, - separateSelectedItems = field.separateSelectedItems, - checkColor = field.checkColor, - state = state; - - @override - __MultiSelectDialogFieldViewState createState() => - __MultiSelectDialogFieldViewState(); -} - -class __MultiSelectDialogFieldViewState - extends State<_MultiSelectDialogFieldView> { - List _selectedItems = []; - - @override - void initState() { - super.initState(); - if (widget.initialValue != null) { - _selectedItems.addAll(widget.initialValue!); - } - } - - Widget _buildInheritedChipDisplay() { - List?> chipDisplayItems = []; - chipDisplayItems = _selectedItems - .map((e) => - widget.items.firstWhereOrNull((element) => e == element.value)) - .toList(); - chipDisplayItems.removeWhere((element) => element == null); - if (widget.chipDisplay != null) { - // if user has specified a chipDisplay, use its params - if (widget.chipDisplay!.disabled!) { - return Container(); - } else { - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.chipDisplay!.colorator ?? widget.colorator, - onTap: (item) { - List? newValues; - if (widget.chipDisplay!.onTap != null) { - dynamic result = widget.chipDisplay!.onTap!(item); - if (result is List) newValues = result; - } - if (newValues != null) { - _selectedItems = newValues; - if (widget.state != null) { - widget.state!.didChange(_selectedItems); - } - } - }, - decoration: widget.chipDisplay!.decoration, - chipColor: widget.chipDisplay!.chipColor ?? - ((widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null), - alignment: widget.chipDisplay!.alignment, - textStyle: widget.chipDisplay!.textStyle, - icon: widget.chipDisplay!.icon, - shape: widget.chipDisplay!.shape, - scroll: widget.chipDisplay!.scroll, - scrollBar: widget.chipDisplay!.scrollBar, - height: widget.chipDisplay!.height, - chipWidth: widget.chipDisplay!.chipWidth, - ); - } - } else { - // user didn't specify a chipDisplay, build the default - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.colorator, - chipColor: (widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null, - ); - } - } - - /// Calls showDialog() and renders a MultiSelectDialog. - _showDialog(BuildContext ctx) async { - await showDialog( - barrierColor: widget.barrierColor, - context: context, - builder: (ctx) { - return MultiSelectDialog( - checkColor: widget.checkColor, - selectedItemsTextStyle: widget.selectedItemsTextStyle, - searchHintStyle: widget.searchHintStyle, - searchTextStyle: widget.searchTextStyle, - itemsTextStyle: widget.itemsTextStyle, - searchIcon: widget.searchIcon, - closeSearchIcon: widget.closeSearchIcon, - unselectedColor: widget.unselectedColor, - backgroundColor: widget.backgroundColor, - colorator: widget.colorator, - searchHint: widget.searchHint, - selectedColor: widget.selectedColor, - onSelectionChanged: widget.onSelectionChanged, - height: widget.dialogHeight, - width: widget.dialogWidth, - listType: widget.listType, - items: widget.items, - title: widget.title ?? const Text("Select"), - initialValue: _selectedItems, - searchable: widget.searchable ?? false, - confirmText: widget.confirmText, - cancelText: widget.cancelText, - separateSelectedItems: widget.separateSelectedItems, - onConfirm: (selected) { - if (widget.state != null) { - widget.state!.didChange(selected); - } - _selectedItems = selected; - if (widget.onConfirm != null) widget.onConfirm!(selected); - }, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - InkWell( - onTap: () { - _showDialog(context); - }, - child: Container( - decoration: widget.state != null - ? widget.decoration ?? - BoxDecoration( - border: Border( - bottom: BorderSide( - color: widget.state != null && widget.state!.hasError - ? Colors.red.shade800.withOpacity(0.6) - : _selectedItems.isNotEmpty - ? (widget.selectedColor != null && - widget.selectedColor != - Colors.transparent) - ? widget.selectedColor! - : Theme.of(context).primaryColor - : Colors.black45, - width: _selectedItems.isNotEmpty - ? (widget.state != null && widget.state!.hasError) - ? 1.4 - : 1.8 - : 1.2, - ), - ), - ) - : widget.decoration, - padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - widget.buttonText ?? const Text("Select"), - widget.buttonIcon ?? const Icon(Icons.arrow_downward), - ], - ), - ), - ), - _buildInheritedChipDisplay(), - widget.state != null && widget.state!.hasError - ? const SizedBox(height: 5) - : Container(), - widget.state != null && widget.state!.hasError - ? Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 4), - child: Text( - widget.state!.errorText!, - style: TextStyle( - color: Colors.red[800], - fontSize: 12.5, - ), - ), - ), - ], - ) - : Container(), - ], - ); - } -} +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import '../util/multi_select_list_type.dart'; +import '../util/multi_select_item.dart'; +import '../chip_display/multi_select_chip_display.dart'; +import 'mult_select_dialog.dart'; + +/// A customizable InkWell widget that opens the MultiSelectDialog +// ignore: must_be_immutable +class MultiSelectDialogField extends FormField> { + /// An enum that determines which type of list to render. + final MultiSelectListType? listType; + + /// Style the Container that makes up the field. + final BoxDecoration? decoration; + + /// Set text that is displayed on the button. + final Text? buttonText; + + /// Specify the button icon. + final Icon? buttonIcon; + + /// The text at the top of the dialog. + final Widget? title; + + /// List of items to select from. + final List> items; + + /// Fires when the an item is selected / unselected. + final void Function(List)? onSelectionChanged; + + /// Overrides the default MultiSelectChipDisplay attached to this field. + /// If you want to remove it, use MultiSelectChipDisplay.none(). + final MultiSelectChipDisplay? chipDisplay; + + /// The list of selected values before interaction. + final List? initialValue; + + /// Fires when confirm is tapped. + final void Function(List) onConfirm; + + /// Toggles search functionality. + final bool searchable; + + /// Text on the confirm button. + final Text? confirmText; + + /// Text on the cancel button. + final Text? cancelText; + + /// Set the color of the space outside the BottomSheet. + final Color? barrierColor; + + /// Sets the color of the checkbox or chip when it's selected. + final Color? selectedColor; + + /// Sets a fixed height on the dialog. + final double? dialogHeight; + + /// Sets a fixed width on the dialog. + final double? dialogWidth; + + /// Set the placeholder text of the search field. + final String? searchHint; + + /// A function that sets the color of selected items based on their value. + /// It will either set the chip color, or the checkbox color depending on the list type. + final Color Function(V)? colorator; + + /// Set the background color of the dialog. + final Color? backgroundColor; + + /// Color of the chip body or checkbox border while not selected. + final Color? unselectedColor; + + /// Replaces the deafult search icon when searchable is true. + final Icon? searchIcon; + + /// Replaces the default close search icon when searchable is true. + final Icon? closeSearchIcon; + + /// Style the text on the chips or list tiles. + final TextStyle? itemsTextStyle; + + /// Style the text on the selected chips or list tiles. + final TextStyle? selectedItemsTextStyle; + + /// Style the text that is typed into the search field. + final TextStyle? searchTextStyle; + + /// Style the search hint. + final TextStyle? searchHintStyle; + + /// Moves the selected items to the top of the list. + final bool separateSelectedItems; + + /// Set the color of the check in the checkbox + final Color? checkColor; + + final AutovalidateMode autovalidateMode; + final FormFieldValidator>? validator; + final FormFieldSetter>? onSaved; + final GlobalKey? key; + FormFieldState>? state; + + MultiSelectDialogField({ + required this.items, + required this.onConfirm, + this.title, + this.buttonText, + this.buttonIcon, + this.listType, + this.decoration, + this.onSelectionChanged, + this.chipDisplay, + this.searchable = false, + this.confirmText, + this.cancelText, + this.barrierColor, + this.selectedColor, + this.searchHint, + this.dialogHeight, + this.dialogWidth, + this.colorator, + this.backgroundColor, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + this.onSaved, + this.validator, + this.initialValue, + this.autovalidateMode = AutovalidateMode.disabled, + this.key, + }) : super( + key: key, + onSaved: onSaved, + validator: validator, + autovalidateMode: autovalidateMode, + initialValue: initialValue ?? [], + builder: (FormFieldState> state) { + _MultiSelectDialogFieldView field = + _MultiSelectDialogFieldView( + title: title, + items: items, + buttonText: buttonText, + buttonIcon: buttonIcon, + chipDisplay: chipDisplay, + decoration: decoration, + listType: listType, + onConfirm: onConfirm, + onSelectionChanged: onSelectionChanged, + initialValue: initialValue, + searchable: searchable, + confirmText: confirmText, + cancelText: cancelText, + barrierColor: barrierColor, + selectedColor: selectedColor, + searchHint: searchHint, + dialogHeight: dialogHeight, + dialogWidth: dialogWidth, + colorator: colorator, + backgroundColor: backgroundColor, + unselectedColor: unselectedColor, + searchIcon: searchIcon, + closeSearchIcon: closeSearchIcon, + itemsTextStyle: itemsTextStyle, + searchTextStyle: searchTextStyle, + searchHintStyle: searchHintStyle, + selectedItemsTextStyle: selectedItemsTextStyle, + separateSelectedItems: separateSelectedItems, + checkColor: checkColor, + ); + return _MultiSelectDialogFieldView._withState(field, state); + }); +} + +// ignore: must_be_immutable +class _MultiSelectDialogFieldView extends StatefulWidget { + final MultiSelectListType? listType; + final BoxDecoration? decoration; + final Text? buttonText; + final Icon? buttonIcon; + final Widget? title; + final List> items; + final void Function(List)? onSelectionChanged; + final MultiSelectChipDisplay? chipDisplay; + final List? initialValue; + final void Function(List)? onConfirm; + final bool? searchable; + final Text? confirmText; + final Text? cancelText; + final Color? barrierColor; + final Color? selectedColor; + final double? dialogHeight; + final double? dialogWidth; + final String? searchHint; + final Color Function(V)? colorator; + final Color? backgroundColor; + final Color? unselectedColor; + final Icon? searchIcon; + final Icon? closeSearchIcon; + final TextStyle? itemsTextStyle; + final TextStyle? selectedItemsTextStyle; + final TextStyle? searchTextStyle; + final TextStyle? searchHintStyle; + final bool separateSelectedItems; + final Color? checkColor; + FormFieldState>? state; + + _MultiSelectDialogFieldView({ + required this.items, + this.title, + this.buttonText, + this.buttonIcon, + this.listType, + this.decoration, + this.onSelectionChanged, + this.onConfirm, + this.chipDisplay, + this.initialValue, + this.searchable, + this.confirmText, + this.cancelText, + this.barrierColor, + this.selectedColor, + this.searchHint, + this.dialogHeight, + this.dialogWidth, + this.colorator, + this.backgroundColor, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + }); + + /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectDialogField. + _MultiSelectDialogFieldView._withState( + _MultiSelectDialogFieldView field, FormFieldState> state) + : items = field.items, + title = field.title, + buttonText = field.buttonText, + buttonIcon = field.buttonIcon, + listType = field.listType, + decoration = field.decoration, + onSelectionChanged = field.onSelectionChanged, + onConfirm = field.onConfirm, + chipDisplay = field.chipDisplay, + initialValue = field.initialValue, + searchable = field.searchable, + confirmText = field.confirmText, + cancelText = field.cancelText, + barrierColor = field.barrierColor, + selectedColor = field.selectedColor, + dialogHeight = field.dialogHeight, + dialogWidth = field.dialogWidth, + searchHint = field.searchHint, + colorator = field.colorator, + backgroundColor = field.backgroundColor, + unselectedColor = field.unselectedColor, + searchIcon = field.searchIcon, + closeSearchIcon = field.closeSearchIcon, + itemsTextStyle = field.itemsTextStyle, + searchHintStyle = field.searchHintStyle, + searchTextStyle = field.searchTextStyle, + selectedItemsTextStyle = field.selectedItemsTextStyle, + separateSelectedItems = field.separateSelectedItems, + checkColor = field.checkColor, + state = state; + + @override + __MultiSelectDialogFieldViewState createState() => + __MultiSelectDialogFieldViewState(); +} + +class __MultiSelectDialogFieldViewState + extends State<_MultiSelectDialogFieldView> { + List _selectedItems = []; + + @override + void initState() { + super.initState(); + if (widget.initialValue != null) { + _selectedItems.addAll(widget.initialValue!); + } + } + + Widget _buildInheritedChipDisplay() { + List?> chipDisplayItems = []; + chipDisplayItems = _selectedItems + .map((e) => + widget.items.firstWhereOrNull((element) => e == element.value)) + .toList(); + chipDisplayItems.removeWhere((element) => element == null); + if (widget.chipDisplay != null) { + // if user has specified a chipDisplay, use its params + if (widget.chipDisplay!.disabled!) { + return Container(); + } else { + return MultiSelectChipDisplay( + items: chipDisplayItems, + colorator: widget.chipDisplay!.colorator ?? widget.colorator, + onTap: (item) { + List? newValues; + if (widget.chipDisplay!.onTap != null) { + dynamic result = widget.chipDisplay!.onTap!(item); + if (result is List) newValues = result; + } + if (newValues != null) { + _selectedItems = newValues; + if (widget.state != null) { + widget.state!.didChange(_selectedItems); + } + } + }, + decoration: widget.chipDisplay!.decoration, + chipColor: widget.chipDisplay!.chipColor ?? + ((widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(0.35) + : null), + alignment: widget.chipDisplay!.alignment, + textStyle: widget.chipDisplay!.textStyle, + icon: widget.chipDisplay!.icon, + shape: widget.chipDisplay!.shape, + scroll: widget.chipDisplay!.scroll, + scrollBar: widget.chipDisplay!.scrollBar, + height: widget.chipDisplay!.height, + chipWidth: widget.chipDisplay!.chipWidth, + ); + } + } else { + // user didn't specify a chipDisplay, build the default + return MultiSelectChipDisplay( + items: chipDisplayItems, + colorator: widget.colorator, + chipColor: (widget.selectedColor != null && + widget.selectedColor != Colors.transparent) + ? widget.selectedColor!.withOpacity(0.35) + : null, + ); + } + } + + /// Calls showDialog() and renders a MultiSelectDialog. + _showDialog(BuildContext ctx) async { + await showDialog( + barrierColor: widget.barrierColor, + context: context, + builder: (ctx) { + return MultiSelectDialog( + checkColor: widget.checkColor, + selectedItemsTextStyle: widget.selectedItemsTextStyle, + searchHintStyle: widget.searchHintStyle, + searchTextStyle: widget.searchTextStyle, + itemsTextStyle: widget.itemsTextStyle, + searchIcon: widget.searchIcon, + closeSearchIcon: widget.closeSearchIcon, + unselectedColor: widget.unselectedColor, + backgroundColor: widget.backgroundColor, + colorator: widget.colorator, + searchHint: widget.searchHint, + selectedColor: widget.selectedColor, + onSelectionChanged: widget.onSelectionChanged, + height: widget.dialogHeight, + width: widget.dialogWidth, + listType: widget.listType, + items: widget.items, + title: widget.title ?? const Text("Select"), + initialValue: _selectedItems, + searchable: widget.searchable ?? false, + confirmText: widget.confirmText, + cancelText: widget.cancelText, + separateSelectedItems: widget.separateSelectedItems, + onConfirm: (selected) { + if (widget.state != null) { + widget.state!.didChange(selected); + } + _selectedItems = selected; + if (widget.onConfirm != null) widget.onConfirm!(selected); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + InkWell( + onTap: () { + _showDialog(context); + }, + child: Container( + decoration: widget.state != null + ? widget.decoration ?? + BoxDecoration( + border: Border( + bottom: BorderSide( + color: widget.state != null && widget.state!.hasError + ? Colors.red.shade800.withOpacity(0.6) + : _selectedItems.isNotEmpty + ? (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor! + : Theme.of(context).primaryColor + : Colors.black45, + width: _selectedItems.isNotEmpty + ? (widget.state != null && widget.state!.hasError) + ? 1.4 + : 1.8 + : 1.2, + ), + ), + ) + : widget.decoration, + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + widget.buttonText ?? const Text("Select"), + widget.buttonIcon ?? const Icon(Icons.arrow_downward), + ], + ), + ), + ), + _buildInheritedChipDisplay(), + widget.state != null && widget.state!.hasError + ? const SizedBox(height: 5) + : Container(), + widget.state != null && widget.state!.hasError + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 4), + child: Text( + widget.state!.errorText!, + style: TextStyle( + color: Colors.red[800], + fontSize: 12.5, + ), + ), + ), + ], + ) + : Container(), + ], + ); + } +} diff --git a/lib/multi_select_flutter.dart b/lib/multi_select_flutter.dart index dd32896..ee79f40 100644 --- a/lib/multi_select_flutter.dart +++ b/lib/multi_select_flutter.dart @@ -1,10 +1,10 @@ -export 'util/multi_select_item.dart'; -export 'util/multi_select_list_type.dart'; -export 'util/multi_select_actions.dart'; -export 'dialog/mult_select_dialog.dart'; -export 'dialog/multi_select_dialog_field.dart'; -export 'bottom_sheet/multi_select_bottom_sheet.dart'; -export 'bottom_sheet/multi_select_bottom_sheet_field.dart'; -export 'chip_display/multi_select_chip_display.dart'; -export 'chip_field/multi_select_chip_field.dart'; -export 'util/horizontal_scrollbar.dart'; +export 'util/multi_select_item.dart'; +export 'util/multi_select_list_type.dart'; +export 'util/multi_select_actions.dart'; +export 'dialog/mult_select_dialog.dart'; +export 'dialog/multi_select_dialog_field.dart'; +export 'bottom_sheet/multi_select_bottom_sheet.dart'; +export 'bottom_sheet/multi_select_bottom_sheet_field.dart'; +export 'chip_display/multi_select_chip_display.dart'; +export 'chip_field/multi_select_chip_field.dart'; +export 'util/horizontal_scrollbar.dart'; diff --git a/lib/util/horizontal_scrollbar.dart b/lib/util/horizontal_scrollbar.dart index bc33aa9..cc1bcb1 100644 --- a/lib/util/horizontal_scrollbar.dart +++ b/lib/util/horizontal_scrollbar.dart @@ -1,5 +1,5 @@ -class HorizontalScrollBar { - final bool isAlwaysShown; - - HorizontalScrollBar({this.isAlwaysShown = false}); -} +class HorizontalScrollBar { + final bool isAlwaysShown; + + HorizontalScrollBar({this.isAlwaysShown = false}); +} diff --git a/lib/util/multi_select_actions.dart b/lib/util/multi_select_actions.dart index d8ad5e0..301dbb0 100644 --- a/lib/util/multi_select_actions.dart +++ b/lib/util/multi_select_actions.dart @@ -1,64 +1,64 @@ -import 'package:flutter/material.dart'; -import 'multi_select_item.dart'; - -/// Contains common actions that are used by different multi select classes. -class MultiSelectActions { - List onItemCheckedChange( - List selectedValues, T itemValue, bool checked) { - if (checked) { - selectedValues.add(itemValue); - } else { - selectedValues.remove(itemValue); - } - return selectedValues; - } - - /// Pops the dialog from the navigation stack and returns the initially selected values. - void onCancelTap(BuildContext ctx, List initiallySelectedValues) { - Navigator.pop(ctx, initiallySelectedValues); - } - - /// Pops the dialog from the navigation stack and returns the selected values. - /// Calls the onConfirm function if one was provided. - void onConfirmTap( - BuildContext ctx, List selectedValues, Function(List)? onConfirm) { - Navigator.pop(ctx, selectedValues); - if (onConfirm != null) { - onConfirm(selectedValues); - } - } - - /// Accepts the search query, and the original list of items. - /// If the search query is valid, return a filtered list, otherwise return the original list. - List> updateSearchQuery( - String? val, List> allItems) { - if (val != null && val.trim().isNotEmpty) { - List> filteredItems = []; - for (var item in allItems) { - if (item.label.toLowerCase().contains(val.toLowerCase())) { - filteredItems.add(item); - } - } - return filteredItems; - } else { - return allItems; - } - } - - /// Toggles the search field. - bool onSearchTap(bool showSearch) { - return !showSearch; - } - - List> separateSelected(List> list) { - List> _selectedItems = []; - List> _nonSelectedItems = []; - - _nonSelectedItems.addAll(list.where((element) => !element.selected)); - _nonSelectedItems.sort((a, b) => a.label.compareTo(b.label)); - _selectedItems.addAll(list.where((element) => element.selected)); - _selectedItems.sort((a, b) => a.label.compareTo(b.label)); - - return [..._selectedItems, ..._nonSelectedItems]; - } -} +import 'package:flutter/material.dart'; +import 'multi_select_item.dart'; + +/// Contains common actions that are used by different multi select classes. +class MultiSelectActions { + List onItemCheckedChange( + List selectedValues, T itemValue, bool checked) { + if (checked) { + selectedValues.add(itemValue); + } else { + selectedValues.remove(itemValue); + } + return selectedValues; + } + + /// Pops the dialog from the navigation stack and returns the initially selected values. + void onCancelTap(BuildContext ctx, List initiallySelectedValues) { + Navigator.pop(ctx, initiallySelectedValues); + } + + /// Pops the dialog from the navigation stack and returns the selected values. + /// Calls the onConfirm function if one was provided. + void onConfirmTap( + BuildContext ctx, List selectedValues, Function(List)? onConfirm) { + Navigator.pop(ctx, selectedValues); + if (onConfirm != null) { + onConfirm(selectedValues); + } + } + + /// Accepts the search query, and the original list of items. + /// If the search query is valid, return a filtered list, otherwise return the original list. + List> updateSearchQuery( + String? val, List> allItems) { + if (val != null && val.trim().isNotEmpty) { + List> filteredItems = []; + for (var item in allItems) { + if (item.label.toLowerCase().contains(val.toLowerCase())) { + filteredItems.add(item); + } + } + return filteredItems; + } else { + return allItems; + } + } + + /// Toggles the search field. + bool onSearchTap(bool showSearch) { + return !showSearch; + } + + List> separateSelected(List> list) { + List> _selectedItems = []; + List> _nonSelectedItems = []; + + _nonSelectedItems.addAll(list.where((element) => !element.selected)); + _nonSelectedItems.sort((a, b) => a.label.compareTo(b.label)); + _selectedItems.addAll(list.where((element) => element.selected)); + _selectedItems.sort((a, b) => a.label.compareTo(b.label)); + + return [..._selectedItems, ..._nonSelectedItems]; + } +} diff --git a/lib/util/multi_select_item.dart b/lib/util/multi_select_item.dart index 6222f22..874facc 100644 --- a/lib/util/multi_select_item.dart +++ b/lib/util/multi_select_item.dart @@ -1,8 +1,8 @@ -/// A model class used to represent a selectable item. -class MultiSelectItem { - final T value; - final String label; - bool selected = false; - - MultiSelectItem(this.value, this.label); -} +/// A model class used to represent a selectable item. +class MultiSelectItem { + final T value; + final String label; + bool selected = false; + + MultiSelectItem(this.value, this.label); +} diff --git a/lib/util/multi_select_list_type.dart b/lib/util/multi_select_list_type.dart index 1152a17..bcfd67f 100644 --- a/lib/util/multi_select_list_type.dart +++ b/lib/util/multi_select_list_type.dart @@ -1,2 +1,2 @@ -/// Used by MultiSelectDialog and MultiSelectBottomSheet to determine which type of list to render. -enum MultiSelectListType { LIST, CHIP } +/// Used by MultiSelectDialog and MultiSelectBottomSheet to determine which type of list to render. +enum MultiSelectListType { LIST, CHIP }