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:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:example/plugin/underscore_to_italic.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:universal_html/html.dart' as html;
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
@ -112,6 +115,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
child: AppFlowyEditor(
|
child: AppFlowyEditor(
|
||||||
editorState: _editorState!,
|
editorState: _editorState!,
|
||||||
editorStyle: _editorStyle,
|
editorStyle: _editorStyle,
|
||||||
|
editable: true,
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
underscoreToItalic,
|
underscoreToItalic,
|
||||||
],
|
],
|
||||||
@ -148,7 +152,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
),
|
),
|
||||||
ActionButton(
|
ActionButton(
|
||||||
icon: const Icon(Icons.import_export),
|
icon: const Icon(Icons.import_export),
|
||||||
onPressed: () => _importDocument(),
|
onPressed: () async => await _importDocument(),
|
||||||
),
|
),
|
||||||
ActionButton(
|
ActionButton(
|
||||||
icon: const Icon(Icons.color_lens),
|
icon: const Icon(Icons.color_lens),
|
||||||
@ -167,28 +171,53 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
void _exportDocument(EditorState editorState) async {
|
void _exportDocument(EditorState editorState) async {
|
||||||
final document = editorState.document.toJson();
|
final document = editorState.document.toJson();
|
||||||
final json = jsonEncode(document);
|
final json = jsonEncode(document);
|
||||||
final directory = await getTemporaryDirectory();
|
if (kIsWeb) {
|
||||||
final path = directory.path;
|
final blob = html.Blob([json], 'text/plain', 'native');
|
||||||
final file = File('$path/editor.json');
|
html.AnchorElement(
|
||||||
await file.writeAsString(json);
|
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) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('The document is saved to the ${file.path}'),
|
content: Text('The document is saved to the ${file.path}'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _importDocument() async {
|
Future<void> _importDocument() async {
|
||||||
final directory = await getTemporaryDirectory();
|
if (kIsWeb) {
|
||||||
final path = directory.path;
|
final result = await FilePicker.platform.pickFiles(
|
||||||
final file = File('$path/editor.json');
|
allowMultiple: false,
|
||||||
setState(() {
|
allowedExtensions: ['json'],
|
||||||
_editorState = null;
|
type: FileType.custom,
|
||||||
_jsonString = file.readAsString();
|
);
|
||||||
});
|
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) {
|
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 path_provider_macos
|
||||||
import rich_clipboard_macos
|
import rich_clipboard_macos
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import wakelock_macos
|
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,12 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- wakelock_macos (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||||
- rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_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`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
- wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
@ -25,15 +22,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
wakelock_macos:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||||
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||||
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||||
|
|
||||||
|
@ -37,12 +37,12 @@ dependencies:
|
|||||||
path: ../
|
path: ../
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
video_player: ^2.4.5
|
|
||||||
pod_player: 0.0.8
|
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
google_fonts: ^3.0.1
|
google_fonts: ^3.0.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
file_picker: ^5.0.1
|
||||||
|
universal_html: ^2.0.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -193,16 +193,24 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||||||
return parent!._path([index, ...previous]);
|
return parent!._path([index, ...previous]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Node deepClone() {
|
Node copyWith({
|
||||||
final newNode = Node(
|
String? type,
|
||||||
type: type, children: LinkedList<Node>(), attributes: {...attributes});
|
LinkedList<Node>? children,
|
||||||
|
Attributes? attributes,
|
||||||
for (final node in children) {
|
}) {
|
||||||
final newNode = node.deepClone();
|
final node = Node(
|
||||||
newNode.parent = this;
|
type: type ?? this.type,
|
||||||
newNode.children.add(newNode);
|
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,
|
LinkedList<Node>? children,
|
||||||
Attributes? attributes,
|
Attributes? attributes,
|
||||||
}) : _delta = delta,
|
}) : _delta = delta,
|
||||||
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
super(
|
||||||
|
children: children ?? LinkedList(),
|
||||||
|
attributes: attributes ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
TextNode.empty({Attributes? attributes})
|
TextNode.empty({Attributes? attributes})
|
||||||
: _delta = Delta([TextInsert('')]),
|
: _delta = Delta([TextInsert('')]),
|
||||||
@ -241,33 +252,27 @@ class TextNode extends Node {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
TextNode copyWith({
|
TextNode copyWith({
|
||||||
String? type,
|
String? type,
|
||||||
LinkedList<Node>? children,
|
LinkedList<Node>? children,
|
||||||
Attributes? attributes,
|
Attributes? attributes,
|
||||||
Delta? delta,
|
Delta? delta,
|
||||||
}) =>
|
}) {
|
||||||
TextNode(
|
final textNode = TextNode(
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
children: children ?? this.children,
|
children: children,
|
||||||
attributes: attributes ?? _attributes,
|
attributes: attributes ?? _attributes,
|
||||||
delta: delta ?? this.delta,
|
delta: delta ?? this.delta,
|
||||||
);
|
);
|
||||||
|
if (children == null && this.children.isNotEmpty) {
|
||||||
@override
|
for (final child in this.children) {
|
||||||
TextNode deepClone() {
|
textNode.children.add(
|
||||||
final newNode = TextNode(
|
child.copyWith()..parent = 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);
|
|
||||||
}
|
}
|
||||||
return newNode;
|
return textNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
String toRawString() => _delta.toRawString();
|
String toRawString() => _delta.toRawString();
|
||||||
|
@ -40,11 +40,9 @@ class Selection {
|
|||||||
bool get isCollapsed => start == end;
|
bool get isCollapsed => start == end;
|
||||||
bool get isSingle => pathEquals(start.path, end.path);
|
bool get isSingle => pathEquals(start.path, end.path);
|
||||||
bool get isForward =>
|
bool get isForward =>
|
||||||
(start.path >= end.path && !pathEquals(start.path, end.path)) ||
|
(start.path > end.path) || (isSingle && start.offset > end.offset);
|
||||||
(isSingle && start.offset > end.offset);
|
|
||||||
bool get isBackward =>
|
bool get isBackward =>
|
||||||
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
|
(start.path < end.path) || (isSingle && start.offset < end.offset);
|
||||||
(isSingle && start.offset < end.offset);
|
|
||||||
|
|
||||||
Selection get normalize {
|
Selection get normalize {
|
||||||
if (isForward) {
|
if (isForward) {
|
||||||
|
@ -4,22 +4,52 @@ import 'dart:math';
|
|||||||
|
|
||||||
extension PathExtensions on Path {
|
extension PathExtensions on Path {
|
||||||
bool operator >=(Path other) {
|
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);
|
final length = min(this.length, other.length);
|
||||||
for (var i = 0; i < length; i++) {
|
for (var i = 0; i < length; i++) {
|
||||||
if (this[i] < other[i]) {
|
if (this[i] < other[i]) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (this[i] > other[i]) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.length < other.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator <=(Path other) {
|
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);
|
final length = min(this.length, other.length);
|
||||||
for (var i = 0; i < length; i++) {
|
for (var i = 0; i < length; i++) {
|
||||||
if (this[i] > other[i]) {
|
if (this[i] > other[i]) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (this[i] < other[i]) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.length > other.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class TransactionBuilder {
|
|||||||
/// Inserts a sequence of nodes at the position of path.
|
/// Inserts a sequence of nodes at the position of path.
|
||||||
insertNodes(Path path, List<Node> nodes) {
|
insertNodes(Path path, List<Node> nodes) {
|
||||||
beforeSelection = state.cursorSelection;
|
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.
|
/// Updates the attributes of nodes.
|
||||||
@ -75,7 +75,7 @@ class TransactionBuilder {
|
|||||||
nodes.add(node);
|
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) {
|
textEdit(TextNode node, Delta Function() f) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
abstract class BuiltInTextWidget extends StatefulWidget {
|
abstract class BuiltInTextWidget extends StatefulWidget {
|
||||||
@ -59,3 +60,58 @@ mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
|
|||||||
return const EdgeInsets.all(0);
|
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
|
// customize
|
||||||
|
|
||||||
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
with
|
||||||
|
SelectableMixin,
|
||||||
|
DefaultSelectable,
|
||||||
|
BuiltInStyleMixin,
|
||||||
|
BuiltInTextWidgetMixin {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget buildWithSingle(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -46,7 +46,11 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
with
|
||||||
|
SelectableMixin,
|
||||||
|
DefaultSelectable,
|
||||||
|
BuiltInStyleMixin,
|
||||||
|
BuiltInTextWidgetMixin {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -62,15 +66,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget buildWithSingle(BuildContext context) {
|
||||||
if (widget.textNode.children.isEmpty) {
|
|
||||||
return _buildWithSingle(context);
|
|
||||||
} else {
|
|
||||||
return _buildWithChildren(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWithSingle(BuildContext context) {
|
|
||||||
final check = widget.textNode.attributes.check;
|
final check = widget.textNode.attributes.check;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: 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.customBuilders = const {},
|
||||||
this.shortcutEvents = const [],
|
this.shortcutEvents = const [],
|
||||||
this.selectionMenuItems = const [],
|
this.selectionMenuItems = const [],
|
||||||
|
this.editable = true,
|
||||||
required this.editorStyle,
|
required this.editorStyle,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -53,6 +54,8 @@ class AppFlowyEditor extends StatefulWidget {
|
|||||||
|
|
||||||
final EditorStyle editorStyle;
|
final EditorStyle editorStyle;
|
||||||
|
|
||||||
|
final bool editable;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||||
}
|
}
|
||||||
@ -106,11 +109,14 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
cursorColor: widget.editorStyle.cursorColor,
|
cursorColor: widget.editorStyle.cursorColor,
|
||||||
selectionColor: widget.editorStyle.selectionColor,
|
selectionColor: widget.editorStyle.selectionColor,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
editable: widget.editable,
|
||||||
child: AppFlowyInput(
|
child: AppFlowyInput(
|
||||||
key: editorState.service.inputServiceKey,
|
key: editorState.service.inputServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
editable: widget.editable,
|
||||||
child: AppFlowyKeyboard(
|
child: AppFlowyKeyboard(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
|
editable: widget.editable,
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
...builtInShortcutEvents,
|
...builtInShortcutEvents,
|
||||||
...widget.shortcutEvents,
|
...widget.shortcutEvents,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy_editor/src/infra/log.dart';
|
import 'package:appflowy_editor/src/infra/log.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@ -43,11 +44,13 @@ abstract class AppFlowyInputService {
|
|||||||
class AppFlowyInput extends StatefulWidget {
|
class AppFlowyInput extends StatefulWidget {
|
||||||
const AppFlowyInput({
|
const AppFlowyInput({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.editable = true,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
|
final bool editable;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,26 +64,39 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
|||||||
|
|
||||||
EditorState get _editorState => widget.editorState;
|
EditorState get _editorState => widget.editorState;
|
||||||
|
|
||||||
|
// Disable space shortcut on the Web platform.
|
||||||
|
final Map<ShortcutActivator, Intent> _shortcuts = kIsWeb
|
||||||
|
? {
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.space):
|
||||||
|
DoNothingAndStopPropagationIntent(),
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_editorState.service.selectionService.currentSelection
|
if (widget.editable) {
|
||||||
.addListener(_onSelectionChange);
|
_editorState.service.selectionService.currentSelection
|
||||||
|
.addListener(_onSelectionChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
close();
|
if (widget.editable) {
|
||||||
_editorState.service.selectionService.currentSelection
|
close();
|
||||||
.removeListener(_onSelectionChange);
|
_editorState.service.selectionService.currentSelection
|
||||||
|
.removeListener(_onSelectionChange);
|
||||||
|
}
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Shortcuts(
|
||||||
|
shortcuts: _shortcuts,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||||
|
|
||||||
// Handle delete text.
|
// Handle delete text.
|
||||||
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
||||||
@ -121,32 +121,40 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _backDeleteToPreviousTextNode(
|
KeyEventResult _backDeleteToPreviousTextNode(
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
TextNode textNode,
|
TextNode textNode,
|
||||||
TransactionBuilder transactionBuilder,
|
TransactionBuilder transactionBuilder,
|
||||||
List<Node> nonTextNodes,
|
List<Node> nonTextNodes,
|
||||||
Selection selection) {
|
Selection selection,
|
||||||
var previous = textNode.previous;
|
) {
|
||||||
bool prevIsNumberList = false;
|
// Not reach to the root.
|
||||||
while (previous != null) {
|
if (textNode.parent?.parent != null) {
|
||||||
if (previous is TextNode) {
|
transactionBuilder
|
||||||
if (previous.subtype == BuiltInAttributeKey.numberList) {
|
..deleteNode(textNode)
|
||||||
prevIsNumberList = true;
|
..insertNode(textNode.parent!.path.next, textNode)
|
||||||
}
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(path: textNode.parent!.path.next, offset: 0),
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
transactionBuilder
|
bool prevIsNumberList = false;
|
||||||
..mergeText(previous, textNode)
|
final previousTextNode = _closestTextNode(textNode.previous);
|
||||||
..deleteNode(textNode)
|
if (previousTextNode != null && previousTextNode is TextNode) {
|
||||||
..afterSelection = Selection.collapsed(
|
if (previousTextNode.subtype == BuiltInAttributeKey.numberList) {
|
||||||
Position(
|
prevIsNumberList = true;
|
||||||
path: previous.path,
|
|
||||||
offset: previous.toRawString().length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
previous = previous.previous;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transactionBuilder
|
||||||
|
..mergeText(previousTextNode, textNode)
|
||||||
|
..deleteNode(textNode)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: previousTextNode.path,
|
||||||
|
offset: previousTextNode.toRawString().length,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionBuilder.operations.isNotEmpty) {
|
if (transactionBuilder.operations.isNotEmpty) {
|
||||||
@ -157,8 +165,8 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prevIsNumberList) {
|
if (prevIsNumberList) {
|
||||||
makeFollowingNodesIncremental(
|
makeFollowingNodesIncremental(editorState, previousTextNode!.path,
|
||||||
editorState, previous!.path, transactionBuilder.afterSelection!);
|
transactionBuilder.afterSelection!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
@ -261,3 +269,22 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder,
|
|||||||
secondOffset: selection.end.offset,
|
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:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.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/extensions/path_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
|
||||||
import './number_list_helper.dart';
|
import './number_list_helper.dart';
|
||||||
|
|
||||||
/// Handle some cases where enter is pressed and shift is not pressed.
|
/// 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.
|
/// 2.2 or insert a empty text node before.
|
||||||
ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||||
(editorState, event) {
|
(editorState, event) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection = editorState.service.selectionService.currentSelection.value;
|
var selection = editorState.service.selectionService.currentSelection.value;
|
||||||
var nodes = editorState.service.selectionService.currentSelectedNodes;
|
var nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
@ -124,7 +120,10 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
|||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
..insertNode(
|
..insertNode(
|
||||||
textNode.path,
|
textNode.path,
|
||||||
TextNode.empty(),
|
textNode.copyWith(
|
||||||
|
children: LinkedList(),
|
||||||
|
delta: Delta(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
..afterSelection = afterSelection
|
..afterSelection = afterSelection
|
||||||
..commit();
|
..commit();
|
||||||
@ -142,21 +141,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
|||||||
Position(path: nextPath, offset: 0),
|
Position(path: nextPath, offset: 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
TransactionBuilder(editorState)
|
final transactionBuilder = TransactionBuilder(editorState);
|
||||||
..insertNode(
|
transactionBuilder.insertNode(
|
||||||
textNode.path.next,
|
textNode.path.next,
|
||||||
textNode.copyWith(
|
textNode.copyWith(
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
delta: textNode.delta.slice(selection.end.offset),
|
delta: textNode.delta.slice(selection.end.offset),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
..deleteText(
|
transactionBuilder.deleteText(
|
||||||
textNode,
|
textNode,
|
||||||
selection.start.offset,
|
selection.start.offset,
|
||||||
textNode.toRawString().length - selection.start.offset,
|
textNode.toRawString().length - selection.start.offset,
|
||||||
)
|
);
|
||||||
..afterSelection = afterSelection
|
if (textNode.children.isNotEmpty) {
|
||||||
..commit();
|
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,
|
// If the new type of a text node is number list,
|
||||||
// the numbers of the following nodes should be incremental.
|
// 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 {
|
class AppFlowyKeyboard extends StatefulWidget {
|
||||||
const AppFlowyKeyboard({
|
const AppFlowyKeyboard({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.editable = true,
|
||||||
required this.shortcutEvents,
|
required this.shortcutEvents,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -50,6 +51,7 @@ class AppFlowyKeyboard extends StatefulWidget {
|
|||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<ShortcutEvent> shortcutEvents;
|
final List<ShortcutEvent> shortcutEvents;
|
||||||
|
final bool editable;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
|
State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
|
||||||
@ -62,7 +64,6 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
|||||||
bool isFocus = true;
|
bool isFocus = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement shortcutEvents
|
|
||||||
List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
|
List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -91,8 +92,12 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void enable() {
|
void enable() {
|
||||||
isFocus = true;
|
if (widget.editable) {
|
||||||
_focusNode.requestFocus();
|
isFocus = true;
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
} else {
|
||||||
|
disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -84,6 +84,7 @@ class AppFlowySelection extends StatefulWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
this.cursorColor = const Color(0xFF00BCF0),
|
this.cursorColor = const Color(0xFF00BCF0),
|
||||||
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||||
|
this.editable = true,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -92,6 +93,7 @@ class AppFlowySelection extends StatefulWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final Color cursorColor;
|
final Color cursorColor;
|
||||||
final Color selectionColor;
|
final Color selectionColor;
|
||||||
|
final bool editable;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowySelection> createState() => _AppFlowySelectionState();
|
State<AppFlowySelection> createState() => _AppFlowySelectionState();
|
||||||
@ -144,15 +146,21 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SelectionGestureDetector(
|
if (!widget.editable) {
|
||||||
onPanStart: _onPanStart,
|
return Container(
|
||||||
onPanUpdate: _onPanUpdate,
|
child: widget.child,
|
||||||
onPanEnd: _onPanEnd,
|
);
|
||||||
onTapDown: _onTapDown,
|
} else {
|
||||||
onDoubleTapDown: _onDoubleTapDown,
|
return SelectionGestureDetector(
|
||||||
onTripleTapDown: _onTripleTapDown,
|
onPanStart: _onPanStart,
|
||||||
child: widget.child,
|
onPanUpdate: _onPanUpdate,
|
||||||
);
|
onPanEnd: _onPanEnd,
|
||||||
|
onTapDown: _onTapDown,
|
||||||
|
onDoubleTapDown: _onDoubleTapDown,
|
||||||
|
onTripleTapDown: _onTripleTapDown,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -184,6 +192,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateSelection(Selection? selection) {
|
void updateSelection(Selection? selection) {
|
||||||
|
if (!widget.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectionRects.clear();
|
selectionRects.clear();
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
@ -323,6 +335,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
|
|
||||||
// compute the selection in range.
|
// compute the selection in range.
|
||||||
if (first != null && last != null) {
|
if (first != null && last != null) {
|
||||||
|
Log.selection.debug('first = $first, last = $last');
|
||||||
final start =
|
final start =
|
||||||
first.getSelectionInRange(panStartOffset, panEndOffset).start;
|
first.getSelectionInRange(panStartOffset, panEndOffset).start;
|
||||||
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
||||||
@ -353,6 +366,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
final normalizedSelection = selection.normalize;
|
final normalizedSelection = selection.normalize;
|
||||||
assert(normalizedSelection.isBackward);
|
assert(normalizedSelection.isBackward);
|
||||||
|
|
||||||
|
Log.selection.debug('update selection areas, $normalizedSelection');
|
||||||
|
|
||||||
for (var i = 0; i < backwardNodes.length; i++) {
|
for (var i = 0; i < backwardNodes.length; i++) {
|
||||||
final node = backwardNodes[i];
|
final node = backwardNodes[i];
|
||||||
final selectable = node.selectable;
|
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/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/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/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/internal_key_event_handlers/whitespace_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
||||||
|
|
||||||
@ -243,4 +244,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
|||||||
command: 'page down',
|
command: 'page down',
|
||||||
handler: pageDownHandler,
|
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/keybinding.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Defines the implementation of shortcut event.
|
/// Defines the implementation of shortcut event.
|
||||||
class ShortcutEvent {
|
class ShortcutEvent {
|
||||||
@ -56,7 +57,10 @@ class ShortcutEvent {
|
|||||||
String? linuxCommand,
|
String? linuxCommand,
|
||||||
}) {
|
}) {
|
||||||
var matched = false;
|
var matched = false;
|
||||||
if (Platform.isWindows &&
|
if (kIsWeb && command != null && command.isNotEmpty) {
|
||||||
|
this.command = command;
|
||||||
|
matched = true;
|
||||||
|
} else if (Platform.isWindows &&
|
||||||
windowsCommand != null &&
|
windowsCommand != null &&
|
||||||
windowsCommand.isNotEmpty) {
|
windowsCommand.isNotEmpty) {
|
||||||
this.command = windowsCommand;
|
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;
|
EditorState get editorState => _editorState;
|
||||||
Node get root => _editorState.document.root;
|
Node get root => _editorState.document.root;
|
||||||
|
|
||||||
|
StateTree get document => _editorState.document;
|
||||||
int get documentLength => _editorState.document.root.children.length;
|
int get documentLength => _editorState.document.root.children.length;
|
||||||
Selection? get documentSelection =>
|
Selection? get documentSelection =>
|
||||||
_editorState.service.selectionService.currentSelection.value;
|
_editorState.service.selectionService.currentSelection.value;
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:network_image_mock/network_image_mock.dart';
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
import '../../infra/test_editor.dart';
|
import '../../infra/test_editor.dart';
|
||||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
@ -267,6 +266,60 @@ void main() async {
|
|||||||
BuiltInAttributeKey.h1,
|
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 {
|
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…
x
Reference in New Issue
Block a user