feat: make the columns block same width width the editor (#7493)

* feat: make the columns block same width width the editor

* chore: turn off column debug mode

* feat: add block selection container in outline block

* feat: use ratio instead of width in simple columns

* fix: document rules

* fix: turn off debug mode

* fix: update the existing columns block data
This commit is contained in:
Lucas 2025-03-10 18:13:15 +08:00 committed by GitHub
parent 7b32a92290
commit c81f87dcdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 223 additions and 107 deletions

View File

@ -104,6 +104,28 @@ class DocumentRules {
} else {
// otherwise, delete the column
deleteColumnsTransaction.deleteNode(column);
final deletedColumnRatio =
column.attributes[SimpleColumnBlockKeys.ratio];
if (deletedColumnRatio != null) {
// update the ratio of the columns
final columnsNode = column.columnsParent;
if (columnsNode != null) {
final length = columnsNode.children.length;
for (final columnNode in columnsNode.children) {
final ratio =
columnNode.attributes[SimpleColumnBlockKeys.ratio] ??
1.0 / length;
if (ratio != null) {
deleteColumnsTransaction.updateNode(columnNode, {
...columnNode.attributes,
SimpleColumnBlockKeys.ratio:
ratio + deletedColumnRatio / (length - 1),
});
}
}
}
}
}
}
}

View File

