mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1239 from LucasXu0/horizontal_rule
feat: implement horizontal rule
This commit is contained in:
commit
c0879e8445
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/plugin/code_block_node_widget.dart';
|
||||
import 'package:example/plugin/horizontal_rule_node_widget.dart';
|
||||
import 'package:example/plugin/tex_block_node_widget.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -121,15 +122,18 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
customBuilders: {
|
||||
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
||||
'tex': TeXBlockNodeWidgetBuidler(),
|
||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
enterInCodeBlock,
|
||||
ignoreKeysInCodeBlock,
|
||||
underscoreToItalic,
|
||||
insertHorizontalRule,
|
||||
],
|
||||
selectionMenuItems: [
|
||||
codeBlockMenuItem,
|
||||
teXBlockMenuItem,
|
||||
horizontalRuleMenuItem,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -46,7 +46,11 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
|
||||
|
||||
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||
name: () => 'Code Block',
|
||||
icon: const Icon(Icons.abc),
|
||||
icon: const Icon(
|
||||
Icons.abc,
|
||||
color: Colors.black,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['code block'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
|
@ -0,0 +1,167 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEvent insertHorizontalRule = ShortcutEvent(
|
||||
key: 'Horizontal rule',
|
||||
command: 'Minus',
|
||||
handler: _insertHorzaontalRule,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1 || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toRawString() == '--') {
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(textNode, 0, 2)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
Node(
|
||||
type: 'horizontal_rule',
|
||||
children: LinkedList(),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
name: () => 'Horizontal rule',
|
||||
icon: const Icon(
|
||||
Icons.horizontal_rule,
|
||||
color: Colors.black,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['horizontal rule'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toRawString().isEmpty) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
Node(
|
||||
type: 'horizontal_rule',
|
||||
children: LinkedList(),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0)
|
||||
..commit();
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
TextNode(
|
||||
type: 'text',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'subtype': 'horizontal_rule',
|
||||
},
|
||||
delta: Delta()..insert('---'),
|
||||
),
|
||||
)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _HorizontalRuleWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
class _HorizontalRuleWidget extends StatefulWidget {
|
||||
const _HorizontalRuleWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
|
||||
}
|
||||
|
||||
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
|
||||
with SelectableMixin {
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() => Position(path: widget.node.path, offset: 0);
|
||||
|
||||
@override
|
||||
Position end() => Position(path: widget.node.path, offset: 1);
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.borderLine;
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
final size = _renderBox.size;
|
||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) =>
|
||||
[Offset.zero & _renderBox.size];
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
}
|
@ -6,7 +6,11 @@ import 'package:flutter_math_fork/flutter_math.dart';
|
||||
|
||||
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
||||
name: () => 'Tex',
|
||||
icon: const Icon(Icons.text_fields_rounded),
|
||||
icon: const Icon(
|
||||
Icons.text_fields_rounded,
|
||||
color: Colors.black,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['tex, latex, katex'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CursorWidget extends StatefulWidget {
|
||||
@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget {
|
||||
required this.rect,
|
||||
required this.color,
|
||||
this.blinkingInterval = 0.5,
|
||||
this.shouldBlink = true,
|
||||
this.cursorStyle = CursorStyle.verticalLine,
|
||||
}) : super(key: key);
|
||||
|
||||
final double blinkingInterval; // milliseconds
|
||||
final bool shouldBlink;
|
||||
final CursorStyle cursorStyle;
|
||||
final Color color;
|
||||
final Rect rect;
|
||||
final LayerLink layerLink;
|
||||
@ -67,11 +72,28 @@ class CursorWidgetState extends State<CursorWidget> {
|
||||
// Ignore the gestures in cursor
|
||||
// to solve the problem that cursor area cannot be selected.
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
color: showCursor ? widget.color : Colors.transparent,
|
||||
),
|
||||
child: _buildCursor(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCursor(BuildContext context) {
|
||||
var color = widget.color;
|
||||
if (widget.shouldBlink && !showCursor) {
|
||||
color = Colors.transparent;
|
||||
}
|
||||
switch (widget.cursorStyle) {
|
||||
case CursorStyle.verticalLine:
|
||||
return Container(
|
||||
color: color,
|
||||
);
|
||||
case CursorStyle.borderLine:
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart';
|
||||
import 'package:appflowy_editor/src/document/selection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CursorStyle {
|
||||
verticalLine,
|
||||
borderLine,
|
||||
}
|
||||
|
||||
/// [SelectableMixin] is used for the editor to calculate the position
|
||||
/// and size of the selection.
|
||||
///
|
||||
@ -53,4 +58,8 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
|
||||
Selection? getWorldBoundaryInOffset(Offset start) {
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get shouldCursorBlink => true;
|
||||
|
||||
CursorStyle get cursorStyle => CursorStyle.verticalLine;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_l
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
|
||||
// Handle delete text.
|
||||
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
||||
@ -84,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
}
|
||||
} else {
|
||||
if (textNodes.isEmpty) {
|
||||
if (nonTextNodes.isNotEmpty) {
|
||||
transactionBuilder.afterSelection =
|
||||
Selection.collapsed(selection.start);
|
||||
}
|
||||
transactionBuilder.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
final startPosition = selection.start;
|
||||
|
@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
rect: cursorRect,
|
||||
color: widget.cursorColor,
|
||||
layerLink: node.layerLink,
|
||||
shouldBlink: selectable.shouldCursorBlink,
|
||||
cursorStyle: selectable.cursorStyle,
|
||||
),
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user