mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-15 18:07:55 +00:00
fix: turn into issues (#6576)
* fix: cover title issues * fix: the selection should be cleared if selecting child node * fix: exclude the blocks that are not supported in the 'turn into' types * fix: add logs * fix: floating toolbar ai status * test: selecting the parent should deselect all the child nodes as well * chore: 'Copy Link' to 'Copy link' * fix: select all and turn into block doesn't work on Windows * test: calculate turn into selection test * fix: option button tests
This commit is contained in:
parent
ec408940e4
commit
7b031b228e
@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -143,5 +144,37 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'selecting the parent should deselect all the child nodes as well',
|
||||||
|
(tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapAnonymousSignInButton();
|
||||||
|
|
||||||
|
const name = 'Test Document';
|
||||||
|
await tester.createNewPageWithNameUnderParent(name: name);
|
||||||
|
await tester.openPage(name);
|
||||||
|
|
||||||
|
// create a nested list
|
||||||
|
// Item 1
|
||||||
|
// Nested Item 1
|
||||||
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
|
await tester.ime.insertText('Item 1');
|
||||||
|
await tester.ime.insertCharacter('\n');
|
||||||
|
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.ime.insertText('Nested Item 1');
|
||||||
|
|
||||||
|
// select the 'Nested Item 1' and then tap the option button of the 'Item 1'
|
||||||
|
final editorState = tester.editor.getCurrentEditorState();
|
||||||
|
final selection = Selection.collapsed(
|
||||||
|
Position(path: [0, 0], offset: 1),
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(editorState.selection, selection);
|
||||||
|
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||||
|
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import 'document_create_and_delete_test.dart'
|
|||||||
import 'document_inline_page_reference_test.dart'
|
import 'document_inline_page_reference_test.dart'
|
||||||
as document_inline_page_reference_test;
|
as document_inline_page_reference_test;
|
||||||
import 'document_more_actions_test.dart' as document_more_actions_test;
|
import 'document_more_actions_test.dart' as document_more_actions_test;
|
||||||
import 'document_option_action_test.dart' as document_option_action_test;
|
|
||||||
import 'document_shortcuts_test.dart' as document_shortcuts_test;
|
import 'document_shortcuts_test.dart' as document_shortcuts_test;
|
||||||
import 'document_text_direction_test.dart' as document_text_direction_test;
|
import 'document_text_direction_test.dart' as document_text_direction_test;
|
||||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||||
@ -40,7 +39,6 @@ void main() {
|
|||||||
document_codeblock_paste_test.main();
|
document_codeblock_paste_test.main();
|
||||||
document_alignment_test.main();
|
document_alignment_test.main();
|
||||||
document_text_direction_test.main();
|
document_text_direction_test.main();
|
||||||
document_option_action_test.main();
|
|
||||||
document_with_image_block_test.main();
|
document_with_image_block_test.main();
|
||||||
document_with_multi_image_block_test.main();
|
document_with_multi_image_block_test.main();
|
||||||
document_inline_page_reference_test.main();
|
document_inline_page_reference_test.main();
|
||||||
|
|||||||
@ -252,6 +252,10 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
|
|||||||
);
|
);
|
||||||
insertedNode.add(afterNode);
|
insertedNode.add(afterNode);
|
||||||
insertedNode.addAll(node.children.map((e) => e.copyWith()));
|
insertedNode.addAll(node.children.map((e) => e.copyWith()));
|
||||||
|
} else if (!EditorOptionActionType.turnInto.supportTypes
|
||||||
|
.contains(node.type)) {
|
||||||
|
afterNode = node.copyWith();
|
||||||
|
insertedNode.add(afterNode);
|
||||||
} else {
|
} else {
|
||||||
insertedNode.add(afterNode);
|
insertedNode.add(afterNode);
|
||||||
}
|
}
|
||||||
@ -267,4 +271,35 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selection? calculateTurnIntoSelection(
|
||||||
|
Node selectedNode,
|
||||||
|
Selection? beforeSelection,
|
||||||
|
) {
|
||||||
|
final path = selectedNode.path;
|
||||||
|
final selection = Selection.collapsed(
|
||||||
|
Position(path: path),
|
||||||
|
);
|
||||||
|
|
||||||
|
// if the previous selection is null or the start path is not in the same level as the current block path,
|
||||||
|
// then update the selection with the current block path
|
||||||
|
// for example,'|' means the selection,
|
||||||
|
// case 1: collapsed selection
|
||||||
|
// - bulleted item 1
|
||||||
|
// - bulleted |item 2
|
||||||
|
// when clicking the bulleted item 1, the bulleted item 1 path should be selected
|
||||||
|
// case 2: not collapsed selection
|
||||||
|
// - bulleted item 1
|
||||||
|
// - bulleted |item 2
|
||||||
|
// - bulleted |item 3
|
||||||
|
// when clicking the bulleted item 1, the bulleted item 1 path should be selected
|
||||||
|
if (beforeSelection == null ||
|
||||||
|
beforeSelection.start.path.length != path.length ||
|
||||||
|
!path.inSelection(beforeSelection)) {
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
// if the beforeSelection start with the current block,
|
||||||
|
// then updating the selection with the beforeSelection that may contains multiple blocks
|
||||||
|
return beforeSelection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
|
||||||
import 'package:appflowy/startup/startup.dart';
|
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
import 'draggable_option_button_feedback.dart';
|
||||||
|
import 'option_button.dart';
|
||||||
|
|
||||||
// this flag is used to disable the tooltip of the block when it is dragged
|
// this flag is used to disable the tooltip of the block when it is dragged
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@ -66,13 +57,13 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
|
|||||||
onDragStarted: _onDragStart,
|
onDragStarted: _onDragStart,
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
feedback: _OptionButtonFeedback(
|
feedback: DraggleOptionButtonFeedback(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
blockComponentContext: widget.blockComponentContext,
|
blockComponentContext: widget.blockComponentContext,
|
||||||
blockComponentBuilder: widget.blockComponentBuilder,
|
blockComponentBuilder: widget.blockComponentBuilder,
|
||||||
),
|
),
|
||||||
child: _OptionButton(
|
child: OptionButton(
|
||||||
isDragging: isDraggingAppFlowyEditorBlock,
|
isDragging: isDraggingAppFlowyEditorBlock,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
@ -130,251 +121,3 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OptionButtonFeedback extends StatefulWidget {
|
|
||||||
const _OptionButtonFeedback({
|
|
||||||
required this.controller,
|
|
||||||
required this.editorState,
|
|
||||||
required this.blockComponentContext,
|
|
||||||
required this.blockComponentBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PopoverController controller;
|
|
||||||
final EditorState editorState;
|
|
||||||
final BlockComponentContext blockComponentContext;
|
|
||||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_OptionButtonFeedback> createState() => _OptionButtonFeedbackState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OptionButtonFeedbackState extends State<_OptionButtonFeedback> {
|
|
||||||
late Node node;
|
|
||||||
late BlockComponentContext blockComponentContext;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_setupLockComponentContext();
|
|
||||||
widget.blockComponentContext.node.addListener(_updateBlockComponentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.blockComponentContext.node
|
|
||||||
.removeListener(_updateBlockComponentContext);
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final maxWidth = (widget.editorState.renderBox?.size.width ??
|
|
||||||
MediaQuery.of(context).size.width) *
|
|
||||||
0.8;
|
|
||||||
|
|
||||||
return Opacity(
|
|
||||||
opacity: 0.7,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
),
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Provider.value(
|
|
||||||
value: widget.editorState,
|
|
||||||
child: _buildBlock(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBlock() {
|
|
||||||
final node = widget.blockComponentContext.node;
|
|
||||||
final builder = widget.blockComponentBuilder[node.type];
|
|
||||||
if (builder == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsupportedRenderBlockTypes = [
|
|
||||||
TableBlockKeys.type,
|
|
||||||
CustomImageBlockKeys.type,
|
|
||||||
MultiImageBlockKeys.type,
|
|
||||||
FileBlockKeys.type,
|
|
||||||
DatabaseBlockKeys.boardType,
|
|
||||||
DatabaseBlockKeys.calendarType,
|
|
||||||
DatabaseBlockKeys.gridType,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (unsupportedRenderBlockTypes.contains(node.type)) {
|
|
||||||
// unable to render table block without provider/context
|
|
||||||
// render a placeholder instead
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: widget.editorState),
|
|
||||||
Provider.value(value: getIt<ReminderBloc>()),
|
|
||||||
],
|
|
||||||
child: builder.build(blockComponentContext),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateBlockComponentContext() {
|
|
||||||
setState(() => _setupLockComponentContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setupLockComponentContext() {
|
|
||||||
node = widget.blockComponentContext.node.copyWith();
|
|
||||||
blockComponentContext = BlockComponentContext(
|
|
||||||
widget.blockComponentContext.buildContext,
|
|
||||||
node,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OptionButton extends StatefulWidget {
|
|
||||||
const _OptionButton({
|
|
||||||
required this.controller,
|
|
||||||
required this.editorState,
|
|
||||||
required this.blockComponentContext,
|
|
||||||
required this.isDragging,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PopoverController controller;
|
|
||||||
final EditorState editorState;
|
|
||||||
final BlockComponentContext blockComponentContext;
|
|
||||||
final ValueNotifier<bool> isDragging;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_OptionButton> createState() => _OptionButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
const _interceptorKey = 'document_option_button_interceptor';
|
|
||||||
|
|
||||||
class _OptionButtonState extends State<_OptionButton> {
|
|
||||||
late final gestureInterceptor = SelectionGestureInterceptor(
|
|
||||||
key: _interceptorKey,
|
|
||||||
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
|
||||||
);
|
|
||||||
|
|
||||||
// the selection will be cleared when tap the option button
|
|
||||||
// so we need to restore the selection after tap the option button
|
|
||||||
Selection? beforeSelection;
|
|
||||||
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
widget.editorState.service.selectionService.registerGestureInterceptor(
|
|
||||||
gestureInterceptor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.editorState.service.selectionService.unregisterGestureInterceptor(
|
|
||||||
_interceptorKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: widget.isDragging,
|
|
||||||
builder: (context, isDragging, child) {
|
|
||||||
return BlockActionButton(
|
|
||||||
svg: FlowySvgs.drag_element_s,
|
|
||||||
showTooltip: !isDragging,
|
|
||||||
richMessage: TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
|
|
||||||
style: context.tooltipTextStyle(),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
|
|
||||||
style: context.tooltipTextStyle(),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
|
||||||
style: context.tooltipTextStyle(),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
|
||||||
style: context.tooltipTextStyle(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPointerDown: () {
|
|
||||||
if (widget.editorState.selection != null) {
|
|
||||||
beforeSelection = widget.editorState.selection;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (widget.editorState.selection != null) {
|
|
||||||
beforeSelection = widget.editorState.selection;
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.controller.show();
|
|
||||||
|
|
||||||
// update selection
|
|
||||||
_updateBlockSelection();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateBlockSelection() {
|
|
||||||
if (beforeSelection == null) {
|
|
||||||
final path = widget.blockComponentContext.node.path;
|
|
||||||
final selection = Selection.collapsed(
|
|
||||||
Position(path: path),
|
|
||||||
);
|
|
||||||
widget.editorState.updateSelectionWithReason(
|
|
||||||
selection,
|
|
||||||
customSelectionType: SelectionType.block,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
widget.editorState.updateSelectionWithReason(
|
|
||||||
beforeSelection!,
|
|
||||||
customSelectionType: SelectionType.block,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isTapInBounds(Offset offset) {
|
|
||||||
if (renderBox == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final localPosition = renderBox!.globalToLocal(offset);
|
|
||||||
final result = renderBox!.paintBounds.contains(localPosition);
|
|
||||||
if (result) {
|
|
||||||
beforeSelection = widget.editorState.selection;
|
|
||||||
} else {
|
|
||||||
beforeSelection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,266 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class DraggleOptionButtonFeedback extends StatefulWidget {
|
||||||
|
const DraggleOptionButtonFeedback({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.editorState,
|
||||||
|
required this.blockComponentContext,
|
||||||
|
required this.blockComponentBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PopoverController controller;
|
||||||
|
final EditorState editorState;
|
||||||
|
final BlockComponentContext blockComponentContext;
|
||||||
|
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DraggleOptionButtonFeedback> createState() =>
|
||||||
|
_DraggleOptionButtonFeedbackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DraggleOptionButtonFeedbackState
|
||||||
|
extends State<DraggleOptionButtonFeedback> {
|
||||||
|
late Node node;
|
||||||
|
late BlockComponentContext blockComponentContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_setupLockComponentContext();
|
||||||
|
widget.blockComponentContext.node.addListener(_updateBlockComponentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.blockComponentContext.node
|
||||||
|
.removeListener(_updateBlockComponentContext);
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final maxWidth = (widget.editorState.renderBox?.size.width ??
|
||||||
|
MediaQuery.of(context).size.width) *
|
||||||
|
0.8;
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: 0.7,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Provider.value(
|
||||||
|
value: widget.editorState,
|
||||||
|
child: _buildBlock(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBlock() {
|
||||||
|
final node = widget.blockComponentContext.node;
|
||||||
|
final builder = widget.blockComponentBuilder[node.type];
|
||||||
|
if (builder == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsupportedRenderBlockTypes = [
|
||||||
|
TableBlockKeys.type,
|
||||||
|
CustomImageBlockKeys.type,
|
||||||
|
MultiImageBlockKeys.type,
|
||||||
|
FileBlockKeys.type,
|
||||||
|
DatabaseBlockKeys.boardType,
|
||||||
|
DatabaseBlockKeys.calendarType,
|
||||||
|
DatabaseBlockKeys.gridType,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (unsupportedRenderBlockTypes.contains(node.type)) {
|
||||||
|
// unable to render table block without provider/context
|
||||||
|
// render a placeholder instead
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider.value(value: widget.editorState),
|
||||||
|
Provider.value(value: getIt<ReminderBloc>()),
|
||||||
|
],
|
||||||
|
child: builder.build(blockComponentContext),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBlockComponentContext() {
|
||||||
|
setState(() => _setupLockComponentContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupLockComponentContext() {
|
||||||
|
node = widget.blockComponentContext.node.copyWith();
|
||||||
|
blockComponentContext = BlockComponentContext(
|
||||||
|
widget.blockComponentContext.buildContext,
|
||||||
|
node,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OptionButton extends StatefulWidget {
|
||||||
|
const _OptionButton({
|
||||||
|
required this.controller,
|
||||||
|
required this.editorState,
|
||||||
|
required this.blockComponentContext,
|
||||||
|
required this.isDragging,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PopoverController controller;
|
||||||
|
final EditorState editorState;
|
||||||
|
final BlockComponentContext blockComponentContext;
|
||||||
|
final ValueNotifier<bool> isDragging;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OptionButton> createState() => _OptionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const _interceptorKey = 'document_option_button_interceptor';
|
||||||
|
|
||||||
|
class _OptionButtonState extends State<_OptionButton> {
|
||||||
|
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||||
|
key: _interceptorKey,
|
||||||
|
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
||||||
|
);
|
||||||
|
|
||||||
|
// the selection will be cleared when tap the option button
|
||||||
|
// so we need to restore the selection after tap the option button
|
||||||
|
Selection? beforeSelection;
|
||||||
|
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
widget.editorState.service.selectionService.registerGestureInterceptor(
|
||||||
|
gestureInterceptor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.editorState.service.selectionService.unregisterGestureInterceptor(
|
||||||
|
_interceptorKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: widget.isDragging,
|
||||||
|
builder: (context, isDragging, child) {
|
||||||
|
return BlockActionButton(
|
||||||
|
svg: FlowySvgs.drag_element_s,
|
||||||
|
showTooltip: !isDragging,
|
||||||
|
richMessage: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPointerDown: () {
|
||||||
|
if (widget.editorState.selection != null) {
|
||||||
|
beforeSelection = widget.editorState.selection;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (widget.editorState.selection != null) {
|
||||||
|
beforeSelection = widget.editorState.selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.controller.show();
|
||||||
|
|
||||||
|
// update selection
|
||||||
|
_updateBlockSelection();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBlockSelection() {
|
||||||
|
if (beforeSelection == null) {
|
||||||
|
final path = widget.blockComponentContext.node.path;
|
||||||
|
final selection = Selection.collapsed(
|
||||||
|
Position(path: path),
|
||||||
|
);
|
||||||
|
widget.editorState.updateSelectionWithReason(
|
||||||
|
selection,
|
||||||
|
customSelectionType: SelectionType.block,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget.editorState.updateSelectionWithReason(
|
||||||
|
beforeSelection!,
|
||||||
|
customSelectionType: SelectionType.block,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTapInBounds(Offset offset) {
|
||||||
|
if (renderBox == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final localPosition = renderBox!.globalToLocal(offset);
|
||||||
|
final result = renderBox!.paintBounds.contains(localPosition);
|
||||||
|
if (result) {
|
||||||
|
beforeSelection = widget.editorState.selection;
|
||||||
|
} else {
|
||||||
|
beforeSelection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
const _interceptorKey = 'document_option_button_interceptor';
|
||||||
|
|
||||||
|
class OptionButton extends StatefulWidget {
|
||||||
|
const OptionButton({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.editorState,
|
||||||
|
required this.blockComponentContext,
|
||||||
|
required this.isDragging,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PopoverController controller;
|
||||||
|
final EditorState editorState;
|
||||||
|
final BlockComponentContext blockComponentContext;
|
||||||
|
final ValueNotifier<bool> isDragging;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OptionButton> createState() => _OptionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OptionButtonState extends State<OptionButton> {
|
||||||
|
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||||
|
key: _interceptorKey,
|
||||||
|
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
||||||
|
);
|
||||||
|
|
||||||
|
// the selection will be cleared when tap the option button
|
||||||
|
// so we need to restore the selection after tap the option button
|
||||||
|
Selection? beforeSelection;
|
||||||
|
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
widget.editorState.service.selectionService.registerGestureInterceptor(
|
||||||
|
gestureInterceptor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.editorState.service.selectionService.unregisterGestureInterceptor(
|
||||||
|
_interceptorKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: widget.isDragging,
|
||||||
|
builder: (context, isDragging, child) {
|
||||||
|
return BlockActionButton(
|
||||||
|
svg: FlowySvgs.drag_element_s,
|
||||||
|
showTooltip: !isDragging,
|
||||||
|
richMessage: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
final selection = widget.editorState.selection;
|
||||||
|
if (selection != null) {
|
||||||
|
beforeSelection = selection.normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.controller.show();
|
||||||
|
|
||||||
|
// update selection
|
||||||
|
_updateBlockSelection(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBlockSelection(BuildContext context) {
|
||||||
|
final cubit = context.read<BlockActionOptionCubit>();
|
||||||
|
final selection = cubit.calculateTurnIntoSelection(
|
||||||
|
widget.blockComponentContext.node,
|
||||||
|
beforeSelection,
|
||||||
|
);
|
||||||
|
Log.info(
|
||||||
|
'update block selection, beforeSelection: $beforeSelection, afterSelection: $selection',
|
||||||
|
);
|
||||||
|
widget.editorState.updateSelectionWithReason(
|
||||||
|
selection,
|
||||||
|
customSelectionType: SelectionType.block,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTapInBounds(Offset offset) {
|
||||||
|
final renderBox = this.renderBox;
|
||||||
|
if (renderBox == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final localPosition = renderBox.globalToLocal(offset);
|
||||||
|
final result = renderBox.paintBounds.contains(localPosition);
|
||||||
|
if (result) {
|
||||||
|
beforeSelection = widget.editorState.selection?.normalized;
|
||||||
|
} else {
|
||||||
|
beforeSelection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,8 +22,8 @@ KeyEventResult _backspaceToTitle({
|
|||||||
required EditorState editorState,
|
required EditorState editorState,
|
||||||
}) {
|
}) {
|
||||||
final coverTitleFocusNode = editorState.document.root.context
|
final coverTitleFocusNode = editorState.document.root.context
|
||||||
?.read<SharedEditorContext>()
|
?.read<SharedEditorContext?>()
|
||||||
.coverTitleFocusNode;
|
?.coverTitleFocusNode;
|
||||||
if (coverTitleFocusNode == null) {
|
if (coverTitleFocusNode == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
@ -108,8 +108,8 @@ KeyEventResult _arrowKeyToTitle({
|
|||||||
required bool Function(Selection selection) checkSelection,
|
required bool Function(Selection selection) checkSelection,
|
||||||
}) {
|
}) {
|
||||||
final coverTitleFocusNode = editorState.document.root.context
|
final coverTitleFocusNode = editorState.document.root.context
|
||||||
?.read<SharedEditorContext>()
|
?.read<SharedEditorContext?>()
|
||||||
.coverTitleFocusNode;
|
?.coverTitleFocusNode;
|
||||||
if (coverTitleFocusNode == null) {
|
if (coverTitleFocusNode == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,6 +110,7 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
|
|||||||
if (UniversalPlatform.isMobile) {
|
if (UniversalPlatform.isMobile) {
|
||||||
return _MobileMentionPageBlock(
|
return _MobileMentionPageBlock(
|
||||||
view: view,
|
view: view,
|
||||||
|
content: state.blockContent,
|
||||||
textStyle: widget.textStyle,
|
textStyle: widget.textStyle,
|
||||||
handleTap: () => handleTap(view),
|
handleTap: () => handleTap(view),
|
||||||
handleDoubleTap: handleDoubleTap,
|
handleDoubleTap: handleDoubleTap,
|
||||||
@ -367,6 +368,7 @@ class _NoAccessMentionPageBlock extends StatelessWidget {
|
|||||||
class _MobileMentionPageBlock extends StatelessWidget {
|
class _MobileMentionPageBlock extends StatelessWidget {
|
||||||
const _MobileMentionPageBlock({
|
const _MobileMentionPageBlock({
|
||||||
required this.view,
|
required this.view,
|
||||||
|
required this.content,
|
||||||
required this.textStyle,
|
required this.textStyle,
|
||||||
required this.handleTap,
|
required this.handleTap,
|
||||||
required this.handleDoubleTap,
|
required this.handleDoubleTap,
|
||||||
@ -374,6 +376,7 @@ class _MobileMentionPageBlock extends StatelessWidget {
|
|||||||
|
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
|
final String content;
|
||||||
final VoidCallback handleTap;
|
final VoidCallback handleTap;
|
||||||
final VoidCallback handleDoubleTap;
|
final VoidCallback handleDoubleTap;
|
||||||
|
|
||||||
@ -385,6 +388,7 @@ class _MobileMentionPageBlock extends StatelessWidget {
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: _MentionPageBlockContent(
|
child: _MentionPageBlockContent(
|
||||||
view: view,
|
view: view,
|
||||||
|
content: content,
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
const _kSmartEditToolbarItemId = 'appflowy.editor.smart_edit';
|
const _kSmartEditToolbarItemId = 'appflowy.editor.smart_edit';
|
||||||
|
|
||||||
@ -39,21 +39,12 @@ class SmartEditActionList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SmartEditActionListState extends State<SmartEditActionList> {
|
class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||||
bool isAIEnabled = false;
|
bool isAIEnabled = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
isAIEnabled = _isAIEnabled();
|
||||||
UserBackendService.getCurrentUserProfile().then((value) {
|
|
||||||
setState(() {
|
|
||||||
isAIEnabled = value.fold(
|
|
||||||
(userProfile) =>
|
|
||||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud,
|
|
||||||
(_) => false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -83,12 +74,13 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isAIEnabled) {
|
if (isAIEnabled) {
|
||||||
|
keepEditorFocusNotifier.increase();
|
||||||
controller.show();
|
controller.show();
|
||||||
} else {
|
} else {
|
||||||
showSnackBarMessage(
|
showToastNotification(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
message:
|
||||||
showCancel: true,
|
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -161,4 +153,12 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isAIEnabled() {
|
||||||
|
final documentContext = widget.editorState.document.root.context;
|
||||||
|
if (documentContext == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !documentContext.read<DocumentBloc>().isLocalMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1535,10 +1535,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -1933,10 +1933,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
string_validator:
|
string_validator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2238,10 +2238,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.5"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -533,5 +533,155 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('calculate selection when turn into', () {
|
||||||
|
// Example:
|
||||||
|
// - bulleted list item 1
|
||||||
|
// - bulleted list item 1-1
|
||||||
|
// - bulleted list item 1-2
|
||||||
|
// - bulleted list item 2
|
||||||
|
// - bulleted list item 2-1
|
||||||
|
// - bulleted list item 2-2
|
||||||
|
// - bulleted list item 3
|
||||||
|
// - bulleted list item 3-1
|
||||||
|
// - bulleted list item 3-2
|
||||||
|
const text = 'bulleted list';
|
||||||
|
const nestedText = 'nested bulleted list';
|
||||||
|
final document = createDocument([
|
||||||
|
bulletedListNode(
|
||||||
|
text: '$text 1',
|
||||||
|
children: [
|
||||||
|
bulletedListNode(text: '$nestedText 1-1'),
|
||||||
|
bulletedListNode(text: '$nestedText 1-2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bulletedListNode(
|
||||||
|
text: '$text 2',
|
||||||
|
children: [
|
||||||
|
bulletedListNode(text: '$nestedText 2-1'),
|
||||||
|
bulletedListNode(text: '$nestedText 2-2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bulletedListNode(
|
||||||
|
text: '$text 3',
|
||||||
|
children: [
|
||||||
|
bulletedListNode(text: '$nestedText 3-1'),
|
||||||
|
bulletedListNode(text: '$nestedText 3-2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
final cubit = BlockActionOptionCubit(
|
||||||
|
editorState: editorState,
|
||||||
|
blockComponentBuilder: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 1: collapsed selection and the selection is in the top level
|
||||||
|
// and tap the turn into button at the [0]
|
||||||
|
final selection1 = Selection.collapsed(
|
||||||
|
Position(path: [0], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0])!,
|
||||||
|
selection1,
|
||||||
|
),
|
||||||
|
selection1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 2: collapsed selection and the selection is in the nested level
|
||||||
|
// and tap the turn into button at the [0]
|
||||||
|
final selection2 = Selection.collapsed(
|
||||||
|
Position(path: [0, 0], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0])!,
|
||||||
|
selection2,
|
||||||
|
),
|
||||||
|
Selection.collapsed(Position(path: [0])),
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 3, collapsed selection and the selection is in the nested level
|
||||||
|
// and tap the turn into button at the [0, 0]
|
||||||
|
final selection3 = Selection.collapsed(
|
||||||
|
Position(path: [0, 0], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0, 0])!,
|
||||||
|
selection3,
|
||||||
|
),
|
||||||
|
selection3,
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 4, not collapsed selection and the selection is in the top level
|
||||||
|
// and tap the turn into button at the [0]
|
||||||
|
final selection4 = Selection(
|
||||||
|
start: Position(path: [0], offset: 1),
|
||||||
|
end: Position(path: [1], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0])!,
|
||||||
|
selection4,
|
||||||
|
),
|
||||||
|
selection4,
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 5, not collapsed selection and the selection is in the nested level
|
||||||
|
// and tap the turn into button at the [0]
|
||||||
|
final selection5 = Selection(
|
||||||
|
start: Position(path: [0, 0], offset: 1),
|
||||||
|
end: Position(path: [0, 1], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0])!,
|
||||||
|
selection5,
|
||||||
|
),
|
||||||
|
Selection.collapsed(Position(path: [0])),
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 6, not collapsed selection and the selection is in the nested level
|
||||||
|
// and tap the turn into button at the [0, 0]
|
||||||
|
final selection6 = Selection(
|
||||||
|
start: Position(path: [0, 0], offset: 1),
|
||||||
|
end: Position(path: [0, 1], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([0])!,
|
||||||
|
selection6,
|
||||||
|
),
|
||||||
|
Selection.collapsed(Position(path: [0])),
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 7, multiple blocks selection, and tap the turn into button of one of the selected nodes
|
||||||
|
final selection7 = Selection(
|
||||||
|
start: Position(path: [0], offset: 1),
|
||||||
|
end: Position(path: [2], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([1])!,
|
||||||
|
selection7,
|
||||||
|
),
|
||||||
|
selection7,
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 8, multiple blocks selection, and tap the turn into button of one of the non-selected nodes
|
||||||
|
final selection8 = Selection(
|
||||||
|
start: Position(path: [0], offset: 1),
|
||||||
|
end: Position(path: [1], offset: 1),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.calculateTurnIntoSelection(
|
||||||
|
editorState.getNodeAtPath([2])!,
|
||||||
|
selection8,
|
||||||
|
),
|
||||||
|
Selection.collapsed(Position(path: [2])),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,7 @@
|
|||||||
"html": "HTML",
|
"html": "HTML",
|
||||||
"clipboard": "Copy to clipboard",
|
"clipboard": "Copy to clipboard",
|
||||||
"csv": "CSV",
|
"csv": "CSV",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy link",
|
||||||
"publishToTheWeb": "Publish to Web",
|
"publishToTheWeb": "Publish to Web",
|
||||||
"publishToTheWebHint": "Create a website with AppFlowy",
|
"publishToTheWebHint": "Create a website with AppFlowy",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
@ -162,7 +162,7 @@
|
|||||||
"openNewTab": "Open in a new tab",
|
"openNewTab": "Open in a new tab",
|
||||||
"moveTo": "Move to",
|
"moveTo": "Move to",
|
||||||
"addToFavorites": "Add to Favorites",
|
"addToFavorites": "Add to Favorites",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy link",
|
||||||
"changeIcon": "Change icon",
|
"changeIcon": "Change icon",
|
||||||
"collapseAllPages": "Collapse all subpages"
|
"collapseAllPages": "Collapse all subpages"
|
||||||
},
|
},
|
||||||
@ -2713,4 +2713,4 @@
|
|||||||
"refreshNote": "After successful upgrade, click <refresh/> to activate your new features.",
|
"refreshNote": "After successful upgrade, click <refresh/> to activate your new features.",
|
||||||
"refresh": "here"
|
"refresh": "here"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user