@ -89,7 +89,7 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
interceptor: (context, targetNode) {
// if the cursor node is in a columns block or a column block,
// we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.
final parentColumnNode = targetNode.parentColumn;
final parentColumnNode = targetNode.columnParent;
if (parentColumnNode != null) {
final position = getDragAreaPosition(
context,
@ -147,7 +147,7 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
interceptor: (context, targetNode) {
// if the cursor node is in a columns block or a column block,
// we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.
final parentColumnNode = targetNode.parentColumn;
final parentColumnNode = targetNode.columnParent;
if (parentColumnNode != null) {
final position = getDragAreaPosition(
context,

View File

@ -1,4 +1,3 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
@ -60,38 +59,37 @@ Future<void> dragToMoveNode(
// 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent
// 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node
final transaction = editorState.transaction;
final targetNodeParent = targetNode.parentColumnsBlock;
final targetNodeParent = targetNode.columnsParent;
if (targetNodeParent != null) {
final length = targetNodeParent.children.length;
final ratios = targetNodeParent.children
.map(
(e) =>
e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length,
)
.map((e) => e * length / (length + 1))
.toList();
final columnNode = simpleColumnNode(
children: [node.deepCopy()],
width: (node.rect.width * 1 / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
ratio: 1.0 / (length + 1),
);
for (final column in targetNodeParent.children) {
final width =
column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
for (final (index, column) in targetNodeParent.children.indexed) {
transaction.updateNode(column, {
...column.attributes,
SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: ratios[index],
});
}
transaction.insertNode(targetNode.path.next, columnNode);
transaction.deleteNode(node);
} else {
final width = targetNode.rect.width / 2 - 16;
final columnsNode = simpleColumnsNode(
children: [
simpleColumnNode(children: [targetNode.deepCopy()], width: width),
simpleColumnNode(children: [node.deepCopy()], width: width),
simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),
simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),
],
);
@ -109,39 +107,37 @@ Future<void> dragToMoveNode(
// 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent
// 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node
final transaction = editorState.transaction;
final targetNodeParent = targetNode.parentColumnsBlock;
final targetNodeParent = targetNode.columnsParent;
if (targetNodeParent != null) {
// find the previous sibling node of the target node
final length = targetNodeParent.children.length;
final ratios = targetNodeParent.children
.map(
(e) =>
e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length,
)
.map((e) => e * length / (length + 1))
.toList();
final columnNode = simpleColumnNode(
children: [node.deepCopy()],
width: (node.rect.width * 1 / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
ratio: 1.0 / (length + 1),
);
for (final column in targetNodeParent.children) {
final width =
column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
for (final (index, column) in targetNodeParent.children.indexed) {
transaction.updateNode(column, {
...column.attributes,
SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: ratios[index],
});
}
transaction.insertNode(targetNode.path.previous, columnNode);
transaction.deleteNode(node);
} else {
final width = targetNode.rect.width / 2 - 16;
final columnsNode = simpleColumnsNode(
children: [
simpleColumnNode(children: [node.deepCopy()], width: width),
simpleColumnNode(children: [targetNode.deepCopy()], width: width),
simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),
simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),
],
);

View File

@ -7,27 +7,62 @@ import 'package:provider/provider.dart';
Node simpleColumnNode({
List<Node>? children,
double? width,
double? ratio,
}) {
return Node(
type: SimpleColumnBlockKeys.type,
children: children ?? [paragraphNode()],
attributes: {
SimpleColumnBlockKeys.width: width,
SimpleColumnBlockKeys.ratio: ratio,
},
);
}
extension SimpleColumnBlockAttributes on Node {
// get the next column node of the current column node
// if the current column node is the last column node, return null
Node? get nextColumn {
final index = path.last;
final parent = this.parent;
if (parent == null || index == parent.children.length - 1) {
return null;
}
return parent.children[index + 1];
}
// get the previous column node of the current column node
// if the current column node is the first column node, return null
Node? get previousColumn {
final index = path.last;
final parent = this.parent;
if (parent == null || index == 0) {
return null;
}
return parent.children[index - 1];
}
}
class SimpleColumnBlockKeys {
const SimpleColumnBlockKeys._();
static const String type = 'simple_column';
/// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead.
///
/// This field is no longer used since v0.6.9
@Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.')
static const String width = 'width';
/// The ratio of the column width.
///
/// The value is a double number between 0 and 1.
static const String ratio = 'ratio';
}
class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder {
SimpleColumnBlockComponentBuilder({super.configuration});
SimpleColumnBlockComponentBuilder({
super.configuration,
});
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
@ -74,16 +109,6 @@ class SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>
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(
@ -121,7 +146,7 @@ class SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>
if (SimpleColumnsBlockConstants.enableDebugBorder) {
child = Container(
color: Colors.green.withValues(
alpha: 0.2,
alpha: 0.3,
),
child: child,
);

View File

@ -1,6 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
@ -26,6 +25,13 @@ class _SimpleColumnBlockWidthResizerState
ValueNotifier<bool> isHovering = ValueNotifier(false);
@override
void dispose() {
isHovering.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
@ -78,25 +84,47 @@ class _SimpleColumnBlockWidthResizerState
// update the column width in memory
final columnNode = widget.columnNode;
final columnsNode = columnNode.columnsParent;
if (columnsNode == null) {
return;
}
final editorWidth = columnsNode.rect.width;
final rect = columnNode.rect;
final width =
columnNode.attributes[SimpleColumnBlockKeys.width] ?? rect.width;
final width = rect.width;
final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio];
final newWidth = width + details.delta.dx;
final transaction = widget.editorState.transaction;
final newRatio = newWidth / editorWidth;
transaction.updateNode(columnNode, {
...columnNode.attributes,
SimpleColumnBlockKeys.width: newWidth.clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: newRatio,
});
final columnsNode = columnNode.parent;
if (columnsNode != null) {
if (newRatio < 0.1 && newRatio < originalRatio) {
return;
}
final nextColumn = columnNode.nextColumn;
if (nextColumn != null) {
final nextColumnRect = nextColumn.rect;
final nextColumnWidth = nextColumnRect.width;
final newNextColumnWidth = nextColumnWidth - details.delta.dx;
final newNextColumnRatio = newNextColumnWidth / editorWidth;
if (newNextColumnRatio < 0.1) {
return;
}
transaction.updateNode(nextColumn, {
...nextColumn.attributes,
SimpleColumnBlockKeys.ratio: newNextColumnRatio,
});
}
transaction.updateNode(columnsNode, {
...columnsNode.attributes,
ColumnsBlockKeys.columnCount: columnsNode.children.length,
});
}
widget.editorState.apply(
transaction,
options: ApplyOptions(inMemoryUpdate: true),
@ -113,9 +141,15 @@ class _SimpleColumnBlockWidthResizerState
// apply the transaction again to make sure the width is updated
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.columnNode, {
...widget.columnNode.attributes,
final columnsNode = widget.columnNode.columnsParent;
if (columnsNode == null) {
return;
}
for (final columnNode in columnsNode.children) {
transaction.updateNode(columnNode, {
...columnNode.attributes,
});
}
widget.editorState.apply(transaction);
isDragging = false;

View File

@ -3,7 +3,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
extension SimpleColumnNodeExtension on Node {
/// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock].
Node? get parentColumnsBlock {
Node? get columnsParent {
Node? currentNode = parent;
while (currentNode != null) {
if (currentNode.type == SimpleColumnsBlockKeys.type) {
@ -15,7 +15,7 @@ extension SimpleColumnNodeExtension on Node {
}
/// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock].
Node? get parentColumn {
Node? get columnParent {
Node? currentNode = parent;
while (currentNode != null) {
if (currentNode.type == SimpleColumnBlockKeys.type) {
@ -27,8 +27,8 @@ extension SimpleColumnNodeExtension on Node {
}
/// Returns whether the current node is in a [SimpleColumnsBlock].
bool get isInColumnsBlock => parentColumnsBlock != null;
bool get isInColumnsBlock => columnsParent != null;
/// Returns whether the current node is in a [SimpleColumnBlock].
bool get isInColumnBlock => parentColumn != null;
bool get isInColumnBlock => columnParent != null;
}

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
@ -5,20 +7,19 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
// if the children is not provided, it will create two columns by default.
// if the columnCount is provided, it will create the specified number of columns.
Node simpleColumnsNode({
List<Node>? children,
int? columnCount,
double? width,
double? ratio,
}) {
columnCount ??= 2;
children ??= List.generate(
columnCount,
(index) => simpleColumnNode(
width: width,
ratio: ratio,
children: [paragraphNode()],
),
);
@ -91,6 +92,13 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_updateColumnsBlock();
}
@override
void dispose() {
scrollController.dispose();
@ -100,23 +108,12 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
@override
Widget build(BuildContext context) {
Widget child = SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: scrollController,
child: Row(
Widget child = Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
);
if (UniversalPlatform.isDesktop) {
// only show the scrollbar on desktop
child = Scrollbar(
controller: scrollController,
child: child,
);
}
child = Align(
alignment: Alignment.topLeft,
child: IntrinsicHeight(
@ -148,19 +145,18 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
}
List<Widget> _buildChildren() {
final length = node.children.length;
final children = <Widget>[];
for (var i = 0; i < node.children.length; i++) {
for (var i = 0; i < length; i++) {
final childNode = node.children[i];
final width =
childNode.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
final double ratio =
childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length;
Widget child = editorState.renderer.build(context, childNode);
child = SizedBox(
width: width.clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
child = Expanded(
flex: (max(ratio, 0.1) * 10000).toInt(),
child: child,
);
@ -176,6 +172,26 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
return children;
}
// Update the existing columns block data
// if the column ratio is not existing, it will be set to 1.0 / columnCount
void _updateColumnsBlock() {
final transaction = editorState.transaction;
final length = node.children.length;
for (int i = 0; i < length; i++) {
final childNode = node.children[i];
final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio];
if (ratio == null) {
transaction.updateNode(childNode, {
...childNode.attributes,
SimpleColumnBlockKeys.ratio: 1.0 / length,
});
}
}
if (transaction.operations.isNotEmpty) {
editorState.apply(transaction);
}
}
@override
Position start() => Position(path: widget.node.path);

View File

@ -227,6 +227,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
delegate: this,
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
selectionAboveBlock: true,
supportTypes: const [BlockSelectionType.block],
child: child,
);
@ -313,7 +314,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
}) {
final imageBox = imageKey.currentContext?.findRenderObject();
if (imageBox is RenderBox) {
return Offset.zero & imageBox.size;
return padding.topLeft & imageBox.size;
}
return Rect.zero;
}

View File

@ -81,7 +81,9 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
with
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
BlockComponentBackgroundColorMixin {
BlockComponentBackgroundColorMixin,
DefaultSelectableMixin,
SelectableMixin {
// Change the value if the heading block type supports heading levels greater than '3'
static const maxVisibleDepth = 6;
@ -95,6 +97,17 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
late EditorState editorState = context.read<EditorState>();
late Stream<EditorTransactionValue> stream = editorState.transactionStream;
@override
GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(
debugLabel: OutlineBlockKeys.type,
);
@override
GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
@override
GlobalKey<State<StatefulWidget>> get forwardKey => widget.node.key;
@override
Widget build(BuildContext context) {
return StreamBuilder(
@ -102,6 +115,19 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
builder: (context, snapshot) {
Widget child = _buildOutlineBlock();
child = BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
remoteSelection: editorState.remoteSelections,
blockColor: editorState.editorStyle.selectionColor,
selectionAboveBlock: true,
supportTypes: const [
BlockSelectionType.block,
],
child: child,
);
if (UniversalPlatform.isDesktopOrWeb) {
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
@ -176,6 +202,7 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
}
return Container(
key: blockComponentKey,
constraints: const BoxConstraints(
minHeight: 40.0,
),

View File

@ -90,13 +90,8 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node(
);
Node _buildColumnsNode(EditorState editorState, int columnCount) {
final selection = editorState.selection;
double? width;
if (selection != null) {
final parentNode = editorState.getNodeAtPath(selection.start.path);
if (parentNode != null) {
width = parentNode.rect.width / columnCount - 16;
}
}
return simpleColumnsNode(columnCount: columnCount, width: width);
return simpleColumnsNode(
columnCount: columnCount,
ratio: 1.0 / columnCount,
);
}