From 4091442e28144c16c54c99bf181a8ea35cc6f492 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 6 Mar 2024 10:53:45 +0800 Subject: [PATCH 1/2] feat: add fixed toolbar example --- example/lib/home_page.dart | 9 + example/lib/pages/fixed_toolbar_editor.dart | 198 ++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 example/lib/pages/fixed_toolbar_editor.dart diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index f5a891545..afd07a67a 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -7,6 +7,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:example/pages/customize_theme_for_editor.dart'; import 'package:example/pages/editor.dart'; import 'package:example/pages/editor_list.dart'; +import 'package:example/pages/fixed_toolbar_editor.dart'; import 'package:example/pages/focus_example_for_editor.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; @@ -192,6 +193,14 @@ class _HomePageState extends State { ), ); }), + _buildListTile(context, 'Fixed Toolbar', () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const FixedToolbarExample(), + ), + ); + }), // Encoder Demo _buildSeparator(context, 'Export To X Demo'), diff --git a/example/lib/pages/fixed_toolbar_editor.dart b/example/lib/pages/fixed_toolbar_editor.dart new file mode 100644 index 000000000..6485768cd --- /dev/null +++ b/example/lib/pages/fixed_toolbar_editor.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class FixedToolbarExample extends StatefulWidget { + const FixedToolbarExample({super.key}); + + @override + State createState() => _FixedToolbarExampleState(); +} + +class _FixedToolbarExampleState extends State { + late final Future editorState; + + @override + void initState() { + super.initState(); + + final jsonString = rootBundle.loadString('assets/example.json'); + editorState = jsonString.then((value) { + return EditorState( + document: Document.fromJson( + Map.from( + json.decode(value), + ), + ), + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + title: const Text('Fixed Toolbar Example'), + titleTextStyle: const TextStyle(color: Colors.white), + iconTheme: const IconThemeData( + color: Colors.white, + ), + ), + body: FutureBuilder( + future: editorState, + builder: (context, snapshot) { + return !snapshot.hasData + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + SizedBox( + height: 36, + child: _FixedToolbar( + editorState: snapshot.data!, + ), + ), + const Divider(), + FutureBuilder( + future: editorState, + builder: (context, snapshot) { + return !snapshot.hasData + ? const Center(child: CircularProgressIndicator()) + : Expanded( + child: AppFlowyEditor( + editorState: snapshot.data!, + ), + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +class _FixedToolbar extends StatelessWidget { + const _FixedToolbar({ + required this.editorState, + }); + + final EditorState editorState; + + @override + Widget build(BuildContext context) { + final items = [ + Icons.format_bold, + Icons.format_italic, + Icons.format_underlined, + Icons.format_strikethrough, + Icons.text_fields, + Icons.format_list_bulleted, + Icons.format_list_numbered, + Icons.format_align_left, + Icons.format_align_center, + Icons.format_align_right, + Icons.format_align_justify, + Icons.link, + Icons.image, + Icons.format_quote, + Icons.code, + Icons.horizontal_rule, + ]; + return ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) => IconButton( + icon: Icon(items[index]), + onPressed: () { + debugPrint(items[index].toString()); + + switch (items[index]) { + case Icons.format_bold: + editorState.toggleAttribute(AppFlowyRichTextKeys.bold); + break; + case Icons.format_italic: + editorState.toggleAttribute(AppFlowyRichTextKeys.italic); + break; + case Icons.format_underlined: + editorState.toggleAttribute(AppFlowyRichTextKeys.underline); + break; + case Icons.format_strikethrough: + editorState.toggleAttribute(AppFlowyRichTextKeys.strikethrough); + break; + case Icons.text_fields: + editorState.formatNode(null, (node) { + return node.copyWith( + type: ParagraphBlockKeys.type, + ); + }); + break; + case Icons.format_list_bulleted: + editorState.formatNode(null, (node) { + return node.copyWith( + type: node.type == BulletedListBlockKeys.type + ? ParagraphBlockKeys.type + : BulletedListBlockKeys.type, + ); + }); + break; + case Icons.format_list_numbered: + editorState.formatNode(null, (node) { + return node.copyWith( + type: node.type == NumberedListBlockKeys.type + ? ParagraphBlockKeys.type + : NumberedListBlockKeys.type, + ); + }); + break; + case Icons.format_align_left: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'left', + }, + ); + }); + break; + case Icons.format_align_center: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'center', + }, + ); + }); + break; + case Icons.format_align_right: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'right', + }, + ); + }); + break; + case Icons.horizontal_rule: + final selection = editorState.selection; + if (selection == null) { + return; + } + final transaction = editorState.transaction; + transaction.insertNode(selection.start.path.next, dividerNode()); + editorState.apply(transaction); + break; + default: + break; + } + }, + ), + separatorBuilder: (_, __) => const VerticalDivider(), + itemCount: items.length, + ); + } +} From 4e13a906fc23133c3b398dd5bfb60308aecf56a9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 7 Mar 2024 00:12:51 +0800 Subject: [PATCH 2/2] feat: highlight bold button if the selected text is all bold --- example/lib/pages/fixed_toolbar_editor.dart | 220 ++++++++++++-------- 1 file changed, 130 insertions(+), 90 deletions(-) diff --git a/example/lib/pages/fixed_toolbar_editor.dart b/example/lib/pages/fixed_toolbar_editor.dart index 6485768cd..15ccdb67c 100644 --- a/example/lib/pages/fixed_toolbar_editor.dart +++ b/example/lib/pages/fixed_toolbar_editor.dart @@ -102,97 +102,137 @@ class _FixedToolbar extends StatelessWidget { Icons.code, Icons.horizontal_rule, ]; - return ListView.separated( - scrollDirection: Axis.horizontal, - itemBuilder: (_, index) => IconButton( - icon: Icon(items[index]), - onPressed: () { - debugPrint(items[index].toString()); - switch (items[index]) { - case Icons.format_bold: - editorState.toggleAttribute(AppFlowyRichTextKeys.bold); - break; - case Icons.format_italic: - editorState.toggleAttribute(AppFlowyRichTextKeys.italic); - break; - case Icons.format_underlined: - editorState.toggleAttribute(AppFlowyRichTextKeys.underline); - break; - case Icons.format_strikethrough: - editorState.toggleAttribute(AppFlowyRichTextKeys.strikethrough); - break; - case Icons.text_fields: - editorState.formatNode(null, (node) { - return node.copyWith( - type: ParagraphBlockKeys.type, - ); - }); - break; - case Icons.format_list_bulleted: - editorState.formatNode(null, (node) { - return node.copyWith( - type: node.type == BulletedListBlockKeys.type - ? ParagraphBlockKeys.type - : BulletedListBlockKeys.type, - ); - }); - break; - case Icons.format_list_numbered: - editorState.formatNode(null, (node) { - return node.copyWith( - type: node.type == NumberedListBlockKeys.type - ? ParagraphBlockKeys.type - : NumberedListBlockKeys.type, - ); - }); - break; - case Icons.format_align_left: - editorState.formatNode(null, (node) { - return node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: 'left', - }, - ); - }); - break; - case Icons.format_align_center: - editorState.formatNode(null, (node) { - return node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: 'center', - }, - ); - }); - break; - case Icons.format_align_right: - editorState.formatNode(null, (node) { - return node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: 'right', - }, - ); - }); - break; - case Icons.horizontal_rule: - final selection = editorState.selection; - if (selection == null) { - return; - } - final transaction = editorState.transaction; - transaction.insertNode(selection.start.path.next, dividerNode()); - editorState.apply(transaction); - break; - default: - break; - } - }, - ), - separatorBuilder: (_, __) => const VerticalDivider(), - itemCount: items.length, + return ValueListenableBuilder( + valueListenable: editorState.selectionNotifier, + builder: (context, selection, _) { + return ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) { + final isBold = _isTextDecorationActive( + editorState, + selection, + AppFlowyRichTextKeys.bold, + ); + return IconButton( + icon: Icon(items[index]), + color: items[index] == Icons.format_bold && isBold + ? Colors.blue + : Colors.black, + onPressed: () { + debugPrint(items[index].toString()); + switch (items[index]) { + case Icons.format_bold: + editorState.toggleAttribute(AppFlowyRichTextKeys.bold); + break; + case Icons.format_italic: + editorState.toggleAttribute(AppFlowyRichTextKeys.italic); + break; + case Icons.format_underlined: + editorState.toggleAttribute(AppFlowyRichTextKeys.underline); + break; + case Icons.format_strikethrough: + editorState + .toggleAttribute(AppFlowyRichTextKeys.strikethrough); + break; + case Icons.text_fields: + editorState.formatNode(null, (node) { + return node.copyWith( + type: ParagraphBlockKeys.type, + ); + }); + break; + case Icons.format_list_bulleted: + editorState.formatNode(null, (node) { + return node.copyWith( + type: node.type == BulletedListBlockKeys.type + ? ParagraphBlockKeys.type + : BulletedListBlockKeys.type, + ); + }); + break; + case Icons.format_list_numbered: + editorState.formatNode(null, (node) { + return node.copyWith( + type: node.type == NumberedListBlockKeys.type + ? ParagraphBlockKeys.type + : NumberedListBlockKeys.type, + ); + }); + break; + case Icons.format_align_left: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'left', + }, + ); + }); + break; + case Icons.format_align_center: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'center', + }, + ); + }); + break; + case Icons.format_align_right: + editorState.formatNode(null, (node) { + return node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: 'right', + }, + ); + }); + break; + case Icons.horizontal_rule: + final selection = editorState.selection; + if (selection == null) { + return; + } + final transaction = editorState.transaction; + transaction.insertNode( + selection.start.path.next, + dividerNode(), + ); + editorState.apply(transaction); + break; + default: + break; + } + }, + ); + }, + separatorBuilder: (_, __) => const VerticalDivider(), + itemCount: items.length, + ); + }, ); } + + bool _isTextDecorationActive( + EditorState editorState, + Selection? selection, + String name, + ) { + selection = selection ?? editorState.selection; + if (selection == null) { + return false; + } + final nodes = editorState.getNodesInSelection(selection); + if (selection.isCollapsed) { + return editorState.toggledStyle.containsKey(name); + } else { + return nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[name] == true, + ); + }); + } + } }