mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: could not input space in editor
This commit is contained in:
parent
3cd9ea5366
commit
c5af7db2cd
@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "appflowy-editor"
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "build/web",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
],
|
||||
"headers": [ {
|
||||
"source": "**/*.@(png|jpg|jpeg|gif)",
|
||||
"headers": [ {
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/plugin/underscore_to_italic.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:example/plugin/underscore_to_italic.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
@ -112,6 +115,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: AppFlowyEditor(
|
||||
editorState: _editorState!,
|
||||
editorStyle: _editorStyle,
|
||||
editable: true,
|
||||
shortcutEvents: [
|
||||
underscoreToItalic,
|
||||
],
|
||||
@ -148,7 +152,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
),
|
||||
ActionButton(
|
||||
icon: const Icon(Icons.import_export),
|
||||
onPressed: () => _importDocument(),
|
||||
onPressed: () async => await _importDocument(),
|
||||
),
|
||||
ActionButton(
|
||||
icon: const Icon(Icons.color_lens),
|
||||
@ -167,28 +171,53 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
void _exportDocument(EditorState editorState) async {
|
||||
final document = editorState.document.toJson();
|
||||
final json = jsonEncode(document);
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
await file.writeAsString(json);
|
||||
if (kIsWeb) {
|
||||
final blob = html.Blob([json], 'text/plain', 'native');
|
||||
html.AnchorElement(
|
||||
href: html.Url.createObjectUrlFromBlob(blob).toString(),
|
||||
)
|
||||
..setAttribute('download', 'editor.json')
|
||||
..click();
|
||||
} else {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
await file.writeAsString(json);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('The document is saved to the ${file.path}'),
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('The document is saved to the ${file.path}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _importDocument() async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = file.readAsString();
|
||||
});
|
||||
Future<void> _importDocument() async {
|
||||
if (kIsWeb) {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: false,
|
||||
allowedExtensions: ['json'],
|
||||
type: FileType.custom,
|
||||
);
|
||||
final bytes = result?.files.first.bytes;
|
||||
if (bytes != null) {
|
||||
final jsonString = const Utf8Decoder().convert(bytes);
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = Future.value(jsonString);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = '${directory.path}/editor.json';
|
||||
final file = File(path);
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = file.readAsString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _switchToPage(int pageIndex) {
|
||||
|
@ -1,165 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 1. define your custom type in example.json
|
||||
/// For example I need to define an image plugin, then I define type equals
|
||||
/// "image", and add "image_src" into "attributes".
|
||||
/// {
|
||||
/// "type": "image",
|
||||
/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" }
|
||||
/// }
|
||||
/// 2. create a class extends [NodeWidgetBuilder]
|
||||
/// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
|
||||
/// and return a widget to render. The returned widget should be
|
||||
/// a StatefulWidget and mixin with [SelectableMixin].
|
||||
///
|
||||
/// 4. override the getter `nodeValidator`
|
||||
/// to verify the data structure in [Node].
|
||||
/// 5. register the plugin with `type` to `AppFlowyEditor` in `main.dart`.
|
||||
/// 6. Congratulations!
|
||||
|
||||
class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return ImageNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => ((node) {
|
||||
return node.type == 'image';
|
||||
});
|
||||
}
|
||||
|
||||
const double placeholderHeight = 132;
|
||||
|
||||
class ImageNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const ImageNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
|
||||
}
|
||||
|
||||
class _ImageNodeWidgetState extends State<ImageNodeWidget>
|
||||
with SelectableMixin {
|
||||
bool isHovered = false;
|
||||
Node get node => widget.node;
|
||||
EditorState get editorState => widget.editorState;
|
||||
String get src => widget.node.attributes['image_src'] as String;
|
||||
|
||||
@override
|
||||
Position end() {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
return Selection.collapsed(Position(path: node.path, offset: 0));
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _build(context);
|
||||
}
|
||||
|
||||
Widget _loadingBuilder(
|
||||
BuildContext context, Widget widget, ImageChunkEvent? evt) {
|
||||
if (evt == null) {
|
||||
return widget;
|
||||
}
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Loading..."),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorBuilder(
|
||||
BuildContext context, Object obj, StackTrace? stackTrace) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Error..."),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _frameBuilder(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
int? frame,
|
||||
bool wasSynchronouslyLoaded,
|
||||
) {
|
||||
if (frame == null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Loading..."),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: (event) {
|
||||
setState(() {
|
||||
isHovered = true;
|
||||
});
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
isHovered = false;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isHovered ? Colors.blue : Colors.grey,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20))),
|
||||
child: Image.network(
|
||||
src,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
frameBuilder: _frameBuilder,
|
||||
loadingBuilder: _loadingBuilder,
|
||||
errorBuilder: _errorBuilder,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pod_player/pod_player.dart';
|
||||
|
||||
class YouTubeLinkNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return LinkNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => ((node) {
|
||||
return node.type == 'youtube_link';
|
||||
});
|
||||
}
|
||||
|
||||
class LinkNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const LinkNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LinkNodeWidget> createState() => _YouTubeLinkNodeWidgetState();
|
||||
}
|
||||
|
||||
class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
|
||||
with SelectableMixin {
|
||||
Node get node => widget.node;
|
||||
EditorState get editorState => widget.editorState;
|
||||
String get src => widget.node.attributes['youtube_link'] as String;
|
||||
|
||||
@override
|
||||
Position end() {
|
||||
// TODO: implement end
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() {
|
||||
// TODO: implement start
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
// TODO: implement getRectsInSelection
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
// TODO: implement getSelectionInRange
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
// TODO: implement getPositionInOffset
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _build(context);
|
||||
}
|
||||
|
||||
late final PodPlayerController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = PodPlayerController(
|
||||
playVideoFrom: PlayVideoFrom.network(
|
||||
src,
|
||||
),
|
||||
)..initialise();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
PodVideoPlayer(controller: controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -8,11 +8,9 @@ import Foundation
|
||||
import path_provider_macos
|
||||
import rich_clipboard_macos
|
||||
import url_launcher_macos
|
||||
import wakelock_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||
}
|
||||
|
@ -6,15 +6,12 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
@ -25,15 +22,12 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
||||
|
@ -37,12 +37,12 @@ dependencies:
|
||||
path: ../
|
||||
provider: ^6.0.3
|
||||
url_launcher: ^6.1.5
|
||||
video_player: ^2.4.5
|
||||
pod_player: 0.0.8
|
||||
path_provider: ^2.0.11
|
||||
google_fonts: ^3.0.1
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
file_picker: ^5.0.1
|
||||
universal_html: ^2.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -193,16 +193,24 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
return parent!._path([index, ...previous]);
|
||||
}
|
||||
|
||||
Node deepClone() {
|
||||
final newNode = Node(
|
||||
type: type, children: LinkedList<Node>(), attributes: {...attributes});
|
||||
|
||||
for (final node in children) {
|
||||
final newNode = node.deepClone();
|
||||
newNode.parent = this;
|
||||
newNode.children.add(newNode);
|
||||
Node copyWith({
|
||||
String? type,
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
final node = Node(
|
||||
type: type ?? this.type,
|
||||
attributes: attributes ?? {..._attributes},
|
||||
children: children ?? LinkedList(),
|
||||
);
|
||||
if (children == null && this.children.isNotEmpty) {
|
||||
for (final child in this.children) {
|
||||
node.children.add(
|
||||
child.copyWith()..parent = node,
|
||||
);
|
||||
}
|
||||
}
|
||||
return newNode;
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,7 +223,10 @@ class TextNode extends Node {
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
}) : _delta = delta,
|
||||
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
||||
super(
|
||||
children: children ?? LinkedList(),
|
||||
attributes: attributes ?? {},
|
||||
);
|
||||
|
||||
TextNode.empty({Attributes? attributes})
|
||||
: _delta = Delta([TextInsert('')]),
|
||||
@ -241,33 +252,27 @@ class TextNode extends Node {
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
TextNode copyWith({
|
||||
String? type,
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
Delta? delta,
|
||||
}) =>
|
||||
TextNode(
|
||||
type: type ?? this.type,
|
||||
children: children ?? this.children,
|
||||
attributes: attributes ?? _attributes,
|
||||
delta: delta ?? this.delta,
|
||||
);
|
||||
|
||||
@override
|
||||
TextNode deepClone() {
|
||||
final newNode = TextNode(
|
||||
type: type,
|
||||
children: LinkedList<Node>(),
|
||||
delta: delta.slice(0),
|
||||
attributes: {...attributes});
|
||||
|
||||
for (final node in children) {
|
||||
final newNode = node.deepClone();
|
||||
newNode.parent = this;
|
||||
newNode.children.add(newNode);
|
||||
}) {
|
||||
final textNode = TextNode(
|
||||
type: type ?? this.type,
|
||||
children: children,
|
||||
attributes: attributes ?? _attributes,
|
||||
delta: delta ?? this.delta,
|
||||
);
|
||||
if (children == null && this.children.isNotEmpty) {
|
||||
for (final child in this.children) {
|
||||
textNode.children.add(
|
||||
child.copyWith()..parent = textNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
return newNode;
|
||||
return textNode;
|
||||
}
|
||||
|
||||
String toRawString() => _delta.toRawString();
|
||||
|
@ -40,11 +40,9 @@ class Selection {
|
||||
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)) ||
|
||||
(isSingle && start.offset > end.offset);
|
||||
(start.path > end.path) || (isSingle && start.offset > end.offset);
|
||||
bool get isBackward =>
|
||||
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
|
||||
(isSingle && start.offset < end.offset);
|
||||
(start.path < end.path) || (isSingle && start.offset < end.offset);
|
||||
|
||||
Selection get normalize {
|
||||
if (isForward) {
|
||||
|
@ -4,22 +4,52 @@ import 'dart:math';
|
||||
|
||||
extension PathExtensions on Path {
|
||||
bool operator >=(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return this > other;
|
||||
}
|
||||
|
||||
bool operator >(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return false;
|
||||
}
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] < other[i]) {
|
||||
return false;
|
||||
} else if (this[i] > other[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.length < other.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator <=(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return this < other;
|
||||
}
|
||||
|
||||
bool operator <(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return false;
|
||||
}
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] > other[i]) {
|
||||
return false;
|
||||
} else if (this[i] < other[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.length > other.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ class TransactionBuilder {
|
||||
/// Inserts a sequence of nodes at the position of path.
|
||||
insertNodes(Path path, List<Node> nodes) {
|
||||
beforeSelection = state.cursorSelection;
|
||||
add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList()));
|
||||
add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
/// Updates the attributes of nodes.
|
||||
@ -75,7 +75,7 @@ class TransactionBuilder {
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList()));
|
||||
add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
textEdit(TextNode node, Delta Function() f) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class BuiltInTextWidget extends StatefulWidget {
|
||||
@ -59,3 +60,58 @@ mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
|
||||
return const EdgeInsets.all(0);
|
||||
}
|
||||
}
|
||||
|
||||
mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
|
||||
implements DefaultSelectable {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.textNode.children.isEmpty) {
|
||||
return buildWithSingle(context);
|
||||
} else {
|
||||
return buildWithChildren(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildWithSingle(BuildContext context);
|
||||
|
||||
Widget buildWithChildren(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildWithSingle(context),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// TODO: customize
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.textNode.children
|
||||
.map(
|
||||
(child) => widget.editorState.service.renderPluginService
|
||||
.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,11 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
|
||||
// customize
|
||||
|
||||
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectable,
|
||||
BuiltInStyleMixin,
|
||||
BuiltInTextWidgetMixin {
|
||||
@override
|
||||
final iconKey = GlobalKey();
|
||||
|
||||
@ -61,7 +65,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget buildWithSingle(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
|
@ -46,7 +46,11 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
|
||||
}
|
||||
|
||||
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectable,
|
||||
BuiltInStyleMixin,
|
||||
BuiltInTextWidgetMixin {
|
||||
@override
|
||||
final iconKey = GlobalKey();
|
||||
|
||||
@ -62,15 +66,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.textNode.children.isEmpty) {
|
||||
return _buildWithSingle(context);
|
||||
} else {
|
||||
return _buildWithChildren(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildWithSingle(BuildContext context) {
|
||||
Widget buildWithSingle(BuildContext context) {
|
||||
final check = widget.textNode.attributes.check;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
@ -106,40 +102,4 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWithChildren(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWithSingle(context),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Column(
|
||||
children: widget.textNode.children
|
||||
.map(
|
||||
(child) => widget.editorState.service.renderPluginService
|
||||
.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
this.customBuilders = const {},
|
||||
this.shortcutEvents = const [],
|
||||
this.selectionMenuItems = const [],
|
||||
this.editable = true,
|
||||
required this.editorStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -53,6 +54,8 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
|
||||
final EditorStyle editorStyle;
|
||||
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||
}
|
||||
@ -106,11 +109,14 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
cursorColor: widget.editorStyle.cursorColor,
|
||||
selectionColor: widget.editorStyle.selectionColor,
|
||||
editorState: editorState,
|
||||
editable: widget.editable,
|
||||
child: AppFlowyInput(
|
||||
key: editorState.service.inputServiceKey,
|
||||
editorState: editorState,
|
||||
editable: widget.editable,
|
||||
child: AppFlowyKeyboard(
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
editable: widget.editable,
|
||||
shortcutEvents: [
|
||||
...builtInShortcutEvents,
|
||||
...widget.shortcutEvents,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/src/infra/log.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@ -43,11 +44,13 @@ abstract class AppFlowyInputService {
|
||||
class AppFlowyInput extends StatefulWidget {
|
||||
const AppFlowyInput({
|
||||
Key? key,
|
||||
this.editable = true,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final EditorState editorState;
|
||||
final bool editable;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@ -61,26 +64,39 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||
|
||||
EditorState get _editorState => widget.editorState;
|
||||
|
||||
// Disable space shortcut on the Web platform.
|
||||
final Map<ShortcutActivator, Intent> _shortcuts = kIsWeb
|
||||
? {
|
||||
LogicalKeySet(LogicalKeyboardKey.space):
|
||||
DoNothingAndStopPropagationIntent(),
|
||||
}
|
||||
: {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.addListener(_onSelectionChange);
|
||||
if (widget.editable) {
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.addListener(_onSelectionChange);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
close();
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onSelectionChange);
|
||||
if (widget.editable) {
|
||||
close();
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onSelectionChange);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return Shortcuts(
|
||||
shortcuts: _shortcuts,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
|
||||
// Handle delete text.
|
||||
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
||||
@ -121,32 +121,40 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
}
|
||||
|
||||
KeyEventResult _backDeleteToPreviousTextNode(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
List<Node> nonTextNodes,
|
||||
Selection selection) {
|
||||
var previous = textNode.previous;
|
||||
bool prevIsNumberList = false;
|
||||
while (previous != null) {
|
||||
if (previous is TextNode) {
|
||||
if (previous.subtype == BuiltInAttributeKey.numberList) {
|
||||
prevIsNumberList = true;
|
||||
}
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
List<Node> nonTextNodes,
|
||||
Selection selection,
|
||||
) {
|
||||
// Not reach to the root.
|
||||
if (textNode.parent?.parent != null) {
|
||||
transactionBuilder
|
||||
..deleteNode(textNode)
|
||||
..insertNode(textNode.parent!.path.next, textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.parent!.path.next, offset: 0),
|
||||
)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
transactionBuilder
|
||||
..mergeText(previous, textNode)
|
||||
..deleteNode(textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: previous.path,
|
||||
offset: previous.toRawString().length,
|
||||
),
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
previous = previous.previous;
|
||||
bool prevIsNumberList = false;
|
||||
final previousTextNode = _closestTextNode(textNode.previous);
|
||||
if (previousTextNode != null && previousTextNode is TextNode) {
|
||||
if (previousTextNode.subtype == BuiltInAttributeKey.numberList) {
|
||||
prevIsNumberList = true;
|
||||
}
|
||||
|
||||
transactionBuilder
|
||||
..mergeText(previousTextNode, textNode)
|
||||
..deleteNode(textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: previousTextNode.path,
|
||||
offset: previousTextNode.toRawString().length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (transactionBuilder.operations.isNotEmpty) {
|
||||
@ -157,8 +165,8 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
||||
}
|
||||
|
||||
if (prevIsNumberList) {
|
||||
makeFollowingNodesIncremental(
|
||||
editorState, previous!.path, transactionBuilder.afterSelection!);
|
||||
makeFollowingNodesIncremental(editorState, previousTextNode!.path,
|
||||
transactionBuilder.afterSelection!);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
@ -261,3 +269,22 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder,
|
||||
secondOffset: selection.end.offset,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Just a simple solution for textNode, need to be optimized.
|
||||
Node? _closestTextNode(Node? node) {
|
||||
if (node is TextNode) {
|
||||
var children = node.children;
|
||||
if (children.isEmpty) {
|
||||
return node;
|
||||
}
|
||||
var last = children.last;
|
||||
while (last.children.isNotEmpty) {
|
||||
last = children.last;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
if (node?.previous != null) {
|
||||
return _closestTextNode(node!.previous!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
import './number_list_helper.dart';
|
||||
|
||||
/// Handle some cases where enter is pressed and shift is not pressed.
|
||||
@ -16,10 +16,6 @@ import './number_list_helper.dart';
|
||||
/// 2.2 or insert a empty text node before.
|
||||
ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
(editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
var selection = editorState.service.selectionService.currentSelection.value;
|
||||
var nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
if (selection == null) {
|
||||
@ -124,7 +120,10 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
TextNode.empty(),
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta(),
|
||||
),
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
@ -142,21 +141,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
Position(path: nextPath, offset: 0),
|
||||
);
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
attributes: attributes,
|
||||
delta: textNode.delta.slice(selection.end.offset),
|
||||
),
|
||||
)
|
||||
..deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
final transactionBuilder = TransactionBuilder(editorState);
|
||||
transactionBuilder.insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
attributes: attributes,
|
||||
delta: textNode.delta.slice(selection.end.offset),
|
||||
),
|
||||
);
|
||||
transactionBuilder.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
);
|
||||
if (textNode.children.isNotEmpty) {
|
||||
final children = textNode.children.toList(growable: false);
|
||||
transactionBuilder.deleteNodes(children);
|
||||
}
|
||||
transactionBuilder.afterSelection = afterSelection;
|
||||
transactionBuilder.commit();
|
||||
|
||||
// If the new type of a text node is number list,
|
||||
// the numbers of the following nodes should be incremental.
|
||||
|
@ -0,0 +1,34 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
// Only Supports BulletedList For Now.
|
||||
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1 || selection == null || !selection.isSingle) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final previous = textNode.previous;
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList ||
|
||||
previous == null ||
|
||||
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
final path = previous.path + [previous.children.length];
|
||||
final afterSelection = Selection(
|
||||
start: selection.start.copyWith(path: path),
|
||||
end: selection.end.copyWith(path: path),
|
||||
);
|
||||
TransactionBuilder(editorState)
|
||||
..deleteNode(textNode)
|
||||
..insertNode(path, textNode)
|
||||
..setAfterSelection(afterSelection)
|
||||
..commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -42,6 +42,7 @@ abstract class AppFlowyKeyboardService {
|
||||
class AppFlowyKeyboard extends StatefulWidget {
|
||||
const AppFlowyKeyboard({
|
||||
Key? key,
|
||||
this.editable = true,
|
||||
required this.shortcutEvents,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
@ -50,6 +51,7 @@ class AppFlowyKeyboard extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
final Widget child;
|
||||
final List<ShortcutEvent> shortcutEvents;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
|
||||
@ -62,7 +64,6 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
bool isFocus = true;
|
||||
|
||||
@override
|
||||
// TODO: implement shortcutEvents
|
||||
List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
|
||||
|
||||
@override
|
||||
@ -91,8 +92,12 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
|
||||
@override
|
||||
void enable() {
|
||||
isFocus = true;
|
||||
_focusNode.requestFocus();
|
||||
if (widget.editable) {
|
||||
isFocus = true;
|
||||
_focusNode.requestFocus();
|
||||
} else {
|
||||
disable();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -84,6 +84,7 @@ class AppFlowySelection extends StatefulWidget {
|
||||
Key? key,
|
||||
this.cursorColor = const Color(0xFF00BCF0),
|
||||
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||
this.editable = true,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
@ -92,6 +93,7 @@ class AppFlowySelection extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Color cursorColor;
|
||||
final Color selectionColor;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowySelection> createState() => _AppFlowySelectionState();
|
||||
@ -144,15 +146,21 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionGestureDetector(
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
onTapDown: _onTapDown,
|
||||
onDoubleTapDown: _onDoubleTapDown,
|
||||
onTripleTapDown: _onTripleTapDown,
|
||||
child: widget.child,
|
||||
);
|
||||
if (!widget.editable) {
|
||||
return Container(
|
||||
child: widget.child,
|
||||
);
|
||||
} else {
|
||||
return SelectionGestureDetector(
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
onTapDown: _onTapDown,
|
||||
onDoubleTapDown: _onDoubleTapDown,
|
||||
onTripleTapDown: _onTripleTapDown,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -184,6 +192,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
@override
|
||||
void updateSelection(Selection? selection) {
|
||||
if (!widget.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionRects.clear();
|
||||
clearSelection();
|
||||
|
||||
@ -323,6 +335,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
// compute the selection in range.
|
||||
if (first != null && last != null) {
|
||||
Log.selection.debug('first = $first, last = $last');
|
||||
final start =
|
||||
first.getSelectionInRange(panStartOffset, panEndOffset).start;
|
||||
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
||||
@ -353,6 +366,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
final normalizedSelection = selection.normalize;
|
||||
assert(normalizedSelection.isBackward);
|
||||
|
||||
Log.selection.debug('update selection areas, $normalizedSelection');
|
||||
|
||||
for (var i = 0; i < backwardNodes.length; i++) {
|
||||
final node = backwardNodes[i];
|
||||
final selectable = node.selectable;
|
||||
|
@ -9,6 +9,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_und
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
||||
|
||||
@ -243,4 +244,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||
command: 'page down',
|
||||
handler: pageDownHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Tab',
|
||||
command: 'tab',
|
||||
handler: tabHandler,
|
||||
),
|
||||
];
|
||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Defines the implementation of shortcut event.
|
||||
class ShortcutEvent {
|
||||
@ -56,7 +57,10 @@ class ShortcutEvent {
|
||||
String? linuxCommand,
|
||||
}) {
|
||||
var matched = false;
|
||||
if (Platform.isWindows &&
|
||||
if (kIsWeb && command != null && command.isNotEmpty) {
|
||||
this.command = command;
|
||||
matched = true;
|
||||
} else if (Platform.isWindows &&
|
||||
windowsCommand != null &&
|
||||
windowsCommand.isNotEmpty) {
|
||||
this.command = windowsCommand;
|
||||
|
@ -0,0 +1,153 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('node.dart', () {
|
||||
test('test node copyWith', () {
|
||||
final node = Node(
|
||||
type: 'example',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
);
|
||||
expect(node.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
node.copyWith().toJson(),
|
||||
node.toJson(),
|
||||
);
|
||||
|
||||
final nodeWithChildren = Node(
|
||||
type: 'example',
|
||||
children: LinkedList()..add(node),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
);
|
||||
expect(nodeWithChildren.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'children': [
|
||||
{
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
nodeWithChildren.copyWith().toJson(),
|
||||
nodeWithChildren.toJson(),
|
||||
);
|
||||
});
|
||||
|
||||
test('test textNode copyWith', () {
|
||||
final textNode = TextNode(
|
||||
type: 'example',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
delta: Delta()..insert('AppFlowy'),
|
||||
);
|
||||
expect(textNode.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
textNode.copyWith().toJson(),
|
||||
textNode.toJson(),
|
||||
);
|
||||
|
||||
final textNodeWithChildren = TextNode(
|
||||
type: 'example',
|
||||
children: LinkedList()..add(textNode),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
delta: Delta()..insert('AppFlowy'),
|
||||
);
|
||||
expect(textNodeWithChildren.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
'children': [
|
||||
{
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
textNodeWithChildren.copyWith().toJson(),
|
||||
textNodeWithChildren.toJson(),
|
||||
);
|
||||
});
|
||||
|
||||
test('test node path', () {
|
||||
Node previous = Node(
|
||||
type: 'example',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
const len = 10;
|
||||
for (var i = 0; i < len; i++) {
|
||||
final node = Node(
|
||||
type: 'example_$i',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
previous.children.add(node..parent = previous);
|
||||
previous = node;
|
||||
}
|
||||
expect(previous.path, List.filled(len, 0));
|
||||
});
|
||||
|
||||
test('test copy with', () {
|
||||
final child = Node(
|
||||
type: 'child',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
final base = Node(
|
||||
type: 'base',
|
||||
attributes: {},
|
||||
children: LinkedList()..add(child),
|
||||
);
|
||||
final node = base.copyWith(
|
||||
type: 'node',
|
||||
);
|
||||
expect(identical(node.attributes, base.attributes), false);
|
||||
expect(identical(node.children, base.children), false);
|
||||
expect(identical(node.children.first, base.children.first), false);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('path_extensions.dart', () {
|
||||
test('test path equality', () {
|
||||
var p1 = [0, 0];
|
||||
var p2 = [0];
|
||||
|
||||
expect(p1 > p2, true);
|
||||
expect(p1 >= p2, true);
|
||||
expect(p1 < p2, false);
|
||||
expect(p1 <= p2, false);
|
||||
|
||||
p1 = [1, 1, 2];
|
||||
p2 = [1, 1, 3];
|
||||
|
||||
expect(p2 > p1, true);
|
||||
expect(p2 >= p1, true);
|
||||
expect(p2 < p1, false);
|
||||
expect(p2 <= p1, false);
|
||||
|
||||
p1 = [2, 0, 1];
|
||||
p2 = [2, 0, 1];
|
||||
|
||||
expect(p2 > p1, false);
|
||||
expect(p1 > p2, false);
|
||||
expect(p2 >= p1, true);
|
||||
expect(p2 <= p1, true);
|
||||
expect(pathEquals(p1, p2), true);
|
||||
});
|
||||
});
|
||||
}
|
@ -19,6 +19,7 @@ class EditorWidgetTester {
|
||||
EditorState get editorState => _editorState;
|
||||
Node get root => _editorState.document.root;
|
||||
|
||||
StateTree get document => _editorState.document;
|
||||
int get documentLength => _editorState.document.root.children.length;
|
||||
Selection? get documentSelection =>
|
||||
_editorState.service.selectionService.currentSelection.value;
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:network_image_mock/network_image_mock.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
@ -267,6 +266,60 @@ void main() async {
|
||||
BuiltInAttributeKey.h1,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Delete the nested bulleted list', (tester) async {
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final node = TextNode(
|
||||
type: 'text',
|
||||
delta: Delta()..insert(text),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
|
||||
},
|
||||
);
|
||||
node.insert(
|
||||
node.copyWith()
|
||||
..insert(
|
||||
node.copyWith(),
|
||||
),
|
||||
);
|
||||
|
||||
final editor = tester.editor..insert(node);
|
||||
await editor.startTesting();
|
||||
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// Welcome to Appflowy 😁
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([0, 0, 0])?.subtype, null);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([0, 1]) != null, true);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 1], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([1]) != null, true);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁Welcome to Appflowy 😁
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: text.length),
|
||||
);
|
||||
expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteFirstImage(WidgetTester tester, bool isBackward) async {
|
||||
|
@ -0,0 +1,151 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('tab_handler.dart', () {
|
||||
testWidgets('press tab in plain text', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
final document = editor.document;
|
||||
|
||||
var selection = Selection.single(path: [0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
});
|
||||
|
||||
testWidgets('press tab in bulleted list', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
)
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
)
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
);
|
||||
await editor.startTesting();
|
||||
var document = editor.document;
|
||||
|
||||
var selection = Selection.single(path: [0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
// Before
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// After
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: 0),
|
||||
);
|
||||
expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([1])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([2]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 1], startOffset: 0),
|
||||
);
|
||||
expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([1]), null);
|
||||
expect(editor.nodeAtPath([2]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
|
||||
// Before
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// After
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
document = editor.document;
|
||||
selection = Selection.single(path: [0, 0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: 0),
|
||||
);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
selection = Selection.single(path: [0, 1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(editor.nodeAtPath([0, 1]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0, 0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user