diff --git a/lib/src/manager/pluto_grid_state_manager.dart b/lib/src/manager/pluto_grid_state_manager.dart index 28de195d6..668491981 100644 --- a/lib/src/manager/pluto_grid_state_manager.dart +++ b/lib/src/manager/pluto_grid_state_manager.dart @@ -79,6 +79,7 @@ class PlutoGridStateChangeNotifier extends PlutoChangeNotifier this.onRowsMoved, this.onColumnsMoved, this.rowColorCallback, + this.cellColorCallback, this.createHeader, this.createFooter, PlutoColumnMenuDelegate? columnMenuDelegate, @@ -142,6 +143,9 @@ class PlutoGridStateChangeNotifier extends PlutoChangeNotifier @override final PlutoRowColorCallback? rowColorCallback; + @override + final PlutoCellColorCallback? cellColorCallback; + @override final CreateHeaderCallBack? createHeader; @@ -223,6 +227,7 @@ class PlutoGridStateManager extends PlutoGridStateChangeNotifier { super.onRowsMoved, super.onColumnsMoved, super.rowColorCallback, + super.cellColorCallback, super.createHeader, super.createFooter, super.columnMenuDelegate, diff --git a/lib/src/manager/state/cell_state.dart b/lib/src/manager/state/cell_state.dart index c5ca41133..4f05df0e2 100644 --- a/lib/src/manager/state/cell_state.dart +++ b/lib/src/manager/state/cell_state.dart @@ -5,6 +5,8 @@ abstract class ICellState { /// currently selected cell. PlutoCell? get currentCell; + PlutoCellColorCallback? get cellColorCallback; + /// The position index value of the currently selected cell. PlutoGridCellPosition? get currentCellPosition; diff --git a/lib/src/manager/state/editing_state.dart b/lib/src/manager/state/editing_state.dart index d07fbd6f2..e64f0100a 100644 --- a/lib/src/manager/state/editing_state.dart +++ b/lib/src/manager/state/editing_state.dart @@ -44,6 +44,8 @@ abstract class IEditingState { bool callOnChangedEvent = true, bool force = false, bool notify = true, + bool eachChange = false, + String? validationError, }); } @@ -212,6 +214,8 @@ mixin EditingState implements IPlutoGridState { bool callOnChangedEvent = true, bool force = false, bool notify = true, + bool eachChange = false, + String? validationError, }) { final currentColumn = cell.column; @@ -239,16 +243,28 @@ mixin EditingState implements IPlutoGridState { currentRow.setState(PlutoRowState.updated); cell.value = value; + cell.validationError = validationError; if (callOnChangedEvent == true && onChanged != null) { - onChanged!(PlutoGridOnChangedEvent( - columnIdx: columnIndex(currentColumn)!, - column: currentColumn, - rowIdx: refRows.indexOf(currentRow), - row: currentRow, - value: value, - oldValue: oldValue, - )); + onChanged!( + eachChange + ? PlutoGridOnEachChangedEvent( + columnIdx: columnIndex(currentColumn)!, + column: currentColumn, + rowIdx: refRows.indexOf(currentRow), + row: currentRow, + value: value, + oldValue: oldValue, + ) + : PlutoGridOnChangedEvent( + columnIdx: columnIndex(currentColumn)!, + column: currentColumn, + rowIdx: refRows.indexOf(currentRow), + row: currentRow, + value: value, + oldValue: oldValue, + ), + ); } notifyListeners(notify, changeCellValue.hashCode); diff --git a/lib/src/manager/state/layout_state.dart b/lib/src/manager/state/layout_state.dart index 0732270bb..cbc53e7fd 100644 --- a/lib/src/manager/state/layout_state.dart +++ b/lib/src/manager/state/layout_state.dart @@ -440,6 +440,40 @@ mixin LayoutState implements IPlutoGridState { notifyListeners(notify, setShowColumnFooter.hashCode); } + bool validate({bool notify = true}) { + bool isValid = true; + for (var rowElement in refRows.originalList) { + for (var cellElement in rowElement.cells.values) { + if (cellElement.initialized) { + if (cellElement.column.validator != null && + !cellElement.column.hide) { + final validationError = cellElement.column.validator?.call( + rowElement, + cellElement, + cellElement.value, + ); + + changeCellValue( + cellElement, + cellElement.value, + force: true, + eachChange: true, + callOnChangedEvent: true, + validationError: validationError, + ); + + if (validationError != null) { + isValid = false; + } + } + } + } + } + + notifyListeners(notify, validate.hashCode); + return isValid; + } + @override void setShowColumnFilter(bool flag, {bool notify = true}) { if (showColumnFilter == flag) { diff --git a/lib/src/model/pluto_cell.dart b/lib/src/model/pluto_cell.dart index 072ef58b5..48c8cfc37 100644 --- a/lib/src/model/pluto_cell.dart +++ b/lib/src/model/pluto_cell.dart @@ -10,6 +10,8 @@ class PlutoCell { final Key _key; + String? _validationError; + dynamic _value; dynamic _valueForSorting; @@ -52,6 +54,18 @@ class PlutoCell { return _value; } + String? get validationError { + if (_needToApplyFormatOnInit) { + _applyFormatOnInit(); + } + + return _validationError; + } + + bool get hasValidationError { + return validationError != null && validationError!.trim().isNotEmpty; + } + set value(dynamic changed) { if (_value == changed) { return; @@ -62,6 +76,13 @@ class PlutoCell { _valueForSorting = null; } + set validationError(String? changed) { + if (_validationError == changed) { + return; + } + _validationError = changed; + } + dynamic get valueForSorting { _valueForSorting ??= _getValueForSorting(); diff --git a/lib/src/model/pluto_column.dart b/lib/src/model/pluto_column.dart index 4945d6632..d78769a62 100644 --- a/lib/src/model/pluto_column.dart +++ b/lib/src/model/pluto_column.dart @@ -2,6 +2,11 @@ import 'package:flutter/material.dart'; import 'package:pluto_grid/pluto_grid.dart'; typedef PlutoColumnValueFormatter = String Function(dynamic value); +typedef PlutoColumnValueValidator = String? Function( + PlutoRow row, + PlutoCell cell, + dynamic value, +); typedef PlutoColumnRenderer = Widget Function( PlutoColumnRendererContext rendererContext); @@ -67,6 +72,9 @@ class PlutoColumn { /// It takes precedence over defaultCellPadding in PlutoGridConfiguration. EdgeInsets? cellPadding; + /// Customisable cellField padding. + EdgeInsets? cellInternalPadding; + /// Text alignment in Cell. (Left, Right, Center) PlutoColumnTextAlign textAlign; @@ -192,6 +200,12 @@ class PlutoColumn { /// Hide the column. bool hide; + /// Trigger [PlutoGridOnEachChangedEvent] for each character change + bool enablePlutoGridOnEachChangedEvent; + + /// Field Validator for the column + PlutoColumnValueValidator? validator; + PlutoColumn({ required this.title, required this.field, @@ -204,6 +218,7 @@ class PlutoColumn { this.filterPadding, this.titleSpan, this.cellPadding, + this.cellInternalPadding, this.textAlign = PlutoColumnTextAlign.start, this.titleTextAlign = PlutoColumnTextAlign.start, this.frozen = PlutoColumnFrozen.none, @@ -226,6 +241,8 @@ class PlutoColumn { this.enableAutoEditing = false, this.enableEditingMode = true, this.hide = false, + this.enablePlutoGridOnEachChangedEvent = false, + this.validator, }) : _key = UniqueKey(), _checkReadOnly = checkReadOnly; diff --git a/lib/src/pluto_grid.dart b/lib/src/pluto_grid.dart index 672b8e4cd..c1f6244dc 100644 --- a/lib/src/pluto_grid.dart +++ b/lib/src/pluto_grid.dart @@ -45,6 +45,9 @@ typedef CreateFooterCallBack = Widget Function( typedef PlutoRowColorCallback = Color Function( PlutoRowColorContext rowColorContext); +typedef PlutoCellColorCallback = Color? Function( + PlutoCellColorContext cellColorContext); + /// [PlutoGrid] is a widget that receives columns and rows and is expressed as a grid-type UI. /// /// [PlutoGrid] supports movement and editing with the keyboard, @@ -72,6 +75,7 @@ class PlutoGrid extends PlutoStatefulWidget { this.createFooter, this.noRowsWidget, this.rowColorCallback, + this.cellColorCallback, this.columnMenuDelegate, this.configuration = const PlutoGridConfiguration(), this.notifierFilterResolver, @@ -291,6 +295,8 @@ class PlutoGrid extends PlutoStatefulWidget { /// {@endtemplate} final PlutoRowColorCallback? rowColorCallback; + final PlutoCellColorCallback? cellColorCallback; + /// {@template pluto_grid_property_columnMenuDelegate} /// Column menu can be customized. /// @@ -515,6 +521,7 @@ class PlutoGridState extends PlutoStateWithChange { onRowsMoved: widget.onRowsMoved, onColumnsMoved: widget.onColumnsMoved, rowColorCallback: widget.rowColorCallback, + cellColorCallback: widget.cellColorCallback, createHeader: widget.createHeader, createFooter: widget.createFooter, columnMenuDelegate: widget.columnMenuDelegate, @@ -1288,6 +1295,26 @@ class PlutoGridOnChangedEvent { } } +class PlutoGridOnEachChangedEvent extends PlutoGridOnChangedEvent { + const PlutoGridOnEachChangedEvent({ + required super.columnIdx, + required super.column, + required super.rowIdx, + required super.row, + super.value, + super.oldValue, + }); + + @override + String toString() { + String out = '[PlutoGridOnEachChangedEvent] '; + out += 'ColumnIndex : $columnIdx, RowIndex : $rowIdx\n'; + out += '::: oldValue : $oldValue\n'; + out += '::: newValue : $value'; + return out; + } +} + /// This is the argument value of the [PlutoGrid.onSelected] callback /// that is called when the [PlutoGrid.mode] value is in select mode. /// @@ -1470,6 +1497,28 @@ class PlutoRowColorContext { }); } +/// Argument of [PlutoGrid.cellColorCallback] callback +/// to dynamically change the background color of a row. +class PlutoCellColorContext { + final PlutoColumn column; + + final int rowIdx; + + final PlutoRow row; + + final PlutoCell cell; + + final PlutoGridStateManager stateManager; + + PlutoCellColorContext({ + required this.column, + required this.rowIdx, + required this.row, + required this.cell, + required this.stateManager, + }); +} + /// Extension class for [ScrollConfiguration.behavior] of [PlutoGrid]. class PlutoScrollBehavior extends MaterialScrollBehavior { const PlutoScrollBehavior({ diff --git a/lib/src/pluto_grid_configuration.dart b/lib/src/pluto_grid_configuration.dart index 9b946b169..6dfa199a1 100644 --- a/lib/src/pluto_grid_configuration.dart +++ b/lib/src/pluto_grid_configuration.dart @@ -192,6 +192,7 @@ class PlutoGridConfiguration { class PlutoGridStyleConfig { const PlutoGridStyleConfig({ + this.bodyPadding = EdgeInsets.zero, this.enableGridBorderShadow = false, this.enableColumnBorderVertical = true, this.enableColumnBorderHorizontal = true, @@ -248,6 +249,7 @@ class PlutoGridStyleConfig { }); const PlutoGridStyleConfig.dark({ + this.bodyPadding = EdgeInsets.zero, this.enableGridBorderShadow = false, this.enableColumnBorderVertical = true, this.enableColumnBorderHorizontal = true, @@ -303,6 +305,9 @@ class PlutoGridStyleConfig { this.gridPopupBorderRadius = BorderRadius.zero, }); + /// body padding + final EdgeInsets bodyPadding; + /// Enable borderShadow in [PlutoGrid]. final bool enableGridBorderShadow; diff --git a/lib/src/ui/cells/text_cell.dart b/lib/src/ui/cells/text_cell.dart index 1bb589444..042ce0c42 100644 --- a/lib/src/ui/cells/text_cell.dart +++ b/lib/src/ui/cells/text_cell.dart @@ -102,6 +102,7 @@ mixin TextCellState on State implements TextFieldProps { widget.stateManager.currentCell!, _initialCellValue, notify: false, + validationError: doValidation(_initialCellValue), ); } @@ -138,7 +139,11 @@ mixin TextCellState on State implements TextFieldProps { return; } - widget.stateManager.changeCellValue(widget.cell, _textController.text); + widget.stateManager.changeCellValue( + widget.cell, + _textController.text, + validationError: doValidation(_textController.text), + ); _textController.text = formattedValue; @@ -151,12 +156,22 @@ mixin TextCellState on State implements TextFieldProps { _cellEditingStatus = _CellEditingStatus.updated; } - void _handleOnChanged(String value) { + void _handleOnChanged(String value, [bool forceChangeCellValue = false]) { _cellEditingStatus = formattedValue != value.toString() ? _CellEditingStatus.changed : _initialCellValue.toString() == value.toString() ? _CellEditingStatus.init : _CellEditingStatus.updated; + + if (forceChangeCellValue || + widget.column.enablePlutoGridOnEachChangedEvent) { + widget.stateManager.changeCellValue( + widget.cell, + _textController.text, + eachChange: true, + validationError: doValidation(value), + ); + } } void _handleOnComplete() { @@ -164,7 +179,7 @@ mixin TextCellState on State implements TextFieldProps { _changeValue(); - _handleOnChanged(old); + _handleOnChanged(old, true); PlatformHelper.onMobile(() { widget.stateManager.setKeepFocus(false); @@ -231,20 +246,19 @@ mixin TextCellState on State implements TextFieldProps { cellFocus.requestFocus(); } - return TextField( + return TextFormField( + autovalidateMode: AutovalidateMode.always, focusNode: cellFocus, controller: _textController, readOnly: widget.column.checkReadOnly(widget.row, widget.cell), onChanged: _handleOnChanged, onEditingComplete: _handleOnComplete, - onSubmitted: (_) => _handleOnComplete(), + onFieldSubmitted: (_) => _handleOnComplete(), onTap: _handleOnTap, style: widget.stateManager.configuration.style.cellTextStyle, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide.none, - ), - contentPadding: EdgeInsets.zero, + decoration: InputDecoration( + border: const OutlineInputBorder(borderSide: BorderSide.none), + contentPadding: widget.column.cellInternalPadding ?? EdgeInsets.zero, ), maxLines: 1, keyboardType: keyboardType, @@ -253,6 +267,18 @@ mixin TextCellState on State implements TextFieldProps { textAlign: widget.column.textAlign.value, ); } + + String? doValidation(String value) { + if (widget.column.validator != null) { + return widget.column.validator?.call( + widget.row, + widget.cell, + value, + ); + } + + return null; + } } enum _CellEditingStatus { diff --git a/lib/src/ui/pluto_base_cell.dart b/lib/src/ui/pluto_base_cell.dart index c15c408c6..52b6d825b 100644 --- a/lib/src/ui/pluto_base_cell.dart +++ b/lib/src/ui/pluto_base_cell.dart @@ -220,9 +220,10 @@ class _CellContainerState extends PlutoStateWithChange<_CellContainer> { required Color cellColorInEditState, required Color cellColorInReadOnlyState, required PlutoGridSelectingMode selectingMode, + required Color? cellColorCallbackColor, }) { if (!hasFocus) { - return gridBackgroundColor; + return cellColorCallbackColor ?? gridBackgroundColor; } if (!isEditing) { @@ -250,9 +251,23 @@ class _CellContainerState extends PlutoStateWithChange<_CellContainer> { required Color? cellColorGroupedRow, required PlutoGridSelectingMode selectingMode, }) { + Color? color; + if (stateManager.cellColorCallback != null) { + color = stateManager.cellColorCallback!( + PlutoCellColorContext( + cell: widget.cell, + column: widget.column, + rowIdx: widget.rowIdx, + row: widget.row, + stateManager: stateManager, + ), + ); + } + if (isCurrentCell) { return BoxDecoration( color: _currentCellColor( + cellColorCallbackColor: color, hasFocus: hasFocus, isEditing: isEditing, readOnly: readOnly, @@ -269,7 +284,7 @@ class _CellContainerState extends PlutoStateWithChange<_CellContainer> { ); } else if (isSelectedCell) { return BoxDecoration( - color: activatedColor, + color: color ?? activatedColor, border: Border.all( color: hasFocus ? activatedBorderColor : inactivatedBorderColor, width: 1, @@ -277,7 +292,7 @@ class _CellContainerState extends PlutoStateWithChange<_CellContainer> { ); } else { return BoxDecoration( - color: isGroupedRowCell ? cellColorGroupedRow : null, + color: isGroupedRowCell ? cellColorGroupedRow : color, border: enableCellVerticalBorder ? BorderDirectional( end: BorderSide( @@ -292,11 +307,22 @@ class _CellContainerState extends PlutoStateWithChange<_CellContainer> { @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: _decoration, - child: Padding( - padding: widget.cellPadding, - child: widget.child, + if (widget.cell.hasValidationError) { + _decoration = _decoration.copyWith( + border: Border.all( + color: Theme.of(context).colorScheme.error, + ), + ); + } + + return Tooltip( + message: widget.cell.validationError ?? "", + child: DecoratedBox( + decoration: _decoration, + child: Padding( + padding: widget.cellPadding, + child: widget.child, + ), ), ); } diff --git a/lib/src/ui/pluto_body_rows.dart b/lib/src/ui/pluto_body_rows.dart index fc37f7209..d7472b974 100644 --- a/lib/src/ui/pluto_body_rows.dart +++ b/lib/src/ui/pluto_body_rows.dart @@ -103,6 +103,7 @@ class PlutoBodyRowsState extends PlutoStateWithChange { itemCount: _rows.length, itemExtent: stateManager.rowTotalHeight, addRepaintBoundaries: false, + padding: stateManager.configuration.style.bodyPadding, itemBuilder: (ctx, i) { return PlutoBaseRow( key: ValueKey('body_row_${_rows[i].key}'),