diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart index 1f321e937a..bafe106f27 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart @@ -43,7 +43,7 @@ class NodeIterator implements Iterator { if (nextOfParent == null) { _currentNode = null; } else { - _currentNode = _findLeadingChild(node); + _currentNode = _findLeadingChild(nextOfParent); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 065ae8b595..5c02955d7b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -110,11 +110,17 @@ class _CheckboxNodeWidgetState extends State .map( (child) => widget.editorState.service.renderPluginService .buildPluginWidget( - NodeWidgetContext( - context: context, - node: child, - editorState: widget.editorState, - ), + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: widget.editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: widget.editorState, + ), ), ) .toList(), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 20ce401f2e..22dd892746 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -35,7 +35,7 @@ class FlowyRichText extends StatefulWidget { this.cursorHeight, this.cursorWidth = 2.0, this.textSpanDecorator, - this.placeholderText = ' ', + this.placeholderText = 'Type \'/\' for commands', this.placeholderTextSpanDecorator, required this.textNode, required this.editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index a183930dc8..824fc07230 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; @@ -83,22 +81,29 @@ class _FlowyInputState extends State void apply(List deltas) { // TODO: implement the detail for (final delta in deltas) { - if (delta is TextEditingDeltaInsertion) { - if (_composingTextRange != null) { - _composingTextRange = TextRange( - start: _composingTextRange!.start, - end: delta.composing.end, - ); - } else { - _composingTextRange = delta.composing; - } + _updateComposing(delta); + if (delta is TextEditingDeltaInsertion) { _applyInsert(delta); } else if (delta is TextEditingDeltaDeletion) { + _applyDelete(delta); } else if (delta is TextEditingDeltaReplacement) { _applyReplacement(delta); - } else if (delta is TextEditingDeltaNonTextUpdate) { - _composingTextRange = null; + } else if (delta is TextEditingDeltaNonTextUpdate) {} + } + } + + void _updateComposing(TextEditingDelta delta) { + if (delta is! TextEditingDeltaNonTextUpdate) { + if (_composingTextRange != null && + delta.composing.end != -1 && + _composingTextRange!.start != -1) { + _composingTextRange = TextRange( + start: _composingTextRange!.start, + end: delta.composing.end, + ); + } else { + _composingTextRange = delta.composing; } } } @@ -123,6 +128,23 @@ class _FlowyInputState extends State } } + void _applyDelete(TextEditingDeltaDeletion delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection.value; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = selectionService.currentSelectedNodes.first as TextNode; + final length = delta.deletedRange.end - delta.deletedRange.start; + TransactionBuilder(_editorState) + ..deleteText(textNode, delta.deletedRange.start, length) + ..commit(); + } else { + // TODO: implement + } + } + void _applyReplacement(TextEditingDeltaReplacement delta) { final selectionService = _editorState.service.selectionService; final currentSelection = selectionService.currentSelection.value; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart index 0aae0b38c8..4d77ef1ebc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart @@ -109,13 +109,19 @@ void showPopupList( .removeListener(clearPopupList); editorState.service.selectionService.currentSelection .addListener(clearPopupList); + + editorState.service.scrollService?.disable(); } void clearPopupList() { + if (_popupListOverlay == null || _editorState == null) { + return; + } _popupListOverlay?.remove(); _popupListOverlay = null; _editorState?.service.keyboardService?.enable(); + _editorState?.service.scrollService?.enable(); _editorState = null; } @@ -214,14 +220,17 @@ class _PopupListWidgetState extends State { if (event.logicalKey == LogicalKeyboardKey.enter) { if (0 <= selectedIndex && selectedIndex < widget.items.length) { + _deleteSlash(); widget.items[selectedIndex].handler(widget.editorState); return KeyEventResult.handled; } - } - - if (event.logicalKey == LogicalKeyboardKey.escape) { + } else if (event.logicalKey == LogicalKeyboardKey.escape) { clearPopupList(); return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + clearPopupList(); + _deleteSlash(); + return KeyEventResult.handled; } var newSelectedIndex = selectedIndex; @@ -242,6 +251,22 @@ class _PopupListWidgetState extends State { } return KeyEventResult.ignored; } + + void _deleteSlash() { + final selection = + widget.editorState.service.selectionService.currentSelection.value; + final nodes = + widget.editorState.service.selectionService.currentSelectedNodes; + if (selection != null && nodes.length == 1) { + TransactionBuilder(widget.editorState) + ..deleteText( + nodes.first as TextNode, + selection.start.offset - 1, + 1, + ) + ..commit(); + } + } } class _PopupListItemWidget extends StatelessWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart index c3a0a6fedc..af48a78c49 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart @@ -6,7 +6,8 @@ mixin FlowyScrollService on State { void scrollTo(double dy); - RenderObject? scrollRenderObject(); + void enable(); + void disable(); } class FlowyScroll extends StatefulWidget { @@ -25,6 +26,8 @@ class _FlowyScrollState extends State with FlowyScrollService { final _scrollController = ScrollController(); final _scrollViewKey = GlobalKey(); + bool _scrollEnabled = true; + @override double get dy => _scrollController.position.pixels; @@ -51,15 +54,22 @@ class _FlowyScrollState extends State with FlowyScrollService { ); } + @override + void disable() { + _scrollEnabled = false; + debugPrint('[scroll] $_scrollEnabled'); + } + + @override + void enable() { + _scrollEnabled = true; + debugPrint('[scroll] $_scrollEnabled'); + } + void _onPointerSignal(PointerSignalEvent event) { - if (event is PointerScrollEvent) { + if (event is PointerScrollEvent && _scrollEnabled) { final dy = (_scrollController.position.pixels + event.scrollDelta.dy); scrollTo(dy); } } - - @override - RenderObject? scrollRenderObject() { - return _scrollViewKey.currentContext?.findRenderObject(); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 6655b5ad04..82088561cb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -287,6 +287,7 @@ class _FlowySelectionState extends State editorState.updateCursorSelection(selection); editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); } @override @@ -333,13 +334,9 @@ class _FlowySelectionState extends State panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy); } - final sortedNodes = - editorState.document.root.children.toList(growable: false); - final first = _lowerBound( - sortedNodes, panStartOffsetWithScrollDyGap, 0, sortedNodes.length) - .selectable; - final last = _upperBound(sortedNodes, panEndOffset!, 0, sortedNodes.length) - .selectable; + final first = + _lowerBoundInDocument(panStartOffsetWithScrollDyGap).selectable; + final last = _upperBoundInDocument(panEndOffset!).selectable; // compute the selection in range. if (first != null && last != null) { @@ -538,19 +535,20 @@ class _FlowySelectionState extends State Node _lowerBoundInDocument(Offset offset) { final sortedNodes = editorState.document.root.children.toList(growable: false); - return _lowerBound(sortedNodes, offset, 0, sortedNodes.length); + return _lowerBound(sortedNodes, offset, 0, sortedNodes.length - 1); } Node _upperBoundInDocument(Offset offset) { final sortedNodes = editorState.document.root.children.toList(growable: false); - return _upperBound(sortedNodes, offset, 0, sortedNodes.length); + return _upperBound(sortedNodes, offset, 0, sortedNodes.length - 1); } /// TODO: Supports multi-level nesting, /// currently only single-level nesting is supported // find the first node's rect.bottom <= offset.dy Node _lowerBound(List sortedNodes, Offset offset, int start, int end) { + assert(start >= 0 && end < sortedNodes.length); var min = start; var max = end; while (min <= max) { @@ -561,7 +559,12 @@ class _FlowySelectionState extends State max = mid - 1; } } - return sortedNodes[min]; + final node = sortedNodes[min]; + if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { + final children = node.children.toList(growable: false); + return _lowerBound(children, offset, 0, children.length - 1); + } + return node; } /// TODO: Supports multi-level nesting, @@ -573,6 +576,7 @@ class _FlowySelectionState extends State int start, int end, ) { + assert(start >= 0 && end < sortedNodes.length); var min = start; var max = end; while (min <= max) { @@ -583,7 +587,12 @@ class _FlowySelectionState extends State max = mid - 1; } } - return sortedNodes[max]; + final node = sortedNodes[max]; + if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { + final children = node.children.toList(growable: false); + return _lowerBound(children, offset, 0, children.length - 1); + } + return node; } }