selectable_draggable_listbox 0.0.2 copy "selectable_draggable_listbox: ^0.0.2" to clipboard
selectable_draggable_listbox: ^0.0.2 copied to clipboard

outdated

Listbox with multiselect, drag & drop between lists, and reorder built in.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:selectable_draggable_listbox/selectable_draggable_listbox.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Selectable Draggable Listbox Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Selectable Draggable Listbox Demo'),
    );
  }
}

class GroceryItem {
  final String name;
  DateTime? lastBought;

  String get lastBoughtShortDtTm => lastBought == null
      ? ''
      : DateFormat('MM/dd/yyyy kk:mm').format(lastBought!);

  GroceryItem({
    required this.name,
    this.lastBought,
  });
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: const Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            // See this widget for: Multi-select, Reorder, and Drag From
            GroceryListWidget(),

            // See this widget for: Single-select, Customized Template, and Drag To
            RecentListWidget(),
          ],
        ),
      ),
    );
  }
}

class GroceryListWidget extends StatefulWidget {
  const GroceryListWidget({
    super.key,
  });

  @override
  State<GroceryListWidget> createState() => _GroceryListWidgetState();
}

class _GroceryListWidgetState extends State<GroceryListWidget> {
  final _groceryList = [
    GroceryItem(name: 'Apples'),
    GroceryItem(name: 'Bananas'),
    GroceryItem(name: 'Milk'),
    GroceryItem(name: 'Cheese'),
    GroceryItem(name: 'Bread'),
  ].forListbox().toList();

  @override
  Widget build(BuildContext context) {
    /// Make a simple listbox item
    /// This demonstrates using the index to show a number prefix
    /// Slightly different for the "drag template" that follows your mouse pointer - removes the number prefix
    Widget makeItemTemplate(
      int index,
      ListItem<GroceryItem> item,
      void Function(ListItem<GroceryItem>)? onSelect,
      bool isDragging,
    ) {
      return SimpleListboxItem(
        key: Key('$index'),
        item: item,
        label: isDragging ? item.data.name : '${index + 1}. ${item.data.name}',
        onSelect: onSelect,
        isDragging: isDragging,
      );
    }

    void onSelect(Iterable<ListItem<GroceryItem>> itemsSelected) {
      debugPrint(
          'Selected: ${itemsSelected.map((e) => e.data.name).join(',')}');
      setState(() {
        for (var item in _groceryList) {
          item.isSelected = itemsSelected.contains(item);
        }
      });
    }

    void onReorder(int oldIndex, int newIndex) {
      debugPrint('Moving item from $oldIndex to $newIndex');
      final element = _groceryList[oldIndex];
      setState(() {
        _groceryList.removeAt(oldIndex);
        _groceryList.insert(newIndex, element);
      });
    }

    return Flexible(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Text(
              'Grocery List',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            const Text('Features: Multi-select, Reorder, Drag From'),
            Expanded(
              child: Builder(
                builder: (context) {
                  return Listbox(
                    // Key is only needed if you want to identify which list you're interacting with in debug:
                    key: const Key('GroceryList'),
                    items: _groceryList,
                    onSelect: onSelect,
                    onReorder: onReorder,
                    itemTemplate: (context, index, item, onSelect) =>
                        makeItemTemplate(index, item, onSelect, false),
                    dragTemplate: (context, index, item) =>
                        makeItemTemplate(index, item, null, true),
                    // Show info in console about how the list is being interacted with:
                    enableDebug: true,
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class RecentListWidget extends StatefulWidget {
  const RecentListWidget({
    super.key,
  });

  @override
  State<RecentListWidget> createState() => _RecentListWidgetState();
}

class _RecentListWidgetState extends State<RecentListWidget> {
  final _recentList = [
    GroceryItem(
      name: 'Apples',
      lastBought: DateTime(2024, 3, 3, 13, 22, 33),
    ),
    GroceryItem(
      name: 'Bread',
      lastBought: DateTime(2024, 3, 4, 10, 14, 12),
    )
  ].forListbox().toList();

  @override
  Widget build(BuildContext context) {
    /// Make a more complex listbox item
    /// This demonstrates using the childTemplate to show a row with a badge
    /// indicating the date the item was dragged to this list
    Widget makeItemTemplate(
      int index,
      ListItem<GroceryItem> item,
      String label,
      void Function(ListItem<GroceryItem>)? onSelect,
      bool isDragPlaceholder,
    ) {
      return TemplatedListboxItem(
        key: Key('$index'),
        item: item,
        childTemplate: (context, item) {
          return Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(label),
                if (!isDragPlaceholder)
                  Badge(
                    label: Text(item.data.lastBoughtShortDtTm),
                  ),
              ],
            ),
          );
        },
        onSelect: onSelect,
      );
    }

    void onSelect(Iterable<ListItem<GroceryItem>> itemsSelected) {
      debugPrint(
          'Selected: ${itemsSelected.map((e) => e.data.name).join(',')}');
      setState(() {
        for (var item in _recentList) {
          item.isSelected = itemsSelected.contains(item);
        }
      });
    }

    void onDrop(Iterable<ListItem<GroceryItem>> itemsDropped, int index) {
      debugPrint(
          'Dropped items ${itemsDropped.map((e) => e.data.name).join(',')} into index $index');

      // It is important to copy the items not just accept them
      // (otherwise they'd have same reference and selected state would cross).
      // Also in this example, we'll avoid adding duplicates.
      final existingNames = _recentList.map((i) => i.data.name);
      final itemsToInsert = itemsDropped
          .where((i) => !existingNames.contains(i.data.name))
          .map((i) => ListItem(
              GroceryItem(name: i.data.name, lastBought: DateTime.now())))
          .toList();
      _recentList.insertAll(index, itemsToInsert);
    }

    return Flexible(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Text(
              'Recently Bought',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            const Text('Features: Single-select, Customized Template, Drag To'),
            Expanded(
              child: Builder(
                builder: (context) {
                  return Listbox(
                    // Key is only needed if you want to identify which list you're interacting with in debug:
                    key: const Key('RecentList'),
                    items: _recentList,
                    onSelect: onSelect,
                    onDrop: onDrop,
                    itemTemplate: (context, index, item, onSelect) =>
                        makeItemTemplate(
                            index, item, item.data.name, onSelect, false),
                    dropPlaceholderTemplate:
                        (context, index, item, itemsToBeDropped) {
                      var itemsLength = itemsToBeDropped.length;

                      // Here we're going to adjust the label that shows in the new list's placeholder,
                      // depending on whether we are dragging 1 item (just show name) or more (count of new items)
                      var label = itemsLength > 1
                          ? '$itemsLength new items...'
                          : itemsLength > 0
                              ? itemsToBeDropped.first.data.name
                              : '';
                      return makeItemTemplate(index, item, label, null, true);
                    },
                    // Can just turn off multiselect if not needed:
                    disableMultiSelect: true,
                    // Show info in console about how the list is being interacted with:
                    enableDebug: true,
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
26
likes
0
points
40
downloads

Publisher

unverified uploader

Weekly Downloads

Listbox with multiselect, drag & drop between lists, and reorder built in.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

collection, flutter

More

Packages that depend on selectable_draggable_listbox