mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
docs: documentation for selection_service
This commit is contained in:
parent
046faf3880
commit
ae0012ba37
@ -1,15 +1,16 @@
|
|||||||
library flowy_editor;
|
library flowy_editor;
|
||||||
|
|
||||||
export 'src/document/state_tree.dart';
|
|
||||||
export 'src/document/node.dart';
|
export 'src/document/node.dart';
|
||||||
export 'src/document/path.dart';
|
export 'src/document/path.dart';
|
||||||
|
export 'src/document/position.dart';
|
||||||
|
export 'src/document/selection.dart';
|
||||||
|
export 'src/document/state_tree.dart';
|
||||||
export 'src/document/text_delta.dart';
|
export 'src/document/text_delta.dart';
|
||||||
export 'src/render/selection/selectable.dart';
|
export 'src/editor_state.dart';
|
||||||
|
export 'src/operation/operation.dart';
|
||||||
export 'src/operation/transaction.dart';
|
export 'src/operation/transaction.dart';
|
||||||
export 'src/operation/transaction_builder.dart';
|
export 'src/operation/transaction_builder.dart';
|
||||||
export 'src/operation/operation.dart';
|
export 'src/render/selection/selectable.dart';
|
||||||
export 'src/editor_state.dart';
|
|
||||||
export 'src/service/editor_service.dart';
|
export 'src/service/editor_service.dart';
|
||||||
export 'src/document/selection.dart';
|
|
||||||
export 'src/document/position.dart';
|
|
||||||
export 'src/service/render_plugin_service.dart';
|
export 'src/service/render_plugin_service.dart';
|
||||||
|
export 'src/service/service.dart';
|
||||||
|
@ -2,15 +2,26 @@ import 'package:flowy_editor/src/document/path.dart';
|
|||||||
import 'package:flowy_editor/src/document/position.dart';
|
import 'package:flowy_editor/src/document/position.dart';
|
||||||
import 'package:flowy_editor/src/extensions/path_extensions.dart';
|
import 'package:flowy_editor/src/extensions/path_extensions.dart';
|
||||||
|
|
||||||
|
/// Selection represents the selected area or the cursor area in the editor.
|
||||||
|
///
|
||||||
|
/// [Selection] is directional.
|
||||||
|
///
|
||||||
|
/// 1. forward,the end position is before the start position.
|
||||||
|
/// 2. backward, the end position is after the start position.
|
||||||
|
/// 3. collapsed, the end position is equal to the start position.
|
||||||
class Selection {
|
class Selection {
|
||||||
final Position start;
|
/// Create a selection with [start], [end].
|
||||||
final Position end;
|
|
||||||
|
|
||||||
Selection({
|
Selection({
|
||||||
required this.start,
|
required this.start,
|
||||||
required this.end,
|
required this.end,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Create a selection with [Path], [startOffset] and [endOffset].
|
||||||
|
///
|
||||||
|
/// The [endOffset] is optional.
|
||||||
|
///
|
||||||
|
/// This constructor will return a collapsed [Selection] if [endOffset] is null.
|
||||||
|
///
|
||||||
Selection.single({
|
Selection.single({
|
||||||
required Path path,
|
required Path path,
|
||||||
required int startOffset,
|
required int startOffset,
|
||||||
@ -18,10 +29,21 @@ class Selection {
|
|||||||
}) : start = Position(path: path, offset: startOffset),
|
}) : start = Position(path: path, offset: startOffset),
|
||||||
end = Position(path: path, offset: endOffset ?? startOffset);
|
end = Position(path: path, offset: endOffset ?? startOffset);
|
||||||
|
|
||||||
|
/// Create a collapsed selection with [position].
|
||||||
Selection.collapsed(Position position)
|
Selection.collapsed(Position position)
|
||||||
: start = position,
|
: start = position,
|
||||||
end = position;
|
end = position;
|
||||||
|
|
||||||
|
final Position start;
|
||||||
|
final Position end;
|
||||||
|
|
||||||
|
bool get isCollapsed => start == end;
|
||||||
|
bool get isSingle => pathEquals(start.path, end.path);
|
||||||
|
bool get isForward =>
|
||||||
|
start.path >= end.path && !pathEquals(start.path, end.path);
|
||||||
|
bool get isBackward =>
|
||||||
|
start.path <= end.path && !pathEquals(start.path, end.path);
|
||||||
|
|
||||||
Selection collapse({bool atStart = false}) {
|
Selection collapse({bool atStart = false}) {
|
||||||
if (atStart) {
|
if (atStart) {
|
||||||
return Selection(start: start, end: start);
|
return Selection(start: start, end: start);
|
||||||
@ -30,13 +52,6 @@ class Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isCollapsed => start == end;
|
|
||||||
bool get isSingle => pathEquals(start.path, end.path);
|
|
||||||
bool get isUpward =>
|
|
||||||
start.path >= end.path && !pathEquals(start.path, end.path);
|
|
||||||
bool get isDownward =>
|
|
||||||
start.path <= end.path && !pathEquals(start.path, end.path);
|
|
||||||
|
|
||||||
Selection copyWith({Position? start, Position? end}) {
|
Selection copyWith({Position? start, Position? end}) {
|
||||||
return Selection(
|
return Selection(
|
||||||
start: start ?? this.start,
|
start: start ?? this.start,
|
||||||
@ -46,13 +61,10 @@ class Selection {
|
|||||||
|
|
||||||
Selection copy() => Selection(start: start, end: end);
|
Selection copy() => Selection(start: start, end: end);
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '[Selection] start = $start, end = $end';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
"start": start.toJson(),
|
'start': start.toJson(),
|
||||||
"end": end.toJson(),
|
'end': end.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,4 +81,7 @@ class Selection {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(start, end);
|
int get hashCode => Object.hash(start, end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '[Selection] start = $start, end = $end';
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/src/editor_state.dart';
|
import 'package:flowy_editor/src/editor_state.dart';
|
||||||
@ -9,15 +10,6 @@ import 'package:flowy_editor/src/render/rich_text/number_list_text.dart';
|
|||||||
import 'package:flowy_editor/src/render/rich_text/quoted_text.dart';
|
import 'package:flowy_editor/src/render/rich_text/quoted_text.dart';
|
||||||
import 'package:flowy_editor/src/render/rich_text/rich_text.dart';
|
import 'package:flowy_editor/src/render/rich_text/rich_text.dart';
|
||||||
import 'package:flowy_editor/src/service/input_service.dart';
|
import 'package:flowy_editor/src/service/input_service.dart';
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
|
|
||||||
import 'package:flowy_editor/src/service/keyboard_service.dart';
|
import 'package:flowy_editor/src/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/src/service/render_plugin_service.dart';
|
import 'package:flowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flowy_editor/src/service/scroll_service.dart';
|
import 'package:flowy_editor/src/service/scroll_service.dart';
|
||||||
@ -34,18 +26,6 @@ NodeWidgetBuilders defaultBuilders = {
|
|||||||
'text/quote': QuotedTextNodeWidgetBuilder(),
|
'text/quote': QuotedTextNodeWidgetBuilder(),
|
||||||
};
|
};
|
||||||
|
|
||||||
List<FlowyKeyEventHandler> defaultKeyEventHandler = [
|
|
||||||
deleteTextHandler,
|
|
||||||
slashShortcutHandler,
|
|
||||||
flowyDeleteNodesHandler,
|
|
||||||
arrowKeysHandler,
|
|
||||||
copyPasteKeysHandler,
|
|
||||||
redoUndoKeysHandler,
|
|
||||||
enterWithoutShiftInTextNodesHandler,
|
|
||||||
updateTextStyleByCommandXHandler,
|
|
||||||
whiteSpaceHandler,
|
|
||||||
];
|
|
||||||
|
|
||||||
class FlowyEditor extends StatefulWidget {
|
class FlowyEditor extends StatefulWidget {
|
||||||
const FlowyEditor({
|
const FlowyEditor({
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -98,7 +78,7 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
child: FlowyKeyboard(
|
child: FlowyKeyboard(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
handlers: [
|
handlers: [
|
||||||
...defaultKeyEventHandler,
|
...defaultKeyEventHandlers,
|
||||||
...widget.keyEventHandlers,
|
...widget.keyEventHandlers,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
|
||||||
|
import 'package:flowy_editor/src/service/keyboard_service.dart';
|
||||||
|
|
||||||
|
List<FlowyKeyEventHandler> defaultKeyEventHandlers = [
|
||||||
|
deleteTextHandler,
|
||||||
|
slashShortcutHandler,
|
||||||
|
flowyDeleteNodesHandler,
|
||||||
|
arrowKeysHandler,
|
||||||
|
copyPasteKeysHandler,
|
||||||
|
redoUndoKeysHandler,
|
||||||
|
enterWithoutShiftInTextNodesHandler,
|
||||||
|
updateTextStyleByCommandXHandler,
|
||||||
|
whiteSpaceHandler,
|
||||||
|
];
|
@ -15,67 +15,60 @@ import 'package:flowy_editor/src/render/selection/cursor_widget.dart';
|
|||||||
import 'package:flowy_editor/src/render/selection/selectable.dart';
|
import 'package:flowy_editor/src/render/selection/selectable.dart';
|
||||||
import 'package:flowy_editor/src/render/selection/selection_widget.dart';
|
import 'package:flowy_editor/src/render/selection/selection_widget.dart';
|
||||||
|
|
||||||
/// Process selection and cursor
|
/// [FlowySelectionService] is responsible for processing
|
||||||
|
/// the [Selection] changes and updates.
|
||||||
|
///
|
||||||
|
/// Usually, this service can be obtained by the following code.
|
||||||
|
/// ```dart
|
||||||
|
/// final selectionService = editorState.service.selectionService;
|
||||||
|
///
|
||||||
|
/// /** get current selection value*/
|
||||||
|
/// final selection = selectionService.currentSelection.value;
|
||||||
|
///
|
||||||
|
/// /** get current selected nodes*/
|
||||||
|
/// final nodes = selectionService.currentSelectedNodes;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||||
/// Returns the current [Selection]
|
/// The current [Selection] in editor.
|
||||||
|
///
|
||||||
|
/// The value is null if there is no nodes are selected.
|
||||||
ValueNotifier<Selection?> get currentSelection;
|
ValueNotifier<Selection?> get currentSelection;
|
||||||
|
|
||||||
/// Returns the current selected [Node]s.
|
/// The current selected [Node]s in editor.
|
||||||
///
|
///
|
||||||
/// The order of the return is determined according to the selected order.
|
/// The order of the result is determined according to the [currentSelection].
|
||||||
|
/// The result are ordered from back to front if the selection is forward.
|
||||||
|
/// The result are ordered from front to back if the selection is backward.
|
||||||
|
///
|
||||||
|
/// For example, Here is an array of selected nodes, [n1, n2, n3].
|
||||||
|
/// The result will be [n3, n2, n1] if the selection is forward,
|
||||||
|
/// and [n1, n2, n3] if the selection is backward.
|
||||||
|
///
|
||||||
|
/// Returns empty result if there is no nodes are selected.
|
||||||
List<Node> get currentSelectedNodes;
|
List<Node> get currentSelectedNodes;
|
||||||
|
|
||||||
/// Update the selection or cursor.
|
/// Updates the selection.
|
||||||
///
|
///
|
||||||
/// If selection is collapsed, this method will
|
/// The editor will update selection area and popup list area
|
||||||
/// update the position of the cursor.
|
/// if the [selection] is not collapsed,
|
||||||
/// Otherwise, will update the selection.
|
/// otherwise, will update the cursor area.
|
||||||
void updateSelection(Selection selection);
|
void updateSelection(Selection selection);
|
||||||
|
|
||||||
/// Clear the selection or cursor.
|
/// Clears the selection area, cursor area and the popup list area.
|
||||||
void clearSelection();
|
void clearSelection();
|
||||||
|
|
||||||
/// ------------------ Selection ------------------------
|
/// Returns the [Node]s in [Selection].
|
||||||
|
|
||||||
List<Rect> rects();
|
|
||||||
|
|
||||||
Position? hitTest(Offset? offset);
|
|
||||||
|
|
||||||
///
|
|
||||||
List<Node> getNodesInSelection(Selection selection);
|
List<Node> getNodesInSelection(Selection selection);
|
||||||
|
|
||||||
/// ------------------ Selection ------------------------
|
/// Returns the [Node] containing to the offset.
|
||||||
|
|
||||||
/// ------------------ Offset ------------------------
|
|
||||||
|
|
||||||
/// Return the [Node] or [Null] in single selection.
|
|
||||||
///
|
///
|
||||||
/// [offset] is under the global coordinate system.
|
/// [offset] must be under the global coordinate system.
|
||||||
Node? getNodeInOffset(Offset offset);
|
Node? getNodeInOffset(Offset offset);
|
||||||
|
|
||||||
/// Returns selected [Node]s. Empty list would be returned
|
// TODO: need to be documented.
|
||||||
/// if no nodes are in range.
|
List<Rect> rects();
|
||||||
///
|
Position? hitTest(Offset? offset);
|
||||||
///
|
|
||||||
/// [start] and [end] are under the global coordinate system.
|
|
||||||
///
|
|
||||||
List<Node> getNodeInRange(Offset start, Offset end);
|
|
||||||
|
|
||||||
/// Return [bool] to identify the [Node] is in Range or not.
|
|
||||||
///
|
|
||||||
/// [start] and [end] are under the global coordinate system.
|
|
||||||
bool isNodeInRange(
|
|
||||||
Node node,
|
|
||||||
Offset start,
|
|
||||||
Offset end,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Return [bool] to identify the [Node] contains [Offset] or not.
|
|
||||||
///
|
|
||||||
/// [offset] is under the global coordinate system.
|
|
||||||
bool isNodeInOffset(Node node, Offset offset);
|
|
||||||
|
|
||||||
/// ------------------ Offset ------------------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlowySelection extends StatefulWidget {
|
class FlowySelection extends StatefulWidget {
|
||||||
@ -207,36 +200,6 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
return _lowerBoundInDocument(offset);
|
return _lowerBoundInDocument(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<Node> getNodeInRange(Offset start, Offset end) {
|
|
||||||
final startNode = _lowerBoundInDocument(start);
|
|
||||||
final endNode = _upperBoundInDocument(end);
|
|
||||||
return NodeIterator(editorState.document, startNode, endNode).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isNodeInOffset(Node node, Offset offset) {
|
|
||||||
final renderBox = node.renderBox;
|
|
||||||
if (renderBox != null) {
|
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
final boxRect = boxOffset & renderBox.size;
|
|
||||||
return boxRect.contains(offset);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isNodeInRange(Node node, Offset start, Offset end) {
|
|
||||||
final renderBox = node.renderBox;
|
|
||||||
if (renderBox != null) {
|
|
||||||
final rect = Rect.fromPoints(start, end);
|
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
final boxRect = boxOffset & renderBox.size;
|
|
||||||
return rect.overlaps(boxRect);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDoubleTapDown(TapDownDetails details) {
|
void _onDoubleTapDown(TapDownDetails details) {
|
||||||
final offset = details.globalPosition;
|
final offset = details.globalPosition;
|
||||||
final node = getNodeInOffset(offset);
|
final node = getNodeInOffset(offset);
|
||||||
@ -395,13 +358,13 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
// text: ghijkl
|
// text: ghijkl
|
||||||
// text: mn>opqr
|
// text: mn>opqr
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
if (selection.isDownward) {
|
if (selection.isBackward) {
|
||||||
newSelection = selection.copyWith(end: selectable.end());
|
newSelection = selection.copyWith(end: selectable.end());
|
||||||
} else {
|
} else {
|
||||||
newSelection = selection.copyWith(start: selectable.start());
|
newSelection = selection.copyWith(start: selectable.start());
|
||||||
}
|
}
|
||||||
} else if (index == nodes.length - 1) {
|
} else if (index == nodes.length - 1) {
|
||||||
if (selection.isDownward) {
|
if (selection.isBackward) {
|
||||||
newSelection = selection.copyWith(start: selectable.start());
|
newSelection = selection.copyWith(start: selectable.start());
|
||||||
} else {
|
} else {
|
||||||
newSelection = selection.copyWith(end: selectable.end());
|
newSelection = selection.copyWith(end: selectable.end());
|
||||||
@ -498,7 +461,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
/// TODO: It is necessary to calculate the relative speed
|
/// TODO: It is necessary to calculate the relative speed
|
||||||
/// according to the gap and move forward more gently.
|
/// according to the gap and move forward more gently.
|
||||||
final distance = 10.0;
|
const distance = 10.0;
|
||||||
if (offset.dy <= topLimit && !isDownward) {
|
if (offset.dy <= topLimit && !isDownward) {
|
||||||
// up
|
// up
|
||||||
editorState.service.scrollService?.scrollTo(dy - distance);
|
editorState.service.scrollService?.scrollTo(dy - distance);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flowy_editor/src/service/keyboard_service.dart';
|
import 'package:flowy_editor/src/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/src/service/render_plugin_service.dart';
|
import 'package:flowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flowy_editor/src/service/scroll_service.dart';
|
import 'package:flowy_editor/src/service/scroll_service.dart';
|
||||||
import 'package:flowy_editor/src/service/selection_service.dart';
|
import 'package:flowy_editor/src/service/selection_service.dart';
|
||||||
import 'package:flowy_editor/src/service/toolbar_service.dart';
|
import 'package:flowy_editor/src/service/toolbar_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowyService {
|
class FlowyService {
|
||||||
// selection service
|
// selection service
|
||||||
|
Loading…
Reference in New Issue
Block a user