parent
612ef5e0dc
commit
e76de1b58e
8 changed files with 635 additions and 0 deletions
@ -0,0 +1,75 @@ |
|||||||
|
import 'dart:async'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
class TableCellWidget extends StatefulWidget { |
||||||
|
const TableCellWidget({ |
||||||
|
required this.cellId, |
||||||
|
required this.cellData, |
||||||
|
required this.onUpdate, |
||||||
|
required this.onTap, |
||||||
|
super.key, |
||||||
|
}); |
||||||
|
final String cellId; |
||||||
|
final String cellData; |
||||||
|
final Function(FocusNode node) onTap; |
||||||
|
final Function(String data) onUpdate; |
||||||
|
|
||||||
|
@override |
||||||
|
State<TableCellWidget> createState() => _TableCellWidgetState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _TableCellWidgetState extends State<TableCellWidget> { |
||||||
|
late final TextEditingController controller; |
||||||
|
late final FocusNode node; |
||||||
|
Timer? _debounce; |
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
controller = TextEditingController(text: widget.cellData); |
||||||
|
node = FocusNode(); |
||||||
|
super.initState(); |
||||||
|
} |
||||||
|
|
||||||
|
void _onTextChanged() { |
||||||
|
if (!_debounce!.isActive) { |
||||||
|
widget.onUpdate(controller.text); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
controller |
||||||
|
..removeListener(_onTextChanged) |
||||||
|
..dispose(); |
||||||
|
node.dispose(); |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Container( |
||||||
|
width: 40, |
||||||
|
constraints: const BoxConstraints( |
||||||
|
minHeight: 50, |
||||||
|
), |
||||||
|
padding: const EdgeInsets.only(left: 5, right: 5, top: 5), |
||||||
|
child: TextFormField( |
||||||
|
controller: controller, |
||||||
|
focusNode: node, |
||||||
|
keyboardType: TextInputType.multiline, |
||||||
|
maxLines: null, |
||||||
|
decoration: const InputDecoration.collapsed(hintText: ''), |
||||||
|
onTap: () { |
||||||
|
widget.onTap.call(node); |
||||||
|
}, |
||||||
|
onTapAlwaysCalled: true, |
||||||
|
onChanged: (value) { |
||||||
|
_debounce = Timer( |
||||||
|
const Duration(milliseconds: 900), |
||||||
|
_onTextChanged, |
||||||
|
); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,234 @@ |
|||||||
|
import 'dart:convert'; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
import 'package:flutter_quill/quill_delta.dart'; |
||||||
|
import '../../../utils/quill_table_utils.dart'; |
||||||
|
import 'table_cell_embed.dart'; |
||||||
|
import 'table_models.dart'; |
||||||
|
|
||||||
|
class CustomTableEmbed extends CustomBlockEmbed { |
||||||
|
const CustomTableEmbed(String value) : super(tableType, value); |
||||||
|
|
||||||
|
static const String tableType = 'table'; |
||||||
|
|
||||||
|
static CustomTableEmbed fromDocument(Document document) => |
||||||
|
CustomTableEmbed(jsonEncode(document.toDelta().toJson())); |
||||||
|
|
||||||
|
Document get document => Document.fromJson(jsonDecode(data)); |
||||||
|
} |
||||||
|
|
||||||
|
//Embed builder |
||||||
|
|
||||||
|
class QuillEditorTableEmbedBuilder extends EmbedBuilder { |
||||||
|
@override |
||||||
|
String get key => 'table'; |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build( |
||||||
|
BuildContext context, |
||||||
|
QuillController controller, |
||||||
|
Embed node, |
||||||
|
bool readOnly, |
||||||
|
bool inline, |
||||||
|
TextStyle textStyle, |
||||||
|
) { |
||||||
|
final tableData = node.value.data; |
||||||
|
return TableWidget( |
||||||
|
tableData: tableData, |
||||||
|
controller: controller, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class TableWidget extends StatefulWidget { |
||||||
|
const TableWidget({ |
||||||
|
required this.tableData, |
||||||
|
required this.controller, |
||||||
|
super.key, |
||||||
|
}); |
||||||
|
final QuillController controller; |
||||||
|
final Map<String, dynamic> tableData; |
||||||
|
|
||||||
|
@override |
||||||
|
State<TableWidget> createState() => _TableWidgetState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _TableWidgetState extends State<TableWidget> { |
||||||
|
TableModel _tableModel = TableModel(columns: {}, rows: {}); |
||||||
|
String _selectedColumnId = ''; |
||||||
|
String _selectedRowId = ''; |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
_tableModel = TableModel.fromMap(widget.tableData); |
||||||
|
super.initState(); |
||||||
|
} |
||||||
|
|
||||||
|
void _addColumn() { |
||||||
|
setState(() { |
||||||
|
final id = '${_tableModel.columns.length + 1}'; |
||||||
|
final position = _tableModel.columns.length; |
||||||
|
_tableModel.columns[id] = ColumnModel(id: id, position: position); |
||||||
|
_tableModel.rows.forEach((key, row) { |
||||||
|
row.cells[id] = ''; |
||||||
|
}); |
||||||
|
}); |
||||||
|
_updateTable(); |
||||||
|
} |
||||||
|
|
||||||
|
void _addRow() { |
||||||
|
setState(() { |
||||||
|
final id = '${_tableModel.rows.length + 1}'; |
||||||
|
final cells = <String, String>{}; |
||||||
|
_tableModel.columns.forEach((key, column) { |
||||||
|
cells[key] = ''; |
||||||
|
}); |
||||||
|
_tableModel.rows[id] = RowModel(id: id, cells: cells); |
||||||
|
}); |
||||||
|
_updateTable(); |
||||||
|
} |
||||||
|
|
||||||
|
void _removeColumn(String columnId) { |
||||||
|
setState(() { |
||||||
|
_tableModel.columns.remove(columnId); |
||||||
|
_tableModel.rows.forEach((key, row) { |
||||||
|
row.cells.remove(columnId); |
||||||
|
}); |
||||||
|
if (_selectedRowId == _selectedColumnId) { |
||||||
|
_selectedRowId = ''; |
||||||
|
} |
||||||
|
_selectedColumnId = ''; |
||||||
|
}); |
||||||
|
_updateTable(); |
||||||
|
} |
||||||
|
|
||||||
|
void _removeRow(String rowId) { |
||||||
|
setState(() { |
||||||
|
_tableModel.rows.remove(rowId); |
||||||
|
_selectedRowId = ''; |
||||||
|
}); |
||||||
|
_updateTable(); |
||||||
|
} |
||||||
|
|
||||||
|
void _updateCell(String columnId, String rowId, String data) { |
||||||
|
setState(() { |
||||||
|
_tableModel.rows[rowId]!.cells[columnId] = data; |
||||||
|
}); |
||||||
|
_updateTable(); |
||||||
|
} |
||||||
|
|
||||||
|
void _updateTable() { |
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||||
|
final offset = getEmbedNode( |
||||||
|
widget.controller, |
||||||
|
widget.controller.selection.start, |
||||||
|
).offset; |
||||||
|
final delta = Delta()..insert({'table': _tableModel.toMap()}); |
||||||
|
widget.controller.replaceText( |
||||||
|
offset, |
||||||
|
1, |
||||||
|
delta, |
||||||
|
TextSelection.collapsed( |
||||||
|
offset: offset, |
||||||
|
), |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Material( |
||||||
|
child: Container( |
||||||
|
decoration: BoxDecoration( |
||||||
|
border: Border.all( |
||||||
|
color: Theme.of(context).textTheme.bodyMedium?.color ?? |
||||||
|
Colors.black)), |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.end, |
||||||
|
children: [ |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.more_vert), |
||||||
|
onPressed: () async { |
||||||
|
final position = renderPosition(context); |
||||||
|
await showMenu<TableOperation>( |
||||||
|
context: context, |
||||||
|
position: position, |
||||||
|
items: [ |
||||||
|
const PopupMenuItem( |
||||||
|
value: TableOperation.addColumn, |
||||||
|
child: Text('Add column'), |
||||||
|
), |
||||||
|
const PopupMenuItem( |
||||||
|
value: TableOperation.addRow, |
||||||
|
child: Text('Add row'), |
||||||
|
), |
||||||
|
const PopupMenuItem( |
||||||
|
value: TableOperation.removeColumn, |
||||||
|
child: Text('Delete column'), |
||||||
|
), |
||||||
|
const PopupMenuItem( |
||||||
|
value: TableOperation.removeRow, |
||||||
|
child: Text('Delete row'), |
||||||
|
), |
||||||
|
]).then((value) { |
||||||
|
if (value != null) { |
||||||
|
if (value == TableOperation.addRow) { |
||||||
|
_addRow(); |
||||||
|
} |
||||||
|
if (value == TableOperation.addColumn) { |
||||||
|
_addColumn(); |
||||||
|
} |
||||||
|
if (value == TableOperation.removeColumn) { |
||||||
|
_removeColumn(_selectedColumnId); |
||||||
|
} |
||||||
|
if (value == TableOperation.removeRow) { |
||||||
|
_removeRow(_selectedRowId); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
), |
||||||
|
const Divider( |
||||||
|
color: Colors.white, |
||||||
|
height: 1, |
||||||
|
), |
||||||
|
Table( |
||||||
|
border: const TableBorder.symmetric( |
||||||
|
inside: BorderSide(color: Colors.white)), |
||||||
|
children: _buildTableRows(), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
List<TableRow> _buildTableRows() { |
||||||
|
final rows = <TableRow>[]; |
||||||
|
|
||||||
|
_tableModel.rows.forEach((rowId, rowModel) { |
||||||
|
final rowCells = <Widget>[]; |
||||||
|
final rowKey = rowId; |
||||||
|
rowModel.cells.forEach((key, value) { |
||||||
|
if (key != 'id') { |
||||||
|
final columnId = key; |
||||||
|
final data = value; |
||||||
|
rowCells.add(TableCellWidget( |
||||||
|
cellId: rowKey, |
||||||
|
onTap: (node) { |
||||||
|
setState(() { |
||||||
|
_selectedColumnId = columnId; |
||||||
|
_selectedRowId = rowModel.id; |
||||||
|
}); |
||||||
|
}, |
||||||
|
cellData: data, |
||||||
|
onUpdate: (data) => _updateCell(columnId, rowKey, data), |
||||||
|
)); |
||||||
|
} |
||||||
|
}); |
||||||
|
rows.add(TableRow(children: rowCells)); |
||||||
|
}); |
||||||
|
return rows; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
class TableModel { |
||||||
|
TableModel({required this.columns, required this.rows}); |
||||||
|
|
||||||
|
factory TableModel.fromMap(Map<String, dynamic> json) { |
||||||
|
return TableModel( |
||||||
|
columns: (json['columns'] as Map<String, dynamic>).map( |
||||||
|
(key, value) => MapEntry( |
||||||
|
key, |
||||||
|
ColumnModel.fromMap( |
||||||
|
value, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
rows: (json['rows'] as Map<String, dynamic>).map( |
||||||
|
(key, value) => MapEntry( |
||||||
|
key, |
||||||
|
RowModel.fromMap( |
||||||
|
value, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
Map<String, ColumnModel> columns; |
||||||
|
Map<String, RowModel> rows; |
||||||
|
|
||||||
|
Map<String, dynamic> toMap() { |
||||||
|
return { |
||||||
|
'columns': columns.map( |
||||||
|
(key, value) => MapEntry( |
||||||
|
key, |
||||||
|
value.toMap(), |
||||||
|
), |
||||||
|
), |
||||||
|
'rows': rows.map( |
||||||
|
(key, value) => MapEntry( |
||||||
|
key, |
||||||
|
value.toMap(), |
||||||
|
), |
||||||
|
), |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class ColumnModel { |
||||||
|
ColumnModel({required this.id, required this.position}); |
||||||
|
|
||||||
|
factory ColumnModel.fromMap(Map<String, dynamic> json) { |
||||||
|
return ColumnModel( |
||||||
|
id: json['id'], |
||||||
|
position: json['position'], |
||||||
|
); |
||||||
|
} |
||||||
|
String id; |
||||||
|
int position; |
||||||
|
|
||||||
|
Map<String, dynamic> toMap() { |
||||||
|
return { |
||||||
|
'id': id, |
||||||
|
'position': position, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RowModel { |
||||||
|
// Key is column ID, value is cell content |
||||||
|
|
||||||
|
RowModel({required this.id, required this.cells}); |
||||||
|
|
||||||
|
factory RowModel.fromMap(Map<String, dynamic> json) { |
||||||
|
return RowModel( |
||||||
|
id: json['id'], |
||||||
|
cells: Map<String, String>.from(json['cells']), |
||||||
|
); |
||||||
|
} |
||||||
|
String id; |
||||||
|
Map<String, String> cells; |
||||||
|
|
||||||
|
Map<String, dynamic> toMap() { |
||||||
|
return { |
||||||
|
'id': id, |
||||||
|
'cells': cells, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
import '../../../models/config/table/table_configurations.dart'; |
||||||
|
import '../../../utils/quill_table_utils.dart'; |
||||||
|
|
||||||
|
class QuillToolbarTableButton extends StatelessWidget { |
||||||
|
const QuillToolbarTableButton({ |
||||||
|
required this.controller, |
||||||
|
this.options = const QuillToolbarTableButtonOptions(), |
||||||
|
super.key, |
||||||
|
}); |
||||||
|
|
||||||
|
final QuillController controller; |
||||||
|
|
||||||
|
final QuillToolbarTableButtonOptions options; |
||||||
|
|
||||||
|
double _iconSize(BuildContext context) { |
||||||
|
final baseFontSize = baseButtonExtraOptions(context)?.iconSize; |
||||||
|
final iconSize = options.iconSize; |
||||||
|
return iconSize ?? baseFontSize ?? kDefaultIconSize; |
||||||
|
} |
||||||
|
|
||||||
|
double _iconButtonFactor(BuildContext context) { |
||||||
|
final baseIconFactor = baseButtonExtraOptions(context)?.iconButtonFactor; |
||||||
|
final iconButtonFactor = options.iconButtonFactor; |
||||||
|
return iconButtonFactor ?? baseIconFactor ?? kDefaultIconButtonFactor; |
||||||
|
} |
||||||
|
|
||||||
|
VoidCallback? _afterButtonPressed(BuildContext context) { |
||||||
|
return options.afterButtonPressed ?? |
||||||
|
baseButtonExtraOptions(context)?.afterButtonPressed; |
||||||
|
} |
||||||
|
|
||||||
|
QuillIconTheme? _iconTheme(BuildContext context) { |
||||||
|
return options.iconTheme ?? baseButtonExtraOptions(context)?.iconTheme; |
||||||
|
} |
||||||
|
|
||||||
|
QuillToolbarBaseButtonOptions? baseButtonExtraOptions(BuildContext context) { |
||||||
|
return context.quillToolbarBaseButtonOptions; |
||||||
|
} |
||||||
|
|
||||||
|
IconData _iconData(BuildContext context) { |
||||||
|
return options.iconData ?? |
||||||
|
baseButtonExtraOptions(context)?.iconData ?? |
||||||
|
Icons.table_chart; |
||||||
|
} |
||||||
|
|
||||||
|
//TODO: implement translations for table insertion tooltip |
||||||
|
String _tooltip(BuildContext context) { |
||||||
|
return options.tooltip ?? |
||||||
|
baseButtonExtraOptions(context)?.tooltip ?? |
||||||
|
'Insert table'; |
||||||
|
} |
||||||
|
|
||||||
|
void _sharedOnPressed(BuildContext context) { |
||||||
|
_onPressedHandler(context); |
||||||
|
_afterButtonPressed(context); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final tooltip = _tooltip(context); |
||||||
|
final iconSize = _iconSize(context); |
||||||
|
final iconButtonFactor = _iconButtonFactor(context); |
||||||
|
final iconData = _iconData(context); |
||||||
|
final childBuilder = |
||||||
|
options.childBuilder ?? baseButtonExtraOptions(context)?.childBuilder; |
||||||
|
|
||||||
|
if (childBuilder != null) { |
||||||
|
return childBuilder( |
||||||
|
QuillToolbarTableButtonOptions( |
||||||
|
afterButtonPressed: _afterButtonPressed(context), |
||||||
|
iconData: iconData, |
||||||
|
iconSize: iconSize, |
||||||
|
iconButtonFactor: iconButtonFactor, |
||||||
|
iconTheme: options.iconTheme, |
||||||
|
tooltip: options.tooltip, |
||||||
|
), |
||||||
|
QuillToolbarTableButtonExtraOptions( |
||||||
|
context: context, |
||||||
|
controller: controller, |
||||||
|
onPressed: () => _sharedOnPressed(context), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return QuillToolbarIconButton( |
||||||
|
icon: Icon( |
||||||
|
iconData, |
||||||
|
size: iconButtonFactor * iconSize, |
||||||
|
), |
||||||
|
tooltip: tooltip, |
||||||
|
isSelected: false, |
||||||
|
onPressed: () => _sharedOnPressed(context), |
||||||
|
iconTheme: _iconTheme(context), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _onPressedHandler(BuildContext context) async { |
||||||
|
final position = renderPosition(context); |
||||||
|
await showMenu(context: context, position: position, items: [ |
||||||
|
const PopupMenuItem(value: 2, child: Text('2x2')), |
||||||
|
const PopupMenuItem(value: 4, child: Text('4x4')), |
||||||
|
const PopupMenuItem(value: 6, child: Text('6x6')), |
||||||
|
]).then( |
||||||
|
(value) { |
||||||
|
if (value != null) { |
||||||
|
insertTable(value, value, controller, ChangeSource.local); |
||||||
|
} |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
import 'package:meta/meta.dart' show immutable; |
||||||
|
|
||||||
|
class QuillToolbarTableButtonExtraOptions |
||||||
|
extends QuillToolbarBaseButtonExtraOptions { |
||||||
|
const QuillToolbarTableButtonExtraOptions({ |
||||||
|
required super.controller, |
||||||
|
required super.context, |
||||||
|
required super.onPressed, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@immutable |
||||||
|
class QuillToolbarTableButtonOptions extends QuillToolbarBaseButtonOptions< |
||||||
|
QuillToolbarTableButtonOptions, QuillToolbarTableButtonExtraOptions> { |
||||||
|
const QuillToolbarTableButtonOptions({ |
||||||
|
super.iconData, |
||||||
|
super.iconSize, |
||||||
|
super.iconButtonFactor, |
||||||
|
|
||||||
|
/// specifies the tooltip text for the image button. |
||||||
|
super.tooltip, |
||||||
|
super.afterButtonPressed, |
||||||
|
super.childBuilder, |
||||||
|
super.iconTheme, |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
import 'package:flutter/widgets.dart' |
||||||
|
show |
||||||
|
BuildContext, |
||||||
|
MediaQuery, |
||||||
|
Offset, |
||||||
|
Overlay, |
||||||
|
Rect, |
||||||
|
RelativeRect, |
||||||
|
RenderBox, |
||||||
|
Size, |
||||||
|
TextSelection; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
import 'package:flutter_quill/quill_delta.dart'; |
||||||
|
|
||||||
|
enum TableOperation { |
||||||
|
addColumn, |
||||||
|
addRow, |
||||||
|
removeColumn, |
||||||
|
removeRow, |
||||||
|
} |
||||||
|
|
||||||
|
RelativeRect renderPosition(BuildContext context, [Size? size]) { |
||||||
|
size ??= MediaQuery.sizeOf(context); |
||||||
|
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; |
||||||
|
final button = context.findRenderObject() as RenderBox; |
||||||
|
final position = RelativeRect.fromRect( |
||||||
|
Rect.fromPoints( |
||||||
|
button.localToGlobal(const Offset(0, -65), ancestor: overlay), |
||||||
|
button.localToGlobal( |
||||||
|
button.size.bottomRight(Offset.zero) + const Offset(-50, 0), |
||||||
|
ancestor: overlay), |
||||||
|
), |
||||||
|
Offset.zero & size * 0.40, |
||||||
|
); |
||||||
|
return position; |
||||||
|
} |
||||||
|
|
||||||
|
void insertTable(int rows, int columns, QuillController quillController, |
||||||
|
ChangeSource? changeFrom) { |
||||||
|
final tableData = _createTableData(rows, columns); |
||||||
|
final delta = Delta()..insert({'table': tableData}); |
||||||
|
final selection = quillController.selection; |
||||||
|
final replacedLength = selection.extentOffset - selection.baseOffset; |
||||||
|
final newBaseOffset = selection.baseOffset; |
||||||
|
final newExtentOffsetCandidate = |
||||||
|
(selection.baseOffset + 1 - replacedLength).toInt(); |
||||||
|
final newExtentOffsetAdjusted = |
||||||
|
newExtentOffsetCandidate < 0 ? 0 : newExtentOffsetCandidate; |
||||||
|
quillController.replaceText( |
||||||
|
newBaseOffset, |
||||||
|
replacedLength, |
||||||
|
delta, |
||||||
|
TextSelection( |
||||||
|
baseOffset: newBaseOffset, extentOffset: newExtentOffsetAdjusted), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, dynamic> _createTableData(int rows, int columns) { |
||||||
|
// Crear el mapa para las columnas |
||||||
|
final columnsData = <String, dynamic>{}; |
||||||
|
for (var col = 0; col < columns; col++) { |
||||||
|
final columnId = '${col + 1}'; |
||||||
|
columnsData[columnId] = {'id': columnId, 'position': col}; |
||||||
|
} |
||||||
|
|
||||||
|
// Crear el mapa para las filas |
||||||
|
final rowsData = <String, dynamic>{}; |
||||||
|
for (var row = 0; row < rows; row++) { |
||||||
|
final rowId = '${row + 1}'; |
||||||
|
rowsData[rowId] = {'id': rowId, 'cells': {}}; |
||||||
|
|
||||||
|
for (var col = 0; col < columns; col++) { |
||||||
|
final columnId = '${col + 1}'; |
||||||
|
rowsData[rowId]['cells'][columnId] = ''; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Combinar las columnas y filas en una estructura de tabla |
||||||
|
final tableData = <String, dynamic>{ |
||||||
|
'columns': columnsData, |
||||||
|
'rows': rowsData, |
||||||
|
}; |
||||||
|
|
||||||
|
return tableData; |
||||||
|
} |
Loading…
Reference in new issue