feat: implement popup list service
@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="4" r="0.5" fill="#333333"/>
|
||||
<circle cx="4" cy="8" r="0.5" fill="#333333"/>
|
||||
<circle cx="4" cy="12" r="0.5" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 512 B |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 561 B |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.25344 3.59998H9.63344V12H8.25344V8.36398H4.65344V12H3.27344V3.59998H4.65344V7.04398H8.25344V3.59998Z" fill="#333333"/>
|
||||
<path d="M12.0325 6.39998H12.9925V12H11.8885V7.56798L10.8325 7.86398L10.5605 6.91998L12.0325 6.39998Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 354 B |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.67531 3.59998H9.05531V12H7.67531V8.36398H4.07531V12H2.69531V3.59998H4.07531V7.04398H7.67531V3.59998Z" fill="#333333"/>
|
||||
<path d="M10.1104 12V11.176L12.0224 9.20798C12.449 8.75998 12.6624 8.38664 12.6624 8.08798C12.6624 7.86931 12.593 7.69331 12.4544 7.55998C12.321 7.42664 12.1477 7.35998 11.9344 7.35998C11.513 7.35998 11.201 7.57864 10.9984 8.01598L10.0704 7.47198C10.2464 7.08798 10.4997 6.79464 10.8304 6.59198C11.161 6.38931 11.5237 6.28798 11.9184 6.28798C12.425 6.28798 12.8597 6.44798 13.2224 6.76798C13.585 7.08264 13.7664 7.50931 13.7664 8.04798C13.7664 8.62931 13.4597 9.22664 12.8464 9.83998L11.7504 10.936H13.8544V12H10.1104Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 771 B |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.62063 3.59998H9.00063V12H7.62063V8.36398H4.02062V12H2.64062V3.59998H4.02062V7.04398H7.62063V3.59998Z" fill="#333333"/>
|
||||
<path d="M12.6637 8.67198C13.0424 8.78398 13.349 8.98131 13.5837 9.26398C13.8237 9.54131 13.9437 9.87731 13.9437 10.272C13.9437 10.848 13.749 11.2986 13.3597 11.624C12.9757 11.9493 12.5037 12.112 11.9437 12.112C11.5064 12.112 11.1144 12.0133 10.7677 11.816C10.4264 11.6133 10.1784 11.3173 10.0237 10.928L10.9677 10.384C11.1064 10.816 11.4317 11.032 11.9437 11.032C12.2264 11.032 12.445 10.9653 12.5997 10.832C12.7597 10.6933 12.8397 10.5066 12.8397 10.272C12.8397 10.0426 12.7597 9.85864 12.5997 9.71998C12.445 9.58131 12.2264 9.51198 11.9437 9.51198H11.7037L11.2797 8.87198L12.3837 7.43198H10.1917V6.39998H13.7117V7.31198L12.6637 8.67198Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 892 B |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/>
|
||||
<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 4L14 8L10 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 4L2 8L6 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 282 B |
@ -69,6 +69,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.4.3+7"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -38,6 +38,7 @@ dependencies:
|
||||
path: ../
|
||||
provider: ^6.0.3
|
||||
url_launcher: ^6.1.5
|
||||
flutter_inappwebview: ^5.4.3+7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -9,6 +9,7 @@ extension NodeExtensions on Node {
|
||||
RenderBox? get renderBox =>
|
||||
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||
|
||||
BuildContext? get context => key?.currentContext;
|
||||
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
|
||||
|
||||
bool inSelection(Selection selection) {
|
||||
|
@ -18,20 +18,20 @@ class FlowySvg extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (name != null) {
|
||||
return SizedBox.fromSize(
|
||||
size: size,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/$name.svg',
|
||||
color: color,
|
||||
package: 'flowy_editor',
|
||||
),
|
||||
return SvgPicture.asset(
|
||||
'assets/images/$name.svg',
|
||||
color: color,
|
||||
package: 'flowy_editor',
|
||||
width: size.width,
|
||||
height: size.width,
|
||||
);
|
||||
} else if (number != null) {
|
||||
final numberText =
|
||||
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
|
||||
return SizedBox.fromSize(
|
||||
size: size,
|
||||
child: SvgPicture.string(numberText),
|
||||
return SvgPicture.string(
|
||||
numberText,
|
||||
width: size.width,
|
||||
height: size.width,
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
|
@ -201,6 +201,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
||||
),
|
||||
);
|
||||
});
|
||||
// TODO: disable scrolling.
|
||||
Overlay.of(context)?.insert(_listToolbarOverlay!);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import 'package:flowy_editor/service/input_service.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||
import 'package:flowy_editor/service/selection_service.dart';
|
||||
|
@ -1,12 +0,0 @@
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// type '/' to trigger shortcut widget
|
||||
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
};
|
@ -0,0 +1,223 @@
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/infra/flowy_svg.dart';
|
||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
final List<PopupListItem> _popupListItems = [
|
||||
PopupListItem(
|
||||
text: 'Text',
|
||||
icon: _popupListIcon('text'),
|
||||
handler: (editorState) => formatText(editorState),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Heading 1',
|
||||
icon: _popupListIcon('h1'),
|
||||
handler: (editorState) => formatHeading(editorState, StyleKey.h1),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Heading 2',
|
||||
icon: _popupListIcon('h2'),
|
||||
handler: (editorState) => formatHeading(editorState, StyleKey.h2),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Heading 3',
|
||||
icon: _popupListIcon('h3'),
|
||||
handler: (editorState) => formatHeading(editorState, StyleKey.h3),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Bullets',
|
||||
icon: _popupListIcon('bullets'),
|
||||
handler: (editorState) => formatBulletedList(editorState),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Numbered list',
|
||||
icon: _popupListIcon('number'),
|
||||
handler: (editorState) => debugPrint('Not implement yet!'),
|
||||
),
|
||||
PopupListItem(
|
||||
text: 'Checkboxes',
|
||||
icon: _popupListIcon('checkbox'),
|
||||
handler: (editorState) => formatCheckbox(editorState),
|
||||
),
|
||||
];
|
||||
|
||||
OverlayEntry? popupListOverlay;
|
||||
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.slash && !event.isMetaPressed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final textNodes = editorState
|
||||
.service.selectionService.currentSelectedNodes.value
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final selection = editorState.service.selectionService.currentSelection;
|
||||
final textNode = textNodes.first;
|
||||
final context = textNode.context;
|
||||
final selectable = textNode.selectable;
|
||||
if (selection == null || context == null || selectable == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final rect = selectable.getCursorRectInPosition(selection.start);
|
||||
final offset = selectable.localToGlobal(rect.topLeft);
|
||||
if (!selection.isCollapsed) {
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
selection.end.offset - selection.start.offset,
|
||||
)
|
||||
..commit();
|
||||
}
|
||||
|
||||
popupListOverlay?.remove();
|
||||
popupListOverlay = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: offset.dy + 15.0,
|
||||
left: offset.dx,
|
||||
child: PopupListWidget(
|
||||
editorState: editorState,
|
||||
items: _popupListItems,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context)?.insert(popupListOverlay!);
|
||||
|
||||
editorState.service.selectionService.currentSelectedNodes
|
||||
.removeListener(clearPopupListOverlay);
|
||||
editorState.service.selectionService.currentSelectedNodes
|
||||
.addListener(clearPopupListOverlay);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
void clearPopupListOverlay() {
|
||||
popupListOverlay?.remove();
|
||||
popupListOverlay = null;
|
||||
}
|
||||
|
||||
class PopupListWidget extends StatefulWidget {
|
||||
const PopupListWidget({
|
||||
Key? key,
|
||||
required this.editorState,
|
||||
required this.items,
|
||||
this.maxItemInRow = 8,
|
||||
}) : super(key: key);
|
||||
|
||||
final EditorState editorState;
|
||||
final List<PopupListItem> items;
|
||||
final int maxItemInRow;
|
||||
|
||||
@override
|
||||
State<PopupListWidget> createState() => _PopupListWidgetState();
|
||||
}
|
||||
|
||||
class _PopupListWidgetState extends State<PopupListWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5,
|
||||
spreadRadius: 1,
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildColumns(widget.items),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildColumns(List<PopupListItem> items) {
|
||||
List<Widget> columns = [];
|
||||
List<Widget> itemWidgets = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (i != 0 && i % (widget.maxItemInRow - 1) == 0) {
|
||||
columns.add(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: itemWidgets,
|
||||
));
|
||||
itemWidgets = [];
|
||||
}
|
||||
itemWidgets.add(_PopupListItemWidget(
|
||||
editorState: widget.editorState, item: items[i]));
|
||||
}
|
||||
if (itemWidgets.isNotEmpty) {
|
||||
columns.add(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: itemWidgets,
|
||||
));
|
||||
itemWidgets = [];
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
|
||||
class _PopupListItemWidget extends StatelessWidget {
|
||||
const _PopupListItemWidget({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final EditorState editorState;
|
||||
final PopupListItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
|
||||
child: TextButton.icon(
|
||||
icon: item.icon,
|
||||
label: Text(
|
||||
item.text,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
item.handler(editorState);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PopupListItem {
|
||||
PopupListItem({
|
||||
required this.text,
|
||||
this.message = '',
|
||||
required this.icon,
|
||||
required this.handler,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String message;
|
||||
final Widget icon;
|
||||
final void Function(EditorState editorState) handler;
|
||||
}
|
||||
|
||||
Widget _popupListIcon(String name) => FlowySvg(
|
||||
name: 'popup_list/$name',
|
||||
color: Colors.black,
|
||||
size: const Size.square(18.0),
|
||||
);
|
@ -27,6 +27,7 @@ flutter:
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
assets:
|
||||
- assets/images/toolbar/
|
||||
- assets/images/popup_list/
|
||||
- assets/images/
|
||||
- assets/document.json
|
||||
# - images/a_dot_burr.jpeg
|
||||
|