feat: add simple columns block in editor (#1038)
* feat: add simple columns block in editor * feat: add divider between column block * fix: add fallback logic in calculate selection function * feat: add column width resizer * feat: integrate columns block in editor on desktop
This commit is contained in:
parent
1df3cb1a1f
commit
c1e9d15572
8 changed files with 562 additions and 29 deletions
|
|
@ -136,6 +136,10 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
|||
Map<String, BlockComponentBuilder> _buildBlockComponentBuilders() {
|
||||
final map = {
|
||||
...standardBlockComponentBuilderMap,
|
||||
|
||||
// columns block
|
||||
ColumnBlockKeys.type: ColumnBlockComponentBuilder(),
|
||||
ColumnsBlockKeys.type: ColumnsBlockComponentBuilder(),
|
||||
};
|
||||
// customize the image block component to show a menu
|
||||
map[ImageBlockKeys.type] = ImageBlockComponentBuilder(
|
||||
|
|
@ -165,7 +169,13 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
|||
// customize the padding
|
||||
map.forEach((key, value) {
|
||||
value.configuration = value.configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: (node) {
|
||||
if (node.type == ColumnsBlockKeys.type ||
|
||||
node.type == ColumnBlockKeys.type) {
|
||||
return EdgeInsets.zero;
|
||||
}
|
||||
return const EdgeInsets.symmetric(vertical: 8.0);
|
||||
},
|
||||
blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(
|
||||
vertical: 1.0,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -287,6 +287,28 @@ class _DragToReorderActionState extends State<DragToReorderAction> {
|
|||
final (verticalPosition, horizontalPosition, _) = position;
|
||||
Path newPath = targetNode.path;
|
||||
|
||||
final realNode = widget.blockComponentContext.node;
|
||||
debugPrint('Moving node($realNode, ${realNode.path}) to path($newPath)');
|
||||
|
||||
// if the horizontal position is right, we need to create a column block
|
||||
if (horizontalPosition == HorizontalPosition.right) {
|
||||
final node = columnsNode(
|
||||
children: [
|
||||
columnNode(
|
||||
children: [targetNode.deepCopy()],
|
||||
),
|
||||
columnNode(
|
||||
children: [realNode.deepCopy()],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode(newPath, node);
|
||||
transaction.deleteNode(targetNode);
|
||||
transaction.deleteNode(realNode);
|
||||
await editorState.apply(transaction);
|
||||
} else {
|
||||
// Determine the new path based on drop position
|
||||
// For VerticalPosition.top, we keep the target node's path
|
||||
if (verticalPosition == VerticalPosition.bottom) {
|
||||
|
|
@ -303,14 +325,12 @@ class _DragToReorderActionState extends State<DragToReorderAction> {
|
|||
return;
|
||||
}
|
||||
|
||||
final realNode = widget.blockComponentContext.node;
|
||||
debugPrint('Moving node($realNode, ${realNode.path}) to path($newPath)');
|
||||
|
||||
// Perform the node move operation
|
||||
final transaction = editorState.transaction;
|
||||
transaction.moveNode(newPath, realNode);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFeedback() {
|
||||
Widget child;
|
||||
|
|
@ -387,7 +407,7 @@ Widget _buildDropArea(
|
|||
color: Colors.red,
|
||||
);
|
||||
|
||||
if (horizontalPosition == HorizontalPosition.right) {
|
||||
if (horizontalPosition == HorizontalPosition.center) {
|
||||
const breakWidth = 22.0;
|
||||
const padding = 8.0;
|
||||
child = Row(
|
||||
|
|
@ -406,6 +426,16 @@ Widget _buildDropArea(
|
|||
),
|
||||
],
|
||||
);
|
||||
} else if (horizontalPosition == HorizontalPosition.right) {
|
||||
return Positioned(
|
||||
top: globalBlockRect.top,
|
||||
height: globalBlockRect.height,
|
||||
left: globalBlockRect.right - 2,
|
||||
child: Container(
|
||||
width: 2,
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
|
|
@ -452,9 +482,10 @@ Widget _buildDropArea(
|
|||
// Horizontal position
|
||||
if (dragOffset.dx < globalBlockRect.left + 44) {
|
||||
horizontalPosition = HorizontalPosition.left;
|
||||
} else {
|
||||
// ignore the middle here, it's not used in this example
|
||||
} else if (dragOffset.dx > globalBlockRect.right / 3.0 * 2.0) {
|
||||
horizontalPosition = HorizontalPosition.right;
|
||||
} else {
|
||||
horizontalPosition = HorizontalPosition.center;
|
||||
}
|
||||
|
||||
// Vertical position
|
||||
|
|
|
|||
|
|
@ -42,7 +42,14 @@ extension EditorStateSelection on EditorState {
|
|||
sortedNodes,
|
||||
start,
|
||||
end,
|
||||
(rect) => rect.bottom <= offset.dy,
|
||||
match: (index, rect) {
|
||||
final isMatch = rect.contains(offset);
|
||||
AppFlowyEditorLog.selection.debug(
|
||||
'findNodeInOffset: $index, rect: $rect, offset: $offset, isMatch: $isMatch',
|
||||
);
|
||||
return isMatch;
|
||||
},
|
||||
compare: (index, rect) => rect.bottom <= offset.dy,
|
||||
);
|
||||
|
||||
final filteredNodes = List.of(sortedNodes)
|
||||
|
|
@ -53,7 +60,14 @@ extension EditorStateSelection on EditorState {
|
|||
sortedNodes,
|
||||
0,
|
||||
filteredNodes.length - 1,
|
||||
(rect) => rect.right <= offset.dx,
|
||||
match: (index, rect) {
|
||||
final isMatch = rect.contains(offset);
|
||||
AppFlowyEditorLog.selection.debug(
|
||||
'findNodeInOffset: $index, rect: $rect, offset: $offset, isMatch: $isMatch',
|
||||
);
|
||||
return isMatch;
|
||||
},
|
||||
compare: (index, rect) => rect.right <= offset.dx,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -81,20 +95,29 @@ extension EditorStateSelection on EditorState {
|
|||
int _findCloseNode(
|
||||
List<Node> sortedNodes,
|
||||
int start,
|
||||
int end,
|
||||
bool Function(Rect rect) compare,
|
||||
) {
|
||||
int end, {
|
||||
bool Function(int index, Rect rect)? match,
|
||||
required bool Function(int index, Rect rect) compare,
|
||||
}) {
|
||||
for (var i = start; i <= end; i++) {
|
||||
final rect = sortedNodes[i].rect;
|
||||
if (match != null && match(i, rect)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
var min = start;
|
||||
var max = end;
|
||||
while (min <= max) {
|
||||
final mid = min + ((max - min) >> 1);
|
||||
final rect = sortedNodes[mid].rect;
|
||||
if (compare(rect)) {
|
||||
if (compare(mid, rect)) {
|
||||
min = mid + 1;
|
||||
} else {
|
||||
max = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return min.clamp(start, end);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
166
lib/src/plugins/blocks/columns/column_block_component.dart
Normal file
166
lib/src/plugins/blocks/columns/column_block_component.dart
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Node columnNode({
|
||||
List<Node>? children,
|
||||
double? width,
|
||||
}) {
|
||||
return Node(
|
||||
type: ColumnBlockKeys.type,
|
||||
children: children ?? [paragraphNode()],
|
||||
attributes: {
|
||||
ColumnBlockKeys.width: width,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class ColumnBlockKeys {
|
||||
const ColumnBlockKeys._();
|
||||
|
||||
static const String type = 'column';
|
||||
|
||||
static const String width = 'width';
|
||||
}
|
||||
|
||||
class ColumnBlockComponentBuilder extends BlockComponentBuilder {
|
||||
ColumnBlockComponentBuilder({super.configuration});
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return ColumnBlockComponent(
|
||||
key: node.key,
|
||||
node: node,
|
||||
showActions: showActions(node),
|
||||
configuration: configuration,
|
||||
actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
BlockComponentValidate get validate => (node) => node.children.isNotEmpty;
|
||||
}
|
||||
|
||||
class ColumnBlockComponent extends BlockComponentStatefulWidget {
|
||||
const ColumnBlockComponent({
|
||||
super.key,
|
||||
required super.node,
|
||||
super.showActions,
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
State<ColumnBlockComponent> createState() => ColumnBlockComponentState();
|
||||
}
|
||||
|
||||
class ColumnBlockComponentState extends State<ColumnBlockComponent>
|
||||
with SelectableMixin, BlockComponentConfigurable {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
final columnKey = GlobalKey();
|
||||
|
||||
late final EditorState editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => IntrinsicHeight(
|
||||
child: editorState.renderer.build(context, e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
child = Padding(
|
||||
key: columnKey,
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() => Position(path: widget.node.path);
|
||||
|
||||
@override
|
||||
Position end() => Position(path: widget.node.path, offset: 1);
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.cover;
|
||||
|
||||
@override
|
||||
Rect getBlockRect({
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
return getRectsInSelection(Selection.invalid()).first;
|
||||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final rects = getRectsInSelection(
|
||||
Selection.collapsed(position),
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
);
|
||||
return rects.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return [];
|
||||
}
|
||||
final parentBox = context.findRenderObject();
|
||||
final renderBox = columnKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && renderBox is RenderBox) {
|
||||
return [
|
||||
renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &
|
||||
renderBox.size,
|
||||
];
|
||||
}
|
||||
return [Offset.zero & _renderBox!.size];
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||
Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>
|
||||
_renderBox!.localToGlobal(offset);
|
||||
}
|
||||
87
lib/src/plugins/blocks/columns/column_width_resizer.dart
Normal file
87
lib/src/plugins/blocks/columns/column_width_resizer.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/plugins/blocks/columns/columns_block_constant.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColumnWidthResizer extends StatefulWidget {
|
||||
const ColumnWidthResizer({
|
||||
super.key,
|
||||
required this.columnNode,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final Node columnNode;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<ColumnWidthResizer> createState() => _ColumnWidthResizerState();
|
||||
}
|
||||
|
||||
class _ColumnWidthResizerState extends State<ColumnWidthResizer> {
|
||||
bool isDragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeLeftRight,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragStart: _onHorizontalDragStart,
|
||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||
child: Container(
|
||||
width: ColumnsBlockConstants.columnWidthResizerWidth,
|
||||
color: ColumnsBlockConstants.columnWidthResizerColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onHorizontalDragStart(DragStartDetails details) {
|
||||
isDragging = true;
|
||||
}
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update the column width in memory
|
||||
final columnNode = widget.columnNode;
|
||||
final rect = columnNode.rect;
|
||||
final width = columnNode.attributes[ColumnBlockKeys.width] ?? rect.width;
|
||||
final newWidth = width + details.delta.dx;
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(columnNode, {
|
||||
...columnNode.attributes,
|
||||
ColumnBlockKeys.width: newWidth.clamp(
|
||||
ColumnsBlockConstants.minimumColumnWidth,
|
||||
double.infinity,
|
||||
),
|
||||
});
|
||||
final columnsNode = columnNode.parent;
|
||||
if (columnsNode != null) {
|
||||
transaction.updateNode(columnsNode, {
|
||||
...columnsNode.attributes,
|
||||
ColumnsBlockKeys.columnCount: columnsNode.children.length,
|
||||
});
|
||||
}
|
||||
widget.editorState.apply(
|
||||
transaction,
|
||||
options: ApplyOptions(inMemoryUpdate: true),
|
||||
);
|
||||
}
|
||||
|
||||
void _onHorizontalDragEnd(DragEndDetails details) {
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply the transaction again to make sure the width is updated
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.columnNode, {
|
||||
...widget.columnNode.attributes,
|
||||
});
|
||||
widget.editorState.apply(transaction);
|
||||
|
||||
isDragging = false;
|
||||
}
|
||||
}
|
||||
205
lib/src/plugins/blocks/columns/columns_block_component.dart
Normal file
205
lib/src/plugins/blocks/columns/columns_block_component.dart
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/plugins/blocks/columns/column_width_resizer.dart';
|
||||
import 'package:appflowy_editor/src/plugins/blocks/columns/columns_block_constant.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Node columnsNode({
|
||||
List<Node>? children,
|
||||
}) {
|
||||
return Node(
|
||||
type: ColumnsBlockKeys.type,
|
||||
children: children ??
|
||||
[
|
||||
for (var i = 0; i < 2; i++)
|
||||
columnNode(
|
||||
children: [
|
||||
paragraphNode(
|
||||
text: 'Column $i',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class ColumnsBlockKeys {
|
||||
const ColumnsBlockKeys._();
|
||||
|
||||
static const String type = 'columns';
|
||||
|
||||
static const String columnCount = 'column_count';
|
||||
}
|
||||
|
||||
class ColumnsBlockComponentBuilder extends BlockComponentBuilder {
|
||||
ColumnsBlockComponentBuilder({super.configuration});
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return ColumnsBlockComponent(
|
||||
key: node.key,
|
||||
node: node,
|
||||
showActions: showActions(node),
|
||||
configuration: configuration,
|
||||
actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
BlockComponentValidate get validate => (node) => node.children.isNotEmpty;
|
||||
}
|
||||
|
||||
class ColumnsBlockComponent extends BlockComponentStatefulWidget {
|
||||
const ColumnsBlockComponent({
|
||||
super.key,
|
||||
required super.node,
|
||||
super.showActions,
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
State<ColumnsBlockComponent> createState() => ColumnsBlockComponentState();
|
||||
}
|
||||
|
||||
class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
|
||||
with SelectableMixin, BlockComponentConfigurable {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
final columnsKey = GlobalKey();
|
||||
|
||||
late final EditorState editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _buildChildren(),
|
||||
),
|
||||
);
|
||||
|
||||
child = Padding(
|
||||
key: columnsKey,
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
List<Widget> _buildChildren() {
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
final childNode = node.children[i];
|
||||
final double? width = childNode.attributes[ColumnBlockKeys.width];
|
||||
Widget child = editorState.renderer.build(context, childNode);
|
||||
|
||||
if (width != null) {
|
||||
child = SizedBox(
|
||||
width: width.clamp(
|
||||
ColumnsBlockConstants.minimumColumnWidth,
|
||||
double.infinity,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
child = Expanded(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
|
||||
if (i != node.children.length - 1) {
|
||||
children.add(
|
||||
ColumnWidthResizer(
|
||||
columnNode: childNode,
|
||||
editorState: editorState,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() => Position(path: widget.node.path);
|
||||
|
||||
@override
|
||||
Position end() => Position(path: widget.node.path, offset: 1);
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.cover;
|
||||
|
||||
@override
|
||||
Rect getBlockRect({
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
return getRectsInSelection(Selection.invalid()).first;
|
||||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final rects = getRectsInSelection(
|
||||
Selection.collapsed(position),
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
);
|
||||
return rects.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return [];
|
||||
}
|
||||
final parentBox = context.findRenderObject();
|
||||
final renderBox = columnsKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && renderBox is RenderBox) {
|
||||
return [
|
||||
renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &
|
||||
renderBox.size,
|
||||
];
|
||||
}
|
||||
return [Offset.zero & _renderBox!.size];
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||
Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>
|
||||
_renderBox!.localToGlobal(offset);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColumnsBlockConstants {
|
||||
const ColumnsBlockConstants._();
|
||||
|
||||
static const double columnWidthResizerWidth = 4;
|
||||
static const Color columnWidthResizerColor = Colors.red;
|
||||
static const double minimumColumnWidth = 128;
|
||||
}
|
||||
|
|
@ -13,3 +13,5 @@ export 'markdown/encoder/parser/parser.dart';
|
|||
export 'quill_delta/quill_delta_encoder.dart';
|
||||
export 'word_count/word_counter_service.dart';
|
||||
export 'pdf/html_to_pdf_encoder.dart';
|
||||
export 'blocks/columns/column_block_component.dart';
|
||||
export 'blocks/columns/columns_block_component.dart';
|
||||
|
|
|
|||
Loading…
Reference in a new issue