feat: observe mention block change and support block navigation (#6568)

* feat: observe mentioned block changes and navigate to block

* test: add delete mentioned block test

* chore: update editor version

* feat: navigate block in same page

* fix: sometimes turn into menu doesn't work

* test: add test

* fix: integration test
This commit is contained in:
Lucas 2024-10-17 13:29:34 +08:00 committed by GitHub
parent 7cad04bbf4
commit 0413100e2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 296 additions and 34 deletions

View file

@ -1,7 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
@ -21,6 +19,7 @@ import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
@ -31,12 +30,14 @@ class DocumentPage extends StatefulWidget {
required this.view,
required this.onDeleted,
this.initialSelection,
this.initialBlockId,
this.fixedTitle,
});
final ViewPB view;
final VoidCallback onDeleted;
final Selection? initialSelection;
final String? initialBlockId;
final String? fixedTitle;
@override
@ -124,13 +125,18 @@ class _DocumentPageState extends State<DocumentPage>
BuildContext context,
DocumentState state,
) {
final editorState = state.editorState;
if (editorState == null) {
return const SizedBox.shrink();
}
final width = context.read<DocumentAppearanceCubit>().state.width;
final Widget child;
if (UniversalPlatform.isMobile) {
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, styleState) => AppFlowyEditorPage(
editorState: state.editorState!,
editorState: editorState,
// if the view's name is empty, focus on the title
autoFocus: widget.view.name.isEmpty ? false : null,
styleCustomizer: EditorStyleCustomizer(
@ -145,10 +151,10 @@ class _DocumentPageState extends State<DocumentPage>
} else {
child = EditorDropHandler(
viewId: widget.view.id,
editorState: state.editorState!,
editorState: editorState,
isLocalMode: context.read<DocumentBloc>().isLocalMode,
child: AppFlowyEditorPage(
editorState: state.editorState!,
editorState: editorState,
// if the view's name is empty, focus on the title
autoFocus: widget.view.name.isEmpty ? false : null,
styleCustomizer: EditorStyleCustomizer(
@ -157,7 +163,7 @@ class _DocumentPageState extends State<DocumentPage>
padding: EditorStyleCustomizer.documentPadding,
),
header: buildCoverAndIcon(context, state),
initialSelection: widget.initialSelection,
initialSelection: _calculateInitialSelection(editorState),
),
);
}
@ -242,16 +248,54 @@ class _DocumentPageState extends State<DocumentPage>
BuildContext context,
ActionNavigationState state,
) {
if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
final path = state.action?.arguments?[ActionArgumentKeys.nodePath];
final action = state.action;
if (action == null ||
action.type != ActionType.jumpToBlock ||
action.objectId != widget.view.id) {
return;
}
final editorState = context.read<DocumentBloc>().state.editorState;
if (editorState != null && widget.view.id == state.action?.objectId) {
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [path])),
);
final editorState = context.read<DocumentBloc>().state.editorState;
if (editorState == null) {
return;
}
final Path? path = _getPathFromAction(action, editorState);
if (path != null) {
debugPrint('jump to block: $path');
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: path)),
);
}
}
Path? _getPathFromAction(NavigationAction action, EditorState editorState) {
Path? path = action.arguments?[ActionArgumentKeys.nodePath];
if (path == null || path.isEmpty) {
final blockId = action.arguments?[ActionArgumentKeys.blockId];
if (blockId != null) {
path = _findNodePathByBlockId(editorState, blockId);
}
}
return path;
}
Path? _findNodePathByBlockId(EditorState editorState, String blockId) {
final document = editorState.document;
final startNode = document.root.children.firstOrNull;
if (startNode == null) {
return null;
}
final nodeIterator = NodeIterator(document: document, startNode: startNode);
while (nodeIterator.moveNext()) {
final node = nodeIterator.current;
if (node.id == blockId) {
return node.path;
}
}
return null;
}
bool shouldRebuildDocument(DocumentState previous, DocumentState current) {
@ -362,4 +406,24 @@ class _DocumentPageState extends State<DocumentPage>
isPaste = false;
}
}
Selection? _calculateInitialSelection(EditorState editorState) {
if (widget.initialSelection != null) {
return widget.initialSelection;
}
if (widget.initialBlockId != null) {
final path = _findNodePathByBlockId(editorState, widget.initialBlockId!);
if (path != null) {
editorState.selectionType = SelectionType.block;
return Selection.collapsed(
Position(
path: path,
),
);
}
}
return null;
}
}