mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement outline block component depth control (#4642)
* feat: add support to control the depth of outline block component * feat: update localization keys * feat: add depth option to `BlockOptionButton` * feat: retrive outline block heading components upto the depth level * feat: add depth option config to editor configuration * test: outline block depth control * feat: add outline block placeholder * refactor: remove redundant codes * ci: trigger github actions * chore: refactor `OptionDepthType` enum * fix: flutter ci error * refactor: removed `finalHeadingLevel` from outline keys * fix: flutter ci error
This commit is contained in:
parent
42cb032bca
commit
15c9a67028
@ -1,11 +1,18 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.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: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';
|
||||||
|
|
||||||
import '../util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
|
const String heading1 = "Heading 1";
|
||||||
|
const String heading2 = "Heading 2";
|
||||||
|
const String heading3 = "Heading 3";
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@ -33,12 +40,8 @@ void main() {
|
|||||||
await tester.createNewPageWithNameUnderParent(
|
await tester.createNewPageWithNameUnderParent(
|
||||||
name: 'outline_test',
|
name: 'outline_test',
|
||||||
);
|
);
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
|
||||||
|
|
||||||
await tester.ime.insertText('# Heading 1\n');
|
|
||||||
await tester.ime.insertText('## Heading 2\n');
|
|
||||||
await tester.ime.insertText('### Heading 3\n');
|
|
||||||
|
|
||||||
|
await insertHeadingComponent(tester);
|
||||||
/* Results in:
|
/* Results in:
|
||||||
* # Heading 1
|
* # Heading 1
|
||||||
* ## Heading 2
|
* ## Heading 2
|
||||||
@ -51,7 +54,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(OutlineBlockWidget),
|
of: find.byType(OutlineBlockWidget),
|
||||||
matching: find.text('Heading 1'),
|
matching: find.text(heading1),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
@ -60,7 +63,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(OutlineBlockWidget),
|
of: find.byType(OutlineBlockWidget),
|
||||||
matching: find.text('Heading 2'),
|
matching: find.text(heading2),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
@ -69,7 +72,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(OutlineBlockWidget),
|
of: find.byType(OutlineBlockWidget),
|
||||||
matching: find.text('Heading 3'),
|
matching: find.text(heading3),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
@ -80,11 +83,86 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(OutlineBlockWidget),
|
of: find.byType(OutlineBlockWidget),
|
||||||
matching: find.text('Heading 1Hello world'),
|
matching: find.text('${heading1}Hello world'),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("control the depth of outline block", (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.createNewPageWithNameUnderParent(
|
||||||
|
name: 'outline_test',
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertHeadingComponent(tester);
|
||||||
|
/* Results in:
|
||||||
|
* # Heading 1
|
||||||
|
* ## Heading 2
|
||||||
|
* ### Heading 3
|
||||||
|
*/
|
||||||
|
|
||||||
|
await tester.editor.tapLineOfEditorAt(3);
|
||||||
|
await insertOutlineInDocument(tester);
|
||||||
|
|
||||||
|
// expect to find only the `heading1` widget under the [OutlineBlockWidget]
|
||||||
|
await hoverAndClickDepthOptionAction(tester, [3], 1);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading2),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading3),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
//////
|
||||||
|
|
||||||
|
/// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget]
|
||||||
|
await hoverAndClickDepthOptionAction(tester, [3], 2);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading3),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
//////
|
||||||
|
|
||||||
|
// expect to find all the headings under the [OutlineBlockWidget]
|
||||||
|
await hoverAndClickDepthOptionAction(tester, [3], 3);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading1),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading2),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(OutlineBlockWidget),
|
||||||
|
matching: find.text(heading3),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
//////
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,3 +175,25 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> hoverAndClickDepthOptionAction(
|
||||||
|
WidgetTester tester,
|
||||||
|
List<int> path,
|
||||||
|
int level,
|
||||||
|
) async {
|
||||||
|
await tester.editor.hoverAndClickOptionMenuButton([3]);
|
||||||
|
await tester.tap(find.byType(AppFlowyPopover).hitTestable().last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Find a total of 4 HoverButtons under the [BlockOptionButton],
|
||||||
|
// in addition to 3 HoverButtons under the [DepthOptionAction] - (child of BlockOptionButton)
|
||||||
|
await tester.tap(find.byType(HoverButton).hitTestable().at(3 + level));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertHeadingComponent(WidgetTester tester) async {
|
||||||
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
|
await tester.ime.insertText('# $heading1\n');
|
||||||
|
await tester.ime.insertText('## $heading2\n');
|
||||||
|
await tester.ime.insertText('### $heading3\n');
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
|||||||
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||||
@ -241,4 +242,25 @@ class EditorOperations {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// hover and click on the option menu button beside the block component.
|
||||||
|
Future<void> hoverAndClickOptionMenuButton(Path path) async {
|
||||||
|
final optionMenuButton = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is BlockComponentActionWrapper &&
|
||||||
|
widget.node.path.equals(path),
|
||||||
|
);
|
||||||
|
await tester.hoverOnWidget(
|
||||||
|
optionMenuButton,
|
||||||
|
onHover: () async {
|
||||||
|
await tester.tapButton(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is BlockOptionButton &&
|
||||||
|
widget.blockComponentContext.node.path.equals(path),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,6 +229,10 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
ImageBlockKeys.type,
|
ImageBlockKeys.type,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final supportDepthBuilderType = [
|
||||||
|
OutlineBlockKeys.type,
|
||||||
|
];
|
||||||
|
|
||||||
final colorAction = [
|
final colorAction = [
|
||||||
OptionAction.divider,
|
OptionAction.divider,
|
||||||
OptionAction.color,
|
OptionAction.color,
|
||||||
@ -239,10 +243,15 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
OptionAction.align,
|
OptionAction.align,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final depthAction = [
|
||||||
|
OptionAction.depth,
|
||||||
|
];
|
||||||
|
|
||||||
final List<OptionAction> actions = [
|
final List<OptionAction> actions = [
|
||||||
...standardActions,
|
...standardActions,
|
||||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||||
|
if (supportDepthBuilderType.contains(entry.key)) ...depthAction,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (PlatformExtension.isDesktop) {
|
if (PlatformExtension.isDesktop) {
|
||||||
|
@ -34,12 +34,15 @@ class BlockOptionButton extends StatelessWidget {
|
|||||||
return ColorOptionAction(editorState: editorState);
|
return ColorOptionAction(editorState: editorState);
|
||||||
case OptionAction.align:
|
case OptionAction.align:
|
||||||
return AlignOptionAction(editorState: editorState);
|
return AlignOptionAction(editorState: editorState);
|
||||||
|
case OptionAction.depth:
|
||||||
|
return DepthOptionAction(editorState: editorState);
|
||||||
default:
|
default:
|
||||||
return OptionActionWrapper(e);
|
return OptionActionWrapper(e);
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return PopoverActionList<PopoverAction>(
|
return PopoverActionList<PopoverAction>(
|
||||||
|
popoverMutex: PopoverMutex(),
|
||||||
direction:
|
direction:
|
||||||
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
||||||
LayoutDirection.rtlLayout
|
LayoutDirection.rtlLayout
|
||||||
@ -136,6 +139,7 @@ class BlockOptionButton extends StatelessWidget {
|
|||||||
case OptionAction.align:
|
case OptionAction.align:
|
||||||
case OptionAction.color:
|
case OptionAction.color:
|
||||||
case OptionAction.divider:
|
case OptionAction.divider:
|
||||||
|
case OptionAction.depth:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
@ -22,7 +22,8 @@ enum OptionAction {
|
|||||||
/// callout background color
|
/// callout background color
|
||||||
color,
|
color,
|
||||||
divider,
|
divider,
|
||||||
align;
|
align,
|
||||||
|
depth;
|
||||||
|
|
||||||
FlowySvgData get svg {
|
FlowySvgData get svg {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -41,7 +42,9 @@ enum OptionAction {
|
|||||||
case OptionAction.divider:
|
case OptionAction.divider:
|
||||||
return const FlowySvgData('editor/divider');
|
return const FlowySvgData('editor/divider');
|
||||||
case OptionAction.align:
|
case OptionAction.align:
|
||||||
return FlowySvgs.align_center_s;
|
return FlowySvgs.m_aa_bulleted_list_s;
|
||||||
|
case OptionAction.depth:
|
||||||
|
return FlowySvgs.tag_s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +64,8 @@ enum OptionAction {
|
|||||||
return LocaleKeys.document_plugins_optionAction_color.tr();
|
return LocaleKeys.document_plugins_optionAction_color.tr();
|
||||||
case OptionAction.align:
|
case OptionAction.align:
|
||||||
return LocaleKeys.document_plugins_optionAction_align.tr();
|
return LocaleKeys.document_plugins_optionAction_align.tr();
|
||||||
|
case OptionAction.depth:
|
||||||
|
return LocaleKeys.document_plugins_optionAction_depth.tr();
|
||||||
case OptionAction.divider:
|
case OptionAction.divider:
|
||||||
throw UnsupportedError('Divider does not have description');
|
throw UnsupportedError('Divider does not have description');
|
||||||
}
|
}
|
||||||
@ -108,6 +113,29 @@ enum OptionAlignType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OptionDepthType {
|
||||||
|
h1(1, "H1"),
|
||||||
|
h2(2, "H2"),
|
||||||
|
h3(3, "H3");
|
||||||
|
|
||||||
|
const OptionDepthType(this.level, this.description);
|
||||||
|
|
||||||
|
final String description;
|
||||||
|
final int level;
|
||||||
|
|
||||||
|
static OptionDepthType fromLevel(int? level) {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return OptionDepthType.h1;
|
||||||
|
case 2:
|
||||||
|
return OptionDepthType.h2;
|
||||||
|
case 3:
|
||||||
|
default:
|
||||||
|
return OptionDepthType.h3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DividerOptionAction extends CustomActionCell {
|
class DividerOptionAction extends CustomActionCell {
|
||||||
@override
|
@override
|
||||||
Widget buildWithContext(BuildContext context) {
|
Widget buildWithContext(BuildContext context) {
|
||||||
@ -285,6 +313,89 @@ class ColorOptionAction extends PopoverActionCell {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DepthOptionAction extends PopoverActionCell {
|
||||||
|
DepthOptionAction({required this.editorState});
|
||||||
|
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? leftIcon(Color iconColor) {
|
||||||
|
return FlowySvg(
|
||||||
|
OptionAction.depth.svg,
|
||||||
|
size: const Size.square(12),
|
||||||
|
).padding(all: 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => LocaleKeys.document_plugins_optionAction_depth.tr();
|
||||||
|
|
||||||
|
@override
|
||||||
|
PopoverActionCellBuilder get builder =>
|
||||||
|
(context, parentController, controller) {
|
||||||
|
final children = buildDepthOptions(context, (depth) async {
|
||||||
|
await onDepthChanged(depth);
|
||||||
|
controller.close();
|
||||||
|
parentController.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: 42,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Widget> buildDepthOptions(
|
||||||
|
BuildContext context,
|
||||||
|
Future<void> Function(OptionDepthType) onTap,
|
||||||
|
) {
|
||||||
|
return OptionDepthType.values
|
||||||
|
.map((e) => OptionDepthWrapper(e))
|
||||||
|
.map(
|
||||||
|
(e) => HoverButton(
|
||||||
|
onTap: () => onTap(e.inner),
|
||||||
|
itemHeight: ActionListSizes.itemHeight,
|
||||||
|
leftIcon: null,
|
||||||
|
name: e.name,
|
||||||
|
rightIcon: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionDepthType depth(Node node) {
|
||||||
|
final level = node.attributes[OutlineBlockKeys.depth];
|
||||||
|
return OptionDepthType.fromLevel(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onDepthChanged(OptionDepthType depth) async {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
final node = selection != null
|
||||||
|
? editorState.getNodeAtPath(selection.start.path)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (node == null || depth == this.depth(node)) return;
|
||||||
|
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.updateNode(
|
||||||
|
node,
|
||||||
|
{OutlineBlockKeys.depth: depth.level},
|
||||||
|
);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionDepthWrapper extends ActionCell {
|
||||||
|
OptionDepthWrapper(this.inner);
|
||||||
|
|
||||||
|
final OptionDepthType inner;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => inner.description;
|
||||||
|
}
|
||||||
|
|
||||||
class OptionActionWrapper extends ActionCell {
|
class OptionActionWrapper extends ActionCell {
|
||||||
OptionActionWrapper(this.inner);
|
OptionActionWrapper(this.inner);
|
||||||
|
|
||||||
|
@ -29,12 +29,17 @@ class OptionActionList extends StatelessWidget {
|
|||||||
return ColorOptionAction(
|
return ColorOptionAction(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
);
|
);
|
||||||
|
} else if (e == OptionAction.depth) {
|
||||||
|
return DepthOptionAction(
|
||||||
|
editorState: editorState,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return OptionActionWrapper(e);
|
return OptionActionWrapper(e);
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return PopoverActionList<PopoverAction>(
|
return PopoverActionList<PopoverAction>(
|
||||||
|
popoverMutex: PopoverMutex(),
|
||||||
direction: PopoverDirection.leftWithCenterAligned,
|
direction: PopoverDirection.leftWithCenterAligned,
|
||||||
actions: popoverActions,
|
actions: popoverActions,
|
||||||
onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
|
onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
|
||||||
@ -105,6 +110,7 @@ class OptionActionList extends StatelessWidget {
|
|||||||
case OptionAction.align:
|
case OptionAction.align:
|
||||||
case OptionAction.color:
|
case OptionAction.color:
|
||||||
case OptionAction.divider:
|
case OptionAction.divider:
|
||||||
|
case OptionAction.depth:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
@ -14,6 +14,7 @@ class OutlineBlockKeys {
|
|||||||
|
|
||||||
static const String type = 'outline';
|
static const String type = 'outline';
|
||||||
static const String backgroundColor = blockComponentBackgroundColor;
|
static const String backgroundColor = blockComponentBackgroundColor;
|
||||||
|
static const String depth = 'depth';
|
||||||
}
|
}
|
||||||
|
|
||||||
// defining the callout block menu item for selection
|
// defining the callout block menu item for selection
|
||||||
@ -73,6 +74,9 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
|||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BlockComponentTextDirectionMixin,
|
BlockComponentTextDirectionMixin,
|
||||||
BlockComponentBackgroundColorMixin {
|
BlockComponentBackgroundColorMixin {
|
||||||
|
// Change the value if the heading block type supports heading levels greater than '3'
|
||||||
|
static const finalHeadingLevel = 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BlockComponentConfiguration get configuration => widget.configuration;
|
BlockComponentConfiguration get configuration => widget.configuration;
|
||||||
|
|
||||||
@ -141,7 +145,11 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
|||||||
style: configuration.placeholderTextStyle(node),
|
style: configuration.placeholderTextStyle(node),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: DecoratedBox(
|
: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 2.0,
|
||||||
|
horizontal: 5.0,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
@ -151,7 +159,19 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
children: children,
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.document_outlineBlock_placeholder.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const VSpace(8.0),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15.0),
|
||||||
|
child: Column(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -166,10 +186,14 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
|||||||
|
|
||||||
Iterable<Node> getHeadingNodes() {
|
Iterable<Node> getHeadingNodes() {
|
||||||
final children = editorState.document.root.children;
|
final children = editorState.document.root.children;
|
||||||
|
final int level =
|
||||||
|
node.attributes[OutlineBlockKeys.depth] ?? finalHeadingLevel;
|
||||||
|
|
||||||
return children.where(
|
return children.where(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.type == HeadingBlockKeys.type &&
|
element.type == HeadingBlockKeys.type &&
|
||||||
element.delta?.isNotEmpty == true,
|
element.delta?.isNotEmpty == true &&
|
||||||
|
element.attributes[HeadingBlockKeys.level] <= level,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
||||||
const PopoverActionList({
|
const PopoverActionList({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.popoverMutex,
|
||||||
required this.actions,
|
required this.actions,
|
||||||
required this.buildChild,
|
required this.buildChild,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
@ -23,6 +24,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final PopoverMutex? popoverMutex;
|
||||||
final List<T> actions;
|
final List<T> actions;
|
||||||
final Widget Function(PopoverController) buildChild;
|
final Widget Function(PopoverController) buildChild;
|
||||||
final Function(T, PopoverController) onSelected;
|
final Function(T, PopoverController) onSelected;
|
||||||
@ -74,6 +76,7 @@ class _PopoverActionListState<T extends PopoverAction>
|
|||||||
);
|
);
|
||||||
} else if (action is PopoverActionCell) {
|
} else if (action is PopoverActionCell) {
|
||||||
return PopoverActionCellWidget<T>(
|
return PopoverActionCellWidget<T>(
|
||||||
|
popoverMutex: widget.popoverMutex,
|
||||||
popoverController: popoverController,
|
popoverController: popoverController,
|
||||||
action: action,
|
action: action,
|
||||||
itemHeight: ActionListSizes.itemHeight,
|
itemHeight: ActionListSizes.itemHeight,
|
||||||
@ -164,11 +167,13 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
|
|||||||
class PopoverActionCellWidget<T extends PopoverAction> extends StatefulWidget {
|
class PopoverActionCellWidget<T extends PopoverAction> extends StatefulWidget {
|
||||||
const PopoverActionCellWidget({
|
const PopoverActionCellWidget({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.popoverMutex,
|
||||||
required this.popoverController,
|
required this.popoverController,
|
||||||
required this.action,
|
required this.action,
|
||||||
required this.itemHeight,
|
required this.itemHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final PopoverMutex? popoverMutex;
|
||||||
final T action;
|
final T action;
|
||||||
final double itemHeight;
|
final double itemHeight;
|
||||||
|
|
||||||
@ -190,6 +195,7 @@ class _PopoverActionCellWidgetState<T extends PopoverAction>
|
|||||||
final rightIcon =
|
final rightIcon =
|
||||||
actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
|
actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
|
mutex: widget.popoverMutex,
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
asBarrier: true,
|
asBarrier: true,
|
||||||
popupBuilder: (context) => actionCell.builder(
|
popupBuilder: (context) => actionCell.builder(
|
||||||
|
@ -74,9 +74,7 @@ String languageFromLocale(Locale locale) {
|
|||||||
return "اردو";
|
return "اردو";
|
||||||
case "hin":
|
case "hin":
|
||||||
return "हिन्दी";
|
return "हिन्दी";
|
||||||
|
|
||||||
// If not found then the language code will be displayed
|
|
||||||
default:
|
|
||||||
return locale.languageCode;
|
|
||||||
}
|
}
|
||||||
|
// If not found then the language code will be displayed
|
||||||
|
return locale.languageCode;
|
||||||
}
|
}
|
||||||
|
@ -779,7 +779,8 @@
|
|||||||
"left": "Left",
|
"left": "Left",
|
||||||
"center": "Center",
|
"center": "Center",
|
||||||
"right": "Right",
|
"right": "Right",
|
||||||
"defaultColor": "Default"
|
"defaultColor": "Default",
|
||||||
|
"depth": "Depth"
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"copiedToPasteBoard": "The image link has been copied to the clipboard",
|
"copiedToPasteBoard": "The image link has been copied to the clipboard",
|
||||||
@ -816,6 +817,9 @@
|
|||||||
"date": "Date",
|
"date": "Date",
|
||||||
"emoji": "Emoji"
|
"emoji": "Emoji"
|
||||||
},
|
},
|
||||||
|
"outlineBlock": {
|
||||||
|
"placeholder": "Table of Contents"
|
||||||
|
},
|
||||||
"textBlock": {
|
"textBlock": {
|
||||||
"placeholder": "Type '/' for commands"
|
"placeholder": "Type '/' for commands"
|
||||||
},
|
},
|
||||||
@ -1276,4 +1280,4 @@
|
|||||||
"userIcon": "User icon"
|
"userIcon": "User icon"
|
||||||
},
|
},
|
||||||
"noLogFiles": "There're no log files"
|
"noLogFiles": "There're no log files"
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user