mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
commit
42fe2f675a
@ -21,7 +21,6 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
// This is the theme of your application.
|
// This is the theme of your application.
|
||||||
//
|
//
|
||||||
@ -64,12 +63,10 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
body: Container(
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
alignment: Alignment.topCenter,
|
||||||
// the App.build method, and use it to set our appbar title.
|
child: _buildBody(),
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
|
||||||
floatingActionButton: _buildExpandableFab(),
|
floatingActionButton: _buildExpandableFab(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:flowy_editor/src/document/path.dart';
|
import 'package:flowy_editor/src/document/path.dart';
|
||||||
import 'package:flowy_editor/src/document/text_delta.dart';
|
import 'package:flowy_editor/src/document/text_delta.dart';
|
||||||
import 'package:flowy_editor/src/operation/operation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import './attributes.dart';
|
import './attributes.dart';
|
||||||
|
|
||||||
@ -182,12 +181,12 @@ class TextNode extends Node {
|
|||||||
}) : _delta = delta,
|
}) : _delta = delta,
|
||||||
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
||||||
|
|
||||||
TextNode.empty()
|
TextNode.empty({Attributes? attributes})
|
||||||
: _delta = Delta([TextInsert('')]),
|
: _delta = Delta([TextInsert('')]),
|
||||||
super(
|
super(
|
||||||
type: 'text',
|
type: 'text',
|
||||||
children: LinkedList(),
|
children: LinkedList(),
|
||||||
attributes: {},
|
attributes: attributes ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Delta get delta {
|
Delta get delta {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ class FlowyRichText extends StatefulWidget {
|
|||||||
const FlowyRichText({
|
const FlowyRichText({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.cursorHeight,
|
this.cursorHeight,
|
||||||
this.cursorWidth = 2.0,
|
this.cursorWidth = 1.0,
|
||||||
this.textSpanDecorator,
|
this.textSpanDecorator,
|
||||||
this.placeholderText = ' ',
|
this.placeholderText = ' ',
|
||||||
this.placeholderTextSpanDecorator,
|
this.placeholderTextSpanDecorator,
|
||||||
@ -41,7 +43,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
final _textKey = GlobalKey();
|
final _textKey = GlobalKey();
|
||||||
final _placeholderTextKey = GlobalKey();
|
final _placeholderTextKey = GlobalKey();
|
||||||
|
|
||||||
final lineHeight = 1.5;
|
final _lineHeight = 1.5;
|
||||||
|
|
||||||
RenderParagraph get _renderParagraph =>
|
RenderParagraph get _renderParagraph =>
|
||||||
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||||
@ -69,13 +71,15 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
final cursorHeight = widget.cursorHeight ??
|
final cursorHeight = widget.cursorHeight ??
|
||||||
_renderParagraph.getFullHeightForCaret(textPosition) ??
|
_renderParagraph.getFullHeightForCaret(textPosition) ??
|
||||||
_placeholderRenderParagraph.getFullHeightForCaret(textPosition) ??
|
_placeholderRenderParagraph.getFullHeightForCaret(textPosition) ??
|
||||||
18.0; // default height
|
16.0; // default height
|
||||||
return Rect.fromLTWH(
|
|
||||||
|
final rect = Rect.fromLTWH(
|
||||||
cursorOffset.dx - (widget.cursorWidth / 2),
|
cursorOffset.dx - (widget.cursorWidth / 2),
|
||||||
cursorOffset.dy,
|
cursorOffset.dy,
|
||||||
widget.cursorWidth,
|
widget.cursorWidth,
|
||||||
cursorHeight,
|
cursorHeight,
|
||||||
);
|
);
|
||||||
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -105,7 +109,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
extentOffset: selection.end.offset,
|
extentOffset: selection.end.offset,
|
||||||
);
|
);
|
||||||
return _renderParagraph
|
return _renderParagraph
|
||||||
.getBoxesForSelection(textSelection)
|
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
|
||||||
.map((box) => box.toRect())
|
.map((box) => box.toRect())
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
@ -138,24 +142,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholderText(BuildContext context) {
|
Widget _buildPlaceholderText(BuildContext context) {
|
||||||
final textSpan = TextSpan(
|
final textSpan = _placeholderTextSpan;
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: widget.placeholderText,
|
|
||||||
style: TextStyle(
|
|
||||||
color: widget.textNode.toRawString().isNotEmpty
|
|
||||||
? Colors.transparent
|
|
||||||
: Colors.grey,
|
|
||||||
fontSize: baseFontSize,
|
|
||||||
height: lineHeight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return RichText(
|
return RichText(
|
||||||
key: _placeholderTextKey,
|
key: _placeholderTextKey,
|
||||||
text: widget.placeholderTextSpanDecorator != null
|
textHeightBehavior: const TextHeightBehavior(
|
||||||
? widget.placeholderTextSpanDecorator!(textSpan)
|
applyHeightToFirstAscent: false, applyHeightToLastDescent: false),
|
||||||
|
text: widget.textSpanDecorator != null
|
||||||
|
? widget.textSpanDecorator!(textSpan)
|
||||||
: textSpan,
|
: textSpan,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -164,6 +157,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
final textSpan = _textSpan;
|
final textSpan = _textSpan;
|
||||||
return RichText(
|
return RichText(
|
||||||
key: _textKey,
|
key: _textKey,
|
||||||
|
textHeightBehavior: const TextHeightBehavior(
|
||||||
|
applyHeightToFirstAscent: false, applyHeightToLastDescent: false),
|
||||||
text: widget.textSpanDecorator != null
|
text: widget.textSpanDecorator != null
|
||||||
? widget.textSpanDecorator!(textSpan)
|
? widget.textSpanDecorator!(textSpan)
|
||||||
: textSpan,
|
: textSpan,
|
||||||
@ -203,8 +198,18 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
.map((insert) => RichTextStyle(
|
.map((insert) => RichTextStyle(
|
||||||
attributes: insert.attributes ?? {},
|
attributes: insert.attributes ?? {},
|
||||||
text: insert.content,
|
text: insert.content,
|
||||||
height: lineHeight,
|
height: _lineHeight,
|
||||||
).toTextSpan())
|
).toTextSpan())
|
||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextSpan get _placeholderTextSpan => TextSpan(children: [
|
||||||
|
RichTextStyle(
|
||||||
|
text: widget.placeholderText,
|
||||||
|
attributes: {
|
||||||
|
StyleKey.color: '0xFF707070',
|
||||||
|
},
|
||||||
|
height: _lineHeight,
|
||||||
|
).toTextSpan()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
@ -192,17 +192,7 @@ class RichTextStyle {
|
|||||||
TextSpan toTextSpan() => _toTextSpan(height);
|
TextSpan toTextSpan() => _toTextSpan(height);
|
||||||
|
|
||||||
double get topPadding {
|
double get topPadding {
|
||||||
if (height == 1.0) {
|
return 0;
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// TODO: Need to be optimized.
|
|
||||||
final painter =
|
|
||||||
TextPainter(text: _toTextSpan(height), textDirection: TextDirection.ltr)
|
|
||||||
..layout();
|
|
||||||
final basePainter =
|
|
||||||
TextPainter(text: _toTextSpan(null), textDirection: TextDirection.ltr)
|
|
||||||
..layout();
|
|
||||||
return painter.height - basePainter.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSpan _toTextSpan(double? height) {
|
TextSpan _toTextSpan(double? height) {
|
||||||
|
@ -4,9 +4,64 @@ import 'package:flowy_editor/src/document/position.dart';
|
|||||||
import 'package:flowy_editor/src/document/selection.dart';
|
import 'package:flowy_editor/src/document/selection.dart';
|
||||||
import 'package:flowy_editor/src/editor_state.dart';
|
import 'package:flowy_editor/src/editor_state.dart';
|
||||||
import 'package:flowy_editor/src/extensions/text_node_extensions.dart';
|
import 'package:flowy_editor/src/extensions/text_node_extensions.dart';
|
||||||
|
import 'package:flowy_editor/src/extensions/path_extensions.dart';
|
||||||
import 'package:flowy_editor/src/operation/transaction_builder.dart';
|
import 'package:flowy_editor/src/operation/transaction_builder.dart';
|
||||||
import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
|
import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
|
||||||
|
|
||||||
|
void insertHeadingAfterSelection(EditorState editorState, String heading) {
|
||||||
|
insertTextNodeAfterSelection(editorState, {
|
||||||
|
StyleKey.subtype: StyleKey.heading,
|
||||||
|
StyleKey.heading: heading,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void insertQuoteAfterSelection(EditorState editorState) {
|
||||||
|
insertTextNodeAfterSelection(editorState, {
|
||||||
|
StyleKey.subtype: StyleKey.quote,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void insertCheckboxAfterSelection(EditorState editorState) {
|
||||||
|
insertTextNodeAfterSelection(editorState, {
|
||||||
|
StyleKey.subtype: StyleKey.checkbox,
|
||||||
|
StyleKey.checkbox: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void insertBulletedListAfterSelection(EditorState editorState) {
|
||||||
|
insertTextNodeAfterSelection(editorState, {
|
||||||
|
StyleKey.subtype: StyleKey.bulletedList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insertTextNodeAfterSelection(
|
||||||
|
EditorState editorState, Attributes attributes) {
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
if (selection == null || nodes.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = nodes.first;
|
||||||
|
if (node is TextNode && node.delta.length == 0) {
|
||||||
|
formatTextNodes(editorState, attributes);
|
||||||
|
} else {
|
||||||
|
final next = selection.end.path.next;
|
||||||
|
final builder = TransactionBuilder(editorState);
|
||||||
|
builder
|
||||||
|
..insertNode(
|
||||||
|
next,
|
||||||
|
TextNode.empty(attributes: attributes),
|
||||||
|
)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(path: next, offset: 0),
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void formatText(EditorState editorState) {
|
void formatText(EditorState editorState) {
|
||||||
formatTextNodes(editorState, {});
|
formatTextNodes(editorState, {});
|
||||||
}
|
}
|
||||||
|
@ -14,43 +14,56 @@ import 'package:flutter/services.dart';
|
|||||||
final List<PopupListItem> _popupListItems = [
|
final List<PopupListItem> _popupListItems = [
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
keywords: ['text'],
|
||||||
icon: _popupListIcon('text'),
|
icon: _popupListIcon('text'),
|
||||||
handler: (editorState) => formatText(editorState),
|
handler: (editorState) {
|
||||||
|
insertTextNodeAfterSelection(editorState, {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Heading 1',
|
text: 'Heading 1',
|
||||||
|
keywords: ['h1', 'heading 1'],
|
||||||
icon: _popupListIcon('h1'),
|
icon: _popupListIcon('h1'),
|
||||||
handler: (editorState) => formatHeading(editorState, StyleKey.h1),
|
handler: (editorState) =>
|
||||||
|
insertHeadingAfterSelection(editorState, StyleKey.h1),
|
||||||
),
|
),
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Heading 2',
|
text: 'Heading 2',
|
||||||
|
keywords: ['h2', 'heading 2'],
|
||||||
icon: _popupListIcon('h2'),
|
icon: _popupListIcon('h2'),
|
||||||
handler: (editorState) => formatHeading(editorState, StyleKey.h2),
|
handler: (editorState) =>
|
||||||
|
insertHeadingAfterSelection(editorState, StyleKey.h2),
|
||||||
),
|
),
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Heading 3',
|
text: 'Heading 3',
|
||||||
|
keywords: ['h3', 'heading 3'],
|
||||||
icon: _popupListIcon('h3'),
|
icon: _popupListIcon('h3'),
|
||||||
handler: (editorState) => formatHeading(editorState, StyleKey.h3),
|
handler: (editorState) =>
|
||||||
|
insertHeadingAfterSelection(editorState, StyleKey.h3),
|
||||||
),
|
),
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Bullets',
|
text: 'Bulleted List',
|
||||||
|
keywords: ['bulleted list'],
|
||||||
icon: _popupListIcon('bullets'),
|
icon: _popupListIcon('bullets'),
|
||||||
handler: (editorState) => formatBulletedList(editorState),
|
handler: (editorState) => insertBulletedListAfterSelection(editorState),
|
||||||
),
|
),
|
||||||
|
// PopupListItem(
|
||||||
|
// text: 'Numbered list',
|
||||||
|
// keywords: ['numbered list'],
|
||||||
|
// icon: _popupListIcon('number'),
|
||||||
|
// handler: (editorState) => debugPrint('Not implement yet!'),
|
||||||
|
// ),
|
||||||
PopupListItem(
|
PopupListItem(
|
||||||
text: 'Numbered list',
|
text: 'To-do List',
|
||||||
icon: _popupListIcon('number'),
|
keywords: ['checkbox', 'todo'],
|
||||||
handler: (editorState) => debugPrint('Not implement yet!'),
|
|
||||||
),
|
|
||||||
PopupListItem(
|
|
||||||
text: 'Checkboxes',
|
|
||||||
icon: _popupListIcon('checkbox'),
|
icon: _popupListIcon('checkbox'),
|
||||||
handler: (editorState) => formatCheckbox(editorState),
|
handler: (editorState) => insertCheckboxAfterSelection(editorState),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
OverlayEntry? _popupListOverlay;
|
OverlayEntry? _popupListOverlay;
|
||||||
EditorState? _editorState;
|
EditorState? _editorState;
|
||||||
|
bool _selectionChangeBySlash = false;
|
||||||
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@ -69,21 +82,19 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
if (selection == null || context == null || selectable == null) {
|
if (selection == null || context == null || selectable == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
final selectionRects = editorState.service.selectionService.selectionRects;
|
||||||
final rect = selectable.getCursorRectInPosition(selection.start);
|
if (selectionRects.isEmpty) {
|
||||||
if (rect == null) {
|
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final offset = selectable.localToGlobal(rect.topLeft);
|
|
||||||
|
|
||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
..replaceText(textNode, selection.start.offset,
|
..replaceText(textNode, selection.start.offset,
|
||||||
selection.end.offset - selection.start.offset, '/')
|
selection.end.offset - selection.start.offset, event.character ?? '')
|
||||||
..commit();
|
..commit();
|
||||||
|
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
showPopupList(context, editorState, offset);
|
_selectionChangeBySlash = false;
|
||||||
|
showPopupList(context, editorState, selectionRects.first.bottomRight);
|
||||||
});
|
});
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
@ -94,8 +105,8 @@ void showPopupList(
|
|||||||
_popupListOverlay?.remove();
|
_popupListOverlay?.remove();
|
||||||
_popupListOverlay = OverlayEntry(
|
_popupListOverlay = OverlayEntry(
|
||||||
builder: (context) => Positioned(
|
builder: (context) => Positioned(
|
||||||
top: offset.dy + 15.0,
|
top: offset.dy,
|
||||||
left: offset.dx + 5.0,
|
left: offset.dx,
|
||||||
child: PopupListWidget(
|
child: PopupListWidget(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
items: _popupListItems,
|
items: _popupListItems,
|
||||||
@ -117,6 +128,15 @@ void clearPopupList() {
|
|||||||
if (_popupListOverlay == null || _editorState == null) {
|
if (_popupListOverlay == null || _editorState == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final selection =
|
||||||
|
_editorState?.service.selectionService.currentSelection.value;
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_selectionChangeBySlash) {
|
||||||
|
_selectionChangeBySlash = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
_popupListOverlay?.remove();
|
_popupListOverlay?.remove();
|
||||||
_popupListOverlay = null;
|
_popupListOverlay = null;
|
||||||
|
|
||||||
@ -142,21 +162,55 @@ class PopupListWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PopupListWidgetState extends State<PopupListWidget> {
|
class _PopupListWidgetState extends State<PopupListWidget> {
|
||||||
final focusNode = FocusNode(debugLabel: 'popup_list_widget');
|
final _focusNode = FocusNode(debugLabel: 'popup_list_widget');
|
||||||
var selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
List<PopupListItem> _items = [];
|
||||||
|
|
||||||
|
int _maxKeywordLength = 0;
|
||||||
|
|
||||||
|
String __keyword = '';
|
||||||
|
String get _keyword => __keyword;
|
||||||
|
set _keyword(String keyword) {
|
||||||
|
__keyword = keyword;
|
||||||
|
|
||||||
|
final items = widget.items
|
||||||
|
.where((item) =>
|
||||||
|
item.keywords.any((keyword) => keyword.contains(_keyword)))
|
||||||
|
.toList(growable: false);
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
var maxKeywordLength = 0;
|
||||||
|
for (var item in _items) {
|
||||||
|
for (var keyword in item.keywords) {
|
||||||
|
maxKeywordLength = max(maxKeywordLength, keyword.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_maxKeywordLength = maxKeywordLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.length >= _maxKeywordLength + 2) {
|
||||||
|
clearPopupList();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedIndex = 0;
|
||||||
|
_items = items;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_items = widget.items;
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -164,7 +218,7 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Focus(
|
return Focus(
|
||||||
focusNode: focusNode,
|
focusNode: _focusNode,
|
||||||
onKey: _onKey,
|
onKey: _onKey,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -178,9 +232,26 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|||||||
],
|
],
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: _items.isEmpty
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? _buildNoResultsWidget(context)
|
||||||
children: _buildColumns(widget.items, selectedIndex),
|
: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildColumns(_items, _selectedIndex),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNoResultsWidget(BuildContext context) {
|
||||||
|
return const Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Material(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(12.0),
|
||||||
|
child: Text(
|
||||||
|
'No results',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -214,26 +285,43 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||||
|
debugPrint('slash on key $event');
|
||||||
if (event is! RawKeyDownEvent) {
|
if (event is! RawKeyDownEvent) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final arrowKeys = [
|
||||||
|
LogicalKeyboardKey.arrowLeft,
|
||||||
|
LogicalKeyboardKey.arrowRight,
|
||||||
|
LogicalKeyboardKey.arrowUp,
|
||||||
|
LogicalKeyboardKey.arrowDown
|
||||||
|
];
|
||||||
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
if (0 <= selectedIndex && selectedIndex < widget.items.length) {
|
if (0 <= _selectedIndex && _selectedIndex < _items.length) {
|
||||||
_deleteSlash();
|
_deleteLastCharacters(length: _keyword.length + 1);
|
||||||
widget.items[selectedIndex].handler(widget.editorState);
|
_items[_selectedIndex].handler(widget.editorState);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
clearPopupList();
|
clearPopupList();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
clearPopupList();
|
if (_keyword.isEmpty) {
|
||||||
_deleteSlash();
|
clearPopupList();
|
||||||
|
} else {
|
||||||
|
_keyword = _keyword.substring(0, _keyword.length - 1);
|
||||||
|
}
|
||||||
|
_deleteLastCharacters();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else if (event.character != null &&
|
||||||
|
!arrowKeys.contains(event.logicalKey)) {
|
||||||
|
_keyword += event.character!;
|
||||||
|
_insertText(event.character!);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newSelectedIndex = selectedIndex;
|
var newSelectedIndex = _selectedIndex;
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||||
newSelectedIndex -= widget.maxItemInRow;
|
newSelectedIndex -= widget.maxItemInRow;
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||||
@ -243,26 +331,44 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
newSelectedIndex += 1;
|
newSelectedIndex += 1;
|
||||||
}
|
}
|
||||||
if (newSelectedIndex != selectedIndex) {
|
if (newSelectedIndex != _selectedIndex) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex));
|
_selectedIndex = max(0, min(_items.length - 1, newSelectedIndex));
|
||||||
});
|
});
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteSlash() {
|
void _deleteLastCharacters({int length = 1}) {
|
||||||
final selection =
|
final selection =
|
||||||
widget.editorState.service.selectionService.currentSelection.value;
|
widget.editorState.service.selectionService.currentSelection.value;
|
||||||
final nodes =
|
final nodes =
|
||||||
widget.editorState.service.selectionService.currentSelectedNodes;
|
widget.editorState.service.selectionService.currentSelectedNodes;
|
||||||
if (selection != null && nodes.length == 1) {
|
if (selection != null && nodes.length == 1) {
|
||||||
|
_selectionChangeBySlash = true;
|
||||||
TransactionBuilder(widget.editorState)
|
TransactionBuilder(widget.editorState)
|
||||||
..deleteText(
|
..deleteText(
|
||||||
nodes.first as TextNode,
|
nodes.first as TextNode,
|
||||||
selection.start.offset - 1,
|
selection.start.offset - length,
|
||||||
1,
|
length,
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _insertText(String text) {
|
||||||
|
final selection =
|
||||||
|
widget.editorState.service.selectionService.currentSelection.value;
|
||||||
|
final nodes =
|
||||||
|
widget.editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
if (selection != null && nodes.length == 1) {
|
||||||
|
_selectionChangeBySlash = true;
|
||||||
|
TransactionBuilder(widget.editorState)
|
||||||
|
..insertText(
|
||||||
|
nodes.first as TextNode,
|
||||||
|
selection.end.offset,
|
||||||
|
text,
|
||||||
)
|
)
|
||||||
..commit();
|
..commit();
|
||||||
}
|
}
|
||||||
@ -318,12 +424,14 @@ class _PopupListItemWidget extends StatelessWidget {
|
|||||||
class PopupListItem {
|
class PopupListItem {
|
||||||
PopupListItem({
|
PopupListItem({
|
||||||
required this.text,
|
required this.text,
|
||||||
|
required this.keywords,
|
||||||
this.message = '',
|
this.message = '',
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.handler,
|
required this.handler,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
|
final List<String> keywords;
|
||||||
final String message;
|
final String message;
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
final void Function(EditorState editorState) handler;
|
final void Function(EditorState editorState) handler;
|
||||||
|
@ -39,8 +39,8 @@ FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) {
|
|||||||
return _toCheckboxList(editorState, textNode);
|
return _toCheckboxList(editorState, textNode);
|
||||||
} else if (_bulletedListSymbols.any(text.startsWith)) {
|
} else if (_bulletedListSymbols.any(text.startsWith)) {
|
||||||
return _toBulletedList(editorState, textNode);
|
return _toBulletedList(editorState, textNode);
|
||||||
} else if (_countOfSign(text) != 0) {
|
} else if (_countOfSign(text, selection) != 0) {
|
||||||
return _toHeadingStyle(editorState, textNode);
|
return _toHeadingStyle(editorState, textNode, selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@ -99,8 +99,12 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) {
|
KeyEventResult _toHeadingStyle(
|
||||||
final x = _countOfSign(textNode.toRawString());
|
EditorState editorState, TextNode textNode, Selection selection) {
|
||||||
|
final x = _countOfSign(
|
||||||
|
textNode.toRawString(),
|
||||||
|
selection,
|
||||||
|
);
|
||||||
final hX = 'h$x';
|
final hX = 'h$x';
|
||||||
if (textNode.attributes.heading == hX) {
|
if (textNode.attributes.heading == hX) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@ -121,9 +125,9 @@ KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _countOfSign(String text) {
|
int _countOfSign(String text, Selection selection) {
|
||||||
for (var i = 6; i >= 0; i--) {
|
for (var i = 6; i >= 0; i--) {
|
||||||
if (text.startsWith('#' * i)) {
|
if (text.substring(0, selection.end.offset).startsWith('#' * i)) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user